From ff30fa46ff59b6460d53477bfce91a32bcc8ec40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Sun, 6 Oct 2024 11:34:43 +0200 Subject: [PATCH] fix(Forms): show error on every value change when using exported validators --- .../hooks/__tests__/useFieldProps.test.tsx | 118 +++++++++++++++++- .../extensions/forms/hooks/useFieldProps.ts | 44 +++++-- 2 files changed, 145 insertions(+), 17 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx index c190ad15af4..e0a1e95e030 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx @@ -3377,7 +3377,7 @@ describe('useFieldProps', () => { expect(screen.queryByRole('alert')).toHaveTextContent('bar') }) expect(myValidator).toHaveBeenCalledTimes(5) - expect(fooValidator).toHaveBeenCalledTimes(4) + expect(fooValidator).toHaveBeenCalledTimes(5) expect(barValidator).toHaveBeenCalledTimes(4) }) @@ -3427,7 +3427,7 @@ describe('useFieldProps', () => { expect(screen.queryByRole('alert')).toHaveTextContent('bar') }) expect(myValidator).toHaveBeenCalledTimes(5) - expect(fooValidator).toHaveBeenCalledTimes(4) + expect(fooValidator).toHaveBeenCalledTimes(5) expect(barValidator).toHaveBeenCalledTimes(4) }) @@ -3525,7 +3525,7 @@ describe('useFieldProps', () => { }) expect(publicValidator).toHaveBeenCalledTimes(9) expect(fooValidator).toHaveBeenCalledTimes(1) - expect(barValidator).toHaveBeenCalledTimes(7) + expect(barValidator).toHaveBeenCalledTimes(8) expect(bazValidator).toHaveBeenCalledTimes(7) expect(internalValidators).toHaveBeenCalledTimes(0) }) @@ -3563,6 +3563,116 @@ describe('useFieldProps', () => { }) }) + it('should show error on every value change', async () => { + const exportedValidator = jest.fn((value) => { + if (value === '1234') { + return Error('Error message') + } + }) + + const myValidator = jest.fn((value, { validators }) => { + const { exportedValidator } = validators + + return [exportedValidator] + }) + + const MockComponent = (props) => { + return ( + + ) + } + + render() + + const input = document.querySelector('input') + + await userEvent.type(input, '123') + fireEvent.blur(input) + + expect(document.querySelector('.dnb-form-status')).toBeNull() + + await userEvent.type(input, '4') + fireEvent.blur(input) + + expect(exportedValidator).toHaveBeenCalledTimes(2) + expect(myValidator).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('Error message') + }) + + await userEvent.type(input, '{Backspace}4') + fireEvent.blur(input) + + expect(exportedValidator).toHaveBeenCalledTimes(3) + expect(myValidator).toHaveBeenCalledTimes(3) + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('Error message') + }) + }) + + it('should support mixed sync and async validators', async () => { + const exportedValidator = jest.fn(async (value) => { + if (value === '1234') { + return Error('Error message') + } + }) + + const myValidator = jest.fn((value, { validators }) => { + const { exportedValidator } = validators + + return [exportedValidator] + }) + + const MockComponent = (props) => { + return ( + + ) + } + + render() + + const input = document.querySelector('input') + + await userEvent.type(input, '123') + fireEvent.blur(input) + + expect(document.querySelector('.dnb-form-status')).toBeNull() + + await userEvent.type(input, '4') + fireEvent.blur(input) + + expect(exportedValidator).toHaveBeenCalledTimes(1) + expect(myValidator).toHaveBeenCalledTimes(4) + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('Error message') + }) + + await userEvent.type(input, '{Backspace}4') + fireEvent.blur(input) + + expect(exportedValidator).toHaveBeenCalledTimes(2) + expect(myValidator).toHaveBeenCalledTimes(6) + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('Error message') + }) + }) + it('should only call returned validators (barValidator should not be called)', async () => { let internalValidators, fooValidator, barValidator, bazValidator @@ -3748,7 +3858,7 @@ describe('useFieldProps', () => { expect(screen.queryByRole('alert')).toHaveTextContent('baz') }) expect(publicValidator).toHaveBeenCalledTimes(9) - expect(barValidator).toHaveBeenCalledTimes(7) + expect(barValidator).toHaveBeenCalledTimes(8) expect(bazValidator).toHaveBeenCalledTimes(7) expect(internalValidators).toHaveBeenCalledTimes(0) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index ddb7989b248..c33d0155924 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -559,25 +559,26 @@ export default function useFieldProps( return result }, []) - const callValidatorFnSync = useCallback( - ( + const callValidatorFnAsync = useCallback( + async ( validator: Validator, value: Value = valueRef.current - ): ReturnType> => { + ): Promise>> => { if (typeof validator !== 'function') { - return // stop here + return } const result = extendWithExportedValidators( validator, - validator(value, additionalArgs) + await validator(value, additionalArgs) ) if (Array.isArray(result)) { for (const validator of result) { if (!hasBeenCalledRef(validator)) { - const result = callValidatorFnSync(validator, value) + const result = await callValidatorFnAsync(validator, value) if (result instanceof Error) { + callStackRef.current = [] return result } } @@ -591,25 +592,37 @@ export default function useFieldProps( [additionalArgs, extendWithExportedValidators, hasBeenCalledRef] ) - const callValidatorFnAsync = useCallback( - async ( + const callValidatorFnSync = useCallback( + ( validator: Validator, value: Value = valueRef.current - ): Promise>> => { + ): ReturnType> => { if (typeof validator !== 'function') { - return + return // stop here } const result = extendWithExportedValidators( validator, - await validator(value, additionalArgs) + validator(value, additionalArgs) ) if (Array.isArray(result)) { + const hasAsyncValidator = result.some((validator) => + isAsync(validator) + ) + if (hasAsyncValidator) { + return new Promise((resolve) => { + callValidatorFnAsync(validator, value).then((result) => { + resolve(result) + }) + }) + } + for (const validator of result) { if (!hasBeenCalledRef(validator)) { - const result = await callValidatorFnAsync(validator, value) + const result = callValidatorFnSync(validator, value) if (result instanceof Error) { + callStackRef.current = [] return result } } @@ -620,7 +633,12 @@ export default function useFieldProps( return result } }, - [additionalArgs, extendWithExportedValidators, hasBeenCalledRef] + [ + additionalArgs, + callValidatorFnAsync, + extendWithExportedValidators, + hasBeenCalledRef, + ] ) /**