diff --git a/src/about/AboutDialog.test.tsx b/src/about/AboutDialog.test.tsx index e1c6e169..d47465db 100644 --- a/src/about/AboutDialog.test.tsx +++ b/src/about/AboutDialog.test.tsx @@ -1,59 +1,29 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021 The Pybricks Authors +// Copyright (c) 2021-2022 The Pybricks Authors -import { I18nContext, I18nManager } from '@shopify/react-i18n'; -import { - getByLabelText, - render, - screen, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { getByLabelText, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; +import { testRender } from '../../test'; import AboutDialog from './AboutDialog'; it('should close when the button is clicked', () => { - const store = { - getState: jest.fn(() => ({ licenses: { list: null } })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - const i18n = new I18nManager({ locale: 'en' }); const close = jest.fn(); - render( - - - close()} /> - - , - ); - userEvent.click(screen.getByLabelText('Close')); + const dialog = testRender( close()} />); + + userEvent.click(dialog.getByLabelText('Close')); expect(close).toHaveBeenCalled(); }); it('should manage license dialog open/close', async () => { - const store = { - getState: jest.fn(() => ({ licenses: { list: null } })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - const i18n = new I18nManager({ locale: 'en' }); - render( - - - undefined} /> - - , - ); + const dialog = testRender( undefined} />); - userEvent.click(screen.getByText('Software Licenses')); + userEvent.click(dialog.getByText('Software Licenses')); expect( - screen.getByText( + dialog.getByText( `${process.env.REACT_APP_NAME} is built on open source software.`, { exact: false, @@ -67,7 +37,7 @@ it('should manage license dialog open/close', async () => { userEvent.click(getByLabelText(licenseDialog, 'Close')); await waitForElementToBeRemoved(() => - screen.queryByText( + dialog.queryByText( `${process.env.REACT_APP_NAME} is built on open source software.`, { exact: false, diff --git a/src/app/App.test.tsx b/src/app/App.test.tsx index 9de4a0c6..50dddbe0 100644 --- a/src/app/App.test.tsx +++ b/src/app/App.test.tsx @@ -1,27 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021 The Pybricks Authors +// Copyright (c) 2021-2022 The Pybricks Authors -import { I18nContext, I18nManager } from '@shopify/react-i18n'; -import { render } from '@testing-library/react'; -import { mock } from 'jest-mock-extended'; import React from 'react'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; -import { RootState } from '../reducers'; +import { testRender } from '../../test'; import App from './App'; it.each([false, true])('should render', (darkMode) => { - const store = mock>({ - getState: () => mock({ settings: { darkMode } }), - }); - - const i18n = new I18nManager({ locale: 'en' }); - - render( - - - - - , - ); + testRender(, { settings: { darkMode } }); }); diff --git a/src/editor/Editor.test.tsx b/src/editor/Editor.test.tsx index dd8e318d..bf6b4bd7 100644 --- a/src/editor/Editor.test.tsx +++ b/src/editor/Editor.test.tsx @@ -1,102 +1,52 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021 The Pybricks Authors +// Copyright (c) 2021-2022 The Pybricks Authors -import { I18nContext, I18nManager } from '@shopify/react-i18n'; import { + RenderResult, fireEvent, - render, - screen, waitFor, waitForElementToBeRemoved, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; +import { testRender } from '../../test'; import Editor from './Editor'; -function getTextArea(): HTMLTextAreaElement { +function getTextArea(editor: RenderResult): HTMLTextAreaElement { // the textarea in ace editor doesn't actually have any contents, but // it gets the focus for input. - return screen.getByDisplayValue('') as HTMLTextAreaElement; + return editor.getByDisplayValue('') as HTMLTextAreaElement; } it('should focus the text area', () => { - const store = { - getState: jest.fn(() => ({ - editor: { current: null }, - settings: { darkMode: false, showDocs: false }, - })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - const i18n = new I18nManager({ locale: 'en' }); - render( - - - - - , - ); + const editor = testRender(); - expect(getTextArea()).toHaveFocus(); + expect(getTextArea(editor)).toHaveFocus(); }); describe('context menu', () => { it('should show the context menu', async () => { - const store = { - getState: jest.fn(() => ({ - editor: { current: null }, - settings: { darkMode: false, showDocs: false }, - })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - const i18n = new I18nManager({ locale: 'en' }); + const editor = testRender(); - render( - - - - - , - ); - - fireEvent.contextMenu(screen.getByText('Write your program here...')); + fireEvent.contextMenu(editor.getByText('Write your program here...')); await waitFor(() => { - expect(screen.getByText('Copy')).toBeInTheDocument(); + expect(editor.getByText('Copy')).toBeInTheDocument(); }); }); it('should hide the context menu when Escape is pressed', async () => { - const store = { - getState: jest.fn(() => ({ - editor: { current: null }, - settings: { darkMode: false, showDocs: false }, - })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - const i18n = new I18nManager({ locale: 'en' }); + const editor = testRender(); - render( - - - - - , - ); + fireEvent.contextMenu(editor.getByText('Write your program here...')); - fireEvent.contextMenu(screen.getByText('Write your program here...')); + expect(editor.getByText('Copy')).toBeInTheDocument(); - expect(screen.getByText('Copy')).toBeInTheDocument(); + userEvent.type(editor.getByText('Copy'), '{esc}'); - userEvent.type(screen.getByText('Copy'), '{esc}'); - - await waitForElementToBeRemoved(() => screen.queryByText('Copy')); + await waitForElementToBeRemoved(() => editor.queryByText('Copy')); // editor should be focused after context menu closes - expect(document.activeElement).toBe(getTextArea()); + expect(document.activeElement).toBe(getTextArea(editor)); }); }); diff --git a/src/status-bar/StatusBar.test.tsx b/src/status-bar/StatusBar.test.tsx index 6b82be23..99fcfecd 100644 --- a/src/status-bar/StatusBar.test.tsx +++ b/src/status-bar/StatusBar.test.tsx @@ -1,94 +1,55 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021 The Pybricks Authors +// Copyright (c) 2021-2022 The Pybricks Authors -import { I18nContext, I18nManager } from '@shopify/react-i18n'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { mock } from 'jest-mock-extended'; import React from 'react'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; +import { testRender } from '../../test'; import { BleConnectionState } from '../ble/reducers'; -import { RootState } from '../reducers'; import StatusBar from './StatusBar'; it('should prevent browser context menu', () => { - const store = { - getState: jest.fn(() => ({ - ble: { connection: BleConnectionState.Disconnected, deviceName: '' }, - })), - dispatch: jest.fn(), - subscribe: jest.fn(), - } as unknown as Store; - render( - - - , - ); + const statusBar = testRender(, { + ble: { connection: BleConnectionState.Disconnected, deviceName: '' }, + }); - expect(fireEvent.contextMenu(screen.getByRole('status'))).toBe(false); + expect(fireEvent.contextMenu(statusBar.getByRole('status'))).toBe(false); }); it('should show popover when hub name is clicked', async () => { const testHubName = 'Test hub'; - const store = mock>({ - getState: () => - mock({ - ble: { - connection: BleConnectionState.Connected, - deviceName: testHubName, - deviceType: 'hub type', - deviceFirmwareVersion: 'v0.0.0', - deviceLowBatteryWarning: false, - deviceBatteryCharging: false, - }, - }), + const statusBar = testRender(, { + ble: { + connection: BleConnectionState.Connected, + deviceName: testHubName, + deviceType: 'hub type', + deviceFirmwareVersion: 'v0.0.0', + deviceLowBatteryWarning: false, + deviceBatteryCharging: false, + }, }); - const i18n = new I18nManager({ locale: 'en' }); + userEvent.click(statusBar.getByText(testHubName)); - render( - - - - - , - ); - - userEvent.click(screen.getByText(testHubName)); - - await waitFor(() => screen.getByText('Connected to:')); + await waitFor(() => statusBar.getByText('Connected to:')); }); it('should show popover when battery is clicked', async () => { const testHubName = 'Test hub'; - const store = mock>({ - getState: () => - mock({ - ble: { - connection: BleConnectionState.Connected, - deviceName: testHubName, - deviceType: 'hub type', - deviceFirmwareVersion: 'v0.0.0', - deviceLowBatteryWarning: false, - deviceBatteryCharging: false, - }, - }), + const statusBar = testRender(, { + ble: { + connection: BleConnectionState.Connected, + deviceName: testHubName, + deviceType: 'hub type', + deviceFirmwareVersion: 'v0.0.0', + deviceLowBatteryWarning: false, + deviceBatteryCharging: false, + }, }); - const i18n = new I18nManager({ locale: 'en' }); + userEvent.click(statusBar.getByTitle('Battery')); - render( - - - - - , - ); - - userEvent.click(screen.getByTitle('Battery')); - - await waitFor(() => screen.getByText('Battery level is OK.')); + await waitFor(() => statusBar.getByText('Battery level is OK.')); }); diff --git a/test/index.ts b/test/index.tsx similarity index 78% rename from test/index.ts rename to test/index.tsx index ba955027..1a610a9e 100644 --- a/test/index.ts +++ b/test/index.tsx @@ -1,9 +1,13 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors -import { AnyAction } from 'redux'; +import { I18nContext, I18nManager } from '@shopify/react-i18n'; +import { RenderResult, render } from '@testing-library/react'; +import React, { ReactElement } from 'react'; +import { Provider } from 'react-redux'; +import { AnyAction, PreloadedState, createStore } from 'redux'; import { END, MulticastChannel, Saga, Task, runSaga, stdChannel } from 'redux-saga'; -import { RootState } from '../src/reducers'; +import { RootState, rootReducer } from '../src/reducers'; import { RootSagaContext } from '../src/sagas'; type RecursivePartial = { @@ -120,3 +124,27 @@ export function lookup(obj: unknown, id: string): string | undefined { } return undefined; } + +/** + * Customized version of @testing-library/react render function. + * + * https://testing-library.com/docs/react-testing-library/setup#custom-render + * + * @param component The component to render. + * @param state Any state required by the component. + * @returns The render result. + */ +export const testRender = ( + component: ReactElement, + state?: PreloadedState, +): RenderResult => { + const store = createStore(rootReducer, state); + + const i18n = new I18nManager({ locale: 'en' }); + + return render( + + {component} + , + ); +};