diff --git a/.changeset/fresh-wolves-beam.md b/.changeset/fresh-wolves-beam.md new file mode 100644 index 0000000000..bd2044a068 --- /dev/null +++ b/.changeset/fresh-wolves-beam.md @@ -0,0 +1,5 @@ +--- +'@sumup/circuit-ui': patch +--- + +Switched the `ToastContext`'s live region element from a `ul` to a `div`: lists shouldn't have `role="status"` since this strips list semantics. diff --git a/packages/circuit-ui/components/Anchor/Anchor.spec.tsx b/packages/circuit-ui/components/Anchor/Anchor.spec.tsx index f02fe05026..02f445eef5 100644 --- a/packages/circuit-ui/components/Anchor/Anchor.spec.tsx +++ b/packages/circuit-ui/components/Anchor/Anchor.spec.tsx @@ -22,7 +22,6 @@ import { renderToHtml, axe, RenderFn, - act, userEvent, } from '../../util/test-utils'; import { ClickEvent } from '../../types/events'; @@ -82,7 +81,7 @@ describe('Anchor', () => { expect(actual).toBeVisible(); }); - it('should call the onClick handler when rendered as a link', () => { + it('should call the onClick handler when rendered as a link', async () => { const props = { ...baseProps, 'href': 'https://sumup.com', @@ -93,14 +92,12 @@ describe('Anchor', () => { }; const { getByTestId } = renderAnchor(render, props); - act(() => { - userEvent.click(getByTestId('anchor')); - }); + await userEvent.click(getByTestId('anchor')); expect(props.onClick).toHaveBeenCalledTimes(1); }); - it('should call the onClick handler when rendered as a button', () => { + it('should call the onClick handler when rendered as a button', async () => { const props = { ...baseProps, 'onClick': jest.fn(), @@ -108,9 +105,7 @@ describe('Anchor', () => { }; const { getByTestId } = renderAnchor(render, props); - act(() => { - userEvent.click(getByTestId('anchor')); - }); + await userEvent.click(getByTestId('anchor')); expect(props.onClick).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/Button/Button.spec.tsx b/packages/circuit-ui/components/Button/Button.spec.tsx index a5c869d9f6..3b9f9110de 100644 --- a/packages/circuit-ui/components/Button/Button.spec.tsx +++ b/packages/circuit-ui/components/Button/Button.spec.tsx @@ -22,7 +22,6 @@ import { renderToHtml, axe, RenderFn, - act, userEvent, } from '../../util/test-utils'; @@ -135,7 +134,7 @@ describe('Button', () => { expect(getByText(loadingLabel)).toBeVisible(); }); - it('should call the onClick handler when clicked', () => { + it('should call the onClick handler when clicked', async () => { const props = { ...baseProps, 'onClick': jest.fn(), @@ -143,9 +142,7 @@ describe('Button', () => { }; const { getByTestId } = renderButton(render, props); - act(() => { - userEvent.click(getByTestId('link-button')); - }); + await userEvent.click(getByTestId('link-button')); expect(props.onClick).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/Card/components/Header/Header.spec.tsx b/packages/circuit-ui/components/Card/components/Header/Header.spec.tsx index 0ef499e773..b2ab72b8cf 100644 --- a/packages/circuit-ui/components/Card/components/Header/Header.spec.tsx +++ b/packages/circuit-ui/components/Card/components/Header/Header.spec.tsx @@ -51,7 +51,7 @@ describe('CardHeader', () => { expect(closeButton).toHaveTextContent(closeButtonLabel); }); - it('should call the onClose prop when the close button is clicked', () => { + it('should call the onClose prop when the close button is clicked', async () => { const onClose = jest.fn(); const { getByRole } = render( @@ -61,7 +61,7 @@ describe('CardHeader', () => { ); const closeButton = getByRole('button'); - userEvent.click(closeButton); + await userEvent.click(closeButton); expect(onClose).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/Checkbox/Checkbox.spec.tsx b/packages/circuit-ui/components/Checkbox/Checkbox.spec.tsx index abba21d4f2..866ce52a01 100644 --- a/packages/circuit-ui/components/Checkbox/Checkbox.spec.tsx +++ b/packages/circuit-ui/components/Checkbox/Checkbox.spec.tsx @@ -20,7 +20,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../util/test-utils'; @@ -87,7 +86,7 @@ describe('Checkbox', () => { expect(inputEl).not.toHaveAttribute('checked'); }); - it('should call the change handler when clicked', () => { + it('should call the change handler when clicked', async () => { const { getByLabelText } = render( Label @@ -97,9 +96,7 @@ describe('Checkbox', () => { exact: false, }); - act(() => { - userEvent.click(inputEl); - }); + await userEvent.click(inputEl); expect(defaultProps.onChange).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx b/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx index bd496f2a20..b507e09dd3 100644 --- a/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx +++ b/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx @@ -30,10 +30,10 @@ const interactionTasks: PublicInteractionTask[] = [ name: 'Tick checkbox', description: 'Click the checkbox and wait for the label to change', run: async ({ container }: InteractionTaskArgs): Promise => { - const checkbox: HTMLElement | null = container.querySelector( + const checkbox = container.querySelector( 'input[type=checkbox]', - ); - userEvent.click(checkbox); + ) as HTMLInputElement; + await userEvent.click(checkbox); await findByText(container, 'Checked'); }, }, diff --git a/packages/circuit-ui/components/CurrencyInput/CurrencyInput.spec.tsx b/packages/circuit-ui/components/CurrencyInput/CurrencyInput.spec.tsx index 345acc5e0f..a98fea41b1 100644 --- a/packages/circuit-ui/components/CurrencyInput/CurrencyInput.spec.tsx +++ b/packages/circuit-ui/components/CurrencyInput/CurrencyInput.spec.tsx @@ -21,7 +21,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../util/test-utils'; import { InputProps } from '../Input'; @@ -70,35 +69,31 @@ describe('CurrencyInput', () => { expect(tref.current).toBe(input); }); - it('should format a en-GB amount correctly', () => { + it('should format a en-GB amount correctly', async () => { const { getByLabelText } = render( , ); const input = getByLabelText(/Amount/) as HTMLInputElement; - act(() => { - userEvent.type(input, '1234.56'); - }); + await userEvent.type(input, '1234.56'); expect(input.value).toBe('1,234.56'); }); - it('should format a de-DE amount correctly', () => { + it('should format a de-DE amount correctly', async () => { const { getByLabelText } = render( , ); const input = getByLabelText(/Amount/) as HTMLInputElement; - act(() => { - userEvent.type(input, '1234,56'); - }); + await userEvent.type(input, '1234,56'); expect(input.value).toBe('1.234,56'); }); - it('should format an amount in a controlled input with an initial numeric value', () => { + it('should format an amount in a controlled input with an initial numeric value', async () => { const ControlledCurrencyInput = () => { const [value, setValue] = useState(1234.5); return ( @@ -119,10 +114,8 @@ describe('CurrencyInput', () => { const input = getByLabelText(/Amount/) as HTMLInputElement; expect(input.value).toBe('1.234,5'); - act(() => { - userEvent.clear(input); - userEvent.type(input, '1234,56'); - }); + await userEvent.clear(input); + await userEvent.type(input, '1234,56'); expect(input.value).toBe('1.234,56'); }); diff --git a/packages/circuit-ui/components/Hamburger/Hamburger.spec.tsx b/packages/circuit-ui/components/Hamburger/Hamburger.spec.tsx index ac63348e66..8da9e6cf41 100644 --- a/packages/circuit-ui/components/Hamburger/Hamburger.spec.tsx +++ b/packages/circuit-ui/components/Hamburger/Hamburger.spec.tsx @@ -18,7 +18,6 @@ import { renderToHtml, axe, render, - act, userEvent, RenderFn, } from '../../util/test-utils'; @@ -58,7 +57,7 @@ describe('Hamburger', () => { /** * Logic tests. */ - it('should call the onClick prop when clicked', () => { + it('should call the onClick prop when clicked', async () => { const onClick = jest.fn(); const { getByTestId } = renderHamburger(render, { ...baseProps, @@ -66,9 +65,7 @@ describe('Hamburger', () => { 'data-testid': 'hamburger', }); - act(() => { - userEvent.click(getByTestId('hamburger')); - }); + await userEvent.click(getByTestId('hamburger')); expect(onClick).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx index 390a37e4f0..874f7c0e0e 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx @@ -148,7 +148,7 @@ describe('ImageInput', () => { const { getByLabelText } = render(); const inputEl = getByLabelText(defaultProps.label) as HTMLInputElement; - userEvent.upload(inputEl, file); + await userEvent.upload(inputEl, file); await waitFor(() => { expect(inputEl.files && inputEl.files[0]).toEqual(file); @@ -188,7 +188,7 @@ describe('ImageInput', () => { const inputEl = getByLabelText(defaultProps.label) as HTMLInputElement; const imageEl = getByRole('img') as HTMLImageElement; - userEvent.upload(inputEl, file); + await userEvent.upload(inputEl, file); await waitFor(() => { expect(imageEl.src).toBe( @@ -202,7 +202,7 @@ describe('ImageInput', () => { const inputEl = getByLabelText(defaultProps.label) as HTMLInputElement; const imageEl = getByRole('img') as HTMLImageElement; - userEvent.upload(inputEl, file); + await userEvent.upload(inputEl, file); await waitFor(() => { expect(imageEl.src).toBe( @@ -210,7 +210,7 @@ describe('ImageInput', () => { ); }); - userEvent.click( + await userEvent.click( getByRole('button', { name: defaultProps.clearButtonLabel }), ); @@ -229,7 +229,7 @@ describe('ImageInput', () => { const { getByLabelText, getByText } = render(); const inputEl = getByLabelText(defaultProps.label) as HTMLInputElement; - userEvent.upload(inputEl, file); + await userEvent.upload(inputEl, file); await waitFor(() => { expect(getByText(errorMessage)).toBeVisible(); diff --git a/packages/circuit-ui/components/ListItem/ListItem.spec.tsx b/packages/circuit-ui/components/ListItem/ListItem.spec.tsx index dab673bb30..04b74d6629 100644 --- a/packages/circuit-ui/components/ListItem/ListItem.spec.tsx +++ b/packages/circuit-ui/components/ListItem/ListItem.spec.tsx @@ -13,8 +13,8 @@ * limitations under the License. */ -import { createRef } from 'react'; -import { SumUpCard } from '@sumup/icons'; +import { createRef, FC } from 'react'; +import { IconProps, SumUpCard } from '@sumup/icons'; import { create, @@ -22,7 +22,6 @@ import { renderToHtml, axe, RenderFn, - act, userEvent, } from '../../util/test-utils'; import Body from '../Body'; @@ -56,7 +55,7 @@ describe('ListItem', () => { it('should render a ListItem with a leading icon', () => { const wrapper = renderListItem(create, { ...baseProps, - leadingComponent: SumUpCard, + leadingComponent: SumUpCard as FC, }); expect(wrapper).toMatchSnapshot(); }); @@ -197,16 +196,14 @@ describe('ListItem', () => { expect(wrapper).toMatchSnapshot(); }); - it('should call the onClick handler when clicked', () => { + it('should call the onClick handler when clicked', async () => { const props = { ...baseProps, onClick: jest.fn(), }; const { getByRole } = renderListItem(render, props); - act(() => { - userEvent.click(getByRole('button')); - }); + await userEvent.click(getByRole('button')); expect(props.onClick).toHaveBeenCalledTimes(1); }); @@ -227,7 +224,7 @@ describe('ListItem', () => { const wrapper = renderListItem(renderToHtml, { ...baseProps, variant: 'navigation', - leadingComponent: SumUpCard, + leadingComponent: SumUpCard as FC, details: 'Details', trailingLabel: 'Trailing label', trailingDetails: 'Trailing details', diff --git a/packages/circuit-ui/components/ListItemGroup/ListItemGroup.spec.tsx b/packages/circuit-ui/components/ListItemGroup/ListItemGroup.spec.tsx index 9f8330871c..328c291d2a 100644 --- a/packages/circuit-ui/components/ListItemGroup/ListItemGroup.spec.tsx +++ b/packages/circuit-ui/components/ListItemGroup/ListItemGroup.spec.tsx @@ -21,7 +21,6 @@ import { renderToHtml, axe, RenderFn, - act, userEvent, } from '../../util/test-utils'; import Body from '../Body'; @@ -116,7 +115,7 @@ describe('ListItemGroup', () => { expect(container).toMatchSnapshot(); }); - it('should render the focused item in a ListItemGroup with interactive items', () => { + it('should render the focused item in a ListItemGroup with interactive items', async () => { const { getAllByRole } = renderListItemGroup(render, { ...baseProps, items: baseProps.items.map((item) => ({ @@ -125,10 +124,8 @@ describe('ListItemGroup', () => { })), }); - act(() => { - userEvent.tab(); - userEvent.tab(); // blur first and focus second item - }); + await userEvent.tab(); + await userEvent.tab(); // blur first and focus second item expect(getAllByRole('button')[1]).toMatchSnapshot(); }); diff --git a/packages/circuit-ui/components/Modal/Modal.spec.tsx b/packages/circuit-ui/components/Modal/Modal.spec.tsx index 43b479dd4d..cef23e5951 100644 --- a/packages/circuit-ui/components/Modal/Modal.spec.tsx +++ b/packages/circuit-ui/components/Modal/Modal.spec.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { render, act, userEvent, axe, waitFor } from '../../util/test-utils'; +import { render, userEvent, axe, waitFor } from '../../util/test-utils'; import { Modal, ModalProps } from './Modal'; @@ -24,7 +24,7 @@ describe('Modal', () => { closeButtonLabel: 'Close modal', onClose: jest.fn(), // eslint-disable-next-line react/prop-types, react/display-name - children: () =>

Hello world!

, + children:

Hello world!

, // Silences the warning about the missing app element. // In user land, the modal is always rendered by the ModalProvider, // which takes care of setting the app element. @@ -45,12 +45,10 @@ describe('Modal', () => { }); }); - it('should call the onClose callback', () => { + it('should call the onClose callback', async () => { const { getByRole } = render(); - act(() => { - userEvent.click(getByRole('button')); - }); + await userEvent.click(getByRole('button')); expect(defaultModal.onClose).toHaveBeenCalled(); }); diff --git a/packages/circuit-ui/components/ModalContext/ModalContext.spec.tsx b/packages/circuit-ui/components/ModalContext/ModalContext.spec.tsx index a23f8a69cd..f85f4c0159 100644 --- a/packages/circuit-ui/components/ModalContext/ModalContext.spec.tsx +++ b/packages/circuit-ui/components/ModalContext/ModalContext.spec.tsx @@ -13,11 +13,15 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React, { useContext } from 'react'; +import { useContext } from 'react'; import * as Collector from '@sumup/collector'; -import { render, act, userEvent, fireEvent } from '../../util/test-utils'; +import { + render, + act, + userEvent as baseUserEvent, + fireEvent, +} from '../../util/test-utils'; import { ModalProvider, ModalContext } from './ModalContext'; import type { ModalComponent } from './types'; @@ -43,6 +47,12 @@ describe('ModalContext', () => { jest.clearAllMocks(); }); + /** + * We need to set up userEvent with delay=null to address this issue: + * https://github.com/testing-library/user-event/issues/833 + */ + const userEvent = baseUserEvent.setup({ delay: null }); + describe('ModalProvider', () => { const dispatch = jest.fn(); // @ts-expect-error TypeScript doesn't allow assigning to the read-only @@ -68,7 +78,7 @@ describe('ModalContext', () => { expect(getByRole('dialog')).toBeVisible(); }); - it('should open and close a modal when the context functions are called', () => { + it('should open and close a modal when the context functions are called', async () => { const Trigger = () => { const { setModal, removeModal } = useContext(ModalContext); return ( @@ -85,15 +95,12 @@ describe('ModalContext', () => { , ); - act(() => { - userEvent.click(getByRole('button', { name: 'Open modal' })); - }); + await userEvent.click(getByRole('button', { name: 'Open modal' })); expect(getByRole('dialog')).toBeVisible(); - act(() => { - userEvent.click(getByRole('button', { name: 'Close modal' })); - }); + await userEvent.click(getByRole('button', { name: 'Close modal' })); + act(() => { jest.runAllTimers(); }); @@ -120,16 +127,15 @@ describe('ModalContext', () => { expect(dispatch).toHaveBeenCalledTimes(1); }); - it('should close the modal when the onClose method is called', () => { + it('should close the modal when the onClose method is called', async () => { const { queryByRole } = render(
, ); - act(() => { - userEvent.click(queryByRole('button')); - }); + const closeButton = queryByRole('button') as HTMLButtonElement; + await userEvent.click(closeButton); act(() => { jest.runAllTimers(); }); diff --git a/packages/circuit-ui/components/ModalContext/createUseModal.spec.tsx b/packages/circuit-ui/components/ModalContext/createUseModal.spec.tsx index 1c66a7f175..057a6a198a 100644 --- a/packages/circuit-ui/components/ModalContext/createUseModal.spec.tsx +++ b/packages/circuit-ui/components/ModalContext/createUseModal.spec.tsx @@ -13,10 +13,7 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React from 'react'; - -import { renderHook, actHook } from '../../util/test-utils'; +import { renderHook, act } from '../../util/test-utils'; import { createUseModal } from './createUseModal'; import { ModalContext } from './ModalContext'; @@ -44,7 +41,7 @@ describe('createUseModal', () => { it('should add the modal when setModal is called', () => { const { result } = renderHook(() => useModal(), { wrapper }); - actHook(() => { + act(() => { result.current.setModal({}); }); @@ -58,11 +55,11 @@ describe('createUseModal', () => { it('should remove the modal when removeModal is called', () => { const { result } = renderHook(() => useModal(), { wrapper }); - actHook(() => { + act(() => { result.current.setModal({}); }); - actHook(() => { + act(() => { result.current.removeModal(); }); diff --git a/packages/circuit-ui/components/NotificationBanner/NotificationBanner.spec.tsx b/packages/circuit-ui/components/NotificationBanner/NotificationBanner.spec.tsx index 88dba4c233..cd59b97b86 100644 --- a/packages/circuit-ui/components/NotificationBanner/NotificationBanner.spec.tsx +++ b/packages/circuit-ui/components/NotificationBanner/NotificationBanner.spec.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { render, axe, userEvent, act } from '../../util/test-utils'; +import { render, axe, userEvent } from '../../util/test-utils'; import { NotificationBanner, @@ -75,12 +75,10 @@ describe('NotificationBanner', () => { * Logic tests. */ describe('business logic', () => { - it('should click on a main button', () => { + it('should click on a main button', async () => { const { getByRole } = renderNotificationBanner(baseProps); - act(() => { - userEvent.click(getByRole('button')); - }); + await userEvent.click(getByRole('button')); expect(baseProps.action.onClick).toHaveBeenCalledTimes(1); }); @@ -95,7 +93,7 @@ describe('NotificationBanner', () => { expect(getByRole('button', { name: /close/i })).toBeVisible(); }); - it('should call onClose when closed', () => { + it('should call onClose when closed', async () => { const props = { ...baseProps, onClose: jest.fn(), @@ -103,9 +101,7 @@ describe('NotificationBanner', () => { }; const { getByRole } = renderNotificationBanner(props); - act(() => { - userEvent.click(getByRole('button', { name: /close/i })); - }); + await userEvent.click(getByRole('button', { name: /close/i })); expect(props.onClose).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/NotificationFullscreen/NotificationFullscreen.spec.tsx b/packages/circuit-ui/components/NotificationFullscreen/NotificationFullscreen.spec.tsx index 1210192ed2..4ff9d0fd93 100644 --- a/packages/circuit-ui/components/NotificationFullscreen/NotificationFullscreen.spec.tsx +++ b/packages/circuit-ui/components/NotificationFullscreen/NotificationFullscreen.spec.tsx @@ -13,9 +13,7 @@ * limitations under the License. */ -import React from 'react'; - -import { render, axe, userEvent, act } from '../../util/test-utils'; +import { render, axe, userEvent } from '../../util/test-utils'; import { NotificationFullscreen, @@ -68,24 +66,20 @@ describe('NotificationFullscreen', () => { }); describe('business logic', () => { - it('should click on a primary action button', () => { + it('should click on a primary action button', async () => { const { getByRole } = renderNotificationFullscreen(baseProps); - act(() => { - userEvent.click(getByRole('button', { name: /Look again/i })); - }); + await userEvent.click(getByRole('button', { name: /Look again/i })); expect(getByRole('button', { name: /Look again/i })).toBeVisible(); expect(baseProps.actions.primary.onClick).toHaveBeenCalledTimes(1); }); - it('should click on a secondary action button', () => { + it('should click on a secondary action button', async () => { const { getAllByRole } = renderNotificationFullscreen(baseProps); - act(() => { - userEvent.click(getAllByRole('link', { name: /Go elsewhere/ })[0]); - }); + await userEvent.click(getAllByRole('link', { name: /Go elsewhere/ })[0]); expect(getAllByRole('link', { name: /Go elsewhere/ })[0]).toBeVisible(); diff --git a/packages/circuit-ui/components/NotificationInline/NotificationInline.spec.tsx b/packages/circuit-ui/components/NotificationInline/NotificationInline.spec.tsx index 28ed2fcb65..f646215361 100644 --- a/packages/circuit-ui/components/NotificationInline/NotificationInline.spec.tsx +++ b/packages/circuit-ui/components/NotificationInline/NotificationInline.spec.tsx @@ -13,10 +13,7 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React from 'react'; - -import { act, axe, render, userEvent, waitFor } from '../../util/test-utils'; +import { axe, render, userEvent, waitFor } from '../../util/test-utils'; import { NotificationInline, @@ -108,7 +105,7 @@ describe('NotificationInline', () => { }); describe('business logic', () => { - it('should click on a call to action button', () => { + it('should click on a call to action button', async () => { const props = { ...baseProps, action: { @@ -118,14 +115,12 @@ describe('NotificationInline', () => { }; const { getByRole } = renderNotificationInline(props); - act(() => { - userEvent.click(getByRole('button')); - }); + await userEvent.click(getByRole('button')); expect(props.action.onClick).toHaveBeenCalledTimes(1); }); - it('should close the notification inline when the onClose method is called', () => { + it('should close the notification inline when the onClose method is called', async () => { const props = { ...baseProps, onClose: jest.fn(), @@ -133,9 +128,7 @@ describe('NotificationInline', () => { }; const { getByRole } = renderNotificationInline(props); - act(() => { - userEvent.click(getByRole('button', { name: /close/i })); - }); + await userEvent.click(getByRole('button', { name: /close/i })); expect(props.onClose).toHaveBeenCalled(); }); diff --git a/packages/circuit-ui/components/NotificationModal/NotificationModal.spec.tsx b/packages/circuit-ui/components/NotificationModal/NotificationModal.spec.tsx index 3827145b7d..327d10ae32 100644 --- a/packages/circuit-ui/components/NotificationModal/NotificationModal.spec.tsx +++ b/packages/circuit-ui/components/NotificationModal/NotificationModal.spec.tsx @@ -13,9 +13,7 @@ * limitations under the License. */ -import React from 'react'; - -import { act, axe, render, userEvent, waitFor } from '../../util/test-utils'; +import { axe, render, userEvent, waitFor } from '../../util/test-utils'; import { NotificationModal, NotificationModalProps } from './NotificationModal'; @@ -69,17 +67,15 @@ describe('NotificationModal', () => { const closeButton = await findByRole('button', { name: /Close Modal/i }); - userEvent.click(closeButton); + await userEvent.click(closeButton); expect(baseNotificationModal.onClose).toHaveBeenCalled(); }); - it('should close the modal without performing any action', () => { + it('should close the modal without performing any action', async () => { renderNotificationModal(baseNotificationModal); - act(() => { - userEvent.click(document.body); - }); + await userEvent.click(document.body); expect(baseNotificationModal.onClose).toHaveBeenCalled(); }); @@ -89,7 +85,7 @@ describe('NotificationModal', () => { const actionButton = await findByRole('button', { name: /Primary/i }); - userEvent.click(actionButton); + await userEvent.click(actionButton); expect(baseNotificationModal.actions.primary.onClick).toHaveBeenCalled(); expect(baseNotificationModal.onClose).toHaveBeenCalled(); diff --git a/packages/circuit-ui/components/NotificationToast/NotificationToast.spec.tsx b/packages/circuit-ui/components/NotificationToast/NotificationToast.spec.tsx index e235543efd..dbadc67d81 100644 --- a/packages/circuit-ui/components/NotificationToast/NotificationToast.spec.tsx +++ b/packages/circuit-ui/components/NotificationToast/NotificationToast.spec.tsx @@ -13,10 +13,13 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React from 'react'; - -import { act, axe, userEvent, render, waitFor } from '../../util/test-utils'; +import { + axe, + userEvent, + render, + waitFor, + waitForElementToBeRemoved, +} from '../../util/test-utils'; import Button from '../Button'; import { ToastProvider } from '../ToastContext'; @@ -27,47 +30,43 @@ import { } from './NotificationToast'; describe('NotificationToast', () => { - const renderNotificationToast = (props: NotificationToastProps) => + beforeEach(() => jest.clearAllMocks()); + + const renderStaticNotificationToast = (props: NotificationToastProps) => render(); + const renderNotificationToast = (props: NotificationToastProps) => { + const App = () => { + const { setToast } = useNotificationToast(); + return ( + + ); + }; + + return render( + + + , + ); + }; + const baseNotificationToast: NotificationToastProps = { onClose: jest.fn(), iconLabel: '', isVisible: false, body: 'This is a toast message', }; + describe('styles', () => { it('should render with default styles', () => { - const { baseElement } = renderNotificationToast(baseNotificationToast); + const { baseElement } = renderStaticNotificationToast( + baseNotificationToast, + ); expect(baseElement).toMatchSnapshot(); }); - it('should render the toast', async () => { - const App = () => { - const { setToast } = useNotificationToast(); - return ( - - ); - }; - - const { findByRole, getByText } = render( - - - , - ); - - act(() => { - userEvent.click(getByText('Open toast')); - }); - - const toastEl = await findByRole('status'); - - await waitFor(() => { - expect(toastEl).toBeVisible(); - }); - }); const variants: NotificationToastProps['variant'][] = [ 'info', 'confirm', @@ -75,45 +74,44 @@ describe('NotificationToast', () => { 'alert', ]; - it.each(variants)( - 'should render notification toast with %s styles', - (variant) => { - const { baseElement } = renderNotificationToast({ - ...baseNotificationToast, - variant, - }); - expect(baseElement).toMatchSnapshot(); - }, - ); + it.each(variants)('should render with %s variant styles', (variant) => { + const { baseElement } = renderStaticNotificationToast({ + ...baseNotificationToast, + variant, + }); + expect(baseElement).toMatchSnapshot(); + }); - it('should render notification toast with headline', () => { - const { baseElement } = renderNotificationToast({ + it('should render with a headline', () => { + const { baseElement } = renderStaticNotificationToast({ ...baseNotificationToast, headline: 'Information', }); expect(baseElement).toMatchSnapshot(); }); }); + describe('business logic', () => { - it('should close the toast when the onClose method is called', async () => { - const App = () => { - const { setToast } = useNotificationToast(); - return ( - - ); - }; - - const { getByText } = render( - - - , + /** + * FIXME: these tests should use jest fake timers instead of waiting for + * NotificationToast timers to run. + */ + it('should open a toast', async () => { + const { findByRole, getByText } = renderNotificationToast( + baseNotificationToast, ); - act(() => { - userEvent.click(getByText('Open toast')); - }); + await userEvent.click(getByText('Open toast')); + + const toastEl = await findByRole('status'); + + expect(toastEl).toBeVisible(); + }); + + it('should close the toast when the onClose method is called', async () => { + const { getByText } = renderNotificationToast(baseNotificationToast); + + await userEvent.click(getByText('Open toast')); await waitFor(() => { expect(getByText('This is a toast message')).toBeVisible(); @@ -121,54 +119,45 @@ describe('NotificationToast', () => { const closeButton = getByText('-'); - act(() => { - userEvent.click(closeButton); - }); + await userEvent.click(closeButton); expect(baseNotificationToast.onClose).toHaveBeenCalled(); }); it('should autodismiss toast after the duration has expired', async () => { - const App = () => { - const { setToast } = useNotificationToast(); - return ( - - ); - }; - - const { getByText } = render( - - - , - ); + const { getByText } = renderNotificationToast(baseNotificationToast); - act(() => { - userEvent.click(getByText('Open toast')); - }); + await userEvent.click(getByText('Open toast')); + + const toastElement = getByText('This is a toast message'); await waitFor(() => { - expect(getByText('This is a toast message')).toBeVisible(); + expect(toastElement).toBeVisible(); }); - await waitFor( - () => { - expect(baseNotificationToast.onClose).toHaveBeenCalledTimes(1); - }, - { timeout: 6000 }, - ); - }); + await waitForElementToBeRemoved(toastElement, { + timeout: 10000, + }); + + expect(baseNotificationToast.onClose).toHaveBeenCalledTimes(1); + }, 10000); }); + /** * Accessibility tests. */ describe('accessibility', () => { it('should meet accessibility guidelines', async () => { - const { container } = renderNotificationToast(baseNotificationToast); + const { container, getByText } = renderNotificationToast( + baseNotificationToast, + ); + + await userEvent.click(getByText('Open toast')); + + await waitFor(() => { + expect(getByText('This is a toast message')).toBeVisible(); + }); + const actual = await axe(container); expect(actual).toHaveNoViolations(); }); diff --git a/packages/circuit-ui/components/NotificationToast/__snapshots__/NotificationToast.spec.tsx.snap b/packages/circuit-ui/components/NotificationToast/__snapshots__/NotificationToast.spec.tsx.snap index 8c1878e33f..21aeaf2e45 100644 --- a/packages/circuit-ui/components/NotificationToast/__snapshots__/NotificationToast.spec.tsx.snap +++ b/packages/circuit-ui/components/NotificationToast/__snapshots__/NotificationToast.spec.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NotificationToast styles should render notification toast with alert styles 1`] = ` +exports[`NotificationToast styles should render with a headline 1`] = ` @keyframes animation-0 { 0% { -webkit-transform: rotate(0deg); @@ -20,7 +20,7 @@ exports[`NotificationToast styles should render notification toast with alert st .circuit-0 { background-color: #FFF; border-radius: 8px; - border: 2px solid #D23F47; + border: 2px solid #3063E9; overflow: hidden; will-change: height; -webkit-transition: opacity 200ms ease-in-out,height 200ms ease-in-out,visibility 200ms ease-in-out; @@ -55,7 +55,7 @@ exports[`NotificationToast styles should render notification toast with alert st -ms-flex-negative: 0; flex-shrink: 0; line-height: 0; - color: #D23F47; + color: #3063E9; } .circuit-3 { @@ -90,9 +90,16 @@ exports[`NotificationToast styles should render notification toast with alert st font-weight: 400; font-size: 16px; line-height: 24px; + font-weight: 700; } .circuit-6 { + font-weight: 400; + font-size: 16px; + line-height: 24px; +} + +.circuit-7 { font-size: 16px; line-height: 24px; display: -webkit-inline-box; @@ -139,39 +146,39 @@ exports[`NotificationToast styles should render notification toast with alert st margin-left: auto; } -.circuit-6:focus { +.circuit-7:focus { outline: 0; box-shadow: 0 0 0 4px #AFD0FE; } -.circuit-6:focus::-moz-focus-inner { +.circuit-7:focus::-moz-focus-inner { border: 0; } -.circuit-6:focus:not(:focus-visible) { +.circuit-7:focus:not(:focus-visible) { box-shadow: none; } -.circuit-6:disabled, -.circuit-6[disabled] { +.circuit-7:disabled, +.circuit-7[disabled] { opacity: 0.5; pointer-events: none; box-shadow: none; } -.circuit-6:hover { +.circuit-7:hover { background-color: #F5F5F5; border-color: #666; } -.circuit-6:active, -.circuit-6[aria-expanded='true'], -.circuit-6[aria-pressed='true'] { +.circuit-7:active, +.circuit-7[aria-expanded='true'], +.circuit-7[aria-pressed='true'] { background-color: #E6E6E6; border-color: #333; } -.circuit-7 { +.circuit-8 { display: block; border-radius: 100%; border: 2px solid currentColor; @@ -188,7 +195,7 @@ exports[`NotificationToast styles should render notification toast with alert st transition: opacity 120ms ease-in-out,visibility 120ms ease-in-out; } -.circuit-9 { +.circuit-10 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -228,7 +235,7 @@ exports[`NotificationToast styles should render notification toast with alert st xmlns="http://www.w3.org/2000/svg" > @@ -239,28 +246,33 @@ exports[`NotificationToast styles should render notification toast with alert st
-

+ Information + +

This is a toast message

@@ -1481,7 +1462,7 @@ exports[`NotificationToast styles should render notification toast with notify s `; -exports[`NotificationToast styles should render with default styles 1`] = ` +exports[`NotificationToast styles should render with notify variant styles 1`] = ` @keyframes animation-0 { 0% { -webkit-transform: rotate(0deg); @@ -1501,7 +1482,7 @@ exports[`NotificationToast styles should render with default styles 1`] = ` .circuit-0 { background-color: #FFF; border-radius: 8px; - border: 2px solid #3063E9; + border: 2px solid #F5C625; overflow: hidden; will-change: height; -webkit-transition: opacity 200ms ease-in-out,height 200ms ease-in-out,visibility 200ms ease-in-out; @@ -1536,7 +1517,24 @@ exports[`NotificationToast styles should render with default styles 1`] = ` -ms-flex-negative: 0; flex-shrink: 0; line-height: 0; - color: #3063E9; + color: #F5C625; +} + +.circuit-2::before { + content: ''; + display: block; + position: absolute; + top: 2px; + left: 2px; + width: calc(100% - 4px); + height: calc(100% - 4px); + background: #000; + border-radius: 100%; +} + +.circuit-2 svg { + position: relative; + z-index: 1; } .circuit-3 { @@ -1709,8 +1707,10 @@ exports[`NotificationToast styles should render with default styles 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/packages/circuit-ui/components/Pagination/Pagination.spec.tsx b/packages/circuit-ui/components/Pagination/Pagination.spec.tsx index 6cdad5aebd..ae777e17ed 100644 --- a/packages/circuit-ui/components/Pagination/Pagination.spec.tsx +++ b/packages/circuit-ui/components/Pagination/Pagination.spec.tsx @@ -59,7 +59,7 @@ describe('Pagination', () => { expect(nextButtonEl).toBeDisabled(); }); - it('should go to the previous page', () => { + it('should go to the previous page', async () => { const onChange = jest.fn(); const { getByText } = renderPagination(render, { ...baseProps, @@ -69,19 +69,19 @@ describe('Pagination', () => { const prevButtonEl = getByText('Previous'); - userEvent.click(prevButtonEl); + await userEvent.click(prevButtonEl); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(2); }); - it('should go to the next page', () => { + it('should go to the next page', async () => { const onChange = jest.fn(); const { getByText } = renderPagination(render, { ...baseProps, onChange }); const nextButtonEl = getByText('Next'); - userEvent.click(nextButtonEl); + await userEvent.click(nextButtonEl); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(2); diff --git a/packages/circuit-ui/components/Pagination/components/PageList/PageList.spec.tsx b/packages/circuit-ui/components/Pagination/components/PageList/PageList.spec.tsx index f07eacfa20..d80c8a2603 100644 --- a/packages/circuit-ui/components/Pagination/components/PageList/PageList.spec.tsx +++ b/packages/circuit-ui/components/Pagination/components/PageList/PageList.spec.tsx @@ -44,12 +44,12 @@ describe('PageList', () => { }); describe('business logic', () => { - it('should call the onChange callback', () => { + it('should call the onChange callback', async () => { const onChange = jest.fn(); const { getByText } = renderPageList(render, { ...baseProps, onChange }); const pageFour = getByText('3'); - userEvent.click(pageFour); + await userEvent.click(pageFour); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(3); diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx index b50ef88be7..9bfc4f93ac 100644 --- a/packages/circuit-ui/components/Popover/Popover.spec.tsx +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -15,7 +15,8 @@ /* eslint-disable react/display-name */ -import { Delete, Add, Download } from '@sumup/icons'; +import { FC } from 'react'; +import { Delete, Add, Download, IconProps } from '@sumup/icons'; import { Placement } from '@popperjs/core'; import * as Collector from '@sumup/collector'; @@ -41,7 +42,10 @@ describe('PopoverItem', () => { return renderFn(); } - const baseProps = { children: 'PopoverItem' }; + const baseProps = { + children: 'PopoverItem', + icon: Download as FC, + }; describe('styles', () => { it('should render as Link when an href (and onClick) is passed', () => { @@ -49,7 +53,6 @@ describe('PopoverItem', () => { ...baseProps, href: 'https://sumup.com', onClick: jest.fn(), - icon: Download, }; const { container } = renderPopoverItem(render, props); const anchorEl = container.querySelector('a'); @@ -57,7 +60,7 @@ describe('PopoverItem', () => { }); it('should render as a `button` when an onClick is passed', () => { - const props = { ...baseProps, onClick: jest.fn(), icon: Download }; + const props = { ...baseProps, onClick: jest.fn() }; const { container } = renderPopoverItem(render, props); const buttonEl = container.querySelector('button'); expect(buttonEl).toBeVisible(); @@ -65,19 +68,18 @@ describe('PopoverItem', () => { }); describe('business logic', () => { - it('should call onClick when rendered as Link', () => { + it('should call onClick when rendered as Link', async () => { const props = { ...baseProps, href: 'https://sumup.com', onClick: jest.fn((event: ClickEvent) => { event.preventDefault(); }), - icon: Download, }; const { container } = renderPopoverItem(render, props); const anchorEl = container.querySelector('a'); if (anchorEl) { - userEvent.click(anchorEl); + await userEvent.click(anchorEl); } expect(props.onClick).toHaveBeenCalledTimes(1); }); @@ -109,13 +111,13 @@ describe('Popover', () => { { onClick: jest.fn(), children: 'Add', - icon: Add, + icon: Add as FC, }, { type: 'divider' }, { onClick: jest.fn(), children: 'Remove', - icon: Delete, + icon: Delete as FC, destructive: true, }, ], @@ -125,6 +127,10 @@ describe('Popover', () => { }; describe('styles', () => { + /** + * FIXME: some of these tests, including style snapshots, throw act() + * warnings. We should look into it. + */ it('should render with default styles', () => { const { baseElement } = renderPopover(baseProps); expect(baseElement).toMatchSnapshot(); @@ -143,109 +149,95 @@ describe('Popover', () => { }); describe('business logic', () => { - it('should open the popover when clicking the trigger element', () => { + it('should open the popover when clicking the trigger element', async () => { const isOpen = false; const onToggle = jest.fn(createStateSetter(isOpen)); const { getByRole } = renderPopover({ ...baseProps, isOpen, onToggle }); const popoverTrigger = getByRole('button'); - act(() => { - userEvent.click(popoverTrigger); - }); + await userEvent.click(popoverTrigger); expect(onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }); it.each([ - ['space', '{space}'], - ['enter', '{enter}'], - ['arrow down', '{arrowDown}'], - ['arrow up', '{arrowUp}'], + ['space', '{ }'], + ['enter', '{Enter}'], + ['arrow down', '{ArrowDown}'], + ['arrow up', '{ArrowUp}'], ])( 'should open the popover when pressing the %s key on the trigger element', - (_, key) => { + async (_, key) => { const isOpen = false; const onToggle = jest.fn(createStateSetter(isOpen)); const { getByRole } = renderPopover({ ...baseProps, isOpen, onToggle }); const popoverTrigger = getByRole('button'); - act(() => { - popoverTrigger.focus(); - userEvent.keyboard(key); - }); + popoverTrigger.focus(); + await userEvent.keyboard(key); expect(onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }, ); - it('should close the popover when clicking outside', () => { + it('should close the popover when clicking outside', async () => { renderPopover(baseProps); - act(() => { - userEvent.click(document.body); - }); + await userEvent.click(document.body); expect(baseProps.onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }); - it('should close the popover when clicking the trigger element', () => { + it('should close the popover when clicking the trigger element', async () => { const { getByRole } = renderPopover(baseProps); const popoverTrigger = getByRole('button'); - act(() => { - userEvent.click(popoverTrigger); - }); + await userEvent.click(popoverTrigger); expect(baseProps.onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }); it.each([ - ['space', '{space}'], - ['enter', '{enter}'], - ['arrow up', '{arrowUp}'], + ['space', '{ }'], + ['enter', '{Enter}'], + ['arrow up', '{ArrowUp}'], ])( 'should close the popover when pressing the %s key on the trigger element', - (_, key) => { + async (_, key) => { const { getByRole } = renderPopover(baseProps); const popoverTrigger = getByRole('button'); - act(() => { - popoverTrigger.focus(); - userEvent.keyboard(key); - }); + popoverTrigger.focus(); + await userEvent.keyboard(key); expect(baseProps.onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }, ); - it('should close the popover when clicking the escape key', () => { + it('should close the popover when clicking the escape key', async () => { renderPopover(baseProps); - act(() => { - userEvent.keyboard('{escape}'); - }); + await userEvent.keyboard('{Escape}'); expect(baseProps.onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); }); - it('should close the popover when clicking a popover item', () => { + it('should close the popover when clicking a popover item', async () => { const { getAllByRole } = renderPopover(baseProps); const popoverItems = getAllByRole('menuitem'); - act(() => { - userEvent.click(popoverItems[0]); - }); + await userEvent.click(popoverItems[0]); expect(baseProps.onToggle).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); diff --git a/packages/circuit-ui/components/RadioButton/RadioButton.spec.tsx b/packages/circuit-ui/components/RadioButton/RadioButton.spec.tsx index 81132104e7..7506b4d7ef 100644 --- a/packages/circuit-ui/components/RadioButton/RadioButton.spec.tsx +++ b/packages/circuit-ui/components/RadioButton/RadioButton.spec.tsx @@ -20,7 +20,6 @@ import { renderToHtml, axe, render, - act, userEvent, } from '../../util/test-utils'; @@ -70,7 +69,7 @@ describe('RadioButton', () => { expect(inputEl).not.toHaveAttribute('checked'); }); - it('should call the change handler when clicked', () => { + it('should call the change handler when clicked', async () => { const onChange = jest.fn(); const { getByLabelText } = render( , @@ -79,9 +78,7 @@ describe('RadioButton', () => { exact: false, }); - act(() => { - userEvent.click(inputEl); - }); + await userEvent.click(inputEl); expect(onChange).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/RadioButtonGroup/RadioButtonGroup.spec.tsx b/packages/circuit-ui/components/RadioButtonGroup/RadioButtonGroup.spec.tsx index fa7834ef03..3d0e2f26ad 100644 --- a/packages/circuit-ui/components/RadioButtonGroup/RadioButtonGroup.spec.tsx +++ b/packages/circuit-ui/components/RadioButtonGroup/RadioButtonGroup.spec.tsx @@ -20,7 +20,6 @@ import { renderToHtml, axe, render, - act, userEvent, } from '../../util/test-utils'; @@ -79,15 +78,13 @@ describe('RadioButtonGroup', () => { expect(getByLabelText('Option 3')).toHaveAttribute('required'); }); - it('should call the change handler when clicked', () => { + it('should call the change handler when clicked', async () => { const onChange = jest.fn(); const { getByLabelText } = render( , ); - act(() => { - userEvent.click(getByLabelText('Option 3')); - }); + await userEvent.click(getByLabelText('Option 3')); expect(onChange).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/Selector/Selector.spec.tsx b/packages/circuit-ui/components/Selector/Selector.spec.tsx index b69abc07e3..b2b2c9df3c 100644 --- a/packages/circuit-ui/components/Selector/Selector.spec.tsx +++ b/packages/circuit-ui/components/Selector/Selector.spec.tsx @@ -18,7 +18,6 @@ import { createRef } from 'react'; import { create, render, - act, userEvent, renderToHtml, axe, @@ -100,7 +99,7 @@ describe('Selector', () => { expect(inputEl).not.toHaveAttribute('checked'); }); - it('should call the change handler when clicked', () => { + it('should call the change handler when clicked', async () => { const { getByLabelText } = render( Label @@ -110,9 +109,7 @@ describe('Selector', () => { exact: false, }); - act(() => { - userEvent.click(inputEl); - }); + await userEvent.click(inputEl); expect(defaultProps.onChange).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/SideNavigation/components/MobileNavigation/MobileNavigation.spec.tsx b/packages/circuit-ui/components/SideNavigation/components/MobileNavigation/MobileNavigation.spec.tsx index 8ec6b9f5df..29ab2b1c20 100644 --- a/packages/circuit-ui/components/SideNavigation/components/MobileNavigation/MobileNavigation.spec.tsx +++ b/packages/circuit-ui/components/SideNavigation/components/MobileNavigation/MobileNavigation.spec.tsx @@ -13,7 +13,6 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ import { Home, Shop } from '@sumup/icons'; import { ClickEvent } from '../../../../types/events'; @@ -117,7 +116,7 @@ describe('MobileNavigation', () => { expect(secondaryLinkEl).not.toBeVisible(); - userEvent.click(primaryLinkEl); + await userEvent.click(primaryLinkEl); await waitFor( () => { @@ -126,7 +125,7 @@ describe('MobileNavigation', () => { { timeout: 300 }, ); - userEvent.click(primaryLinkEl); + await userEvent.click(primaryLinkEl); await waitFor( () => { @@ -136,7 +135,7 @@ describe('MobileNavigation', () => { ); }); - it('should close the modal when clicking a primary link', () => { + it('should close the modal when clicking a primary link', async () => { const onClick = jest.fn((event: ClickEvent) => { event.preventDefault(); }); @@ -155,7 +154,7 @@ describe('MobileNavigation', () => { const primaryLinkEl = getByRole('link', { name: /home/i }); - userEvent.click(primaryLinkEl); + await userEvent.click(primaryLinkEl); expect(baseProps.onClose).toHaveBeenCalledTimes(1); expect(onClick).toHaveBeenCalledTimes(1); @@ -195,7 +194,7 @@ describe('MobileNavigation', () => { expect(secondaryLinkEl).not.toBeVisible(); - userEvent.click(primaryLinkEl); + await userEvent.click(primaryLinkEl); await waitFor( () => { @@ -204,7 +203,7 @@ describe('MobileNavigation', () => { { timeout: 300 }, ); - userEvent.click(secondaryLinkEl); + await userEvent.click(secondaryLinkEl); expect(baseProps.onClose).toHaveBeenCalledTimes(1); expect(onClick).toHaveBeenCalledTimes(1); diff --git a/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.spec.tsx b/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.spec.tsx index 4dd64c81d8..124912e31f 100644 --- a/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.spec.tsx +++ b/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.spec.tsx @@ -22,7 +22,6 @@ import { render, axe, RenderFn, - act, userEvent, } from '../../../../util/test-utils'; @@ -86,7 +85,7 @@ describe('PrimaryLink', () => { }); describe('business logic', () => { - it('should call the onClick handler when clicked', () => { + it('should call the onClick handler when clicked', async () => { const props = { ...baseProps, onClick: jest.fn((event: ClickEvent) => { @@ -95,9 +94,7 @@ describe('PrimaryLink', () => { }; const { getByRole } = renderPrimaryLink(render, props); - act(() => { - userEvent.click(getByRole('link')); - }); + await userEvent.click(getByRole('link')); expect(props.onClick).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.spec.tsx b/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.spec.tsx index 4dfdd33620..285d9dc4bb 100644 --- a/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.spec.tsx +++ b/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.spec.tsx @@ -19,7 +19,6 @@ import { render, axe, RenderFn, - act, userEvent, } from '../../../../util/test-utils'; @@ -82,7 +81,7 @@ describe('SecondaryLinks', () => { }); describe('business logic', () => { - it('should call the onClick handler when clicked', () => { + it('should call the onClick handler when clicked', async () => { const onClick = jest.fn((event: ClickEvent) => { event.preventDefault(); }); @@ -101,9 +100,7 @@ describe('SecondaryLinks', () => { }; const { getByRole } = renderSecondaryLinks(render, props); - act(() => { - userEvent.click(getByRole('link')); - }); + await userEvent.click(getByRole('link')); expect(onClick).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/SidePanel/SidePanel.spec.tsx b/packages/circuit-ui/components/SidePanel/SidePanel.spec.tsx index aec8c6c7f7..5a475fd8ba 100644 --- a/packages/circuit-ui/components/SidePanel/SidePanel.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/SidePanel.spec.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { render, act, userEvent, axe } from '../../util/test-utils'; +import { render, userEvent, axe, waitFor } from '../../util/test-utils'; import { SidePanel, SidePanelProps } from './SidePanel'; @@ -21,6 +21,20 @@ jest.mock('../../util/id', () => ({ uniqueId: () => 'the_one', })); +/** + * We need to patch the key event because `react-modal` uses the deprecated + * `KeyboardEvent.keyCode`. + * See https://github.com/testing-library/user-event/issues/969 + */ +function patchKeyEvent(e: KeyboardEvent) { + Object.defineProperty(e, 'keyCode', { + get: () => (e.code === 'Escape' ? 27 : 0), + }); +} +beforeAll(() => { + document.addEventListener('keydown', patchKeyEvent, { capture: true }); +}); + describe('SidePanel', () => { const baseProps: SidePanelProps = { backButtonLabel: 'Back', @@ -32,7 +46,7 @@ describe('SidePanel', () => { isMobile: false, isStacked: false, onBack: undefined, - onClose: undefined, + onClose: () => {}, top: '0px', // Silences the warning about the missing app element. // In user land, the side panel is always rendered by the SidePanelProvider, @@ -69,18 +83,16 @@ describe('SidePanel', () => { expect(getByText(baseProps.headline)).toBeVisible(); }); - it('should call the onClose callback from the close button', () => { + it('should call the onClose callback from the close button', async () => { const onClose = jest.fn(); const { getByTitle } = renderComponent({ onClose }); - act(() => { - userEvent.click(getByTitle(baseProps.closeButtonLabel)); - }); + await userEvent.click(getByTitle(baseProps.closeButtonLabel)); expect(onClose).toHaveBeenCalled(); }); - it('should call the onClose callback from the onClose render prop', () => { + it('should call the onClose callback from the onClose render prop', async () => { const onClose = jest.fn(); const { getByTestId } = renderComponent({ children: ({ onClose: onCloseRenderProp }) => ( @@ -91,29 +103,29 @@ describe('SidePanel', () => { onClose, }); - act(() => { - userEvent.click(getByTestId('close')); - }); + await userEvent.click(getByTestId('close')); expect(onClose).toHaveBeenCalled(); }); - it('should call the onClose callback when Esc is pressed', () => { + it('should call the onClose callback when Esc is pressed', async () => { const onClose = jest.fn(); - renderComponent({ onClose }); + const { getByText } = renderComponent({ onClose }); - act(() => { - userEvent.keyboard('{escape}'); - }); + const sidePanel = getByText('Close'); - expect(onClose).toHaveBeenCalled(); + await waitFor(() => expect(sidePanel).toBeVisible()); + + await userEvent.keyboard('{Escape}'); + + await waitFor(() => expect(onClose).toHaveBeenCalled()); }); describe('when the panel is not stacked', () => { it('should not show the back button', () => { const { queryByTitle } = renderComponent(); - expect(queryByTitle(baseProps.backButtonLabel)).toBeNull(); + expect(queryByTitle(baseProps.backButtonLabel as string)).toBeNull(); }); }); @@ -125,24 +137,22 @@ describe('SidePanel', () => { onBack, }); - expect(getByTitle(baseProps.backButtonLabel)).toBeVisible(); + expect(getByTitle(baseProps.backButtonLabel as string)).toBeVisible(); }); - it('should call the onBack callback from the back button', () => { + it('should call the onBack callback from the back button', async () => { const onBack = jest.fn(); const { getByTitle } = renderComponent({ isStacked: true, onBack, }); - act(() => { - userEvent.click(getByTitle(baseProps.backButtonLabel)); - }); + await userEvent.click(getByTitle(baseProps.backButtonLabel as string)); expect(onBack).toHaveBeenCalled(); }); - it('should call the onBack callback from the onBack render prop', () => { + it('should call the onBack callback from the onBack render prop', async () => { const onBack = jest.fn(); const { getByTestId } = renderComponent({ children: ({ onBack: onBackRenderProp }) => ( @@ -154,23 +164,19 @@ describe('SidePanel', () => { onBack, }); - act(() => { - userEvent.click(getByTestId('back')); - }); + await userEvent.click(getByTestId('back')); expect(onBack).toHaveBeenCalled(); }); - it('should call the onBack callback when Esc is pressed', () => { + it('should call the onBack callback when Esc is pressed', async () => { const onBack = jest.fn(); renderComponent({ isStacked: true, onBack, }); - act(() => { - userEvent.keyboard('{escape}'); - }); + await userEvent.keyboard('{Escape}'); expect(onBack).toHaveBeenCalled(); }); @@ -195,6 +201,9 @@ describe('SidePanel', () => { }); }); + /** + * FIXME: calling axe here can trigger an act() warning. + */ it('should meet accessibility guidelines', async () => { const { container } = renderComponent(); const actual = await axe(container); diff --git a/packages/circuit-ui/components/SidePanel/SidePanelContext.spec.tsx b/packages/circuit-ui/components/SidePanel/SidePanelContext.spec.tsx index 850d343225..484fb9db7d 100644 --- a/packages/circuit-ui/components/SidePanel/SidePanelContext.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/SidePanelContext.spec.tsx @@ -13,10 +13,14 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React, { useContext } from 'react'; +import { useContext } from 'react'; -import { render, act, userEvent, waitFor } from '../../util/test-utils'; +import { + render, + act, + userEvent as baseUserEvent, + waitFor, +} from '../../util/test-utils'; import { uniqueId } from '../../util/id'; import { useMedia } from '../../hooks/useMedia'; @@ -55,6 +59,12 @@ describe('SidePanelContext', () => { jest.resetModules(); }); + /** + * We need to set up userEvent with delay=null to address this issue: + * https://github.com/testing-library/user-event/issues/833 + */ + const userEvent = baseUserEvent.setup({ delay: null }); + describe('SidePanelProvider', () => { const getPanel = () => ({ backButtonLabel: 'Back', @@ -65,6 +75,11 @@ describe('SidePanelContext', () => { id: uniqueId(), onClose: undefined, tracking: undefined, + // Silences the warning about the missing app element. + // In user land, the side panel is always rendered by the SidePanelProvider, + // which takes care of setting the app element. + // http://reactcommunity.org/react-modal/accessibility/#app-element + ariaHideApp: false, }); const renderComponent = (Trigger, props = {}) => @@ -117,7 +132,7 @@ describe('SidePanelContext', () => { expect(baseElement).toMatchSnapshot(); }); - it('should render the side panel and the resized container', () => { + it('should render the side panel and the resized container', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return renderOpenButton(setSidePanel); @@ -125,14 +140,12 @@ describe('SidePanelContext', () => { const { baseElement, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(baseElement).toMatchSnapshot(); }); - it('should render the side panel on mobile resolutions', () => { + it('should render the side panel on mobile resolutions', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return renderOpenButton(setSidePanel); @@ -142,14 +155,12 @@ describe('SidePanelContext', () => { const { baseElement, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(baseElement).toMatchSnapshot(); }); - it('should render the side panel with offset for the top navigation', () => { + it('should render the side panel with offset for the top navigation', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return renderOpenButton(setSidePanel); @@ -159,16 +170,14 @@ describe('SidePanelContext', () => { withTopNavigation: true, }); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(baseElement).toMatchSnapshot(); }); }); describe('setSidePanel', () => { - it('should open a side panel', () => { + it('should open a side panel', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return renderOpenButton(setSidePanel); @@ -176,9 +185,7 @@ describe('SidePanelContext', () => { const { getByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByRole('dialog')).toBeVisible(); }); @@ -191,20 +198,15 @@ describe('SidePanelContext', () => { const { getAllByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); - - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Open panel')); await waitFor(() => { expect(getAllByRole('dialog')).toHaveLength(1); }); }); - it('should open a second side panel', () => { + it('should open a second side panel', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return ( @@ -221,10 +223,8 @@ describe('SidePanelContext', () => { const { getAllByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - userEvent.click(getByText('Open second panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Open second panel')); expect(getAllByRole('dialog')).toHaveLength(2); }); @@ -246,23 +246,19 @@ describe('SidePanelContext', () => { const { getAllByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - userEvent.click(getByText('Open second panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Open second panel')); expect(getAllByRole('dialog')).toHaveLength(2); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); await waitFor(() => { expect(getAllByRole('dialog')).toHaveLength(1); }); }); - it('should send an open tracking event', () => { + it('should send an open tracking event', async () => { const Trigger = () => { const { setSidePanel } = useContext(SidePanelContext); return renderOpenButton(setSidePanel, { @@ -272,9 +268,7 @@ describe('SidePanelContext', () => { const { getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(mockSendEvent).toHaveBeenCalledWith({ component: 'side-panel', @@ -284,7 +278,7 @@ describe('SidePanelContext', () => { }); describe('removeSidePanel', () => { - it('should close the side panel', () => { + it('should close the side panel', async () => { const Trigger = () => { const { setSidePanel, removeSidePanel } = useContext(SidePanelContext); @@ -298,15 +292,12 @@ describe('SidePanelContext', () => { const { getByRole, queryByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByRole('dialog')).toBeVisible(); - act(() => { - userEvent.click(getByText('Close panel')); - }); + await userEvent.click(getByText('Close panel')); + act(() => { jest.runAllTimers(); }); @@ -314,7 +305,7 @@ describe('SidePanelContext', () => { expect(queryByRole('dialog')).toBeNull(); }); - it('should close all side panels stacked above the one being closed', () => { + it('should close all side panels stacked above the one being closed', async () => { const Trigger = () => { const { setSidePanel, removeSidePanel } = useContext(SidePanelContext); @@ -334,16 +325,12 @@ describe('SidePanelContext', () => { const { getAllByRole, queryByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - userEvent.click(getByText('Open second panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Open second panel')); expect(getAllByRole('dialog')).toHaveLength(2); - act(() => { - userEvent.click(getByText('Close panel')); - }); + await userEvent.click(getByText('Close panel')); act(() => { jest.runAllTimers(); }); @@ -351,7 +338,7 @@ describe('SidePanelContext', () => { expect(queryByRole('dialog')).toBeNull(); }); - it('should not close side panels stacked below the one being closed', () => { + it('should not close side panels stacked below the one being closed', async () => { const Trigger = () => { const { setSidePanel, removeSidePanel } = useContext(SidePanelContext); @@ -370,16 +357,12 @@ describe('SidePanelContext', () => { const { getAllByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - userEvent.click(getByText('Open second panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Open second panel')); expect(getAllByRole('dialog')).toHaveLength(2); - act(() => { - userEvent.click(getByText('Close panel')); - }); + await userEvent.click(getByText('Close panel')); act(() => { jest.runAllTimers(); }); @@ -387,7 +370,7 @@ describe('SidePanelContext', () => { expect(getAllByRole('dialog')).toHaveLength(1); }); - it('should not close the side panel when there is no match', () => { + it('should not close the side panel when there is no match', async () => { const Trigger = () => { const { setSidePanel, removeSidePanel } = useContext(SidePanelContext); @@ -401,21 +384,19 @@ describe('SidePanelContext', () => { const { getByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByRole('dialog')).toBeVisible(); + await userEvent.click(getByText('Close panel')); act(() => { - userEvent.click(getByText('Close panel')); jest.runAllTimers(); }); expect(getByRole('dialog')).toBeVisible(); }); - it('should call the onClose callback of the side panel', () => { + it('should call the onClose callback of the side panel', async () => { const onClose = jest.fn(); const Trigger = () => { const { setSidePanel, removeSidePanel } = @@ -430,19 +411,17 @@ describe('SidePanelContext', () => { const { getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); + await userEvent.click(getByText('Close panel')); act(() => { - userEvent.click(getByText('Close panel')); jest.runAllTimers(); }); expect(onClose).toHaveBeenCalled(); }); - it('should send a close tracking event', () => { + it('should send a close tracking event', async () => { const Trigger = () => { const { setSidePanel, removeSidePanel } = useContext(SidePanelContext); @@ -458,14 +437,12 @@ describe('SidePanelContext', () => { const { getByRole, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByRole('dialog')).toBeVisible(); + await userEvent.click(getByText('Close panel')); act(() => { - userEvent.click(getByText('Close panel')); jest.runAllTimers(); }); @@ -477,7 +454,7 @@ describe('SidePanelContext', () => { }); describe('updateSidePanel', () => { - it('should update the side panel', () => { + it('should update the side panel', async () => { const Trigger = () => { const { setSidePanel, updateSidePanel } = useContext(SidePanelContext); @@ -493,21 +470,19 @@ describe('SidePanelContext', () => { const { getByTestId, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByTestId('children')).toHaveTextContent('Side panel content'); + await userEvent.click(getByText('Update panel')); act(() => { - userEvent.click(getByText('Update panel')); jest.runAllTimers(); }); expect(getByTestId('children')).toHaveTextContent('Updated content'); }); - it('should not update the side panel when there is no match', () => { + it('should not update the side panel when there is no match', async () => { const Trigger = () => { const { setSidePanel, updateSidePanel } = useContext(SidePanelContext); @@ -527,14 +502,12 @@ describe('SidePanelContext', () => { const { getByTestId, getByText } = renderComponent(Trigger); - act(() => { - userEvent.click(getByText('Open panel')); - }); + await userEvent.click(getByText('Open panel')); expect(getByTestId('children')).toHaveTextContent('Side panel content'); + await userEvent.click(getByText('Update panel')); act(() => { - userEvent.click(getByText('Update panel')); jest.runAllTimers(); }); diff --git a/packages/circuit-ui/components/SidePanel/components/DesktopSidePanel/DesktopSidePanel.spec.tsx b/packages/circuit-ui/components/SidePanel/components/DesktopSidePanel/DesktopSidePanel.spec.tsx index b39b2fb5a4..5a566c9db0 100644 --- a/packages/circuit-ui/components/SidePanel/components/DesktopSidePanel/DesktopSidePanel.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/components/DesktopSidePanel/DesktopSidePanel.spec.tsx @@ -70,6 +70,9 @@ describe('DesktopSidePanel', () => { expect(getByRole('dialog')).toHaveAttribute('aria-modal', 'true'); }); + /** + * FIXME: calling axe here can trigger an act() warning. + */ it('should meet accessibility guidelines', async () => { jest.useRealTimers(); const { container } = renderComponent(); diff --git a/packages/circuit-ui/components/SidePanel/components/Header/Header.spec.tsx b/packages/circuit-ui/components/SidePanel/components/Header/Header.spec.tsx index 72d2bd14c1..4af34366e3 100644 --- a/packages/circuit-ui/components/SidePanel/components/Header/Header.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/components/Header/Header.spec.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { render, act, userEvent, axe } from '../../../../util/test-utils'; +import { render, userEvent, axe } from '../../../../util/test-utils'; import { Header, HeaderProps } from './Header'; @@ -46,13 +46,11 @@ describe('Header', () => { expect(getByText(baseProps.headline)).toBeVisible(); }); - it('should call the onClose callback from the close button', () => { + it('should call the onClose callback from the close button', async () => { const onClose = jest.fn(); const { getByTitle } = renderComponent({ onClose }); - act(() => { - userEvent.click(getByTitle(baseProps.closeButtonLabel)); - }); + await userEvent.click(getByTitle(baseProps.closeButtonLabel)); expect(onClose).toHaveBeenCalled(); }); @@ -66,15 +64,13 @@ describe('Header', () => { expect(getByTitle(baseProps.backButtonLabel)).toBeVisible(); }); - it('should call the onBack callback from the back button', () => { + it('should call the onBack callback from the back button', async () => { const onBack = jest.fn(); const { getByTitle } = renderComponent({ onBack, }); - act(() => { - userEvent.click(getByTitle(baseProps.backButtonLabel)); - }); + await userEvent.click(getByTitle(baseProps.backButtonLabel)); expect(onBack).toHaveBeenCalled(); }); diff --git a/packages/circuit-ui/components/SidePanel/components/MobileSidePanel/MobileSidePanel.spec.tsx b/packages/circuit-ui/components/SidePanel/components/MobileSidePanel/MobileSidePanel.spec.tsx index ffb14580d1..5b3bc9afc1 100644 --- a/packages/circuit-ui/components/SidePanel/components/MobileSidePanel/MobileSidePanel.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/components/MobileSidePanel/MobileSidePanel.spec.tsx @@ -103,6 +103,9 @@ describe('MobileSidePanel', () => { expect(getByRole('dialog')).toHaveAttribute('aria-modal', 'true'); }); + /** + * FIXME: calling axe here can trigger an act() warning. + */ it('should meet accessibility guidelines', async () => { const { container } = renderComponent(); const actual = await axe(container); diff --git a/packages/circuit-ui/components/SidePanel/useSidePanel.spec.tsx b/packages/circuit-ui/components/SidePanel/useSidePanel.spec.tsx index b45277a0db..a2da9358ea 100644 --- a/packages/circuit-ui/components/SidePanel/useSidePanel.spec.tsx +++ b/packages/circuit-ui/components/SidePanel/useSidePanel.spec.tsx @@ -13,10 +13,7 @@ * limitations under the License. */ -/* eslint-disable react/display-name */ -import React from 'react'; - -import { renderHook, actHook } from '../../util/test-utils'; +import { renderHook } from '../../util/test-utils'; import { useSidePanel } from './useSidePanel'; import { SidePanelContext } from './SidePanelContext'; @@ -60,9 +57,7 @@ describe('useSidePanel', () => { it('should open the side panel when setSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.setSidePanel(panel); - }); + result.current.setSidePanel(panel); const expected = { ...panel, @@ -75,10 +70,8 @@ describe('useSidePanel', () => { it('should open the side panel of a given group when setSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.setSidePanel(panel); - result.current.setSidePanel({ ...panel, group: testId }); - }); + result.current.setSidePanel(panel); + result.current.setSidePanel({ ...panel, group: testId }); const expected = { ...panel, @@ -91,10 +84,8 @@ describe('useSidePanel', () => { it('should update the side panel when updateSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.updateSidePanel({ - children:

Updated content

, - }); + result.current.updateSidePanel({ + children:

Updated content

, }); const expected = { @@ -107,11 +98,9 @@ describe('useSidePanel', () => { it('should update the side panel of a given group when updateSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.updateSidePanel({ - children:

Updated content

, - group: testId, - }); + result.current.updateSidePanel({ + children:

Updated content

, + group: testId, }); const expected = { @@ -124,13 +113,8 @@ describe('useSidePanel', () => { it('should remove the side panel when removeSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.setSidePanel(panel); - }); - - actHook(() => { - result.current.removeSidePanel(); - }); + result.current.setSidePanel(panel); + result.current.removeSidePanel(); const expected = defaultId; expect(removeSidePanel).toHaveBeenCalledWith(expected); @@ -139,29 +123,23 @@ describe('useSidePanel', () => { it('should remove the side panel of a given group when removeSidePanel is called', () => { const { result } = renderHook(() => useSidePanel(), { wrapper }); - actHook(() => { - result.current.setSidePanel(panel); - result.current.setSidePanel({ ...panel, group: testId }); - }); + result.current.setSidePanel(panel); + result.current.setSidePanel({ ...panel, group: testId }); - actHook(() => { - result.current.removeSidePanel(testId); - }); + result.current.removeSidePanel(testId); const expected = testId; expect(removeSidePanel).toHaveBeenCalledWith(expected); }); it('should remove the side panel when the component is unmounted', () => { - const { result, unmount } = renderHook(() => useSidePanel(), { wrapper }); - - actHook(() => { - result.current.setSidePanel(panel); + const { result, unmount } = renderHook(() => useSidePanel(), { + wrapper, }); - actHook(() => { - unmount(); - }); + result.current.setSidePanel(panel); + + unmount(); const expected = defaultId; expect(removeSidePanel).toHaveBeenCalledWith(expected); diff --git a/packages/circuit-ui/components/Sidebar/components/Aggregator/Aggregator.spec.tsx b/packages/circuit-ui/components/Sidebar/components/Aggregator/Aggregator.spec.tsx index 0934a8296f..4c3a739191 100644 --- a/packages/circuit-ui/components/Sidebar/components/Aggregator/Aggregator.spec.tsx +++ b/packages/circuit-ui/components/Sidebar/components/Aggregator/Aggregator.spec.tsx @@ -18,7 +18,6 @@ import { renderToHtml, axe, render, - act, userEvent, RenderFn, } from '../../../../util/test-utils'; @@ -58,13 +57,11 @@ describe('Aggregator', () => { expect(actual).toMatchSnapshot(); }); - it('should render and match snapshot when open', () => { + it('should render and match snapshot when open', async () => { const { container, getByTestId } = renderComponent(render); const aggregatorEl = getByTestId('aggregator'); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(container.children).toMatchSnapshot(); }); @@ -77,7 +74,7 @@ describe('Aggregator', () => { }); describe('interactions', () => { - it('should show children and call onClick when clicking the aggregator', () => { + it('should show children and call onClick when clicking the aggregator', async () => { const onClick = jest.fn(); const { getByTestId } = renderComponent(render, { onClick }); const aggregatorEl = getByTestId('aggregator'); @@ -85,15 +82,13 @@ describe('Aggregator', () => { expect(childEl).not.toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(childEl).toBeVisible(); expect(onClick).toHaveBeenCalledTimes(1); }); - it('should show children when clicking the aggregator and no onClick handle is passed', () => { + it('should show children when clicking the aggregator and no onClick handle is passed', async () => { const { getByTestId } = renderComponent(render, { onClick: undefined, }); @@ -102,14 +97,12 @@ describe('Aggregator', () => { expect(childEl).not.toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(childEl).toBeVisible(); }); - it('should not toggle when clicking again on the aggregator with a selected child', () => { + it('should not toggle when clicking again on the aggregator with a selected child', async () => { const children = ( child @@ -123,21 +116,17 @@ describe('Aggregator', () => { expect(childEl).toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(childEl).toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(onClick).toHaveBeenCalledTimes(2); expect(childEl).toBeVisible(); }); - it('should close when there are no selected children', () => { + it('should close when there are no selected children', async () => { const onClick = jest.fn(); const { getByTestId } = renderComponent(render, { onClick }); const aggregatorEl = getByTestId('aggregator'); @@ -145,15 +134,11 @@ describe('Aggregator', () => { expect(childEl).not.toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(childEl).toBeVisible(); - act(() => { - userEvent.click(aggregatorEl); - }); + await userEvent.click(aggregatorEl); expect(onClick).toHaveBeenCalledTimes(2); expect(childEl).not.toBeVisible(); diff --git a/packages/circuit-ui/components/Step/hooks/useStep.spec.ts b/packages/circuit-ui/components/Step/hooks/useStep.spec.ts index ea7f53c739..f1ab33c8a5 100644 --- a/packages/circuit-ui/components/Step/hooks/useStep.spec.ts +++ b/packages/circuit-ui/components/Step/hooks/useStep.spec.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, renderHook, waitFor } from '../../../util/test-utils'; import { useStep } from './useStep'; @@ -36,24 +36,6 @@ describe('useStep', () => { unmount(); }); - it('should warn if cycle is used without totalSteps prop in dev environment', () => { - const { result } = renderHook(() => useStep({ cycle: true })); - const expectedError = new Error( - 'Cannot use `cycle` prop without `totalSteps` prop.', - ); - - expect(result.error).toEqual(expectedError); - }); - - it('should warn if autoPlay is used without stepDuration prop', () => { - const { result } = renderHook(() => useStep({ autoPlay: true })); - const expectedError = Error( - 'Cannot use `autoPlay` prop without `stepDuration` prop.', - ); - - expect(result.error).toEqual(expectedError); - }); - it('should return actions and prop getters', () => { const expected = expect.objectContaining({ actions: expect.objectContaining({ @@ -214,11 +196,10 @@ describe('useStep', () => { unmount(); }); - // eslint-disable-next-line max-len it('should automatically change steps based on step and animation duration', async () => { const initialStep = 1; const stepInterval = 1; - const { result, waitForNextUpdate, unmount } = renderHook(() => + const { result, unmount } = renderHook(() => useStep({ initialStep, stepInterval, @@ -228,16 +209,17 @@ describe('useStep', () => { }), ); - expect(result.current.state.paused).toEqual(false); expect(result.current.state.step).toEqual(initialStep); + expect(result.current.state.paused).toEqual(false); expect(result.current.state.previousStep).toEqual( initialStep - stepInterval, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.state.step).toEqual(initialStep + stepInterval); + }); expect(result.current.state.paused).toEqual(false); - expect(result.current.state.step).toEqual(initialStep + stepInterval); expect(result.current.state.previousStep).toEqual(initialStep); act(() => { @@ -251,11 +233,10 @@ describe('useStep', () => { unmount(); }); - // eslint-disable-next-line max-len it('should accept functions for step and animation duration', async () => { const initialStep = 1; const stepInterval = 1; - const { result, waitForNextUpdate, unmount } = renderHook(() => + const { result, unmount } = renderHook(() => useStep({ initialStep, stepInterval, @@ -265,16 +246,17 @@ describe('useStep', () => { }), ); - expect(result.current.state.paused).toEqual(false); expect(result.current.state.step).toEqual(initialStep); + expect(result.current.state.paused).toEqual(false); expect(result.current.state.previousStep).toEqual( initialStep - stepInterval, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.state.step).toEqual(initialStep + stepInterval); + }); expect(result.current.state.paused).toEqual(false); - expect(result.current.state.step).toEqual(initialStep + stepInterval); expect(result.current.state.previousStep).toEqual(initialStep); unmount(); diff --git a/packages/circuit-ui/components/Table/Table.spec.tsx b/packages/circuit-ui/components/Table/Table.spec.tsx index 9e6dd63613..43bb3ed877 100644 --- a/packages/circuit-ui/components/Table/Table.spec.tsx +++ b/packages/circuit-ui/components/Table/Table.spec.tsx @@ -18,7 +18,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../util/test-utils'; import Badge from '../Badge'; @@ -125,7 +124,7 @@ describe('Table', () => { }); describe('Interaction tests', () => { - it('should call the row click callback', () => { + it('should call the row click callback', async () => { const onRowClickMock = jest.fn(); const index = 0; const { getAllByRole } = render( @@ -134,17 +133,15 @@ describe('Table', () => { const rowElements = getAllByRole('row'); - act(() => { - // rowElements[0] is the hidden first row - userEvent.click(rowElements[1]); - }); + // rowElements[0] is the hidden first row + await userEvent.click(rowElements[1]); expect(onRowClickMock).toHaveBeenCalledTimes(1); expect(onRowClickMock).toHaveBeenCalledWith(index); }); describe('sorting', () => { - it('should sort a column in ascending order', () => { + it('should sort a column in ascending order', async () => { const { getAllByRole } = render( , ); @@ -152,9 +149,7 @@ describe('Table', () => { const letterHeaderEl = getAllByRole('columnheader')[0]; const cellEls = getAllByRole('cell'); - act(() => { - userEvent.click(letterHeaderEl); - }); + await userEvent.click(letterHeaderEl); const sortedRow = ['a', 'b', 'c']; @@ -185,7 +180,7 @@ describe('Table', () => { }); }); - it('should sort a column in descending order', () => { + it('should sort a column in descending order', async () => { const { getAllByRole } = render(
, ); @@ -193,12 +188,8 @@ describe('Table', () => { const letterHeaderEl = getAllByRole('columnheader')[0]; const cellEls = getAllByRole('cell'); - act(() => { - userEvent.click(letterHeaderEl); - }); - act(() => { - userEvent.click(letterHeaderEl); - }); + await userEvent.click(letterHeaderEl); + await userEvent.click(letterHeaderEl); const sortedRow = ['c', 'b', 'a']; @@ -229,7 +220,7 @@ describe('Table', () => { }); }); - it('should call a custom sort callback', () => { + it('should call a custom sort callback', async () => { const onSortByMock = jest.fn(); const index = 0; const nextDirection = 'ascending'; @@ -239,9 +230,7 @@ describe('Table', () => { const headerElements = getAllByRole('columnheader'); - act(() => { - userEvent.click(headerElements[0]); - }); + await userEvent.click(headerElements[0]); expect(onSortByMock).toHaveBeenCalledTimes(1); expect(onSortByMock).toHaveBeenCalledWith(index, rows, nextDirection); diff --git a/packages/circuit-ui/components/Table/components/SortArrow/SortArrow.spec.tsx b/packages/circuit-ui/components/Table/components/SortArrow/SortArrow.spec.tsx index 4c26adeece..01b4fb0776 100644 --- a/packages/circuit-ui/components/Table/components/SortArrow/SortArrow.spec.tsx +++ b/packages/circuit-ui/components/Table/components/SortArrow/SortArrow.spec.tsx @@ -18,7 +18,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../../../util/test-utils'; @@ -43,14 +42,12 @@ describe('SortArrow', () => { }); describe('Logic tests', () => { - it('should call the onClick callback', () => { + it('should call the onClick callback', async () => { const onClick = jest.fn(); const { getByTestId } = render( , ); - act(() => { - userEvent.click(getByTestId('sort')); - }); + await userEvent.click(getByTestId('sort')); expect(onClick).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/circuit-ui/components/Table/components/TableHead/TableHead.spec.tsx b/packages/circuit-ui/components/Table/components/TableHead/TableHead.spec.tsx index bdbd8ea8b4..e592c5ecdb 100644 --- a/packages/circuit-ui/components/Table/components/TableHead/TableHead.spec.tsx +++ b/packages/circuit-ui/components/Table/components/TableHead/TableHead.spec.tsx @@ -18,7 +18,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../../../util/test-utils'; import { Cell, Direction } from '../../types'; @@ -49,30 +48,26 @@ describe('TableHead', () => { }); describe('onClick', () => { - it('should not dispatch the onSortBy handler when the column is not sortable', () => { + it('should not dispatch the onSortBy handler when the column is not sortable', async () => { const headers = ['Foo']; const onSortByMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.click(getByRole('columnheader')); - }); + await userEvent.click(getByRole('columnheader')); expect(onSortByMock).not.toHaveBeenCalled(); }); - it('should dispatch the onSortBy handler', () => { + it('should dispatch the onSortBy handler', async () => { const headers: Cell[] = [{ children: 'Foo', sortable: true, sortLabel }]; const onSortByMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.click(getByRole('columnheader')); - }); + await userEvent.click(getByRole('columnheader')); expect(onSortByMock).toHaveBeenCalledTimes(1); expect(onSortByMock).toHaveBeenCalledWith(0); @@ -80,30 +75,26 @@ describe('TableHead', () => { }); describe('onSortEnter', () => { - it('should not dispatch the onSortEnter handler when the column is not sortable', () => { + it('should not dispatch the onSortEnter handler when the column is not sortable', async () => { const headers = ['Foo']; const onSortEnterMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.hover(getByRole('columnheader')); - }); + await userEvent.hover(getByRole('columnheader')); expect(onSortEnterMock).not.toHaveBeenCalled(); }); - it('should dispatch the onSortEnter handler', () => { + it('should dispatch the onSortEnter handler', async () => { const headers: Cell[] = [{ children: 'Foo', sortable: true, sortLabel }]; const onSortEnterMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.hover(getByRole('columnheader')); - }); + await userEvent.hover(getByRole('columnheader')); expect(onSortEnterMock).toHaveBeenCalledTimes(1); expect(onSortEnterMock).toHaveBeenCalledWith(0); @@ -111,31 +102,27 @@ describe('TableHead', () => { }); describe('onSortLeave', () => { - it('should dispatch the onSortLeave handler', () => { + it('should dispatch the onSortLeave handler', async () => { const headers: Cell[] = [{ children: 'Foo', sortable: true, sortLabel }]; const onSortLeaveMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.unhover(getByRole('columnheader')); - }); + await userEvent.unhover(getByRole('columnheader')); expect(onSortLeaveMock).toHaveBeenCalledTimes(1); expect(onSortLeaveMock).toHaveBeenCalledWith(0); }); - it('should not dispatch the onSortLeave handler when the column is not sortable', () => { + it('should not dispatch the onSortLeave handler when the column is not sortable', async () => { const headers = ['Foo']; const onSortLeaveMock = jest.fn(); const { getByRole } = render( , ); - act(() => { - userEvent.unhover(getByRole('columnheader')); - }); + await userEvent.unhover(getByRole('columnheader')); expect(onSortLeaveMock).not.toHaveBeenCalled(); }); diff --git a/packages/circuit-ui/components/Table/components/TableRow/TableRow.spec.tsx b/packages/circuit-ui/components/Table/components/TableRow/TableRow.spec.tsx index 2382d2992f..e9dc86b201 100644 --- a/packages/circuit-ui/components/Table/components/TableRow/TableRow.spec.tsx +++ b/packages/circuit-ui/components/Table/components/TableRow/TableRow.spec.tsx @@ -18,7 +18,6 @@ import { render, renderToHtml, axe, - act, userEvent, } from '../../../../util/test-utils'; @@ -42,7 +41,7 @@ describe('TableRow', () => { }); describe('Logic tests', () => { - it('should call the onClick when clicked', () => { + it('should call the onClick when clicked', async () => { const onClick = jest.fn(); const { getByTestId } = render( @@ -51,14 +50,13 @@ describe('TableRow', () => { ); const rowEl = getByTestId('row'); - act(() => { - rowEl.focus(); - userEvent.click(rowEl); - }); + rowEl.focus(); + await userEvent.click(rowEl); + expect(onClick).toHaveBeenCalledTimes(1); }); - it('should call the onClick when navigating with the keyboard', () => { + it('should call the onClick when navigating with the keyboard', async () => { const onClick = jest.fn(); const { getByTestId } = render( @@ -67,11 +65,9 @@ describe('TableRow', () => { ); const rowEl = getByTestId('row'); - act(() => { - rowEl.focus(); - userEvent.type(rowEl, '{enter}'); - userEvent.type(rowEl, ' '); - }); + rowEl.focus(); + await userEvent.type(rowEl, '{enter}'); + await userEvent.type(rowEl, ' '); expect(onClick).toHaveBeenCalledTimes(2); }); diff --git a/packages/circuit-ui/components/Tag/Tag.spec.tsx b/packages/circuit-ui/components/Tag/Tag.spec.tsx index 200e7dc91f..1b5036e4c7 100644 --- a/packages/circuit-ui/components/Tag/Tag.spec.tsx +++ b/packages/circuit-ui/components/Tag/Tag.spec.tsx @@ -20,7 +20,6 @@ import { renderToHtml, axe, render, - act, userEvent, } from '../../util/test-utils'; @@ -115,12 +114,10 @@ describe('Tag', () => { expect(getByTestId('tag-close')).not.toBeNull(); }); - it('should call onRemove when closed', () => { + it('should call onRemove when closed', async () => { const { getByTestId } = render(SomeTest); - act(() => { - userEvent.click(getByTestId('tag-close')); - }); + await userEvent.click(getByTestId('tag-close')); expect(props.onRemove).toHaveBeenCalledTimes(1); }); diff --git a/packages/circuit-ui/components/TextArea/useAutoExpand.spec.tsx b/packages/circuit-ui/components/TextArea/useAutoExpand.spec.tsx index bb50bfd0b4..4464585d3d 100644 --- a/packages/circuit-ui/components/TextArea/useAutoExpand.spec.tsx +++ b/packages/circuit-ui/components/TextArea/useAutoExpand.spec.tsx @@ -13,20 +13,24 @@ * limitations under the License. */ -import { renderHook } from '@testing-library/react-hooks'; -import userEvent from '@testing-library/user-event'; -import React, { FormEvent } from 'react'; -import { render, screen } from '@testing-library/react'; +import { MutableRefObject, FormEvent } from 'react'; +import { renderHook, userEvent, render, screen } from '../../util/test-utils'; import { InputElement } from '../Input/Input'; +import { TextArea, TextAreaProps } from './TextArea'; import { useAutoExpand } from './useAutoExpand'; +const baseTextareaProps: TextAreaProps = { + label: 'Test', + noMargin: true, +}; + const createTextAreaRef = (props = {}) => { - render(