From 82d8388bb372649e38395f40cdd8d4ce34f699e3 Mon Sep 17 00:00:00 2001 From: Illia Rudniev Date: Fri, 27 Dec 2024 16:11:36 +0200 Subject: [PATCH] feat: added field descriptions & updated tests (#2914) --- .../InputsShowcase/InputsShowcase.tsx | 13 ++ .../AutocompleteField/AutocompleteField.tsx | 2 + .../fields/CheckboxField/CheckboxField.tsx | 39 ++-- .../CheckboxField/CheckboxField.unit.test.tsx | 199 ++++++------------ .../fields/CheckboxList/CheckboxList.tsx | 2 + .../CheckboxList/CheckboxList.unit.test.tsx | 36 +++- .../fields/DateField/DateField.tsx | 2 + .../fields/DateField/DateField.unit.test.tsx | 12 ++ .../fields/FieldList/FieldList.tsx | 2 + .../fields/FieldList/FieldList.unit.test.tsx | 12 ++ .../fields/FileField/FileField.tsx | 2 + .../fields/FileField/FileField.unit.test.tsx | 10 + .../MultiselectField/MultiselectField.tsx | 2 + .../MultiselectField.unit.test.tsx | 13 ++ .../fields/PhoneField/PhoneField.tsx | 2 + .../PhoneField/PhoneField.unit.test.tsx | 16 ++ .../fields/RadioField/RadioField.tsx | 2 + .../RadioField/RadioField.unit.test.tsx | 32 +++ .../fields/SelectField/SelectField.tsx | 2 + .../SelectField/SelectField.unit.test.tsx | 16 ++ .../fields/TagsField/TagsField.tsx | 2 + .../fields/TagsField/TagsField.unit.test.tsx | 16 ++ .../fields/TextField/TextField.tsx | 2 + .../fields/TextField/TextField.unit.test.tsx | 16 ++ .../FieldDescription/FieldDescription.tsx | 14 ++ .../FieldDescription.unit.test.tsx | 43 ++++ .../layouts/FieldDescription/index.ts | 1 + .../layouts/FieldLayout/FieldLayout.tsx | 9 +- .../FieldLayout/FieldLayout.unit.test.tsx | 186 +++++----------- .../organisms/Form/DynamicForm/types/index.ts | 1 + .../useValidate/useValidate.unit.test.ts | 2 +- 31 files changed, 406 insertions(+), 302 deletions(-) create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx create mode 100644 packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx index dd43c34b59..88c034e861 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/_stories/InputsShowcase/InputsShowcase.tsx @@ -12,6 +12,7 @@ const schema: Array> = [ params: { label: 'Text Field', placeholder: 'Enter text', + description: 'This is a text field for entering any text value', }, validate: [], }, @@ -22,6 +23,7 @@ const schema: Array> = [ params: { label: 'Autocomplete Field', placeholder: 'Select an option', + description: 'This is an autocomplete field that provides suggestions as you type', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -35,6 +37,7 @@ const schema: Array> = [ valueDestination: 'checkboxlist', params: { label: 'Checkbox List Field', + description: 'Select multiple options from this list of checkboxes', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -48,6 +51,7 @@ const schema: Array> = [ valueDestination: 'date', params: { label: 'Date Field', + description: 'Select a date from the calendar', }, }, { @@ -56,6 +60,7 @@ const schema: Array> = [ valueDestination: 'multiselect', params: { label: 'Multiselect Field', + description: 'Select multiple options from the dropdown list', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -69,6 +74,7 @@ const schema: Array> = [ valueDestination: 'select', params: { label: 'Select Field', + description: 'Choose a single option from the dropdown list', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -82,6 +88,7 @@ const schema: Array> = [ valueDestination: 'checkbox', params: { label: 'Checkbox Field', + description: 'Toggle this checkbox for a yes/no selection', }, }, { @@ -90,6 +97,7 @@ const schema: Array> = [ valueDestination: 'phone', params: { label: 'Phone Field', + description: 'Enter a phone number with country code selection', defaultCountry: 'il', }, }, @@ -99,6 +107,7 @@ const schema: Array> = [ valueDestination: 'radio', params: { label: 'Radio Field', + description: 'Select one option from these radio buttons', options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, @@ -112,6 +121,7 @@ const schema: Array> = [ valueDestination: 'tags', params: { label: 'Tags Field', + description: 'Add multiple tags by typing and pressing enter', }, }, { @@ -121,6 +131,7 @@ const schema: Array> = [ params: { label: 'File Field', placeholder: 'Select File', + description: 'Upload a file from your device', }, }, { @@ -129,6 +140,7 @@ const schema: Array> = [ valueDestination: 'fieldlist', params: { label: 'Field List', + description: 'A list of repeatable form fields that can be added or removed', }, children: [ { @@ -138,6 +150,7 @@ const schema: Array> = [ params: { label: 'Text Field', placeholder: 'Enter text', + description: 'Enter text for this list item', }, validate: [ { diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx index 4ff28ea7ac..25cc14708a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/AutocompleteField/AutocompleteField.tsx @@ -9,6 +9,7 @@ import { useStack } from '../FieldList/providers/StackProvider'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; export interface IAutocompleteFieldOption { label: string; @@ -46,6 +47,7 @@ export const AutocompleteField: TDynamicFormField = ({ onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx index 468ddc2488..fef6a3c28c 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.tsx @@ -1,34 +1,45 @@ -import { Checkbox } from '@/components/atoms'; +import { Checkbox, Label } from '@/components/atoms'; +import { useDynamicForm } from '../../context'; import { useElement, useField } from '../../hooks/external'; +import { useRequired } from '../../hooks/external/useRequired'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; -import { FieldLayout } from '../../layouts/FieldLayout'; -import { TDynamicFormField } from '../../types'; +import { ICommonFieldParams, TDynamicFormField } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; -export const CheckboxField: TDynamicFormField = ({ element }) => { +export const CheckboxField: TDynamicFormField = ({ element }) => { useMountEvent(element); useUnmountEvent(element); + const { label } = element.params || {}; const { stack } = useStack(); const { id } = useElement(element, stack); const { value, onChange, onFocus, onBlur, disabled } = useField( element, stack, ); + const { values } = useDynamicForm(); + const isRequired = useRequired(element, values); return ( - - +
+
+ + +
+ - +
); }; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx index 3f2afafc92..1681e64dc6 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxField/CheckboxField.unit.test.tsx @@ -1,189 +1,120 @@ -import { cleanup, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../context'; import { useElement, useField } from '../../hooks/external'; +import { useRequired } from '../../hooks/external/useRequired'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; -import { FieldErrors } from '../../layouts/FieldErrors'; -import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxField } from './CheckboxField'; -// Mock dependencies -vi.mock('@/components/atoms', () => ({ - Checkbox: vi.fn((props: any) => ( - props.onCheckedChange(e.target.checked)} - disabled={props.disabled} - onFocus={props.onFocus} - onBlur={props.onBlur} - data-testid="test-checkbox" - id={props.id} - /> - )), -})); - -vi.mock('../FieldList/providers/StackProvider', () => ({ - useStack: vi.fn(), -})); - -vi.mock('../../hooks/external/useField', () => ({ - useField: vi.fn(), -})); - -vi.mock('../../hooks/external/useElement', () => ({ - useElement: vi.fn(), -})); - -vi.mock('../../layouts/FieldLayout', () => ({ - FieldLayout: vi.fn(({ children }) =>
{children}
), -})); - -vi.mock('../../layouts/FieldErrors', () => ({ - FieldErrors: vi.fn(() =>
), -})); - -vi.mock('../../hooks/internal/useMountEvent', () => ({ - useMountEvent: vi.fn(), -})); - -vi.mock('../../hooks/internal/useUnmountEvent', () => ({ - useUnmountEvent: vi.fn(), -})); +vi.mock('../../context'); +vi.mock('../FieldList/providers/StackProvider'); +vi.mock('../../hooks/external'); +vi.mock('../../hooks/external/useRequired'); +vi.mock('../../hooks/internal/useMountEvent'); +vi.mock('../../hooks/internal/useUnmountEvent'); describe('CheckboxField', () => { - const mockStack = [0]; const mockElement = { - id: 'test-checkbox', - type: '', - } as unknown as IFormElement; - - const mockFieldProps = { - value: false, - onChange: vi.fn(), - onFocus: vi.fn(), - onBlur: vi.fn(), - disabled: false, - } as unknown as ReturnType; + id: 'test', + type: 'checkbox', + params: { + label: 'Test Label', + }, + } as unknown as IFormElement; + + const mockOnChange = vi.fn(); + const mockOnFocus = vi.fn(); + const mockOnBlur = vi.fn(); beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - vi.mocked(useStack).mockReturnValue({ stack: mockStack }); - vi.mocked(useField).mockReturnValue(mockFieldProps); - vi.mocked(useElement).mockReturnValue({ id: 'test-checkbox-id' } as unknown as ReturnType< - typeof useElement - >); + vi.mocked(useDynamicForm).mockReturnValue({ + values: {}, + } as unknown as IDynamicFormContext); + vi.mocked(useStack).mockReturnValue({ stack: [] }); + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: false }); + vi.mocked(useField).mockReturnValue({ + value: false, + onChange: mockOnChange, + onFocus: mockOnFocus, + onBlur: mockOnBlur, + disabled: false, + touched: false, + }); + vi.mocked(useRequired).mockReturnValue(false); + vi.mocked(useMountEvent).mockReturnValue(); + vi.mocked(useUnmountEvent).mockReturnValue(); }); - it('renders checkbox with correct initial state', () => { - render(); - const checkbox = screen.getByTestId('test-checkbox'); - expect(checkbox).toBeInTheDocument(); - expect(checkbox).not.toBeChecked(); - expect(checkbox).toHaveAttribute('id', 'test-checkbox-id'); + afterEach(() => { + vi.clearAllMocks(); }); - it('renders field layout and errors', () => { + it('renders checkbox with label', () => { render(); - expect(screen.getByTestId('field-layout')).toBeInTheDocument(); - expect(screen.getByTestId('field-errors')).toBeInTheDocument(); - expect(vi.mocked(FieldLayout)).toHaveBeenCalledWith( - expect.objectContaining({ - element: mockElement, - layout: 'horizontal', - }), - expect.any(Object), - ); + + expect(screen.getByRole('checkbox')).toBeInTheDocument(); + expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); }); - it('renders checked checkbox when value is true', () => { - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - value: true, - }); + it('renders required label when isRequired is true', () => { + vi.mocked(useRequired).mockReturnValue(true); render(); - expect(screen.getByTestId('test-checkbox')).toBeChecked(); - }); - it('handles onChange events', async () => { - const mockOnChange = vi.fn(); - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - onChange: mockOnChange, - }); + expect(screen.getByText('Test Label')).toBeInTheDocument(); + }); + it('handles checkbox state changes', async () => { render(); - const checkbox = screen.getByTestId('test-checkbox'); + const checkbox = screen.getByRole('checkbox'); await userEvent.click(checkbox); - expect(mockOnChange).toHaveBeenCalledWith(true); + expect(mockOnChange).toHaveBeenCalled(); }); it('handles focus events', async () => { - const user = userEvent.setup(); render(); - const checkbox = screen.getByTestId('test-checkbox'); - await user.click(checkbox); + screen.getByRole('checkbox'); + await userEvent.tab(); - expect(mockFieldProps.onFocus).toHaveBeenCalled(); + expect(mockOnFocus).toHaveBeenCalled(); }); it('handles blur events', async () => { - const user = userEvent.setup(); render(); - const checkbox = screen.getByTestId('test-checkbox'); - await user.click(checkbox); - await user.tab(); + const checkbox = screen.getByRole('checkbox'); + checkbox.focus(); + checkbox.blur(); - expect(mockFieldProps.onBlur).toHaveBeenCalled(); + expect(mockOnBlur).toHaveBeenCalled(); }); it('disables checkbox when disabled prop is true', () => { vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, + value: false, + onChange: mockOnChange, + onFocus: mockOnFocus, + onBlur: mockOnBlur, disabled: true, + touched: false, }); render(); - expect(screen.getByTestId('test-checkbox')).toBeDisabled(); - }); - - it('handles undefined value as unchecked', () => { - vi.mocked(useField).mockReturnValue({ - ...mockFieldProps, - value: undefined, - }); - render(); - expect(screen.getByTestId('test-checkbox')).not.toBeChecked(); + expect(screen.getByRole('checkbox')).toBeDisabled(); }); - it('should call useMountEvent with element', () => { - const mockUseMountEvent = vi.mocked(useMountEvent); + it('calls mount and unmount events', () => { render(); - expect(mockUseMountEvent).toHaveBeenCalledWith(mockElement); - }); - it('should call useUnmountEvent with element', () => { - const mockUseUnmountEvent = vi.mocked(useUnmountEvent); - const { unmount } = render(); - unmount(); - expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); - }); - - it('should render FieldErrors with element prop', () => { - render(); - expect(FieldErrors).toHaveBeenCalledWith( - expect.objectContaining({ element: mockElement }), - expect.anything(), - ); + expect(useMountEvent).toHaveBeenCalledWith(mockElement); + expect(useUnmountEvent).toHaveBeenCalledWith(mockElement); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx index ef1ac01b72..c931b71d62 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.tsx @@ -4,6 +4,7 @@ import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -58,6 +59,7 @@ export const CheckboxListField: TDynamicFormField = ({ ))} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx index a511257ffa..551dd72fbb 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/CheckboxList/CheckboxList.unit.test.tsx @@ -1,20 +1,15 @@ -import { ctw } from '@/common'; import { createTestId } from '@/components/organisms/Renderer'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { CheckboxListField, ICheckboxListFieldParams } from './CheckboxList'; -// Mock dependencies -vi.mock('@/common', () => ({ - ctw: vi.fn(), -})); - vi.mock('@/components/organisms/Renderer', () => ({ createTestId: vi.fn(), })); @@ -31,6 +26,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('@/components/atoms', () => ({ Checkbox: vi.fn((props: any) => ( ({ onFocus={props.onFocus} onBlur={props.onBlur} className={props.className} + disabled={props.disabled} /> )), })); @@ -78,7 +78,6 @@ describe('CheckboxListField', () => { vi.clearAllMocks(); vi.mocked(createTestId).mockReturnValue('test-checkbox-list'); - vi.mocked(ctw).mockImplementation((...args: any[]) => args.filter(Boolean).join(' ')); vi.mocked(useStack).mockReturnValue({ stack: [] }); vi.mocked(useField).mockReturnValue({ @@ -192,6 +191,21 @@ describe('CheckboxListField', () => { expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); }); + it('disables all checkboxes when disabled is true', () => { + vi.mocked(useField).mockReturnValue({ + value: ['opt1'], + onChange: vi.fn(), + onFocus: vi.fn(), + onBlur: vi.fn(), + disabled: true, + } as unknown as ReturnType); + + render(); + + const container = screen.getByTestId('test-checkbox-list'); + expect(container).toHaveClass('pointer-events-none opacity-50'); + }); + it('should call useMountEvent with element', () => { const mockUseMountEvent = vi.mocked(useMountEvent); render(); @@ -204,6 +218,14 @@ describe('CheckboxListField', () => { expect(mockUseUnmountEvent).toHaveBeenCalledWith(mockElement); }); + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); + it('should render FieldErrors with element prop', () => { render(); expect(FieldErrors).toHaveBeenCalledWith( diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx index bbb97e6c1f..150a92a0b5 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.tsx @@ -9,6 +9,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external/useField'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -64,6 +65,7 @@ export const DateField: TDynamicFormField = ({ element }) => { onChange={handleChange} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx index 2a350aa578..5a62ac6358 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/DateField/DateField.unit.test.tsx @@ -6,6 +6,7 @@ import { useField } from '../../hooks/external/useField'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { DateField, IDateFieldParams } from './DateField'; @@ -53,6 +54,9 @@ vi.mock('../../hooks/internal/useUnmountEvent'); vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); describe('DateField', () => { beforeEach(() => { @@ -213,4 +217,12 @@ describe('DateField', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx index d66ade5683..d469b417f2 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.tsx @@ -5,6 +5,7 @@ import { useDynamicForm } from '../../context'; import { useElement } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { TDynamicFormField } from '../../types'; import { useFieldList } from './hooks/useFieldList'; @@ -59,6 +60,7 @@ export const FieldList: TDynamicFormField = props => {
+ ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx index 11e5af5ad3..682877e506 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FieldList/FieldList.unit.test.tsx @@ -5,6 +5,7 @@ import { useElement } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { FieldList } from './FieldList'; @@ -20,6 +21,9 @@ vi.mock('../../hooks/internal/useUnmountEvent'); vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); vi.mock('@/components/atoms', () => ({ Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( @@ -168,4 +172,12 @@ describe('FieldList', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx index 047d2d6306..4861f1ed7a 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.tsx @@ -7,6 +7,7 @@ import { useCallback, useMemo, useRef } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -104,6 +105,7 @@ export const FileField: TDynamicFormField = ({ element }) => { className="hidden" /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx index 9667b4b8c3..44eea4b749 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/FileField/FileField.unit.test.tsx @@ -28,6 +28,9 @@ vi.mock('../../layouts/FieldLayout', () => ({ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(({ element }) =>
{element.id}
), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(({ element }) =>
{element.id}
), +})); describe('FileField', () => { const mockElement = { @@ -157,4 +160,11 @@ describe('FileField', () => { expect(useMountEvent).toHaveBeenCalledWith(mockElement); expect(useUnmountEvent).toHaveBeenCalledWith(mockElement); }); + + it('renders field description with element prop', () => { + render(); + const description = screen.getByTestId('field-description'); + expect(description).toBeInTheDocument(); + expect(description).toHaveTextContent(mockElement.id); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx index e89ea1971a..82abb57625 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -46,6 +47,7 @@ export const MultiselectField: TDynamicFormField = ({ e options={element.params?.options || []} renderSelected={renderSelected} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx index 9bb0c448d7..0e33020745 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/MultiselectField/MultiselectField.unit.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; @@ -52,6 +53,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: vi.fn(({ children }) =>
{children}
), })); @@ -229,4 +234,12 @@ describe('MultiselectField', () => { expect.anything(), ); }); + + it('should render FieldDescription with element prop', () => { + render(); + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ element: mockElement }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx index b7a48f0215..cbd61a5883 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormElement } from '../../types'; @@ -38,6 +39,7 @@ export const PhoneField: TDynamicFormElement = ({ ele onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx index 6e0eb28f9d..bdbcab4b46 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/PhoneField/PhoneField.unit.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { IFormElement } from '../../types'; @@ -32,6 +33,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + vi.mock('../../layouts/FieldLayout', () => ({ FieldLayout: vi.fn(({ children }) =>
{children}
), })); @@ -122,6 +127,17 @@ describe('PhoneField', () => { ); }); + it('should render FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); + it('should pass stack to createTestId', () => { const mockStack = [0, 1]; vi.mocked(useStack).mockReturnValue({ stack: mockStack }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx index 63f179666b..ac4220db03 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.tsx @@ -2,6 +2,7 @@ import { Label, RadioGroup } from '@/components/atoms'; import { RadioGroupItem } from '@/components/atoms/RadioGroup/RadioGroup.Item'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -46,6 +47,7 @@ export const RadioField: TDynamicFormField = ({ element }) => ))} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx index 7f8f23ac79..a2589eb856 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/RadioField/RadioField.unit.test.tsx @@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useElement, useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; +import { FieldErrors } from '../../layouts/FieldErrors'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { IRadioFieldParams, RadioField } from './RadioField'; @@ -20,6 +22,14 @@ vi.mock('@/components/organisms/Renderer', () => ({ createTestId: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + +vi.mock('../../layouts/FieldErrors', () => ({ + FieldErrors: vi.fn(), +})); + describe('RadioField', () => { const mockElement = { id: 'test-radio', @@ -114,4 +124,26 @@ describe('RadioField', () => { expect(screen.getByTestId('test-radio-radio-group')).toBeInTheDocument(); expect(screen.getAllByTestId('test-radio-radio-group-item')).toHaveLength(2); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); + + it('renders FieldErrors with element prop', () => { + render(); + + expect(FieldErrors).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.anything(), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx index 8521a28548..99deaf15f7 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.tsx @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -54,6 +55,7 @@ export const SelectField: TDynamicFormField = ({ element }) onBlur={onBlur} onFocus={onFocus} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx index bc36f4beae..a154988708 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/SelectField/SelectField.unit.test.tsx @@ -7,6 +7,7 @@ import { useElement, useField } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { TBaseFields } from '../../repositories/fields-repository'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; @@ -68,6 +69,10 @@ vi.mock('../../layouts/FieldErrors', () => ({ FieldErrors: () => null, })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('SelectField', () => { const mockElement = { id: 'test-id', @@ -296,4 +301,15 @@ describe('SelectField', () => { unmount(); }); + + it('should render FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx index 7501006fe4..ace36afc75 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.tsx @@ -1,6 +1,7 @@ import { TagsInput } from '@/components/molecules'; import { createTestId } from '@/components/organisms/Renderer'; import { useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { ICommonFieldParams, TDynamicFormField } from '../../types'; @@ -25,6 +26,7 @@ export const TagsField: TDynamicFormField = ({ element }) => { onFocus={onFocus} disabled={disabled} /> + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx index 55c660a213..a3689d0c0d 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TagsField/TagsField.unit.test.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TDeepthLevelStack } from '../../../Validator'; import { useElement, useField } from '../../hooks/external'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { TagsField } from './TagsField'; @@ -30,6 +31,10 @@ vi.mock('../FieldList/providers/StackProvider', () => ({ useStack: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('TagsField', () => { const mockElement = { id: 'test-tags', @@ -111,4 +116,15 @@ describe('TagsField', () => { expect.anything(), ); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx index 815e25cc41..145e7ecac3 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.tsx @@ -5,6 +5,7 @@ import { useCallback } from 'react'; import { useElement, useField } from '../../hooks/external'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { FieldErrors } from '../../layouts/FieldErrors'; import { FieldLayout } from '../../layouts/FieldLayout'; import { TDynamicFormField } from '../../types'; @@ -64,6 +65,7 @@ export const TextField: TDynamicFormField = ({ element }) => { value={value?.toString() || ''} // Ensure value is string or number /> )} + ); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx index d15d796b27..5b08f88725 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/fields/TextField/TextField.unit.test.tsx @@ -6,6 +6,7 @@ import { useElement, useField } from '../../hooks/external'; import { useEvents } from '../../hooks/internal/useEvents'; import { useMountEvent } from '../../hooks/internal/useMountEvent'; import { useUnmountEvent } from '../../hooks/internal/useUnmountEvent'; +import { FieldDescription } from '../../layouts/FieldDescription'; import { IFormElement } from '../../types'; import { useStack } from '../FieldList/providers/StackProvider'; import { ITextFieldParams, TextField } from './TextField'; @@ -65,6 +66,10 @@ vi.mock('../../hooks/internal/useUnmountEvent', () => ({ useUnmountEvent: vi.fn(), })); +vi.mock('../../layouts/FieldDescription', () => ({ + FieldDescription: vi.fn(), +})); + describe('TextField', () => { const mockStack = [0]; const mockElement = { @@ -256,4 +261,15 @@ describe('TextField', () => { unmount(); }); + + it('renders FieldDescription with element prop', () => { + render(); + + expect(FieldDescription).toHaveBeenCalledWith( + expect.objectContaining({ + element: mockElement, + }), + expect.any(Object), + ); + }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx new file mode 100644 index 0000000000..f72b0a6708 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.tsx @@ -0,0 +1,14 @@ +import { FunctionComponent } from 'react'; +import { IFormElement } from '../../types'; + +interface IFieldDescriptionProps { + element: IFormElement; +} + +export const FieldDescription: FunctionComponent = ({ element }) => { + const { description } = element.params || {}; + + if (!description) return null; + + return

{description}

; +}; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx new file mode 100644 index 0000000000..fd175297eb --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/FieldDescription.unit.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { IFormElement } from '../../types'; +import { FieldDescription } from './FieldDescription'; + +describe('FieldDescription', () => { + const mockElement = { + id: 'test-field', + params: { + description: 'Test description', + }, + } as unknown as IFormElement; + + it('should render description text when description is provided', () => { + render(); + expect(screen.getByText('Test description')).toBeInTheDocument(); + }); + + it('should apply correct styling classes', () => { + render(); + const description = screen.getByText('Test description'); + expect(description).toHaveClass('text-sm', 'text-gray-400'); + }); + + it('should not render anything when description is not provided', () => { + const elementWithoutDescription = { + id: 'test-field', + params: {}, + } as unknown as IFormElement; + + render(); + expect(screen.queryByText(/Test description/)).not.toBeInTheDocument(); + }); + + it('should not render anything when params is undefined', () => { + const elementWithoutParams = { + id: 'test-field', + } as unknown as IFormElement; + + render(); + expect(screen.queryByRole('paragraph')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts new file mode 100644 index 0000000000..1bd72d5c29 --- /dev/null +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldDescription/index.ts @@ -0,0 +1 @@ +export * from './FieldDescription'; diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx index 5b8d48ec1f..0cbc9c1117 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.tsx @@ -32,7 +32,6 @@ export const FieldLayout: FunctionComponent = ({ className={ctw('flex py-2', { 'gap-2': Boolean(label), 'flex-col': layout === 'vertical', - 'flex-row flex-row-reverse items-center justify-end': layout === 'horizontal', })} >
@@ -42,13 +41,7 @@ export const FieldLayout: FunctionComponent = ({ )}
-
- {children} -
+
{children}
); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx index 7767594936..61bf8a9833 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx +++ b/packages/ui/src/components/organisms/Form/DynamicForm/layouts/FieldLayout/FieldLayout.unit.test.tsx @@ -1,199 +1,109 @@ -import { cleanup, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { IDynamicFormContext, useDynamicForm } from '../../context'; +import { useStack } from '../../fields/FieldList/providers/StackProvider'; import { useElement } from '../../hooks/external'; import { useRequired } from '../../hooks/external/useRequired'; import { IFormElement } from '../../types'; import { FieldLayout } from './FieldLayout'; -// Mock dependencies -vi.mock('@/common', () => ({ - ctw: vi.fn((base, conditionals) => { - if (conditionals) { - return Object.entries(conditionals) - .filter(([_, value]) => value) - .map(([key]) => key) - .concat(base) - .join(' '); - } - - return base; - }), -})); - -vi.mock('@/components/atoms', () => ({ - Label: ({ children, ...props }: { children: React.ReactNode; [key: string]: any }) => ( - - ), -})); - -vi.mock('../../context', () => ({ - useDynamicForm: vi.fn(() => ({ values: {} })), -})); - -vi.mock('../../fields/FieldList/providers/StackProvider', () => ({ - useStack: vi.fn(() => ({ stack: [] })), -})); - -vi.mock('../../hooks/external', () => ({ - useElement: vi.fn(element => ({ id: element.id, hidden: false })), -})); - -vi.mock('../../hooks/external/useRequired', () => ({ - useRequired: vi.fn(), -})); +vi.mock('../../context'); +vi.mock('../../fields/FieldList/providers/StackProvider'); +vi.mock('../../hooks/external'); +vi.mock('../../hooks/external/useRequired'); describe('FieldLayout', () => { - beforeEach(() => { - cleanup(); - vi.clearAllMocks(); - vi.restoreAllMocks(); - }); - const mockElement = { - id: 'test-field', + id: 'test', + type: 'text', params: { label: 'Test Label', }, - } as unknown as IFormElement; + } as unknown as IFormElement; - it('should render children', () => { + beforeEach(() => { + vi.mocked(useDynamicForm).mockReturnValue({ + values: {}, + } as unknown as IDynamicFormContext); + vi.mocked(useStack).mockReturnValue({ stack: [] }); + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: false }); vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - expect(screen.getByTestId('child')).toBeInTheDocument(); }); - it('should render with correct data-testid', () => { - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - expect(screen.getByTestId('test-field-field-layout')).toBeInTheDocument(); + afterEach(() => { + vi.clearAllMocks(); }); - it('should not render label when label prop is not provided', () => { - vi.mocked(useRequired).mockReturnValue(false); - - const elementWithoutLabel = { - id: 'test-field', - params: {}, - } as unknown as IFormElement; - + it('renders children and label when provided', () => { render( - -
Child Content
+ +
Test Child
, ); - expect(screen.queryByText(/Test Label/)).not.toBeInTheDocument(); + expect(screen.getByTestId('test-id-field-layout')).toBeInTheDocument(); + expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); + expect(screen.getByText('Test Child')).toBeInTheDocument(); }); - it('should render required label when field is required', () => { + it('renders required label when isRequired is true', () => { vi.mocked(useRequired).mockReturnValue(true); render( -
Child Content
+
Test Child
, ); expect(screen.getByText('Test Label')).toBeInTheDocument(); }); - it('should render optional label when field is not required', () => { - vi.mocked(useRequired).mockReturnValue(false); + it('does not render when hidden is true', () => { + vi.mocked(useElement).mockReturnValue({ id: 'test-id', originId: 'test-id', hidden: true }); render( -
Child Content
+
Test Child
, ); - expect(screen.getByText('Test Label (optional)')).toBeInTheDocument(); + expect(screen.queryByTestId('test-id-field-layout')).not.toBeInTheDocument(); }); - it('should render label with correct htmlFor attribute', () => { - vi.mocked(useRequired).mockReturnValue(false); + it('renders without label when not provided', () => { + const elementWithoutLabel = { + ...mockElement, + params: {}, + }; render( - -
Child Content
+ +
Test Child
, ); - const label = screen.getByText('Test Label (optional)'); - expect(label).toHaveAttribute('for', 'test-field'); + expect(screen.queryByRole('label')).not.toBeInTheDocument(); }); - it('should render label with correct id attribute', () => { - vi.mocked(useRequired).mockReturnValue(false); - + it('applies horizontal layout classes when specified', () => { render( - -
Child Content
+ +
Test Child
, ); - const label = screen.getByText('Test Label (optional)'); - expect(label).toHaveAttribute('id', 'test-field-label'); + const container = screen.getByTestId('test-id-field-layout').children[0]; + expect(container?.className).not.toContain('flex-col'); }); - it('should not render anything when hidden is true', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: true } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - + it('applies vertical layout classes by default', () => { render( -
Child Content
-
, - ); - - expect(screen.queryByTestId('test-field-field-layout')).not.toBeInTheDocument(); - }); - - it('should apply correct classes for vertical layout', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: false } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
-
, - ); - - const container = screen.getByTestId('test-field-field-layout').children[0] as HTMLElement; - expect(container.className).toContain('flex-col'); - }); - - it('should apply correct classes for horizontal layout', () => { - vi.mocked(useElement).mockReturnValue({ id: 'test-field', hidden: false } as ReturnType< - typeof useElement - >); - vi.mocked(useRequired).mockReturnValue(false); - - render( - -
Child Content
+
Test Child
, ); - const container = screen.getByTestId('test-field-field-layout').children[0] as HTMLElement; - expect(container.className).toContain('flex-row'); - expect(container.className).toContain('items-center'); - expect(container.className).toContain('flex-row-reverse'); - expect(container.className).toContain('justify-end'); + const container = screen.getByTestId('test-id-field-layout').children[0]; + expect(container?.className).toContain('flex-col'); }); }); diff --git a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts index 7ad64143f1..9911d4401b 100644 --- a/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts +++ b/packages/ui/src/components/organisms/Form/DynamicForm/types/index.ts @@ -6,6 +6,7 @@ import { IFormEventElement, TElementEvent } from '../hooks/internal/useEvents/ty export interface ICommonFieldParams { label?: string; placeholder?: string; + description?: string; } export interface IFormElement { diff --git a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts index 59186a71eb..f27d026236 100644 --- a/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts +++ b/packages/ui/src/components/organisms/Form/Validator/hooks/internal/useValidate/useValidate.unit.test.ts @@ -423,7 +423,7 @@ describe('useValidate', () => { rerender(); - await vi.advanceTimersByTimeAsync(550); + await vi.advanceTimersByTimeAsync(600); expect(result.current.errors).toEqual([]); });