Testing Guide
Lufa uses Playwright Component Testing with React. Tests render components in a real Chromium browser and cover behavior, keyboard interaction, automated accessibility checks, responsive behavior, and screenshots.
The test package is packages/design-system/playwright.
Active projects​
playwright-ct.config.ts defines two Chromium projects:
| Project | Purpose |
|---|---|
chromium-light | Runs the complete component suite with the desktop Chrome device profile. |
chromium-dark | Runs tests whose full title matches Visual Regression with dark color scheme. |
Firefox, WebKit, and mobile projects are not currently configured.
File and snapshot layout​
Tests mirror the public API categories:
packages/design-system/playwright/
├── src/
│ ├── composition/
│ ├── content/
│ ├── documentation/
│ ├── foundation/
│ ├── hooks/
│ ├── interaction/
│ └── utility/
└── __snapshots__/
└── src/<category>/<Component>.spec.tsx-snapshots/
Snapshot names include the Playwright project and platform, for example:
__snapshots__/src/content/Link.spec.tsx-snapshots/
link-all-variants-chromium-light-darwin.png
link-all-variants-chromium-dark-linux.png
Running tests​
From packages/design-system/playwright:
pnpm test
pnpm test-ct:ui
pnpm test-ct src/content/Link.spec.tsx --project=chromium-light
pnpm test-ct:update-snapshots
From the repository root:
pnpm ds:playwright:ci
pnpm ds:playwright:ui
pnpm ds:playwright:update-snapshots
The update script refreshes all snapshots and then runs the repository compression script.
Behavioral tests​
Use semantic locators and assert what users can observe:
import { expect, test } from '@playwright/experimental-ct-react';
import { Button } from '@grasdouble/lufa_design-system';
test('activates with the keyboard', async ({ mount, page }) => {
let activations = 0;
const button = await mount(<Button onClick={() => activations++}>Save</Button>);
await button.focus();
await page.keyboard.press('Enter');
await expect(button).toBeFocused();
expect(activations).toBe(1);
});
Prefer getByRole, getByLabel, and visible text over CSS classes. Class assertions are appropriate only when the class is itself the behavior under test.
Accessibility audits​
The suite uses @axe-core/playwright:
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/experimental-ct-react';
import { Link } from '@grasdouble/lufa_design-system';
test('has no automated accessibility violations', async ({ mount, page }) => {
await mount(
<main>
<h1>Resources</h1>
<Link href="/docs">Documentation</Link>
</main>
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Do not disable axe rules to hide a component defect. A narrowly scoped disable is acceptable only for document-level rules that the component harness cannot satisfy, and the reason should be clear in the test.
Automated scans complement, rather than replace:
- keyboard and focus-order tests
- accessible-name and state assertions
- reduced-motion behavior
- contrast review
- screen-reader checks for complex widgets
Visual regression​
Place screenshot tests inside a describe block containing Visual Regression so both configured projects select them:
test.describe('Visual Regression', () => {
test('renders all Link variants', async ({ mount }) => {
const component = await mount(
<div>
<Link href="/default">Default</Link>
<Link href="/plain" variant="plain">
Plain
</Link>
</div>
);
await expect(component).toHaveScreenshot('link-all-variants.png', {
animations: 'disabled',
});
});
});
Playwright waits for web fonts before screenshots. Use locator assertions for application-specific readiness; do not add fixed waitForTimeout delays.
Required coverage​
For a new or changed interactive API, cover:
- default rendering and public props
- user-visible behavior
- keyboard and focus behavior
- ARIA names, relationships, and state
- an axe scan in representative markup
- responsive behavior when relevant
- light and dark visual regression for visual changes