Testing
Tools used to test the application:
- ESLint (static code analysis of TypeScript files)
- Stylelint (static code analysis of CSS files)
- Markdownlint (static code analysis of Markdown files)
- Jest (unit tests)
- Playwright (visual and functional component testing)
Generally, npm test and npm run test:playwright-ct:all should be run within
their designated Docker containers before pushing changes to the repository.
Tools
You can run all tests with a single command:
npm run lint && npm test && npm run test:playwright-ct:all
Linters (ESLint, Markdownlint, Stylelint)
Run linters either all together:
npm run lint
or run linters individually:
npm run <eslint|markdownlint|stylelint>
Jest
npm run test:jest
Playwright
Configuration
Test parameters can be tweaked by creating and tweaking .env file.
Running Tests
Run tests:
npm run test:playwright-ct:<all|all-with-update>
You can also run specific tests by passing a path to the test files:
npm run test:playwright-ct:<all|all-with-update> -- -- <match_path>
You can also pass any CLI command to the test runner:
npm run test:playwright-ct:<all|all-with-update> -- -- <cli_argument>
Writing Tests
Component tests mount stories: named exports of the *.story.tsx file next
to the spec. Specs import test and expect from tests/playwright, whose
mount fixture navigates to the Playwright Component Testing page in
tests/playwright/ct/ (served by webpack-dev-server, which Playwright
starts automatically) and mounts a story by its ID, which is the story file
name followed by the export name, with plain data props.
Props are sent to the browser as data, so JSX cannot be passed to a story.
Describe elements passed in props with element() from tests/playwright,
e.g. element('TestIcon') or element('div', { children: 'Label' }).
Callbacks cannot be passed to a story either. To assert them, add
a *SpyForTest story wrapped with withSpy that registers the callbacks with
useSpy, and read the recorded values in the spec with getSpyValue.
useSpy(name, select) records select(...args) of every call, the first
argument by default. The values are recorded asynchronously, hence
expect.poll().
Visual tests are table-driven. Each test case is
{ name, props, onBeforeTest?, onBeforeSnapshot? }. Reusable test cases are
available in propTests from tests/playwright, component specific ones live
in the _propTests/ directory next to the spec. mixPropTests() combines
multiple arrays of test cases into all their combinations. Snapshots are stored
next to the spec in <ComponentName>.spec.tsx-snapshots/.
Story file src/components/Button/__tests__/Button.story.tsx:
import React from 'react';
import { Button } from '..';
import type { ButtonProps } from '..';
import {
useSpy,
withSpy,
} from '../../../../tests/playwright/utils/spy';
import type { StoryProps } from '../../../../tests/playwright';
type ButtonForTestProps = StoryProps<ButtonProps, 'label'>;
export const ButtonForTest = ({
label = 'Button',
...props
}: ButtonForTestProps) => (
<Button
label={label}
{...props}
/>
);
export const ButtonSpyForTest = withSpy((props: ButtonForTestProps) => {
const onClick = useSpy('onClick', () => true);
return (
<ButtonForTest
onClick={onClick}
{...props}
/>
);
});
Spec file src/components/Button/__tests__/Button.spec.tsx:
import {
element,
expect,
mixPropTests,
propTests,
test,
} from '../../../../tests/playwright';
test.describe('Button', () => {
test.describe('visual', () => {
[
...propTests.defaultComponentPropTest,
...propTests.sizePropTest,
...mixPropTests([
propTests.actionColorPropTest,
propTests.priorityPropTest,
]),
].forEach(({
name,
onBeforeTest,
onBeforeSnapshot,
props,
}) => {
test(name, async ({
mount,
page,
}) => {
if (onBeforeTest) {
await onBeforeTest(page);
}
const component = await mount('Button/ButtonForTest', props);
if (onBeforeSnapshot) {
await onBeforeSnapshot(page, component);
}
const screenshot = await component.screenshot();
expect(screenshot).toMatchSnapshot();
});
});
});
test.describe('non-visual', () => {
test('id', async ({ mount }) => {
const component = await mount('Button/ButtonForTest', {
id: 'testId',
});
await expect(component).toHaveAttribute('id', 'testId');
});
});
test.describe('functionality', () => {
test('calls onClick when clicked', async ({ mount }) => {
const component = await mount('Button/ButtonSpyForTest');
await component.getByRole('button').click();
await expect.poll(() => component.getSpyValue('onClick')).toEqual([true]);
});
test('is disabled when feedbackIcon is set', async ({ mount }) => {
const component = await mount('Button/ButtonSpyForTest', {
feedbackIcon: element('span', { children: 'Placeholder' }),
});
const button = component.getByRole('button');
await button.click({ force: true });
await expect(button).toBeDisabled();
await expect.poll(() => component.getSpyValue('onClick')).toEqual([]);
});
});
});
Opening Test Report
After running Playwright tests, test report can be served by using the following command:
npm run test:playwright-ct:show-report
Then open the displayed URL (typically http://localhost:9323)
in your browser. Please note that the test report is only available
if the tests were run prior to serving the report.