diff --git a/package.json b/package.json index 47a8cb39d..67e2a2bfc 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", + "jest-canvas-mock": "^2.5.8", "jest-environment-jsdom": "^29.7.0", "jest-transform-css": "^6.0.1", "jsdom": "^22.1.0", diff --git a/src/components/Form.tsx b/src/components/Form.tsx index 2520b3f51..7cfbd8829 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -1,8 +1,40 @@ import { CSSProperties, useEffect, useRef, useState } from 'react'; -import { EventEmitter, Form as FormClass, Webform, Utils } from '@formio/js'; +import { EventEmitter, Form as FormClass, Webform, Utils, Formio } from '@formio/js'; import { Component, Form as CoreFormType } from '@formio/core'; import structuredClone from '@ungap/structured-clone'; +// Patch SignatureComponent to propagate the modified flag correctly when drawing +if ( + Formio && + Formio.Components && + Formio.Components.components && + Formio.Components.components.signature +) { + const SignatureComponent = Formio.Components.components.signature; + const originalSetValue = SignatureComponent.prototype.setValue; + SignatureComponent.prototype.setValue = function (value: any, flags: any = {}) { + const originalTriggerChange = this.triggerChange; + if (originalTriggerChange) { + this.triggerChange = (...args: any[]) => { + const finalArgs = args.length > 0 ? args : [flags]; + return originalTriggerChange.apply(this, finalArgs); + }; + } + try { + return originalSetValue.call(this, value, flags); + } finally { + if (originalTriggerChange) { + this.triggerChange = originalTriggerChange; + } + } + }; +} + + + + + + export type PartialExcept = Partial> & Required>; @@ -254,6 +286,11 @@ export const Form = (props: FormProps) => { const currentFormJson = useRef(null); const { formConstructor, formSource, formReadyCallback } = getEffectiveProps(props); + const formReadyCallbackRef = useRef(formReadyCallback); + useEffect(() => { + formReadyCallbackRef.current = formReadyCallback; + }, [formReadyCallback]); + const { src, form, @@ -330,8 +367,8 @@ export const Form = (props: FormProps) => { } } - if (formReadyCallback) { - formReadyCallback(instance); + if (formReadyCallbackRef.current) { + formReadyCallbackRef.current(instance); } setFormInstance((prevInstance) => { if (prevInstance) { @@ -345,7 +382,7 @@ export const Form = (props: FormProps) => { }; createInstance(); - }, [formConstructor, formReadyCallback, formSource, options, url]); + }, [formConstructor, formSource, options, url]); useEffect(() => { let onAnyHandler = null; diff --git a/src/components/__tests__/Signature.test.tsx b/src/components/__tests__/Signature.test.tsx new file mode 100644 index 000000000..18ae876b7 --- /dev/null +++ b/src/components/__tests__/Signature.test.tsx @@ -0,0 +1,63 @@ +import 'jest-canvas-mock'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { Form } from '../Form'; + +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +globalThis.ResizeObserver = ResizeObserver; + +const signatureForm = { + display: 'form' as const, + components: [ + { + label: 'Signature', + key: 'signature', + type: 'signature', + input: true, + }, + ], +}; + +test('signature change is marked as modified', async () => { + let modified: boolean | undefined; + + const formReady = new Promise((resolve) => { + render( +
{ + modified = isModified; + }} + onFormReady={(form) => { + resolve(form); + }} + />, + ); + }); + + const form = await formReady; + const signature = form.components[0]; + + signature.signaturePad.toDataURL = jest.fn( + () => 'data:image/png;base64,test', + ); + + signature.signaturePad.dispatchEvent( + new CustomEvent('endStroke'), + ); + + expect( + document.querySelector('.signature-pad'), + ).toBeInTheDocument(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(modified).toBe(true); +}); + \ No newline at end of file