# React Testing

You are writing tests for a React 19 app, often on Next.js 16, with Vitest, React Testing Library and Playwright. Write tests for the versions in `package.json`, not the ones in your training data: Vitest moved to `test.projects` and provider packages for browser mode, React 19 removed the old test utilities, and async Server Components cannot be rendered in a unit test.

## Before you write code

- Read `package.json` for `vitest` (5.x), `@testing-library/react` (16.x, with `@testing-library/dom` as a peer), `@testing-library/user-event` (14.x), `@playwright/test`, `msw` (2.x), `jsdom` or `happy-dom`, and the test scripts. Use the runner the project has; never add Jest to a Vitest project.
- Read `vitest.config.ts`, its setup files and `playwright.config.ts`, and follow the existing layout and names (`*.test.tsx` beside the code, `e2e/*.spec.ts`).
- In a Next.js project, read `node_modules/next/dist/docs/01-app/02-guides/testing/` for the installed version.
- Run the existing suite once first, so you know which failures were already there.

## What to test where

- Pure logic (schemas, formatters, reducers, data access functions): Vitest in the `node` environment. Fast, and most of the suite.
- Client Components and hooks: Vitest with React Testing Library in `jsdom` (or `happy-dom`), driven the way a user drives them.
- Components that need real layout, focus, pointer events or CSS (positioned popovers, drag, virtualized lists): Vitest browser mode or Playwright, not jsdom with a pile of stubs.
- Async Server Components, routing, caching, auth redirects and streaming: Playwright against a production build.
- One test per behavior a user or caller can observe. Never test implementation details: internal state, hook call counts, class names, render counts.

## Vitest

```ts
// vitest.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [react()],
  resolve: { tsconfigPaths: true }, // Vite 8; older Vite needs vite-tsconfig-paths
  test: {
    coverage: { include: ["app/**", "components/**", "lib/**"], provider: "v8" },
    projects: [
      { test: { environment: "node", include: ["**/*.test.ts"], name: "unit" } },
      {
        test: {
          environment: "jsdom",
          include: ["**/*.test.tsx"],
          name: "dom",
          setupFiles: ["./test/setup-dom.ts"],
        },
      },
    ],
    restoreMocks: true,
  },
});
```

- Projects inherit the root config (`extends` defaults to `true` in Vitest 5). The `vitest.workspace` file is gone.
- Vitest 5 clears mock calls before each test by default; add `restoreMocks: true`, and `unstubEnvs`/`unstubGlobals` when you stub, so nothing leaks between tests.
- Testing Library cleans up after each test only when `globals: true`. Without globals, call `cleanup()` in `afterEach` in the DOM setup file, beside `import "@testing-library/jest-dom/vitest"` for the matchers.
- jsdom lacks `ResizeObserver`, `matchMedia`, `scrollIntoView` and pointer capture. Stub one in the setup file when a component needs it; when a test needs many, move it to browser mode.
- Browser mode: `browser: { enabled: true, provider: playwright(), instances: [{ browser: "chromium" }] }` with `playwright` from `@vitest/browser-playwright`. Import `page` and `userEvent` from `vitest/browser` and assert with `await expect.element(locator)`, which retries.
- Coverage uses `@vitest/coverage-v8`. Set `coverage.include` so untested files count; gate on thresholds the team agreed on, never on 100 percent.
- Mock with `vi.mock(path, factory)` (hoisted: share values through `vi.hoisted`), `vi.spyOn`, `vi.stubEnv`, `vi.useFakeTimers()` and `vi.setSystemTime()`. Mock at a boundary you own (the data layer, the session helper), never React or the unit under test.

## React Testing Library

- Query like a user: `getByRole` with a `name` first (`getByRole("button", { name: "Save" })`), then `getByLabelText`, then `getByText`. `getByTestId` only when nothing accessible exists, and then fix the markup if you can.
- Use `screen`. `findBy*` for anything that appears after an await, `queryBy*` only to assert absence, and `waitFor` with one assertion and no side effects inside.
- Create `const user = userEvent.setup()` before `render`, then `await user.click()`, `await user.type()`, `await user.keyboard("{Escape}")`. Use `fireEvent` only for events userEvent cannot produce. With fake timers, `userEvent.setup({ advanceTimers: vi.advanceTimersByTime })`.
- Assert what the user perceives: `toBeVisible()`, `toHaveAccessibleName()`, `toBeDisabled()`, `toHaveValue()`, `toHaveAttribute("aria-expanded", "true")`.
- Test a hook through a component that uses it, or with `renderHook` from `@testing-library/react`.
- Put providers (theme, i18n, a query client created fresh per test) in a `render` helper with a `wrapper` in `test/utils.tsx`.
- Snapshot only small, stable values (an inline snapshot of a string or object), never whole component trees.

```tsx
test("flags an invalid email", async () => {
  const user = userEvent.setup();
  render(<SignupForm />);
  await user.type(screen.getByLabelText("Email"), "nope");
  await user.click(screen.getByRole("button", { name: "Sign up" }));
  expect(await screen.findByText("Enter a valid email.")).toBeVisible();
  expect(screen.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
});
```

## Next.js: Server Components and Server Functions

- Testing Library cannot render async Server Components. Do not `render(await Page())`: nested async components, `cookies()` and caching break it. Test the functions the page calls and the synchronous components it renders; cover the page in Playwright.
- Test a Server Function as a function: build a `FormData`, call it with a previous state, assert the returned state. Mock the session helper for signed out, wrong user and owner.
- Next.js cache and request APIs throw outside a request: mock `next/cache` (`updateTag`, `revalidateTag`, `revalidatePath`, `refresh`) and `next/headers` in these tests. `redirect()` throws an error whose message is `NEXT_REDIRECT`: `await expect(action(state, data)).rejects.toThrow("NEXT_REDIRECT")`.
- `import "server-only"` throws under Vitest; mock it once in the setup file: `vi.mock("server-only", () => ({}))`.
- Client Components using `useRouter`, `usePathname` or `useSearchParams` need `vi.mock("next/navigation", …)`; Next.js ships no test router.

```ts
vi.mock("next/cache", () => ({ revalidateTag: vi.fn(), updateTag: vi.fn() }));
vi.mock("@/lib/session", () => ({ requireUser: vi.fn(async () => ({ id: "u1" })) }));

test("returns a field error for an empty title", async () => {
  const data = new FormData();
  data.set("title", "");
  const state = await createPost({}, data);
  expect(state.errors?.title).toBeDefined();
});
```

## Mocking the network

- Mock HTTP with MSW 2, not by stubbing `fetch` or mocking your API client. Keep handlers in `test/handlers.ts`: `http.get(url, () => HttpResponse.json(data))`.
- In Vitest: `setupServer(...handlers)` from `msw/node`, `server.listen({ onUnhandledRequest: "error" })` in `beforeAll`, `server.resetHandlers()` in `afterEach`, `server.close()` in `afterAll`. Override per test with `server.use()`, including failures: `HttpResponse.json(body, { status: 500 })` and `HttpResponse.error()`.
- In Playwright, `page.route()` sees only the browser's requests. What a Server Component fetches comes from the Next.js server: point it at a test backend or a seeded database through environment variables (Next.js also has an experimental test proxy, `experimental.testProxy`, with an MSW fixture).

## Playwright end to end

- Locate with `page.getByRole`, `getByLabel` and `getByText`; narrow with chaining and `filter({ hasText })`, not CSS or XPath.
- Use web-first assertions, which retry: `await expect(locator).toBeVisible()`, `toHaveText`, `toHaveURL`, `toHaveCount`. Never `page.waitForTimeout()`, never `expect(await locator.isVisible()).toBe(true)`.
- Every test stands alone: its own data, no order dependence. Sign in once in a setup project that saves `storageState`, and reuse it through `dependencies: ["setup"]`.
- Config: `webServer: { command: "npm run build && npm run start", url: "http://localhost:3000", reuseExistingServer: !process.env.CI }` and `use: { baseURL: "http://localhost:3000", trace: "on-first-retry" }`. Test the production build: `next dev` compiles on demand and makes tests slow and flaky.
- In CI: `forbidOnly: !!process.env.CI`, `retries: 2`, and upload the HTML report and traces when a run fails. Read the trace before changing a test.
- `toHaveScreenshot()` for a few layout-critical views only, with dynamic content masked and baselines made on the CI's OS.
- With Cache Components, `instant()` from `@next/playwright` asserts what shows before dynamic content streams in.

## Accessibility checks

- In Playwright, run `new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]).analyze()` from `@axe-core/playwright` on each key page and each open dialog or menu, and expect no violations.
- In Vitest, run `axe-core` on the rendered container for component states (errors shown, menu open). jsdom cannot compute color contrast; that check happens in the browser.
- Write one keyboard test per interactive flow: Tab to the control, operate it with Enter, Space, arrows and Escape, and check where focus lands.

## Do not

- Do not use Enzyme, `react-test-renderer` (deprecated in React 19), `react-dom/test-utils` (only a deprecated `act` is left: import `act` from `react`) or `@testing-library/react-hooks`.
- Do not query by class, tag or DOM shape, assert on state or props, or wrap Testing Library calls in extra `act()`.
- Do not write `vitest.workspace` files, sleep for fixed times, or let Vitest's include pick up Playwright's `*.spec.ts` files.
- Do not mock the unit under test, React itself, or `fetch` globally.
- Do not commit `.only`, or a skipped test without a reason in its name.

## Before you finish

- Run the type checker, `vitest run` (never watch mode in scripts or CI) and the Playwright tests for the flows you touched.
- Make each new test fail once by breaking the code it covers, then restore the code.
- Run new end-to-end tests with `--repeat-each=3` to catch flakiness.
- Say what unit tests cover, what only end-to-end tests cover, and what is left untested.
