This document defines standards for the component test suite (Vitest and React Testing Library and SSR helpers).
Split by concern:
Component.client.test.tsx– interactive behavior, events, keyboard, accessibility.Component.ssr.test.tsx– structural SSR output (roles, landmark semantics, critical classes, conditional omission).Component.hydration.test.tsx– SSR → client integrity and one post-hydration interaction.
Monolithic Component.test.tsx files are deprecated. A Component.test.deprecated placeholder documents migration.
getByRole(withnameoption)getByLabelTextgetByPlaceholderTextgetByText(scoped, exact)getByTestId(only when no semantic alternative)- Avoid class / tag selectors – brittle; only as last resort with explanatory comment.
- Behavior > implementation. Avoid asserting internal class lists unless they encode a semantic/variant contract.
- Prefer
aria-*/ role correctness and visible text over raw HTML substrings. - For variant classes (e.g.
--small,--error), assert presence of the variant, not the whole class string.
Each interactive component should have at least:
- One a11y smoke test using
axe(no critical violations). - Assertions for: labels,
aria-describedbyrelationships,aria-current, focus trapping / escape (if applicable), keyboard navigation.
Pattern:
const jsx = <Component {...props} />;
const { server, client } = hydrateWithoutMismatch({ ssr: jsx, client: jsx });
// Post-hydration interaction (e.g. click toggle, select radio)Validate one enhanced behavior after hydration to ensure event listeners and progressive enhancement logic attached.
Include at least one test covering:
- Empty / minimal props (component renders nothing or fallback correctly)
- Disabled or read-only behavior (interaction prevented)
- Large collections (force overflow path – mock measurements if needed)
- Keyboard navigation sequences (
Tab,ArrowUp/Down,Escape)
Centralise repetitive option/nav arrays in src/test-utils/builders.ts:
buildRadiosOptions({ count: 3, withConditional: true });
buildNavItems({ length: 6, currentIndex: 2 });Helps evolve shapes without touching many files.
- Avoid broad snapshots of full HTML trees (too noisy).
- Allow curated structural snapshots for complex, deterministic markup (e.g. responsive navigation skeleton) – store in
__snapshots__/with clear intent.
Introduce thresholds and ratchet gradually:
- Statements: 60% (raise over time)
- Branches: 45%
- Lines: 60%
- Functions: 55%
- Debounced / timed logic: inject or mock timers (
vi.useFakeTimers()), advance deterministically. - Avoid relying on JSDOM layout metrics (
offsetWidth) directly; mock measurements; isolate overflow calculations.
| Do | Don’t |
|---|---|
| Use role-based queries | Assert internal implementation details not part of public contract |
| Add one post-hydration interaction | Only check for hydration mismatch and stop |
| Provide negative & keyboard cases | Cover only happy path presence |
| Centralise test data | Duplicate large option arrays inline |
| Add a11y smoke tests | Rely solely on manual visual checks |
- Add axe utility and initial smoke tests for 3–5 core components.
- Introduce builders and refactor Radios / Select tests.
- Enhance hydration tests with an interaction.
- Add keyboard navigation tests to Radios, Tabs, Menu-like components.
- Ratchet coverage thresholds after stabilisation.
it('changes selection with ArrowDown', () => {
render(<Radios name="contact" options={buildRadiosOptions({ count: 3 })} />);
const radios = screen.getAllByRole('radio');
radios[0].focus();
fireEvent.keyDown(radios[0], { key: 'ArrowDown' });
expect(radios[1]).toBeChecked();
});- New component PRs MUST ship the triad and a11y smoke and at least one keyboard or negative path test if interactive.
- Any refactor altering accessible name/role must update or add corresponding assertions.
This document will evolve; propose additions via PR comments.