diff --git a/front/src/modules/ui/field/meta-types/input/components/ChipFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/ChipFieldInput.tsx index c5383c6a3..8e9694cdf 100644 --- a/front/src/modules/ui/field/meta-types/input/components/ChipFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/ChipFieldInput.tsx @@ -5,7 +5,7 @@ import { useChipField } from '../../hooks/useChipField'; import { FieldInputEvent } from './DateFieldInput'; -type ChipFieldInputProps = { +export type ChipFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/DoubleTextChipFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/DoubleTextChipFieldInput.tsx index 5b0e2689a..06f874dc7 100644 --- a/front/src/modules/ui/field/meta-types/input/components/DoubleTextChipFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/DoubleTextChipFieldInput.tsx @@ -6,7 +6,7 @@ import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField'; import { FieldInputEvent } from './DateFieldInput'; -type DoubleTextChipFieldInputProps = { +export type DoubleTextChipFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/DoubleTextFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/DoubleTextFieldInput.tsx index 0adce6ee0..aa7d0d50d 100644 --- a/front/src/modules/ui/field/meta-types/input/components/DoubleTextFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/DoubleTextFieldInput.tsx @@ -6,7 +6,7 @@ import { useDoubleTextField } from '../../hooks/useDoubleTextField'; import { FieldInputEvent } from './DateFieldInput'; -type DoubleTextFieldInputProps = { +export type DoubleTextFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/EmailFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/EmailFieldInput.tsx index e52ede464..a43425a1f 100644 --- a/front/src/modules/ui/field/meta-types/input/components/EmailFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/EmailFieldInput.tsx @@ -5,7 +5,7 @@ import { useEmailField } from '../../hooks/useEmailField'; import { FieldInputEvent } from './DateFieldInput'; -type EmailFieldInputProps = { +export type EmailFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/MoneyFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/MoneyFieldInput.tsx index 40cc6c337..53a60474c 100644 --- a/front/src/modules/ui/field/meta-types/input/components/MoneyFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/MoneyFieldInput.tsx @@ -4,7 +4,7 @@ import { useMoneyField } from '../../hooks/useMoneyField'; export type FieldInputEvent = (persist: () => void) => void; -type MoneyFieldInputProps = { +export type MoneyFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/NumberFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/NumberFieldInput.tsx index a40af62a2..427fb0c88 100644 --- a/front/src/modules/ui/field/meta-types/input/components/NumberFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/NumberFieldInput.tsx @@ -4,7 +4,7 @@ import { useNumberField } from '../../hooks/useNumberField'; export type FieldInputEvent = (persist: () => void) => void; -type NumberFieldInputProps = { +export type NumberFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/PhoneFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/PhoneFieldInput.tsx index dfd1e42cc..377652616 100644 --- a/front/src/modules/ui/field/meta-types/input/components/PhoneFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/PhoneFieldInput.tsx @@ -4,7 +4,7 @@ import { usePhoneField } from '../../hooks/usePhoneField'; import { FieldInputEvent } from './DateFieldInput'; -type PhoneFieldInputProps = { +export type PhoneFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/ProbabilityFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/ProbabilityFieldInput.tsx index 7884152bb..f8f200785 100644 --- a/front/src/modules/ui/field/meta-types/input/components/ProbabilityFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/ProbabilityFieldInput.tsx @@ -5,7 +5,7 @@ import { useProbabilityField } from '../../hooks/useProbabilityField'; import { FieldInputEvent } from './DateFieldInput'; -type ProbabilityFieldInputProps = { +export type ProbabilityFieldInputProps = { onSubmit?: FieldInputEvent; }; diff --git a/front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx index 4404e1f81..59b6d43a7 100644 --- a/front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx @@ -17,7 +17,7 @@ const StyledRelationPickerContainer = styled.div` top: -8px; `; -type RelationFieldInputProps = { +export type RelationFieldInputProps = { onSubmit?: FieldInputEvent; onCancel?: () => void; }; diff --git a/front/src/modules/ui/field/meta-types/input/components/TextFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/TextFieldInput.tsx index 7473314cc..55d7b3716 100644 --- a/front/src/modules/ui/field/meta-types/input/components/TextFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/TextFieldInput.tsx @@ -5,7 +5,7 @@ import { useTextField } from '../../hooks/useTextField'; import { FieldInputEvent } from './DateFieldInput'; -type TextFieldInputProps = { +export type TextFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/URLFieldInput.tsx b/front/src/modules/ui/field/meta-types/input/components/URLFieldInput.tsx index 633cd3622..aa7909578 100644 --- a/front/src/modules/ui/field/meta-types/input/components/URLFieldInput.tsx +++ b/front/src/modules/ui/field/meta-types/input/components/URLFieldInput.tsx @@ -4,7 +4,7 @@ import { useURLField } from '../../hooks/useURLField'; import { FieldInputEvent } from './DateFieldInput'; -type URLFieldInputProps = { +export type URLFieldInputProps = { onClickOutside?: FieldInputEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx new file mode 100644 index 000000000..2b78eed28 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx @@ -0,0 +1,177 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useChipField } from '../../../hooks/useChipField'; +import { ChipFieldInput, ChipFieldInputProps } from '../ChipFieldInput'; + +const ChipFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setContentFieldValue } = useChipField(); + + useEffect(() => { + setContentFieldValue(value); + }, [setContentFieldValue, value]); + + return <>; +}; + +type ChipFieldInputWithContextProps = ChipFieldInputProps & { + value: string; + entityId?: string; +}; + +const ChipFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: ChipFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/ChipFieldInput', + component: ChipFieldInputWithContext, + args: { + value: 'chip', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + parameters: { + clearMocks: true, + }, + decorators: [clearMocksDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx new file mode 100644 index 000000000..ea28dddeb --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx @@ -0,0 +1,194 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDoubleTextChipField } from '../../../hooks/useDoubleTextChipField'; +import { + DoubleTextChipFieldInput, + DoubleTextChipFieldInputProps, +} from '../DoubleTextChipFieldInput'; + +const DoubleTextChipFieldValueSetterEffect = ({ + firstValue, + secondValue, +}: { + firstValue: string; + secondValue: string; +}) => { + const { setFirstValue, setSecondValue } = useDoubleTextChipField(); + + useEffect(() => { + setFirstValue(firstValue); + setSecondValue(secondValue); + }, [firstValue, secondValue, setFirstValue, setSecondValue]); + + return <>; +}; + +type DoubleTextChipFieldInputWithContextProps = + DoubleTextChipFieldInputProps & { + firstValue: string; + secondValue: string; + entityId?: string; + }; + +const DoubleTextChipFieldInputWithContext = ({ + entityId, + firstValue, + secondValue, + onClickOutside, + onEnter, + onEscape, + onTab, + onShiftTab, +}: DoubleTextChipFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/DoubleTextChipFieldInput', + component: DoubleTextChipFieldInputWithContext, + args: { + firstValue: 'first value', + secondValue: 'second value', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + parameters: { + clearMocks: true, + }, + decorators: [clearMocksDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx new file mode 100644 index 000000000..41a03cb4e --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx @@ -0,0 +1,191 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDoubleTextField } from '../../../hooks/useDoubleTextField'; +import { + DoubleTextFieldInput, + DoubleTextFieldInputProps, +} from '../DoubleTextFieldInput'; + +const DoubleTextFieldValueSetterEffect = ({ + firstValue, + secondValue, +}: { + firstValue: string; + secondValue: string; +}) => { + const { setFirstValue, setSecondValue } = useDoubleTextField(); + + useEffect(() => { + setFirstValue(firstValue); + setSecondValue(secondValue); + }, [firstValue, secondValue, setFirstValue, setSecondValue]); + + return <>; +}; + +type DoubleTextFieldInputWithContextProps = DoubleTextFieldInputProps & { + firstValue: string; + secondValue: string; + entityId?: string; +}; + +const DoubleTextFieldInputWithContext = ({ + entityId, + firstValue, + secondValue, + onClickOutside, + onEnter, + onEscape, + onTab, + onShiftTab, +}: DoubleTextFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/DoubleTextFieldInput', + component: DoubleTextFieldInputWithContext, + args: { + firstValue: 'first value', + secondValue: 'second value', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = await canvas.findByTestId( + 'data-field-input-click-outside-div', + ); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx new file mode 100644 index 000000000..bc427e1ce --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useEmailField } from '../../../hooks/useEmailField'; +import { EmailFieldInput, EmailFieldInputProps } from '../EmailFieldInput'; + +const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useEmailField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type EmailFieldInputWithContextProps = EmailFieldInputProps & { + value: string; + entityId?: string; +}; + +const EmailFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: EmailFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/EmailFieldInput', + component: EmailFieldInputWithContext, + args: { + value: 'username@email.com', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx new file mode 100644 index 000000000..df5add509 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useMoneyField } from '../../../hooks/useMoneyField'; +import { MoneyFieldInput, MoneyFieldInputProps } from '../MoneyFieldInput'; + +const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useMoneyField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type MoneyFieldInputWithContextProps = MoneyFieldInputProps & { + value: number; + entityId?: string; +}; + +const MoneyFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: MoneyFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/MoneyFieldInput', + component: MoneyFieldInputWithContext, + args: { + value: 1000, + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx new file mode 100644 index 000000000..4be6fbe32 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useNumberField } from '../../../hooks/useNumberField'; +import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput'; + +const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useNumberField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type NumberFieldInputWithContextProps = NumberFieldInputProps & { + value: number; + entityId?: string; +}; + +const NumberFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: NumberFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/NumberFieldInput', + component: NumberFieldInputWithContext, + args: { + value: 1000, + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx new file mode 100644 index 000000000..bdadd31c6 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { usePhoneField } from '../../../hooks/usePhoneField'; +import { PhoneFieldInput, PhoneFieldInputProps } from '../PhoneFieldInput'; + +const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = usePhoneField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type PhoneFieldInputWithContextProps = PhoneFieldInputProps & { + value: string; + entityId?: string; +}; + +const PhoneFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: PhoneFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/PhoneFieldInput', + component: PhoneFieldInputWithContext, + args: { + value: '+1-12-123-456', + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx new file mode 100644 index 000000000..e4bf4c767 --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx @@ -0,0 +1,106 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, 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 { useProbabilityField } from '../../../hooks/useProbabilityField'; +import { + ProbabilityFieldInput, + ProbabilityFieldInputProps, +} from '../ProbabilityFieldInput'; + +const ProbabilityFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useProbabilityField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type ProbabilityFieldInputWithContextProps = ProbabilityFieldInputProps & { + value: number; + entityId?: string; +}; + +const ProbabilityFieldInputWithContext = ({ + entityId, + value, + onSubmit, +}: ProbabilityFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( + + + + + ); +}; + +const submitJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + submitJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/ProbabilityFieldInput', + component: ProbabilityFieldInputWithContext, + args: { + value: 25, + isPositive: true, + onSubmit: submitJestFn, + }, + argTypes: { + onSubmit: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Submit: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(submitJestFn).toHaveBeenCalledTimes(0); + + const item = (await canvas.findByText('25%'))?.nextElementSibling + ?.firstElementChild; + + if (item) { + userEvent.click(item); + } + + expect(submitJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx new file mode 100644 index 000000000..d1a5c59ae --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -0,0 +1,134 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useRelationField } from '../../../hooks/useRelationField'; +import { + RelationFieldInput, + RelationFieldInputProps, +} from '../RelationFieldInput'; + +const RelationFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useRelationField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type RelationFieldInputWithContextProps = RelationFieldInputProps & { + value: number; + entityId?: string; +}; + +const RelationFieldInputWithContext = ({ + entityId, + value, + onSubmit, + onCancel, +}: RelationFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const submitJestFn = jest.fn(); +const cancelJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + submitJestFn.mockClear(); + cancelJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/RelationFieldInput', + component: RelationFieldInputWithContext, + args: { + useEditButton: true, + onSubmit: submitJestFn, + onCancel: cancelJestFn, + }, + argTypes: { + onSubmit: { control: false }, + onCancel: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + msw: graphqlMocks, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentWithRecoilScopeDecorator], +}; + +export const Submit: Story = { + decorators: [ComponentWithRecoilScopeDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(submitJestFn).toHaveBeenCalledTimes(0); + + const item = await canvas.findByText('Jane Doe'); + + userEvent.click(item); + + expect(submitJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Cancel: Story = { + decorators: [ComponentWithRecoilScopeDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(cancelJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(cancelJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx new file mode 100644 index 000000000..6235a994f --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useTextField } from '../../../hooks/useTextField'; +import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput'; + +const TextFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useTextField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type TextFieldInputWithContextProps = TextFieldInputProps & { + value: string; + entityId?: string; +}; + +const TextFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: TextFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/TextFieldInput', + component: TextFieldInputWithContext, + args: { + value: 'text', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx b/front/src/modules/ui/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx new file mode 100644 index 000000000..30bde150d --- /dev/null +++ b/front/src/modules/ui/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useURLField } from '../../../hooks/useURLField'; +import { URLFieldInput, URLFieldInputProps } from '../URLFieldInput'; + +const URLFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useURLField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type URLFieldInputWithContextProps = URLFieldInputProps & { + value: string; + entityId?: string; +}; + +const URLFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: URLFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/Field/Input/URLFieldInput', + component: URLFieldInputWithContext, + args: { + value: 'https://username.domain', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/testing/mock-data/users.ts b/front/src/testing/mock-data/users.ts index 1c0a33833..fcd426b69 100644 --- a/front/src/testing/mock-data/users.ts +++ b/front/src/testing/mock-data/users.ts @@ -1,5 +1,6 @@ import { Activity, + Attachment, ColorScheme, Company, User, @@ -34,6 +35,7 @@ type MockedUser = Pick< >; assignedActivities: Array; authoredActivities: Array; + authoredAttachments: Array; companies: Array; comments: Array; }; @@ -74,6 +76,7 @@ export const mockedUsersData: Array = [ locale: 'en', colorScheme: ColorScheme.System, }, + authoredAttachments: [], assignedActivities: [], authoredActivities: [], companies: [], @@ -113,6 +116,7 @@ export const mockedUsersData: Array = [ locale: 'en', colorScheme: ColorScheme.System, }, + authoredAttachments: [], assignedActivities: [], authoredActivities: [], companies: [], @@ -156,6 +160,7 @@ export const mockedOnboardingUsersData: Array = [ locale: 'en', colorScheme: ColorScheme.System, }, + authoredAttachments: [], assignedActivities: [], authoredActivities: [], companies: [], @@ -196,6 +201,7 @@ export const mockedOnboardingUsersData: Array = [ locale: 'en', colorScheme: ColorScheme.System, }, + authoredAttachments: [], assignedActivities: [], authoredActivities: [], companies: [], diff --git a/front/yarn.lock b/front/yarn.lock index e0211e924..317b5fccd 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -19490,6 +19490,7 @@ workbox-window@6.6.1: workbox-core "6.6.1" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==