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}
+ ,
+ );
+};