---
name: react-testing
description: Design and write a React test strategy and the tests themselves — unit tests with Vitest/Jest and React Testing Library, API mocking with MSW, hook testing, component visual/interaction tests in Storybook, and end-to-end tests with Playwright. Use this whenever the user asks how to test a React component, hook, form, or page; wants to set up a testing pipeline; asks about coverage, flaky tests, mocking fetch/axios, testing async state, or "what should I test". Also use it when generating tests for React code the user has just written, even if they don't say the word "test".
metadata:
  technology: React
  type: testing
---

# React Testing

Tests exist to let the team refactor without fear. Optimise for confidence per minute of maintenance, not for coverage percentage.

## 1. The testing pyramid for React (what to write, how much)

| Layer | Tool | Share | Tests what |
|---|---|---|---|
| Static | TypeScript strict, ESLint, `typescript-eslint` | Free | Type errors, unused code, hook rules |
| Unit (pure) | Vitest (Vite) / Jest (CRA, Next.js legacy) | ~20% | Domain logic in `model/`, utils, reducers, selectors — no React |
| Component / integration | Vitest + React Testing Library + MSW | ~60% | A feature slice rendered with real children, real hooks, mocked network |
| Visual / interaction | Storybook + `@storybook/test` + Chromatic (optional) | ~10% | Design-system primitives, states, a11y |
| End-to-end | Playwright | ~10% | 5–15 critical user journeys against a real build |

Push the user toward integration tests at the feature boundary. Testing a single `<Button>` with 40 cases is low value; testing "user fills the checkout form, submits, sees confirmation" with the real form, real validation, and mocked API is high value.

## 2. Setup that prevents most flakiness

```ts
// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'jsdom',            // or 'happy-dom' for speed
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    css: false,
    coverage: { provider: 'v8', thresholds: { statements: 70 } },
  },
});
```

```ts
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { server } from './msw/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());
```

`onUnhandledRequest: 'error'` is the single most valuable line: any real network call fails loudly instead of hanging.

Provide a `renderWithProviders` helper in `src/test/` that wraps in the real `QueryClientProvider` (with `retry: false`), router, theme, and i18n. Every component test uses it; nobody hand-wires providers.

## 3. Component tests with Testing Library — the rules

- **Query by role and accessible name first.** `getByRole('button', { name: /save/i })`. It tests what users and screen readers see, and it breaks when a11y breaks. Fall back to `getByLabelText`, `getByText`; `getByTestId` is the last resort and needs a comment.
- **`userEvent`, not `fireEvent`.** `await user.type(...)`, `await user.click(...)` simulate real interaction sequences (focus, keydown, input).
- **`findBy*` for async appearance, `waitFor` for async disappearance/assertions.** Never `setTimeout` in tests.
- **Assert on outcomes, not implementation.** Don't assert that `setState` was called; assert the DOM now shows the result.
- **One behaviour per test, named as a sentence:** `it('shows validation error when email is empty on submit')`.
- **Don't test the library.** No tests that React Hook Form validates, that TanStack Query caches. Test *your* wiring: the right message appears for *your* schema.

Template:

```tsx
it('submits the form and shows confirmation', async () => {
  server.use(http.post('/api/orders', () => HttpResponse.json({ id: '42' })));
  const user = userEvent.setup();
  renderWithProviders(<Checkout />);

  await user.type(screen.getByLabelText(/email/i), 'a@b.com');
  await user.click(screen.getByRole('button', { name: /place order/i }));

  expect(await screen.findByText(/order #42 confirmed/i)).toBeInTheDocument();
});
```

## 4. Mocking

- **Network → MSW.** Handlers live in `src/test/msw/handlers/<feature>.ts`, mirror the real API shape, and are reused by Storybook and Playwright. Override per test with `server.use(...)`.
- **Time → `vi.useFakeTimers()` / `vi.setSystemTime()`**; restore in `afterEach`.
- **Modules → `vi.mock`** only for true externals (analytics, feature flags, `window.matchMedia`). Mocking your own modules is a design smell — inject via props or context instead.
- **Never mock `useState`, `useEffect`, or the query client.**

## 5. Hooks

- Test hooks through the component that uses them when possible.
- For reusable hooks in `shared/hooks`, use `renderHook` from Testing Library with a wrapper providing needed context; drive with `act` and assert `result.current`.
- Async hooks: `await waitFor(() => expect(result.current.isSuccess).toBe(true))`.

## 6. Storybook as a test layer

- Every `shared/ui` primitive has stories for each visual state (default, hover, disabled, error, loading, long text, RTL).
- Interaction tests via `play` functions using `@storybook/test`; run headless in CI with `@storybook/test-runner`.
- Add `@storybook/addon-a11y` and fail CI on violations for the design system.
- Reuse MSW handlers via `msw-storybook-addon` so stories render real data flows.

## 7. End-to-end with Playwright

- Test **journeys**, not pages: sign-up → onboarding → first key action; purchase; permission-gated flows.
- Use `page.getByRole` locators, `test.step` for readability, and auth via stored `storageState` — log in once, not per test.
- Run against a production build (`next build && next start`) in CI, not the dev server.
- Isolate data: each test creates what it needs via API and cleans up, or runs against a seeded ephemeral DB.
- Retries: `retries: 2` in CI only; a test needing retries locally is a bug to fix, not to hide.
- Trace on first retry (`trace: 'on-first-retry'`) so failures are debuggable from the artifact.

## 8. CI shape

```
lint + tsc  →  unit + component (parallel, sharded)  →  build  →  storybook test-runner  →  playwright (sharded, retries)
```

Fail the pipeline on: type errors, lint errors, test failures, coverage drop below threshold on changed files, a11y violations in Storybook. Don't gate on a global coverage number climbing forever.

## 9. When asked to "write tests for this component"

1. Read the component and identify user-visible behaviours (states, transitions, errors, edge cases) — list them first.
2. Check for existing `renderWithProviders` and MSW handlers; reuse, don't duplicate.
3. Write one `it` per behaviour with role-based queries and `userEvent`.
4. Cover: happy path, validation/error path, loading/empty state, keyboard interaction where relevant.
5. Run the tests. If something needs `getByTestId`, ask whether the component needs an accessible name instead.

## Flaky-test triage

| Symptom | Likely cause | Fix |
|---|---|---|
| Passes alone, fails in suite | Shared module state, MSW handler not reset | `resetHandlers`, avoid module-level mutable state |
| Fails on CI only | Timing, viewport, timezone | Fake timers, set `TZ=UTC`, fixed viewport |
| "not wrapped in act(...)" | State update after test ended | `await findBy*`, unmount, or await the promise you fired |
| Random `getBy` failures | Element not yet rendered | Use `findBy*` |
