/frontend-testing
Write or strengthen tests that assert real behavior, not helper output. Mock-heavy tests that
bypass the UI are near-worthless: the closer a mock sits to the component, the less the test
proves. Conventions live in the testing rule (installed at .claude/rules/testing.md) —
follow them; this skill is the procedure.
Runs in flow-frontend. Tests are co-located *.test.tsx/*.test.ts files run by Vitest
in jsdom. The React plugin varies: the apps use @vitejs/plugin-react-swc, several packages
use plain @vitejs/plugin-react — read the target package's vitest.config.ts. One test file
per source file. Run a package's suite with pnpm test.
Steps
Identify the unit and its real entry point. For a component, that is rendering it and
driving it through user-visible interactions — not calling its internal handlers directly.
For a hook, render it inside a host component or renderHook.
Render and assert the DOM. Use render + screen from @testing-library/react and
userEvent for interaction. Every test makes at least one positive assertion on rendered
output that a user could observe:
- Query by role/label/text (
getByRole, findByText), not test-id, unless the element has
no accessible handle.
- Assert visible state changed (
toBeInTheDocument, toBeDisabled, toHaveTextContent),
not that an internal function was called.
await async UI with findBy*/waitFor; cleanup() in afterEach.
Exercise the real UI path. Click the actual button, type into the actual field, submit
the actual form. A test that calls onSubmit(values) directly proves nothing about whether
the form wires up. Mock only at true boundaries — the RPC client / server action — never the
component's own logic or converters.
Cover combinatorial cases with a table. When behavior varies across inputs (variant ×
disabled, role × feature-flag, empty/one/many), drive a it.each / describe.each table so
the matrix is explicit and readable. Include the edge rows: empty list, single item, error
state, loading state.
Add a canary for silent failures. If a test could pass while the feature is mis-wired
(e.g. a parity test where both sides read the same stub), add a positive assertion that
fails loudly when the wiring breaks. A green suite must mean the feature works.
Hand E2E to Playwright. Vitest covers component and unit behavior. Cross-page flows,
auth, real navigation, and business-intent journeys belong in Playwright against the running
stack — do not fake them in jsdom. To author those, use the Playwright planner/generator
agents; to make them runnable, use the e2e-test-fixture skill.
Run and report. pnpm test (or scope to the package). Report what is covered, which
cases the table exercises, and any behavior you deliberately pushed to E2E.
Anti-patterns to reject
- Asserting a mock was called instead of asserting the rendered result.
- Testing a converter's output by importing the converter — test it through the component that
uses it, or test the converter directly as a pure function, but never mock it inside a
component test.
- A snapshot as the only assertion. Snapshots catch nothing intentional; assert specifics.
Gotchas
- The apps' vitest configs use
@vitejs/plugin-react-swc, not @vitejs/plugin-react. Copying
a plugin import from a package config into an app config (or vice versa) silently changes the
transform.
- Keep
cleanup() in afterEach. Several packages (primary-nav, flow-auth,
connect-server-actions, others) have no global cleanup in their vitest.setup.ts, so
omitting it there leaks DOM between tests. Where a global setup does call it (the apps,
ui-toolkit), the extra call is idempotent.
1---2name: frontend-testing3description: Scaffolds and strengthens flow-frontend tests with Vitest + React Testing Library, asserting rendered DOM through the real UI path, and hands E2E off to Playwright. Use when the user says "add a test", "write tests for this component", "strengthen these tests", "test this hook", or "/frontend-testing".4---56# /frontend-testing78Write or strengthen tests that assert real behavior, not helper output. Mock-heavy tests that9bypass the UI are near-worthless: the closer a mock sits to the component, the less the test10proves. Conventions live in the `testing` rule (installed at `.claude/rules/testing.md`) —11follow them; this skill is the procedure.1213Runs in **flow-frontend**. Tests are co-located `*.test.tsx`/`*.test.ts` files run by Vitest14in jsdom. The React plugin varies: the apps use `@vitejs/plugin-react-swc`, several packages15use plain `@vitejs/plugin-react` — read the target package's `vitest.config.ts`. One test file16per source file. Run a package's suite with `pnpm test`.1718## Steps19201. **Identify the unit and its real entry point.** For a component, that is rendering it and21 driving it through user-visible interactions — not calling its internal handlers directly.22 For a hook, render it inside a host component or `renderHook`.23242. **Render and assert the DOM.** Use `render` + `screen` from `@testing-library/react` and25 `userEvent` for interaction. Every test makes at least one positive assertion on rendered26 output that a user could observe:27 - Query by role/label/text (`getByRole`, `findByText`), not test-id, unless the element has28 no accessible handle.29 - Assert visible state changed (`toBeInTheDocument`, `toBeDisabled`, `toHaveTextContent`),30 not that an internal function was called.31 - `await` async UI with `findBy*`/`waitFor`; `cleanup()` in `afterEach`.32333. **Exercise the real UI path.** Click the actual button, type into the actual field, submit34 the actual form. A test that calls `onSubmit(values)` directly proves nothing about whether35 the form wires up. Mock only at true boundaries — the RPC client / server action — never the36 component's own logic or converters.37384. **Cover combinatorial cases with a table.** When behavior varies across inputs (variant ×39 disabled, role × feature-flag, empty/one/many), drive a `it.each` / `describe.each` table so40 the matrix is explicit and readable. Include the edge rows: empty list, single item, error41 state, loading state.42435. **Add a canary for silent failures.** If a test could pass while the feature is mis-wired44 (e.g. a parity test where both sides read the same stub), add a positive assertion that45 fails loudly when the wiring breaks. A green suite must mean the feature works.46476. **Hand E2E to Playwright.** Vitest covers component and unit behavior. Cross-page flows,48 auth, real navigation, and business-intent journeys belong in Playwright against the running49 stack — do not fake them in jsdom. To author those, use the Playwright planner/generator50 agents; to make them runnable, use the `e2e-test-fixture` skill.51527. **Run and report.** `pnpm test` (or scope to the package). Report what is covered, which53 cases the table exercises, and any behavior you deliberately pushed to E2E.5455## Anti-patterns to reject5657- Asserting a mock was called instead of asserting the rendered result.58- Testing a converter's output by importing the converter — test it through the component that59 uses it, or test the converter directly as a pure function, but never mock it inside a60 component test.61- A snapshot as the only assertion. Snapshots catch nothing intentional; assert specifics.6263## Gotchas6465- The apps' vitest configs use `@vitejs/plugin-react-swc`, not `@vitejs/plugin-react`. Copying66 a plugin import from a package config into an app config (or vice versa) silently changes the67 transform.68- Keep `cleanup()` in `afterEach`. Several packages (`primary-nav`, `flow-auth`,69 `connect-server-actions`, others) have no global cleanup in their `vitest.setup.ts`, so70 omitting it there leaks DOM between tests. Where a global setup does call it (the apps,71 `ui-toolkit`), the extra call is idempotent.