From 12534dc6aa37b72ebb24a8e388bfcfa82c7c95a1 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 20 Dec 2024 11:33:51 +0200 Subject: [PATCH] feat: added more form stories & tests update --- .../Form/DynamicForm/DynamicForm.stories.tsx | 10 + .../ConditionalRenderingShowcase.tsx | 28 +++ .../ConditionalRenderingShowcase/index.ts | 1 + .../ConditionalRenderingShowcase/schema.ts | 80 ++++++ .../ValidationShowcase/ValidationShowcase.tsx | 28 +++ .../_stories/ValidationShowcase/index.ts | 0 .../_stories/ValidationShowcase/schema.ts | 136 ++++++++++ .../hooks/useFileUpload/useFileUpload.ts | 7 + .../useFileUpload/useFileUpload.unit.test.ts | 237 ++++++++---------- .../check-if-required/check-if-required.ts | 12 +- .../check-if-required.unit.test.ts | 163 +++++++----- .../useRuleEngine/useRuleEngine.unit.test.ts | 2 +- 12 files changed, 503 insertions(+), 201 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx index 27269ee1bc..62f763ac93 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/DynamicForm.stories.tsx @@ -1,6 +1,8 @@ import { DynamicFormV2 } from './DynamicForm'; +import { ConditionalRenderingShowcaseComponent } from './_stories/ConditionalRenderingShowcase'; import { FileUploadShowcaseComponent } from './_stories/FileUploadShowcase'; import { InputsShowcaseComponent } from './_stories/InputsShowcase'; +import { ValidationShowcaseComponent } from './_stories/ValidationShowcase/ValidationShowcase'; export default { component: DynamicFormV2, @@ -13,3 +15,11 @@ export const InputsShowcase = { export const FileUploadShowcase = { render: () => , }; + +export const ValidationShowcase = { + render: () => , +}; + +export const ConditionalRenderingShowcase = { + render: () => , +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx new file mode 100644 index 0000000000..d884587cbe --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/ConditionalRenderingShowcase.tsx @@ -0,0 +1,28 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { schema } from './schema'; + +export const ConditionalRenderingShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts new file mode 100644 index 0000000000..811fb8fbee --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/index.ts @@ -0,0 +1 @@ +export * from './ConditionalRenderingShowcase'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts new file mode 100644 index 0000000000..44aee0761d --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ConditionalRenderingShowcase/schema.ts @@ -0,0 +1,80 @@ +import { IFormElement } from '../../types'; + +export const schema: Array> = [ + { + id: 'first-name', + element: 'textfield', + valueDestination: 'firstName', + params: { + label: 'First Name', + placeholder: 'Enter something to reveal some more!', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'First name is required', + applyWhen: { + type: 'json-logic', + value: { + '!': { var: 'forceEverythingOptionnal' }, + }, + }, + }, + ], + }, + { + id: 'reveal-more', + element: 'checkboxfield', + valueDestination: 'revealMore', + params: { + label: 'Reveal More', + }, + }, + { + id: 'force-everything-optionnal', + element: 'checkboxfield', + valueDestination: 'forceEverythingOptionnal', + params: { + label: 'Force everything to be optionnal', + }, + }, + { + id: 'last-name', + element: 'textfield', + valueDestination: 'lastName', + params: { + label: 'Last Name', + }, + hidden: [ + { + engine: 'json-logic', + value: { + and: [{ '!': { var: 'firstName' } }, { '!': { var: 'revealMore' } }], + }, + }, + ], + validate: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + applyWhen: { + type: 'json-logic', + value: { + and: [{ '!!': { var: 'firstName' } }, { '!': { var: 'forceEverythingOptionnal' } }], + }, + }, + }, + ], + }, + { + id: 'submit', + element: 'submitbutton', + valueDestination: 'submit', + params: { + label: 'Submit', + disableWhenFormIsInvalid: true, + }, + }, +]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx new file mode 100644 index 0000000000..e0cf5e3095 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/ValidationShowcase.tsx @@ -0,0 +1,28 @@ +import { AnyObject } from '@/common'; +import { useState } from 'react'; +import { JSONEditorComponent } from '../../../Validator/_stories/components/JsonEditor/JsonEditor'; +import { DynamicFormV2 } from '../../DynamicForm'; +import { schema } from './schema'; + +export const ValidationShowcaseComponent = () => { + const [context, setContext] = useState({}); + + return ( +
+
+ { + console.log('onSubmit'); + }} + onChange={setContext} + // onEvent={console.log} + /> +
+
+ +
+
+ ); +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts new file mode 100644 index 0000000000..50c12c7b9c --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/ValidationShowcase/schema.ts @@ -0,0 +1,136 @@ +import { IFormElement } from '../../types'; + +export const schema: Array> = [ + { + id: 'first-name-field', + element: 'textfield', + valueDestination: 'firstName', + params: { + label: 'First Name', + placeholder: 'Enter your first name', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'First name is required', + }, + ], + }, + { + id: 'last-name-field', + element: 'textfield', + valueDestination: 'lastName', + params: { + label: 'Last Name', + placeholder: 'Enter your last name', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Last name is required', + }, + ], + }, + { + id: 'date-of-birth-field', + element: 'datefield', + valueDestination: 'dateOfBirth', + params: { + label: 'Date of Birth', + placeholder: 'Enter your date of birth', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Date of birth is required', + }, + ], + }, + { + id: 'passport-photo', + element: 'filefield', + valueDestination: 'passportPhoto', + params: { + label: 'Passport Photo', + placeholder: 'Select your passport photo', + }, + validate: [ + { + type: 'required', + value: {}, + message: 'Passport photo is required', + applyWhen: { + type: 'json-logic', + value: { + '!': { var: 'iDontHaveDocument' }, + }, + }, + }, + ], + }, + { + id: 'idont-have-document-checkbox', + element: 'checkboxfield', + valueDestination: 'iDontHaveDocument', + params: { + label: "I don't have a document", + }, + }, + { + id: 'workplaces', + valueDestination: 'workplaces', + element: 'fieldlist', + params: { + label: 'Workplaces', + addButtonLabel: 'Add Workplace', + }, + validate: [ + { type: 'required', value: {}, message: 'Workplaces are required' }, + { + type: 'minLength', + value: { minLength: 2 }, + message: 'At least {minLength} workplaces are required', + }, + ], + children: [ + { + id: 'workplace-name', + element: 'textfield', + valueDestination: 'workplaces[$0].workplaceName', + params: { + label: 'Workplace Name', + }, + validate: [{ type: 'required', value: {}, message: 'Workplace name is required' }], + }, + { + id: 'workplace-start-date', + element: 'datefield', + valueDestination: 'workplaces[$0].workplaceStartDate', + params: { + label: 'Workplace Start Date', + }, + validate: [{ type: 'required', value: {}, message: 'Workplace start date is required' }], + }, + { + id: 'certificate-of-employment', + element: 'filefield', + valueDestination: 'workplaces[$0].certificateOfEmployment', + params: { + label: 'Certificate of Employment', + }, + validate: [], + }, + ], + }, + { + id: 'submit-button', + element: 'submitbutton', + valueDestination: 'submit', + params: { + label: 'Submit', + }, + }, +]; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts index aa008c76d6..a62703f7a8 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.ts @@ -27,6 +27,13 @@ export const useFileUpload = ( const { uploadSettings } = params; + if (!uploadSettings) { + onChange(e.target?.files?.[0] as File); + console.log('Failed to upload, no upload settings provided'); + + return; + } + const uploadParams = { ...uploadSettings, method: uploadSettings?.method || 'POST', diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts index afbe738769..d8f3b3b27d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/hooks/useFileUpload/useFileUpload.unit.test.ts @@ -1,171 +1,150 @@ -import { cleanup, renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useDynamicForm } from '../../../../context'; -import { useElement, useField } from '../../../../hooks/external'; -import { useTaskRunner } from '../../../../providers/TaskRunner/hooks/useTaskRunner'; -import { IFormElement } from '../../../../types'; -import { useStack } from '../../../FieldList/providers/StackProvider'; -import { IFileFieldParams } from '../../FileField'; +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { formatHeaders, uploadFile } from './helpers'; import { useFileUpload } from './useFileUpload'; -vi.mock('../../../../context'); -vi.mock('../../../../hooks/external'); -vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner'); -vi.mock('../../../FieldList/providers/StackProvider'); -vi.mock('./helpers'); +vi.mock('./helpers', () => ({ + uploadFile: vi.fn(), + formatHeaders: vi.fn(), +})); -describe('useFileUpload', () => { - const mockElement = { - id: 'test-file', - params: { - uploadSettings: { - url: 'test-url', - resultPath: 'test-path', - }, - }, - } as IFormElement; +vi.mock('../../../../context', () => ({ + useDynamicForm: () => ({ + metadata: { test: 'metadata' }, + }), +})); - const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' }); - const mockEvent = { - target: { - files: [mockFile], - }, - } as unknown as React.ChangeEvent; +vi.mock('../../../../hooks/external', () => ({ + useElement: () => ({ id: 'test-id' }), + useField: () => ({ onChange: vi.fn() }), +})); - beforeEach(() => { - vi.mocked(useStack).mockReturnValue({ - stack: [], - }); +vi.mock('../../../../providers/TaskRunner/hooks/useTaskRunner', () => ({ + useTaskRunner: () => ({ + addTask: vi.fn(), + removeTask: vi.fn(), + }), +})); - vi.mocked(useElement).mockReturnValue({ - id: 'test-file', - originId: 'test-file', - hidden: false, - }); +vi.mock('../../../FieldList/providers/StackProvider', () => ({ + useStack: () => ({ stack: [] }), +})); - vi.mocked(useTaskRunner).mockReturnValue({ - addTask: vi.fn(), - removeTask: vi.fn(), - tasks: [], - isRunning: false, - runTasks: vi.fn(), - }); +const mockedUploadFile = vi.mocked(uploadFile); +const mockedFormatHeaders = vi.mocked(formatHeaders); - vi.mocked(useDynamicForm).mockReturnValue({ - metadata: {}, - fieldHelpers: { - getTouched: vi.fn(), - getValue: vi.fn(), - setTouched: vi.fn(), - setValue: vi.fn(), - touchAllFields: vi.fn(), +describe('useFileUpload', () => { + const mockElement = { + id: 'test-field', + element: 'file', + valueDestination: 'file', + }; + + const createEvent = (file: File) => + ({ + target: { + files: [file], }, - values: {}, - touched: {}, - elementsMap: {}, - callbacks: {}, - submit: vi.fn(), - }); - - vi.mocked(useField).mockReturnValue({ - value: null, - touched: false, - disabled: false, - onChange: vi.fn(), - onBlur: vi.fn(), - onFocus: vi.fn(), - }); - - vi.mocked(uploadFile).mockResolvedValue('uploaded-file-result'); - vi.mocked(formatHeaders).mockReturnValue({}); - }); + } as unknown as React.ChangeEvent); - afterEach(() => { - cleanup(); + beforeEach(() => { vi.clearAllMocks(); }); - it('handles file upload on change', async () => { - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + it('should handle file change without upload settings', async () => { + const { result } = renderHook(() => useFileUpload(mockElement, {})); + const mockFile = new File(['test'], 'test.txt'); - await result.current.handleChange(mockEvent); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); - expect(vi.mocked(uploadFile)).toHaveBeenCalledWith(mockFile, expect.any(Object)); - expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith( - 'uploaded-file-result', - ); + expect(mockedUploadFile).not.toHaveBeenCalled(); }); - it('handles file upload on submit', async () => { - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'submit' })); + it('should upload file immediately when uploadOn is "change"', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; - await result.current.handleChange(mockEvent); + mockedUploadFile.mockResolvedValue('uploaded-file-url'); + mockedFormatHeaders.mockReturnValue({ 'Content-Type': 'application/json' }); - expect(vi.mocked(useField).mock.results?.[0]?.value?.onChange).toHaveBeenCalledWith(mockFile); - expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.addTask).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'test-file', - element: mockElement, - run: expect.any(Function), + const { result } = renderHook(() => + useFileUpload(mockElement, { + uploadOn: 'change', + uploadSettings, }), ); - }); - it('removes existing task before handling new file', async () => { - const { result } = renderHook(() => useFileUpload(mockElement)); + const mockFile = new File(['test'], 'test.txt'); - await result.current.handleChange(mockEvent); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); - expect(vi.mocked(useTaskRunner).mock.results?.[0]?.value?.removeTask).toHaveBeenCalledWith( - 'test-file', + expect(mockedUploadFile).toHaveBeenCalledWith( + mockFile, + expect.objectContaining({ + url: 'test-url', + method: 'POST', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }), ); }); - it('handles upload failure gracefully', async () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => void 0); - vi.mocked(uploadFile).mockRejectedValueOnce(new Error('Upload failed')); + it('should queue upload task when uploadOn is "submit"', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; - const { result } = renderHook(() => useFileUpload(mockElement, { uploadOn: 'change' })); + const { result } = renderHook(() => + useFileUpload(mockElement, { + uploadOn: 'submit', + uploadSettings, + }), + ); - await result.current.handleChange(mockEvent); + const mockFile = new File(['test'], 'test.txt'); - expect(consoleError).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); - consoleError.mockRestore(); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); + + expect(mockedUploadFile).not.toHaveBeenCalled(); }); - it('formats headers with metadata', async () => { - vi.mocked(useDynamicForm).mockReturnValue({ - metadata: { token: '123' }, - fieldHelpers: { - getTouched: vi.fn(), - getValue: vi.fn(), - setTouched: vi.fn(), - setValue: vi.fn(), - touchAllFields: vi.fn(), - }, - values: {}, - touched: {}, - elementsMap: {}, - callbacks: {}, - submit: vi.fn(), - }); + it('should handle upload errors gracefully', async () => { + const uploadSettings = { + url: 'test-url', + resultPath: 'data.url', + headers: { 'Content-Type': 'application/json' }, + }; + + mockedUploadFile.mockRejectedValue(new Error('Upload failed')); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn() as any); const { result } = renderHook(() => useFileUpload(mockElement, { - uploadSettings: { - headers: { Authorization: 'Bearer {token}' }, - url: 'test-url', - resultPath: 'test-path', - }, + uploadOn: 'change', + uploadSettings, }), ); - await result.current.handleChange(mockEvent); + const mockFile = new File(['test'], 'test.txt'); - expect(vi.mocked(formatHeaders)).toHaveBeenCalledWith( - { Authorization: 'Bearer {token}' }, - { token: '123' }, - ); + await act(async () => { + await result.current.handleChange(createEvent(mockFile)); + }); + + expect(consoleSpy).toHaveBeenCalledWith('Failed to upload file.', expect.any(Error)); + expect(result.current.isUploading).toBe(false); + + consoleSpy.mockRestore(); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts index 925ce02297..e1fbac6044 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.ts @@ -1,4 +1,3 @@ -import { IRule } from '@/components/organisms/Form/hooks'; import { executeRules } from '@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'; import { IFormElement } from '../../../../../types'; @@ -11,9 +10,14 @@ export const checkIfRequired = (element: IFormElement, context: object) => { const isRequired = requiredLikeValidators.length ? requiredLikeValidators.some(validator => { - const { applyWhen = [] } = validator; - const shouldValidate = (applyWhen as IRule[])?.length - ? executeRules(context, applyWhen as IRule[]).every(result => result.result) + const { applyWhen } = validator; + const shouldValidate = applyWhen + ? executeRules(context, [ + { + engine: applyWhen.type, + value: applyWhen.value, + }, + ]).every(result => result.result) : true; if (!shouldValidate) return false; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts index dcdf9c17a7..f5eb3725ef 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/hooks/external/useRequired/helpers/check-if-required/check-if-required.unit.test.ts @@ -1,120 +1,149 @@ import { IRuleExecutionResult } from '@/components/organisms/Form/hooks'; import { executeRules } from '@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { TBaseValidators } from '@/components/organisms/Form/Validator'; +import { describe, expect, it, vi } from 'vitest'; import { IFormElement } from '../../../../../types'; import { checkIfRequired } from './check-if-required'; -vi.mock('@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules', () => ({ - executeRules: vi.fn(), -})); +vi.mock('@/components/organisms/Form/hooks/useRuleEngine/utils/execute-rules'); -describe('checkIfRequired', () => { - const mockContext = { someContext: 'value' }; - - beforeEach(() => { - vi.clearAllMocks(); - }); +const mockedExecuteRules = vi.mocked(executeRules); - it('should return false when validate array is empty', () => { - const element = { +describe('checkIfRequired', () => { + it('should return false when there are no validators', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [], - } as unknown as IFormElement; + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(false); }); - it('should return false when no required validators exist', () => { - const element = { + it('should return false when there are no required validators', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ - { type: 'minLength', value: 5 }, - { type: 'maxLength', value: 10 }, + { + type: 'custom' as TBaseValidators, + value: {}, + message: 'Custom message', + }, ], - } as unknown as IFormElement; + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(false); }); - it('should return true when required validator exists without applyWhen rules', () => { - const element = { - validate: [{ type: 'required', value: true }], - } as unknown as IFormElement; - - const result = checkIfRequired(element, mockContext); - - expect(result).toBe(true); - }); - - it('should return true when validator with considerRequred exists without applyWhen rules', () => { - const element = { - validate: [{ type: 'someType', considerRequred: true }], - } as unknown as IFormElement; + it('should return true when there is a required validator with no conditions', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', + validate: [ + { + type: 'required', + value: {}, + message: 'Field is required', + }, + ], + }; - const result = checkIfRequired(element, mockContext); + const result = checkIfRequired(element, {}); expect(result).toBe(true); }); - it('should use executeRules when applyWhen rules exist', () => { - const element = { + it('should return true when there is a considerRequired validator with no conditions', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { - type: 'required', - applyWhen: [{ someRule: true }], + type: 'custom', + considerRequred: true, + value: {}, + message: 'Field is required', }, - ], - } as unknown as IFormElement; + ] as unknown as IFormElement['validate'], + }; - vi.mocked(executeRules).mockReturnValue([{ rule: {}, result: true }] as IRuleExecutionResult[]); + const result = checkIfRequired(element, {}); - const result = checkIfRequired(element, mockContext); - - expect(executeRules).toHaveBeenCalledWith(mockContext, [{ someRule: true }]); expect(result).toBe(true); }); - it('should return false when executeRules returns false', () => { - const element = { + it('should evaluate applyWhen conditions when present', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { type: 'required', - applyWhen: [{ someRule: true }], + value: {}, + message: 'Field is required', + applyWhen: { + type: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, }, ], - } as unknown as IFormElement; + }; - vi.mocked(executeRules).mockReturnValue([ - { rule: {}, result: false }, - ] as IRuleExecutionResult[]); + const context = { someField: true }; - const result = checkIfRequired(element, mockContext); + mockedExecuteRules.mockReturnValue([{ result: true }] as IRuleExecutionResult[]); - expect(result).toBe(false); + const result = checkIfRequired(element, context); + + expect(result).toBe(true); + expect(mockedExecuteRules).toHaveBeenCalledWith(context, [ + { + engine: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, + ]); }); - it('should return true if any required validator is applicable', () => { - const element = { + it('should return false when applyWhen condition evaluates to false', () => { + const element: IFormElement = { + id: 'test', + element: 'test', + valueDestination: 'test', validate: [ { type: 'required', - applyWhen: [{ rule1: true }], - }, - { - type: 'required', - applyWhen: [{ rule2: true }], + value: {}, + message: 'Field is required', + applyWhen: { + type: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, }, ], - } as unknown as IFormElement; + }; - vi.mocked(executeRules) - .mockReturnValueOnce([{ rule: {}, result: false }] as IRuleExecutionResult[]) - .mockReturnValueOnce([{ rule: {}, result: true }] as IRuleExecutionResult[]); + const context = { someField: false }; - const result = checkIfRequired(element, mockContext); + mockedExecuteRules.mockReturnValue([{ result: false, rule: {} }] as IRuleExecutionResult[]); - expect(result).toBe(true); + const result = checkIfRequired(element, context); + + expect(result).toBe(false); + expect(mockedExecuteRules).toHaveBeenCalledWith(context, [ + { + engine: 'json-logic', + value: { '==': [{ var: 'someField' }, true] }, + }, + ]); }); }); diff --git a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts index 51e7f976b7..1e4c5bd961 100644 --- a/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/hooks/useRuleEngine/useRuleEngine.unit.test.ts @@ -105,7 +105,7 @@ describe('useRuleEngine', () => { expect(result.current).toEqual([]); // Wait for custom delayed execution - await vi.advanceTimersByTimeAsync(customDelay); + await vi.advanceTimersByTimeAsync(1050); // Assert after delay expect(result.current).toEqual(expectedResults);