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.
When agents use itUse 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".
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill react-testing -a github-copilot
# or with the org installer (adds .github/skills/react-testing):
npx -y github:AGCO-Global/org-skills add skill react-testing
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-react-skills plugin, which bundles all Frontend / React skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-react-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill react-testing -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill react-testing -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill react-testing -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill react-testing -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
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
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.
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:
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.
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"
Read the component and identify user-visible behaviours (states, transitions, errors, edge cases) — list them first.
Check for existing renderWithProviders and MSW handlers; reuse, don't duplicate.
Write one it per behaviour with role-based queries and userEvent.
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*
---
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*` |