diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/FieldDisplayContextProvider.tsx b/front/src/modules/ui/field/meta-types/__stories__/FieldContextProvider.tsx similarity index 82% rename from front/src/modules/ui/field/meta-types/display/components/__stories__/FieldDisplayContextProvider.tsx rename to front/src/modules/ui/field/meta-types/__stories__/FieldContextProvider.tsx index 4e06f2bf2..c10a4f672 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/FieldDisplayContextProvider.tsx +++ b/front/src/modules/ui/field/meta-types/__stories__/FieldContextProvider.tsx @@ -3,17 +3,17 @@ import { GenericFieldContextType, } from '@/ui/field/contexts/FieldContext'; -type FieldDisplayContextProviderProps = { +type FieldContextProviderProps = { children: React.ReactNode; fieldDefinition: GenericFieldContextType['fieldDefinition']; entityId?: string; }; -export const FieldDisplayContextProvider = ({ +export const FieldContextProvider = ({ children, fieldDefinition, entityId, -}: FieldDisplayContextProviderProps) => { +}: FieldContextProviderProps) => { return ( { @@ -30,7 +29,7 @@ const DateFieldDisplayWithContext = ({ entityId, }: DateFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/DateFieldDisplay', + title: 'UI/Field/Display/DateFieldDisplay', component: DateFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx index edf9739d5..7e3cbecb9 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx @@ -3,11 +3,10 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useDoubleTextField } from '../../../hooks/useDoubleTextField'; import { DoubleTextFieldDisplay } from '../DoubleTextFieldDisplay'; // Import your component -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const DoubleTextFieldDisplayValueSetterEffect = ({ firstValue, secondValue, @@ -37,7 +36,7 @@ const DoubleTextFieldDisplayWithContext = ({ entityId, }: DoubleTextFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/DoubleTextFieldDisplay', + title: 'UI/Field/Display/DoubleTextFieldDisplay', component: DoubleTextFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx index fc68abb42..7300a945a 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx @@ -4,11 +4,10 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useEmailField } from '../../../hooks/useEmailField'; import { EmailFieldDisplay } from '../EmailFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { const { setFieldValue } = useEmailField(); @@ -29,7 +28,7 @@ const EmailFieldDisplayWithContext = ({ entityId, }: EmailFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/EmailFieldDisplay', + title: 'UI/Field/Display/EmailFieldDisplay', component: EmailFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx index b9a1a1af0..c00d48c09 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx @@ -6,11 +6,10 @@ import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { CatalogStory } from '~/testing/types'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useMoneyField } from '../../../hooks/useMoneyField'; import { MoneyFieldDisplay } from '../MoneyFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => { const { setFieldValue } = useMoneyField(); @@ -31,7 +30,7 @@ const MoneyFieldDisplayWithContext = ({ entityId, }: MoneyFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/MoneyFieldDisplay', + title: 'UI/Field/Display/MoneyFieldDisplay', component: MoneyFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx index e333d1b34..30bd14a1e 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx @@ -6,11 +6,10 @@ import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { CatalogStory } from '~/testing/types'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useNumberField } from '../../../hooks/useNumberField'; import { NumberFieldDisplay } from '../NumberFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { const { setFieldValue } = useNumberField(); @@ -31,7 +30,7 @@ const NumberFieldDisplayWithContext = ({ entityId, }: NumberFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/NumberFieldDisplay', + title: 'UI/Field/Display/NumberFieldDisplay', component: NumberFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx index d202745bf..f1fd4966f 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx @@ -4,11 +4,10 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { usePhoneField } from '../../../hooks/usePhoneField'; import { PhoneFieldDisplay } from '../PhoneFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { const { setFieldValue } = usePhoneField(); @@ -29,7 +28,7 @@ const PhoneFieldDisplayWithContext = ({ entityId, }: PhoneFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/PhoneFieldDisplay', + title: 'UI/Field/Display/PhoneFieldDisplay', component: PhoneFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx index a2a5376ea..2c1f1764c 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx @@ -6,11 +6,10 @@ import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { CatalogStory } from '~/testing/types'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useTextField } from '../../../hooks/useTextField'; import { TextFieldDisplay } from '../TextFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const TextFieldValueSetterEffect = ({ value }: { value: string }) => { const { setFieldValue } = useTextField(); @@ -31,7 +30,7 @@ const TextFieldDisplayWithContext = ({ entityId, }: TextFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/TextFieldDisplay', + title: 'UI/Field/Display/TextFieldDisplay', component: TextFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx b/front/src/modules/ui/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx index 4d50e98d1..e3f1f122a 100644 --- a/front/src/modules/ui/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx +++ b/front/src/modules/ui/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx @@ -4,11 +4,10 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useURLField } from '../../../hooks/useURLField'; import { URLFieldDisplay } from '../URLFieldDisplay'; -import { FieldDisplayContextProvider } from './FieldDisplayContextProvider'; - const URLFieldValueSetterEffect = ({ value }: { value: string }) => { const { setFieldValue } = useURLField(); @@ -29,7 +28,7 @@ const URLFieldDisplayWithContext = ({ entityId, }: URLFieldDisplayWithContextProps) => { return ( - - + ); }; const meta: Meta = { - title: 'UI/Field/URLFieldDisplay', + title: 'UI/Field/Display/URLFieldDisplay', component: URLFieldDisplayWithContext, }; diff --git a/front/src/modules/ui/field/meta-types/input/components/BooleanFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/BooleanFieldInput.tsx index 9b6a48e51..bc2e87233 100644 --- a/front/src/modules/ui/field/meta-types/input/components/BooleanFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/BooleanFieldInput.tsx @@ -5,11 +5,15 @@ import { useBooleanField } from '../../hooks/useBooleanField'; import { FieldInputEvent } from './DateFieldInput'; -type BooleanFieldInputProps = { +export type BooleanFieldInputProps = { onSubmit?: FieldInputEvent; + testId?: string; }; -export const BooleanFieldInput = ({ onSubmit }: BooleanFieldInputProps) => { +export const BooleanFieldInput = ({ + onSubmit, + testId, +}: BooleanFieldInputProps) => { const { fieldValue } = useBooleanField(); const persistField = usePersistField(); @@ -18,5 +22,11 @@ export const BooleanFieldInput = ({ onSubmit }: BooleanFieldInputProps) => { onSubmit?.(() => persistField(newValue)); }; - return ; + return ( + + ); }; diff --git a/front/src/modules/ui/field/meta-types/input/components/DateFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/DateFieldInput.tsx index 0a96f2fdf..fecf97c7e 100644 --- a/front/src/modules/ui/field/meta-types/input/components/DateFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/DateFieldInput.tsx @@ -6,7 +6,7 @@ import { useDateField } from '../../hooks/useDateField'; export type FieldInputEvent = (persist: () => void) => void; -type DateFieldInputProps = { +export type DateFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx new file mode 100644 index 000000000..2e0bf8e26 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -0,0 +1,100 @@ +import { useEffect } from 'react'; +import { jest } from '@storybook/jest'; +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useBooleanField } from '../../../hooks/useBooleanField'; +import { + BooleanFieldInput, + BooleanFieldInputProps, +} from '../BooleanFieldInput'; + +const BooleanFieldValueSetterEffect = ({ value }: { value: boolean }) => { + const { setFieldValue } = useBooleanField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type BooleanFieldInputWithContextProps = BooleanFieldInputProps & { + value: boolean; + entityId?: string; +}; + +const BooleanFieldInputWithContext = ({ + value, + entityId, + onSubmit, +}: BooleanFieldInputWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/Field/Input/BooleanFieldInput', + component: BooleanFieldInputWithContext, + args: { + value: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +const submitJestFn = jest.fn(); + +export const Default: Story = {}; + +export const Toggle: Story = { + args: { + onSubmit: submitJestFn, + }, + argTypes: { + onSubmit: { + control: false, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const input = canvas.getByTestId('boolean-field-input'); + + const trueText = await within(input).findByText('True'); + + await expect(trueText).toBeInTheDocument(); + + await expect(submitJestFn).toHaveBeenCalledTimes(0); + + await userEvent.click(input); + + await expect(input).toHaveTextContent('False'); + + await expect(submitJestFn).toHaveBeenCalledTimes(1); + + await userEvent.click(input); + + await expect(input).toHaveTextContent('True'); + + await expect(submitJestFn).toHaveBeenCalledTimes(2); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx new file mode 100644 index 000000000..ac7a10880 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx @@ -0,0 +1,131 @@ +import { useEffect } from 'react'; +import { expect } from '@storybook/jest'; +import { jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDateField } from '../../../hooks/useDateField'; +import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput'; + +const formattedDate = new Date(); + +const DateFieldValueSetterEffect = ({ value }: { value: Date }) => { + const { setFieldValue } = useDateField(); + + useEffect(() => { + setFieldValue(value.toISOString()); + }, [setFieldValue, value]); + + return <>; +}; + +type DateFieldInputWithContextProps = DateFieldInputProps & { + value: Date; + entityId?: string; +}; + +const DateFieldInputWithContext = ({ + value, + entityId, + onEscape, + onEnter, + onClickOutside, +}: DateFieldInputWithContextProps) => { + const setHotkeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotkeyScope('hotkey-scope'); + }, [setHotkeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const escapeJestFn = jest.fn(); +const enterJestFn = jest.fn(); +const clickOutsideJestFn = jest.fn(); + +const meta: Meta = { + title: 'UI/Field/Input/DateFieldInput', + component: DateFieldInputWithContext, + args: { + value: formattedDate, + onEscape: escapeJestFn, + onEnter: enterJestFn, + onClickOutside: clickOutsideJestFn, + }, + argTypes: { + onEscape: { + control: false, + }, + onEnter: { + control: false, + }, + onClickOutside: { + control: false, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + await userEvent.click(emptyDiv); + + await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Escape: Story = { + play: async () => { + await expect(escapeJestFn).toHaveBeenCalledTimes(0); + + await userEvent.keyboard('{esc}'); + + await expect(escapeJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Enter: Story = { + play: async () => { + await expect(enterJestFn).toHaveBeenCalledTimes(0); + + await userEvent.keyboard('{enter}'); + + await expect(enterJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/modules/ui/input/components/BooleanInput.tsx b/front/src/modules/ui/input/components/BooleanInput.tsx index 0b32b9bdd..1498aab56 100644 --- a/front/src/modules/ui/input/components/BooleanInput.tsx +++ b/front/src/modules/ui/input/components/BooleanInput.tsx @@ -20,9 +20,14 @@ const StyledEditableBooleanFieldValue = styled.div` type BooleanInputProps = { value: boolean; onToggle?: (newValue: boolean) => void; + testId?: string; }; -export const BooleanInput = ({ value, onToggle }: BooleanInputProps) => { +export const BooleanInput = ({ + value, + onToggle, + testId, +}: BooleanInputProps) => { const [internalValue, setInternalValue] = useState(value); useEffect(() => { @@ -37,7 +42,10 @@ export const BooleanInput = ({ value, onToggle }: BooleanInputProps) => { const theme = useTheme(); return ( - + {internalValue ? ( ) : (