agentleFS
jaballerjaballer/react-claude-skills/react-testing/SKILL.md

react-testing

Skill0 starsChanged 5 months agoMITView on GitHub

Testing strategy for React apps — when to reach for Playwright (E2E) vs Vitest (unit/component) vs both, the senior testing pyramid for an SPA, Playwright setup with config + webServer + per-spec project structure, Vitest setup with jsdom + Testing Library, a11y-first locators (getByRole, getByLabel, toBeFocused, toHaveAttribute), assertion patterns for aria-pressed/aria-current/role=alert/focus-on-route-change, @axe-core/playwright for automated a11y audits, running against localhost vs deployed URL via env var, what NOT to test, common flakiness sources and fixes. Use whenever the project needs tests added — interview "would you add tests?" question, real production CI, or post-build polish layer. Triggers on "test", "testing", "playwright", "vitest", "jest", "e2e", "end-to-end", "unit test", "component test", "@playwright/test", "@axe-core/playwright", "getByRole", "testing library", "rtl", "smoke test", "ci tests", "test runner".

  • Reads credentials
  • Installs packages

19 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use orch-pipeline, parallel-execution-optimizer and 9 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • renepfSkill

    orch-pipeline

    Shared orchestration engine for the orch-* skill family. Defines the gated Research-Plan-TDD-Review-Commit pipeline, the size classifier, the agent map, and the two human gates that the orch-* operation skills delegate to. Not usually invoked directly.

    Also in 4 projects that use this

  • marcusgollSkill

    parallel-execution-optimizer

    Identify and execute independent operations in parallel for 3-5x speedup. Auto-analyzes task dependencies, groups into batches, launches parallel Task() calls. Applies to /optimize (5 checks), /ship pre-flight (5 checks), /implement (task batching), /prototype (N screens). Auto-triggers when detecting multiple independent operations in a phase.

    Also in 5 projects that use this

  • ronmkrSkill

    vite-patterns

    Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization. Activate when working with vite.config.ts, Vite plugins, or Vite-based projects.

    Also in 6 projects that use this

  • RojiosSkill

    defi-amm-security

    Security checklist for Solidity AMM contracts, liquidity pools, and swap flows. Covers reentrancy, CEI ordering, donation or inflation attacks, oracle manipulation, slippage, admin controls, and integer math.

    Also in 5 projects that use this

  • renepfSkill

    prediction-market-oracle-research

    Research prediction markets as data sources or oracle signals for products, agents, dashboards, and corporate decision intelligence. Use for source-grounded analysis of market-implied probabilities, caveats, and integration patterns without investment advice.

    Also in 3 projects that use this

  • ronmkrSkill

    quarkus-security

    Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.

    Also in 4 projects that use this

  • +1 more from ronmkr
  • +2 more from renepf

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 19 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Don't make one giant e2e.spec.ts.

    line 103
  2. Distinctive:

    Use role-based queries by default.

    line 107
  3. Distinctive:

    If you can't find a role-based query, that's often a signal the UI itself is missing semantics.

    line 133

“Common” is measured: kept by at least 6 in 10 of the 19 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

react-testing

Senior testing isn't about coverage percentages — it's about catching the regressions that matter while staying fast and stable. This skill gives you the decision framework and paste-ready setup for both layers.

The senior testing pyramid for a typical React SPA

Most SPAs need two thin layers, not five:

LayerToolWhat to testRoughly
Unit / pure logicVitestPure functions, transforms, adapters, the parts of the API client that aren't fetch20% of test code
E2EPlaywrightThe 4-6 happy paths a real user takes, plus focus / a11y wiring80% of test code
Component tests in RTL(skip by default)Most "component tests" duplicate either unit or E2E coverage with more boilerplate0%

When to add RTL component tests anyway:

  • A single component has gnarly internal state worth isolating (a complex form, a step machine)
  • You have a component library that's the deliverable, and each export needs its own spec
  • A11y-sensitive primitive (combobox, listbox) where the internal interaction matrix is the product

For a typical product SPA, skip the middle layer. Playwright's getByRole queries give you the same a11y guarantees as RTL with the real browser doing the rendering.

The Playwright recipe (this is what to install)

npm install -D @playwright/test
npx playwright install chromium

Then playwright.config.ts — the version that lets one spec suite test both localhost AND the deployed URL:

import { defineConfig, devices } from "@playwright/test";

const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5173";
const usingExternalServer = !!process.env.E2E_BASE_URL;

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI ? "github" : "list",
  use: {
    baseURL,
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
  // Only spin up Vite when we're not pointing at a deployed URL.
  webServer: usingExternalServer
    ? undefined
    : {
        command: "npm run dev",
        url: "http://localhost:5173",
        reuseExistingServer: !process.env.CI,
        timeout: 30_000,
      },
});

And the package.json scripts:

{
  "scripts": {
    "e2e": "playwright test",
    "e2e:ui": "playwright test --ui",
    "e2e:prod": "E2E_BASE_URL=https://your-app.netlify.app playwright test"
  }
}

Why these three and not more:

  • e2e — local headless run, the daily driver
  • e2e:ui — debugging a failing spec, time-travel snapshots, locator inspector
  • e2e:prod — smoke test the live deploy after pushing; CI can run this on a schedule

.gitignore additions:

/test-results
/playwright-report
/playwright/.cache

What to put in e2e/ — the 4-spec template

For a typical SPA, four spec files cover almost everything worth testing:

e2e/
  homepage.spec.ts    Landing/list page: data renders, filters work, pagination wires up
  detail.spec.ts      Navigation in + out of a detail route; focus management
  errors.spec.ts      404, network failure, invalid input — every error state
  a11y.spec.ts        Keyboard order, landmarks, no console errors, axe scan

Don't make one giant e2e.spec.ts. Per-file describe blocks let Playwright parallelize and let you read failures faster.

A11y-first locators — the senior signal

Use role-based queries by default. Anything else is a smell that you're testing implementation, not behavior.

// Good — describes user intent
await page.getByRole("button", { name: /audio/i }).click();
await page.getByRole("heading", { level: 1, name: "Post title" });
await page.getByRole("alert");
await page.getByRole("navigation", { name: "Pagination" });
await page.getByRole("link", { name: /skip to main content/i });
await page.getByLabel("Email");

// OK — when role isn't expressive enough
await page.getByText(/\d+ shown/);
await page.locator("main article");

// Smell — testing structure, not behavior
await page.locator(".filter-chip-button");        // CSS class
await page.locator("#filter-3");                  // generated ID
await page.locator("[data-testid='filter']");     // testid is a last resort

Order of preference (matches React Testing Library guidance):

  1. getByRole with name (matches what a screen reader announces)
  2. getByLabel, getByPlaceholder, getByText
  3. getByTestId — only when nothing semantic exists, which should be rare

If you can't find a role-based query, that's often a signal the UI itself is missing semantics.

Assertion patterns for the four a11y wirings you'll most often test

These are the patterns that came up in the kratecms-reader build. Memorize them.

Focus moves to the H1 on SPA route change

await page.getByRole("link", { name: "Some post" }).click();
const h1 = page.getByRole("heading", { level: 1 });
await expect(h1).toBeFocused();
await expect(h1).toHaveAttribute("tabindex", "-1");   // confirms it's the right pattern

aria-current="page" on the active nav / pagination item

const pagination = page.getByRole("navigation", { name: "Pagination" });
await expect(pagination.getByRole("button", { name: "Page 1" }))
  .toHaveAttribute("aria-current", "page");

await pagination.getByRole("button", { name: "Page 2" }).click();
await expect(pagination.getByRole("button", { name: "Page 2" }))
  .toHaveAttribute("aria-current", "page");

aria-pressed on toggle buttons (filter chips, etc.)

const audioChip = page.getByRole("group", { name: "Filter by category" })
  .getByRole("button", { name: /^audio/ });
await audioChip.click();
await expect(audioChip).toHaveAttribute("aria-pressed", "true");

role="alert" for error states

await page.goto("/posts/99999");
const alert = page.getByRole("alert");
await expect(alert).toBeVisible();
await expect(alert).toContainText(/not found/i);
// 4xx errors shouldn't offer retry
await expect(alert.getByRole("button", { name: /try again/i })).toHaveCount(0);

Skip link revealed on first Tab

await page.goto("/");
await page.keyboard.press("Tab");
await expect(page.getByRole("link", { name: /skip to main content/i }))
  .toBeFocused();

The no-console-errors spec — every project should have this

Catches the bugs that don't break a render but ship a noisy console:

test("no console errors on a typical browse session", async ({ page }) => {
  const errors: string[] = [];
  page.on("console", (msg) => {
    if (msg.type() === "error") errors.push(msg.text());
  });
  page.on("pageerror", (err) => errors.push(err.message));

  await page.goto("/");
  // walk a real path
  await page.locator("main article").first().getByRole("link").first().click();
  await page.waitForURL(/\/posts\//);
  await page.getByRole("link", { name: /back|all posts/i }).click();

  // filter out known third-party noise
  const ownErrors = errors.filter(
    (e) => !/soundcloud\.com|youtube\.com|X-Frame-Options/i.test(e),
  );
  expect(ownErrors, ownErrors.join("\n")).toHaveLength(0);
});

Two key things:

  1. Listen on both console (msg.type() === 'error') and pageerror (uncaught exceptions).
  2. Filter known third-party noise — third-party iframes are notorious for X-Frame-Options complaints that you can't fix.

@axe-core/playwright for automated a11y scans

npm install -D @axe-core/playwright
import AxeBuilder from "@axe-core/playwright";

test("homepage has no axe violations", async ({ page }) => {
  await page.goto("/");
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Axe catches ~30-40% of automatable a11y issues (color contrast, missing alt, mis-wired ARIA). It's not a substitute for manual testing — but landing it in CI prevents regressions from sneaking in.

Common refinements:

const results = await new AxeBuilder({ page })
  .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])   // pin standards
  .disableRules(["color-contrast"])                          // if a known design exception
  .exclude("iframe")                                         // skip third-party content
  .analyze();

The Vitest recipe (only if you need it)

npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom

vitest.config.ts:

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

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./vitest.setup.ts"],
    globals: true,
  },
});

vitest.setup.ts:

import "@testing-library/jest-dom/vitest";

Then a spec that tests the parts worth isolating — pure functions, not components:

// src/api/client.test.ts
import { describe, it, expect } from "vitest";
import { normalizeMeta } from "./client";

describe("normalizeMeta", () => {
  it("unboxes [v, v] arrays back to scalars", () => {
    expect(normalizeMeta({
      current_page: [3, 3], last_page: [5, 5], per_page: [15, 15], total: [73, 73],
      from: 31, to: 45, path: "x",
    }).current_page).toBe(3);
  });

  it("passes through scalar values unchanged (forward-compat with fixed API)", () => {
    expect(normalizeMeta({
      current_page: 3, last_page: 5, per_page: 15, total: 73,
      from: 31, to: 45, path: "x",
    } as any).current_page).toBe(3);
  });
});

When to add a Vitest component test:

  • The component has internal state that's hard to drive via a real browser
  • You'd otherwise need 20 lines of E2E setup to test one branch
  • The component is a primitive in a design system

When NOT to add a component test:

  • The "component test" would be render() → click → assert text — that's an E2E with extra steps
  • The behavior depends on routing, network, or browser APIs — use Playwright
  • You're testing prop combinations — use a TypeScript test (expectTypeOf) instead

What NOT to test

Don't testWhy
Implementation details (state shape, internal hooks)Refactors break the test for no reason
Third-party iframe contentYou don't own it; flake guaranteed
Exact pixel layouts (use visual regression instead)Pixels shift with font hinting, AA
The framework itself (does useState work?)Not your code
Every prop combination (use TS types)TypeScript catches this at compile time
Snapshots of large treesReviewers blind-accept; not a real test

Flakiness — the 5 most common causes and fixes

CauseFix
await page.waitForTimeout(500)Use waitForURL, waitForResponse, or auto-retrying assertions. Hard timeouts are flake-bait.
Asserting on text that depends on live API dataAssert on structure ("at least one card visible") not specific titles. Pin to a stable record for content assertions.
Animations not finished when assertingawait expect(thing).toBeVisible() waits for stable layout. If still flaky, gate animations on prefers-reduced-motion in the test fixture.
Network race between two fetchesawait page.waitForResponse(url) before asserting on the result.
State leaking between testsUse test.beforeEach to reset. Never share page instances across tests.

Running against localhost vs the deployed URL

The E2E_BASE_URL pattern in the config above lets one suite serve two purposes:

npm run e2e         # localhost — daily driver, runs Vite via webServer
npm run e2e:prod    # deployed URL — smoke test after a deploy

For CI: run npm run e2e on every PR (catches regressions before merge). Schedule npm run e2e:prod to run hourly or after deploys (catches infra drift).

Don't point E2E_BASE_URL at production for the PR run — every PR would hit your live site dozens of times.

When the interviewer asks "what about tests?"

Senior answer follows this skill:

  1. "I'd add Playwright for the routing, focus management, and filter behavior — those are the 4-6 happy paths a user takes."
  2. "Vitest for the pure helpers — normalizeMeta, the YouTube ID extractor. Not for components; the E2E covers them."
  3. "I skip component-level RTL by default unless I have a primitive that needs prop-by-prop coverage."
  4. "axe in CI for automated a11y regressions, plus manual keyboard pass before shipping."
  5. "I'd run E2E against localhost in PRs and against the deployed URL on a schedule."

That's the answer, in 30 seconds, that signals senior.

Narration phrases

  • "I used getByRole everywhere — if I can't find a role-based query, that's a signal the UI is missing semantics."
  • "The focus-on-route-change spec is a one-liner: await expect(h1).toBeFocused(). That's the single highest-value a11y assertion for an SPA."
  • "I have one no-console-errors spec walking a typical browse path — catches the bugs that don't break a render but ship a noisy console."
  • "I skipped component tests — Playwright covers them with the real browser, and Vitest covers the pure helpers. The middle layer would duplicate both."
  • "Vitest config for normalizeMeta is six lines including imports. Not worth a heavier framework."

Authoritative references

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.