From a36ecb871b5e7ea3726a46bf2075bf9d62c34eba Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 12 Sep 2024 15:15:02 +0200 Subject: [PATCH] test: Add test utils in studio-components --- .../StudioBooleanToggleGroup.test.tsx | 33 ++++++++++--------- .../StudioButton/StudioButton.test.tsx | 32 ++++++++++-------- .../StudioCenter/StudioCenter.test.tsx | 6 ++-- .../StudioCodeFragment.test.tsx | 22 ++++++------- .../StudioDecimalInput.test.tsx | 11 ++++--- .../StudioDeleteButton.test.tsx | 14 ++++---- .../StudioIconTextfield.test.tsx | 9 ++--- .../StudioLabelWrapper.test.tsx | 13 +++----- .../StudioPropertyButton.test.tsx | 18 +++++----- .../StudioPropertyFieldset.test.tsx | 26 ++++++++------- .../StudioPropertyGroup.test.tsx | 18 +++++----- .../StudioSectionHeader.test.tsx | 9 +++-- .../StudioTextarea/StudioTextarea.test.tsx | 24 +++++++++----- .../StudioTextfield/StudioTextfield.test.tsx | 11 ++++--- .../src/test-utils/selectors.ts | 7 ++++ .../src/test-utils/testCustomAttributes.ts | 25 ++++++++++++++ .../src/test-utils/testRefForwarding.ts | 31 +++++++++++++++++ .../test-utils/testRootClassNameAppending.ts | 26 +++++++++++++++ 18 files changed, 216 insertions(+), 119 deletions(-) create mode 100644 frontend/libs/studio-components/src/test-utils/selectors.ts create mode 100644 frontend/libs/studio-components/src/test-utils/testCustomAttributes.ts create mode 100644 frontend/libs/studio-components/src/test-utils/testRefForwarding.ts create mode 100644 frontend/libs/studio-components/src/test-utils/testRootClassNameAppending.ts diff --git a/frontend/libs/studio-components/src/components/StudioBooleanToggleGroup/StudioBooleanToggleGroup.test.tsx b/frontend/libs/studio-components/src/components/StudioBooleanToggleGroup/StudioBooleanToggleGroup.test.tsx index e406a3c46e2..935c5aafd87 100644 --- a/frontend/libs/studio-components/src/components/StudioBooleanToggleGroup/StudioBooleanToggleGroup.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioBooleanToggleGroup/StudioBooleanToggleGroup.test.tsx @@ -1,9 +1,19 @@ -import type { RefObject } from 'react'; -import React, { createRef } from 'react'; +import type { ForwardedRef } from 'react'; +import React from 'react'; +import type { RenderResult } from '@testing-library/react'; import { render, screen, within } from '@testing-library/react'; import type { StudioBooleanToggleGroupProps } from './StudioBooleanToggleGroup'; import { StudioBooleanToggleGroup } from './StudioBooleanToggleGroup'; import userEvent from '@testing-library/user-event'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; + +// Test data: +const trueLabel = 'True'; +const falseLabel = 'False'; +const defaultProps: StudioBooleanToggleGroupProps = { + trueLabel, + falseLabel, +}; describe('StudioBooleanToggleGroup', () => { it('Renders a toggle group with toggles with the given labels', () => { @@ -75,23 +85,16 @@ describe('StudioBooleanToggleGroup', () => { }); it('Forwards the ref object to the toggle group element if given', () => { - const ref = createRef(); - const { container } = renderBooleanToggle({}, ref); - expect(ref.current).toBe(container.firstChild); // eslint-disable-line testing-library/no-node-access + testRefForwarding((ref) => renderBooleanToggle({}, ref)); }); const getTrueToggle = () => screen.getByRole('radio', { name: trueLabel }); const getFalseToggle = () => screen.getByRole('radio', { name: falseLabel }); }); -const trueLabel = 'True'; -const falseLabel = 'False'; -const defaultProps: StudioBooleanToggleGroupProps = { - trueLabel, - falseLabel, -}; - -const renderBooleanToggle = ( +function renderBooleanToggle( props: Partial = {}, - ref?: RefObject, -) => render(); + ref?: ForwardedRef, +): RenderResult { + return render(); +} diff --git a/frontend/libs/studio-components/src/components/StudioButton/StudioButton.test.tsx b/frontend/libs/studio-components/src/components/StudioButton/StudioButton.test.tsx index 391601d0a0e..c94179d6f90 100644 --- a/frontend/libs/studio-components/src/components/StudioButton/StudioButton.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioButton/StudioButton.test.tsx @@ -1,9 +1,12 @@ -import type { RefObject } from 'react'; -import React, { createRef } from 'react'; +import type { ForwardedRef } from 'react'; +import React from 'react'; import type { StudioButtonProps } from './StudioButton'; import { StudioButton } from './StudioButton'; import { render, screen } from '@testing-library/react'; import type { IconPlacement } from '../../types/IconPlacement'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; +import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; +import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; // Mocks: jest.mock('./StudioButton.module.css', () => ({ @@ -16,7 +19,7 @@ describe('StudioButton', () => { it('Renders a button with the given content', () => { const children = 'Button content'; renderButton({ children }); - expect(screen.getByRole('button', { name: children })).toBeInTheDocument(); + expect(getButtonByName(children)).toBeInTheDocument(); }); it.each(iconPlacementCases)( @@ -36,22 +39,21 @@ describe('StudioButton', () => { const iconTestId = 'icon'; const icon = ; renderButton({ icon, iconPlacement, children }); - expect(screen.getByRole('button', { name: children })).toBeInTheDocument(); + expect(getButtonByName(children)).toBeInTheDocument(); expect(screen.getByTestId('icon')).toBeInTheDocument(); }, ); + it('Appends custom attributes to the button element', () => { + testCustomAttributes(renderButton, getButton); + }); + it('Appends given classname to internal classname', () => { - const className = 'test-class'; - const { container } = renderButton({ className }); - expect(container.firstChild).toHaveClass(className); // eslint-disable-line testing-library/no-node-access - expect(container.firstChild).toHaveClass('studioButton'); // eslint-disable-line testing-library/no-node-access + testRootClassNameAppending((className) => renderButton({ className })); }); - it('Forwards the ref object to the button element if given', () => { - const ref = createRef(); - renderButton({ children: 'Test' }, ref); - expect(ref.current).toBe(screen.getByRole('button')); + it('Forwards the ref to the button element if given', () => { + testRefForwarding((ref) => renderButton({}, ref), getButton); }); it('Supports render asChild', () => { @@ -65,5 +67,9 @@ describe('StudioButton', () => { }); }); -const renderButton = (props: StudioButtonProps, ref?: RefObject) => +const renderButton = (props: StudioButtonProps, ref?: ForwardedRef) => render(); + +const getButton = (): HTMLButtonElement => screen.getByRole('button') as HTMLButtonElement; +const getButtonByName = (name: string): HTMLButtonElement => + screen.getByRole('button', { name }) as HTMLButtonElement; diff --git a/frontend/libs/studio-components/src/components/StudioCenter/StudioCenter.test.tsx b/frontend/libs/studio-components/src/components/StudioCenter/StudioCenter.test.tsx index b00d2fd0e24..b5304682cee 100644 --- a/frontend/libs/studio-components/src/components/StudioCenter/StudioCenter.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioCenter/StudioCenter.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { StudioCenter } from './StudioCenter'; +import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; // Mocks: jest.mock('./SchemaEditor.module.css', () => ({ @@ -19,9 +20,6 @@ describe('StudioCenter', () => { }); it('Appends given classname to internal classname', () => { - const className = 'test-class'; - const { container } = render(); - expect(container.firstChild).toHaveClass(className); // eslint-disable-line testing-library/no-node-access - expect(container.firstChild).toHaveClass('root'); // eslint-disable-line testing-library/no-node-access + testRootClassNameAppending((className) => render()); }); }); diff --git a/frontend/libs/studio-components/src/components/StudioCodeFragment/StudioCodeFragment.test.tsx b/frontend/libs/studio-components/src/components/StudioCodeFragment/StudioCodeFragment.test.tsx index b62d40b3897..b17cbdbff3e 100644 --- a/frontend/libs/studio-components/src/components/StudioCodeFragment/StudioCodeFragment.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioCodeFragment/StudioCodeFragment.test.tsx @@ -1,6 +1,10 @@ -import React, { createRef } from 'react'; +import type { ForwardedRef } from 'react'; +import React from 'react'; import { render, screen } from '@testing-library/react'; import { StudioCodeFragment } from './StudioCodeFragment'; +import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; +import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; jest.mock('./StudioCodeFragment.module.css', () => ({ code: 'code', @@ -15,21 +19,17 @@ describe('StudioCodeFragment', () => { }); it('Appends given classname to internal classname', () => { - const className = 'test-class'; - const { container } = render(); - expect(container.firstChild).toHaveClass(className); - expect(container.firstChild).toHaveClass('code'); + testRootClassNameAppending((className) => render()); }); it('Adds any additonal props to the element', () => { - const dataTestId = 'test'; - render(); - expect(screen.getByTestId(dataTestId)).toBeInTheDocument(); + const renderComponent = (attributes) => render(); + testCustomAttributes(renderComponent); }); it('Forwards the ref object to the code element if given', () => { - const ref = createRef(); - const { container } = render(); - expect(ref.current).toBe(container.firstChild); + const renderComponent = (ref: ForwardedRef) => + render(); + testRefForwarding(renderComponent); }); }); diff --git a/frontend/libs/studio-components/src/components/StudioDecimalInput/StudioDecimalInput.test.tsx b/frontend/libs/studio-components/src/components/StudioDecimalInput/StudioDecimalInput.test.tsx index 83d47ba416b..d084518feaf 100644 --- a/frontend/libs/studio-components/src/components/StudioDecimalInput/StudioDecimalInput.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioDecimalInput/StudioDecimalInput.test.tsx @@ -1,9 +1,10 @@ -import type { RefObject } from 'react'; +import type { ForwardedRef } from 'react'; import React from 'react'; import { render as rtlRender, screen } from '@testing-library/react'; import type { StudioDecimalInputProps } from './StudioDecimalInput'; import { StudioDecimalInput } from './StudioDecimalInput'; import userEvent from '@testing-library/user-event'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; const description = 'description'; const onChange = jest.fn(); @@ -166,15 +167,15 @@ describe('StudioDecimalInput', () => { }); it('Accepts a ref prop', () => { - const ref = React.createRef(); - render({}, ref); - expect(ref.current).toBe(screen.getByRole('textbox')); + const renderComponent = (ref: ForwardedRef) => render({}, ref); + const getTextbox = () => screen.getByRole('textbox'); + testRefForwarding(renderComponent, getTextbox); }); }); const render = ( props: Partial = {}, - ref?: RefObject, + ref?: ForwardedRef, ) => { const allProps: StudioDecimalInputProps = { ...defaultProps, diff --git a/frontend/libs/studio-components/src/components/StudioDeleteButton/StudioDeleteButton.test.tsx b/frontend/libs/studio-components/src/components/StudioDeleteButton/StudioDeleteButton.test.tsx index 5a7141212ec..f61b26d7bc6 100644 --- a/frontend/libs/studio-components/src/components/StudioDeleteButton/StudioDeleteButton.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioDeleteButton/StudioDeleteButton.test.tsx @@ -1,10 +1,11 @@ -import type { RefObject } from 'react'; -import React, { createRef } from 'react'; +import type { ForwardedRef } from 'react'; +import React from 'react'; import { render, screen } from '@testing-library/react'; import { StudioDeleteButton } from './StudioDeleteButton'; import type { StudioDeleteButtonProps } from './StudioDeleteButton'; import userEvent from '@testing-library/user-event'; import { StudioButton } from '../StudioButton'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; describe('StudioDeleteButton', () => { afterEach(jest.clearAllMocks); @@ -55,9 +56,7 @@ describe('StudioDeleteButton', () => { }); it('Forwards the ref object to the button element if given', () => { - const ref = createRef(); - renderDeleteButton({}, ref); - expect(ref.current).toBe(getDeleteButton()); + testRefForwarding((ref) => renderDeleteButton({}, ref), getDeleteButton); }); it('Supports polymorphism', () => { @@ -70,7 +69,8 @@ describe('StudioDeleteButton', () => { expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); - const getDeleteButton = () => screen.getByRole('button', { name: buttonLabel }); + const getDeleteButton = (): HTMLButtonElement => + screen.getByRole('button', { name: buttonLabel }); }); const confirmMessage = 'Er du sikker på at du vil slette dette?'; @@ -83,5 +83,5 @@ const defaultProps: StudioDeleteButtonProps = { }; const renderDeleteButton = ( props: Partial = {}, - ref?: RefObject, + ref?: ForwardedRef, ) => render(); diff --git a/frontend/libs/studio-components/src/components/StudioIconTextfield/StudioIconTextfield.test.tsx b/frontend/libs/studio-components/src/components/StudioIconTextfield/StudioIconTextfield.test.tsx index b455fe2cef8..f5ac15a1ade 100644 --- a/frontend/libs/studio-components/src/components/StudioIconTextfield/StudioIconTextfield.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioIconTextfield/StudioIconTextfield.test.tsx @@ -4,6 +4,7 @@ import { StudioIconTextfield } from './StudioIconTextfield'; import type { StudioIconTextfieldProps } from './StudioIconTextfield'; import { KeyVerticalIcon } from '@studio/icons'; import userEvent from '@testing-library/user-event'; +import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; describe('StudioIconTextfield', () => { it('render the icon', async () => { @@ -39,12 +40,8 @@ describe('StudioIconTextfield', () => { }); it('should forward the rest of the props to the input', () => { - renderStudioIconTextfield({ - icon:
, - label: 'Your ID', - disabled: true, - }); - expect(screen.getByLabelText('Your ID')).toBeDisabled(); + const getTextbox = (): HTMLInputElement => screen.getByRole('textbox') as HTMLInputElement; + testCustomAttributes(renderStudioIconTextfield, getTextbox); }); }); const renderStudioIconTextfield = (props: StudioIconTextfieldProps) => { diff --git a/frontend/libs/studio-components/src/components/StudioLabelWrapper/StudioLabelWrapper.test.tsx b/frontend/libs/studio-components/src/components/StudioLabelWrapper/StudioLabelWrapper.test.tsx index 831882e0d54..2a868995c6f 100644 --- a/frontend/libs/studio-components/src/components/StudioLabelWrapper/StudioLabelWrapper.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioLabelWrapper/StudioLabelWrapper.test.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { StudioLabelWrapper } from './StudioLabelWrapper'; import { render, screen } from '@testing-library/react'; +import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; jest.mock('./StudioLabelWrapper.module.css', () => ({ studioLabelWrapper: 'studioLabelWrapper', @@ -31,17 +33,10 @@ describe('StudioLabelWrapper', () => { ); it('Appends given classname to internal classname', () => { - const className = 'test-class'; - const { container } = render( - Test, - ); - expect(container.firstChild).toHaveClass(className); - expect(container.firstChild).toHaveClass('studioLabelWrapper'); + testRootClassNameAppending((className) => render()); }); it('Forwards the ref object to the span element if given', () => { - const ref = React.createRef(); - const { container } = render(Test); - expect(ref.current).toBe(container.firstChild); + testRefForwarding((ref) => render()); }); }); diff --git a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyButton/StudioPropertyButton.test.tsx b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyButton/StudioPropertyButton.test.tsx index caa88416d47..7762870b94b 100644 --- a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyButton/StudioPropertyButton.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyButton/StudioPropertyButton.test.tsx @@ -1,9 +1,11 @@ -import type { RefObject } from 'react'; +import type { ForwardedRef } from 'react'; import React from 'react'; import type { StudioPropertyButtonProps } from './StudioPropertyButton'; import { StudioPropertyButton } from './StudioPropertyButton'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { testRootClassNameAppending } from '../../../test-utils/testRootClassNameAppending'; +import { testRefForwarding } from '../../../test-utils/testRefForwarding'; // Test data: const property = 'Test property'; @@ -37,15 +39,11 @@ describe('StudioPropertyButton', () => { }); it('Appends the given class name', () => { - const className = 'test-class'; - renderButton({ className }); - expect(screen.getByRole('button', { name: property })).toHaveClass(className); + testRootClassNameAppending((className) => renderButton({ className })); }); it('Forwards a ref to the button', () => { - const ref = React.createRef(); - renderButton({}, ref); - expect(ref.current).toBe(screen.getByRole('button')); + testRefForwarding((ref) => renderButton({}, ref), getButton); }); it('Calls the onClick function when the button is clicked', async () => { @@ -58,11 +56,13 @@ describe('StudioPropertyButton', () => { it('Renders a compact button when the compact prop is true', () => { renderButton({ compact: true }); - expect(screen.getByRole('button')).toHaveClass('compact'); + expect(getButton()).toHaveClass('compact'); }); }); const renderButton = ( props: Partial = {}, - ref?: RefObject, + ref?: ForwardedRef, ) => render(); + +const getButton = (): HTMLButtonElement => screen.getByRole('button'); diff --git a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyFieldset/StudioPropertyFieldset.test.tsx b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyFieldset/StudioPropertyFieldset.test.tsx index d73e50e1fab..06fb9b79e34 100644 --- a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyFieldset/StudioPropertyFieldset.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyFieldset/StudioPropertyFieldset.test.tsx @@ -1,6 +1,8 @@ -import React, { createRef } from 'react'; +import React from 'react'; import { render, screen, within } from '@testing-library/react'; import { StudioPropertyFieldset } from './StudioPropertyFieldset'; +import { testRootClassNameAppending } from '../../../test-utils/testRootClassNameAppending'; +import { testRefForwarding } from '../../../test-utils/testRefForwarding'; jest.mock('./StudioPropertyFieldset.module.css', () => ({ propertyFieldset: 'propertyFieldset', @@ -13,7 +15,7 @@ describe('StudioPropertyFieldset', () => { it('Renders a group component with the given legend', () => { const legend = 'Test legend'; render(); - screen.getByRole('group', { name: legend }); + getGroupByName(legend); }); it('Renders the menubar', () => { @@ -26,21 +28,23 @@ describe('StudioPropertyFieldset', () => { }); it('Appends the given class name to the default one', () => { - const className = 'test-class'; - render(); - screen.getByRole('group'); - expect(screen.getByRole('group')).toHaveClass(className); - expect(screen.getByRole('group')).toHaveClass('propertyFieldset'); + testRootClassNameAppending(function (className) { + return render(); + }); }); it('Forwards the ref object to the fieldset element if given', () => { - const ref = createRef(); - render(); - expect(ref.current).toBe(screen.getByRole('group')); + testRefForwarding( + (ref) => render(), + getGroup, + ); }); it('Renders a compact fieldset when the compact prop is true', () => { render(); - expect(screen.getByRole('group')).toHaveClass('compact'); + expect(getGroup()).toHaveClass('compact'); }); }); + +const getGroup = (): HTMLFieldSetElement => screen.getByRole('group'); +const getGroupByName = (name: string): HTMLFieldSetElement => screen.getByRole('group', { name }); diff --git a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyGroup/StudioPropertyGroup.test.tsx b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyGroup/StudioPropertyGroup.test.tsx index 56bd7589c97..f655956597c 100644 --- a/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyGroup/StudioPropertyGroup.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioProperty/StudioPropertyGroup/StudioPropertyGroup.test.tsx @@ -1,6 +1,8 @@ -import React, { createRef } from 'react'; +import React from 'react'; import { render, screen } from '@testing-library/react'; import { StudioPropertyGroup } from './StudioPropertyGroup'; +import { testRefForwarding } from '../../../test-utils/testRefForwarding'; +import { testRootClassNameAppending } from '../../../test-utils/testRootClassNameAppending'; jest.mock('./StudioPropertyGroup.module.css', () => ({ listWrapper: 'listWrapper', @@ -14,18 +16,14 @@ describe('StudioPropertyGroup', () => { }); it('Appends the given class name', () => { - const className = 'test'; - const testId = 'test-id'; - render(); - const listWrapper = screen.getByTestId(testId); - expect(listWrapper).toHaveClass(className); - expect(listWrapper).toHaveClass('listWrapper'); + testRootClassNameAppending((cn) => render()); }); it('Forwards the ref object if given', () => { - const ref = createRef(); const testId = 'test-id'; - render(); - expect(ref.current).toBe(screen.getByTestId(testId)); + testRefForwarding( + (ref) => render(), + () => screen.getByTestId(testId), + ); }); }); diff --git a/frontend/libs/studio-components/src/components/StudioSectionHeader/StudioSectionHeader.test.tsx b/frontend/libs/studio-components/src/components/StudioSectionHeader/StudioSectionHeader.test.tsx index 514c1b35327..f8665427c35 100644 --- a/frontend/libs/studio-components/src/components/StudioSectionHeader/StudioSectionHeader.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioSectionHeader/StudioSectionHeader.test.tsx @@ -1,6 +1,8 @@ import React, { type ForwardedRef } from 'react'; import { render, screen } from '@testing-library/react'; import { StudioSectionHeader, type StudioSectionHeaderProps } from './StudioSectionHeader'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; +import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; describe('StudioSectionHeader', () => { it('should display icon if provided', () => { @@ -24,14 +26,11 @@ describe('StudioSectionHeader', () => { }); it('should be able to pass HTMLDivElement attributes', () => { - renderStudioSectionHeader({ className: 'test-class-name' }); - expect(screen.getByTestId('headerTestId')).toHaveClass('test-class-name'); + testCustomAttributes(renderStudioSectionHeader); }); it('should be possible to use the ref-api to get the underlying HTMLDivElement', () => { - const ref = React.createRef(); - renderStudioSectionHeader({ ref }); - expect(ref.current).toBeInTheDocument(); + testRefForwarding((ref) => renderStudioSectionHeader({ ref })); }); }); diff --git a/frontend/libs/studio-components/src/components/StudioTextarea/StudioTextarea.test.tsx b/frontend/libs/studio-components/src/components/StudioTextarea/StudioTextarea.test.tsx index 84950986695..8a003468315 100644 --- a/frontend/libs/studio-components/src/components/StudioTextarea/StudioTextarea.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioTextarea/StudioTextarea.test.tsx @@ -1,9 +1,11 @@ import type { StudioTextareaProps } from './StudioTextarea'; import { StudioTextarea } from './StudioTextarea'; +import type { RenderResult } from '@testing-library/react'; import { render, screen } from '@testing-library/react'; -import type { RefObject } from 'react'; +import type { ForwardedRef } from 'react'; import React from 'react'; import userEvent from '@testing-library/user-event'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; describe('StudioTextarea', () => { it('Renders a textarea', () => { @@ -105,7 +107,7 @@ describe('StudioTextarea', () => { expect(screen.getByText(errorAfterBlur)).toBeInTheDocument(); }); - it('Diplays the message provided through the errorAfterBlur prop when the user types something after blurring', async () => { + it('Displays the message provided through the errorAfterBlur prop when the user types something after blurring', async () => { const user = userEvent.setup(); const errorAfterBlur = 'error message'; renderTextarea({ errorAfterBlur }); @@ -142,14 +144,18 @@ describe('StudioTextarea', () => { expect(screen.getByText(error)).toBeInTheDocument(); }); - it('Forwards the ref object to the textarea element if given', () => { - const ref = React.createRef(); - renderTextarea({}, ref); - expect(ref.current).toBe(screen.getByRole('textbox')); + describe('Ref forwarding', () => { + testRefForwarding((ref) => renderTextarea({}, ref), getTextarea); }); }); -const renderTextarea = ( +function renderTextarea( props: Partial = {}, - ref?: RefObject, -) => render(); + ref?: ForwardedRef, +): RenderResult { + return render(); +} + +function getTextarea(): HTMLTextAreaElement { + return screen.getByRole('textbox'); +} diff --git a/frontend/libs/studio-components/src/components/StudioTextfield/StudioTextfield.test.tsx b/frontend/libs/studio-components/src/components/StudioTextfield/StudioTextfield.test.tsx index 2f49ce0fa3d..ebd2f89bebf 100644 --- a/frontend/libs/studio-components/src/components/StudioTextfield/StudioTextfield.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioTextfield/StudioTextfield.test.tsx @@ -4,7 +4,8 @@ import type { StudioTextfieldProps } from './StudioTextfield'; import { StudioTextfield } from './StudioTextfield'; import { StudioTextarea } from '../StudioTextarea'; import userEvent from '@testing-library/user-event'; -import type { RefObject } from 'react'; +import type { ForwardedRef } from 'react'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; describe('StudioTextfield', () => { it('Renders a text field', () => { @@ -144,13 +145,13 @@ describe('StudioTextfield', () => { }); it('Forwards the ref object to the textarea element if given', () => { - const ref = React.createRef(); - renderTextfield({}, ref); - expect(ref.current).toBe(screen.getByRole('textbox')); + testRefForwarding((ref) => renderTextfield({}, ref), getTextfield); }); }); const renderTextfield = ( props: Partial = {}, - ref?: RefObject, + ref?: ForwardedRef, ) => render(); + +const getTextfield = (): HTMLInputElement => screen.getByRole('textbox'); diff --git a/frontend/libs/studio-components/src/test-utils/selectors.ts b/frontend/libs/studio-components/src/test-utils/selectors.ts new file mode 100644 index 00000000000..09f09ef4e0f --- /dev/null +++ b/frontend/libs/studio-components/src/test-utils/selectors.ts @@ -0,0 +1,7 @@ +import type { RenderResult } from '@testing-library/react'; + +export function getRootElementFromContainer( + container: RenderResult['container'], +): Element { + return container.children.item(0) as Element; +} diff --git a/frontend/libs/studio-components/src/test-utils/testCustomAttributes.ts b/frontend/libs/studio-components/src/test-utils/testCustomAttributes.ts new file mode 100644 index 00000000000..773dcce4f59 --- /dev/null +++ b/frontend/libs/studio-components/src/test-utils/testCustomAttributes.ts @@ -0,0 +1,25 @@ +import type { RenderResult } from '@testing-library/react'; +import { getRootElementFromContainer } from './selectors'; +import type { HTMLAttributes } from 'react'; + +type CustomAttributes = { + [key: `data-${string}`]: string; +}; + +export function testCustomAttributes< + Element extends HTMLElement = HTMLElement, + Props extends HTMLAttributes = HTMLAttributes, +>( + renderComponent: (customAttributes: Props) => RenderResult, + getTargetElement: (container: RenderResult['container']) => Element = getRootElementFromContainer, +): void { + const customAttributes: Props = { + 'data-test-attribute-1': 'test-1', + 'data-test-attribute-2': 'test-2', + } satisfies CustomAttributes as unknown as Props; + const { container } = renderComponent(customAttributes); + const targetElement: Element = getTargetElement(container); + Object.entries(customAttributes).forEach(([key, value]) => { + expect(targetElement).toHaveAttribute(key, value); + }); +} diff --git a/frontend/libs/studio-components/src/test-utils/testRefForwarding.ts b/frontend/libs/studio-components/src/test-utils/testRefForwarding.ts new file mode 100644 index 00000000000..1f9c8174149 --- /dev/null +++ b/frontend/libs/studio-components/src/test-utils/testRefForwarding.ts @@ -0,0 +1,31 @@ +import type { ForwardedRef, RefObject } from 'react'; +import { createRef } from 'react'; +import type { RenderResult } from '@testing-library/react'; +import { getRootElementFromContainer } from './selectors'; + +export function testRefForwarding( + renderComponent: (ref: ForwardedRef) => RenderResult, + getTargetElement: (container: RenderResult['container']) => Element = getRootElementFromContainer, +): void { + testObjectRefForwarding(renderComponent, getTargetElement); + testCallbackRefForwarding(renderComponent, getTargetElement); +} + +function testObjectRefForwarding( + ...[renderComponent, getTargetElement]: Parameters> +): void { + const ref: RefObject = createRef(); + const { container, unmount } = renderComponent(ref); + expect(ref.current).toBe(getTargetElement(container)); + unmount(); +} + +function testCallbackRefForwarding( + ...[renderComponent, getTargetElement]: Parameters> +): void { + const ref = jest.fn(); + const { container, unmount } = renderComponent(ref); + expect(ref).toHaveBeenCalledTimes(1); + expect(ref).toHaveBeenCalledWith(getTargetElement(container)); + unmount(); +} diff --git a/frontend/libs/studio-components/src/test-utils/testRootClassNameAppending.ts b/frontend/libs/studio-components/src/test-utils/testRootClassNameAppending.ts new file mode 100644 index 00000000000..de97b340025 --- /dev/null +++ b/frontend/libs/studio-components/src/test-utils/testRootClassNameAppending.ts @@ -0,0 +1,26 @@ +import type { RenderResult } from '@testing-library/react'; +import { getRootElementFromContainer } from './selectors'; + +export function testRootClassNameAppending( + renderComponent: (className?: string) => RenderResult, +): void { + const className = 'test-class'; + const { container: defaultContainer } = renderComponent(); + const defaultClasses: string[] = getRootClasses(defaultContainer); + const { container: containerWithCLass } = renderComponent(className); + const rootElement: HTMLElement = getRootElementFromContainer(containerWithCLass); + defaultClasses.forEach((defaultClass) => { + expect(rootElement).toHaveClass(defaultClass); + }); + expect(rootElement).toHaveClass(className); +} + +function getRootClasses(container: RenderResult['container']): string[] { + const rootElement: HTMLElement = getRootElementFromContainer(container); + const className = rootElement.className; + return classListFromClassName(className); +} + +function classListFromClassName(className: string): string[] { + return className.trim().split(/\s+/); +}