affaan-m/ecc

react-testing

React component testing with React Testing Library, Vitest/Jest, MSW for network mocking, accessibility assertions with axe, and the decision boundary between component tests and Playwright/Cypress end-to-end runs.

查看源码

先看实际效果

使用示例

React: mock HTTP requests and an error response

Complete original MSW setup and per-test 500 override blocks. Requires MSW APIs matching the shown http/HttpResponse interface, a configured test runner, React Testing Library, UserPage and its expected error message. Keep the setup file loaded before tests; no test server or actual request ran here.

示例输入

Original source block 1

```ts
// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```

Original source block 2

```tsx
test("renders error on 500", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});
```
许可

Complete original source code blocks or author-defined blank templates are preserved, including imports, comments, example names, placeholders and omitted project definitions. Multiple blocks are assembled with editorial headings/fences only. Titles and summaries state dependencies and scope. These are author usage patterns or templates, not site-executed code, observed test results, measured performance or filled private-project reports. No upstream Skill instructions, installers, model/API calls, databases, browsers, accounts, mail or source tests were executed.

MIT License

Copyright (c) 2026 Affaan Mustafa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
使用示例

React: test a form through labels and visible success

Complete author test using userEvent, renderWithProviders and an MSW POST handler. Supply the test imports, configured MSW server, UserForm and the provider helper from the same source. Alice and alice@example.com are sample data; no form was submitted or passing result observed.

示例输入

test("submits user form and shows success", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "alice@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});
许可

Complete original source code blocks or author-defined blank templates are preserved, including imports, comments, example names, placeholders and omitted project definitions. Multiple blocks are assembled with editorial headings/fences only. Titles and summaries state dependencies and scope. These are author usage patterns or templates, not site-executed code, observed test results, measured performance or filled private-project reports. No upstream Skill instructions, installers, model/API calls, databases, browsers, accounts, mail or source tests were executed.

MIT License

Copyright (c) 2026 Affaan Mustafa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
使用示例

React: test counter hooks and a stable query provider

Complete author custom-hook block with both counter cases and the async useUser case. Requires the project hooks, QueryClient/provider imports, waitFor, test globals and matching network mocks. The QueryClient is deliberately instantiated once per test outside the wrapper; these are expected assertions, not observed fetched data.

示例输入

import { renderHook, act } from "@testing-library/react";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter accepts initial value", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser fetches user data", async () => {
  // Instantiate QueryClient ONCE per test outside the wrapper so it survives re-renders.
  // Creating it inside the wrapper closure resets cache state on every render, producing flaky tests.
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
许可

Complete original source code blocks or author-defined blank templates are preserved, including imports, comments, example names, placeholders and omitted project definitions. Multiple blocks are assembled with editorial headings/fences only. Titles and summaries state dependencies and scope. These are author usage patterns or templates, not site-executed code, observed test results, measured performance or filled private-project reports. No upstream Skill instructions, installers, model/API calls, databases, browsers, accounts, mail or source tests were executed.

MIT License

Copyright (c) 2026 Affaan Mustafa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
使用示例

React: verify an error boundary and restore the console spy

Complete author Broken component and error-boundary test. Requires React Testing Library/test imports and the project ErrorBoundary. The finally block restores console.error after the expected throw; no component was executed or accessibility outcome inferred.

示例输入

function Broken() {
  throw new Error("boom");
}

test("error boundary renders fallback", () => {
  // Suppress React's console.error noise for the expected throw, then restore so
  // the spy does not leak across tests and hide real errors elsewhere.
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("Something went wrong")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});
许可

Complete original source code blocks or author-defined blank templates are preserved, including imports, comments, example names, placeholders and omitted project definitions. Multiple blocks are assembled with editorial headings/fences only. Titles and summaries state dependencies and scope. These are author usage patterns or templates, not site-executed code, observed test results, measured performance or filled private-project reports. No upstream Skill instructions, installers, model/API calls, databases, browsers, accounts, mail or source tests were executed.

MIT License

Copyright (c) 2026 Affaan Mustafa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

使用方法

  1. 复制安装命令,在自己的智能体环境安装前先检查源码包。
  2. 从原始文档核对所需工具、凭据和支持的宿主。
  3. 说明输入材料与期望交付物,并对照来源检查输出。
npx skills add https://github.com/affaan-m/ecc --skill react-testing

开始前需了解

  • 安装不包含模型订阅、API 额度或访问文件的权限。
  • 仓库星数和安装量反映活跃情况,不等于独立质量或安全测试。
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

React Testing

Comprehensive React testing patterns for behavior-focused component tests, custom hook tests, accessibility assertions, and network-level mocking.

When to Activate

  • Writing tests for React components, custom hooks, or pages
  • Adding test coverage to legacy untested components
  • Migrating from Enzyme or class-component-era patterns to React Testing Library
  • Setting up Vitest or Jest for a new React project
  • Mocking HTTP requests in tests
  • Asserting accessibility violations
  • Deciding which tests belong in RTL vs Playwright Component Testing vs full E2E

Core Principle

Test what the user sees and does, not implementation details.

A test should:

  • Render the component with the same providers it has in production
  • Interact with it via accessible queries (role, label) and userEvent
  • Assert visible output and observable side effects (callback fired, request sent)

A test should NOT:

  • Inspect component state, props passed to children, or which hooks were called
  • Mock React itself or framework hooks
  • Assert on the number of renders or DOM structure beyond what affects users

Library Choice

RunnerWhenNote
VitestVite, Remix, modern setupsFaster, native ESM, Jest-compatible API
JestNext.js, CRA, established reposDefault for many React projects
Playwright Component TestingReal browser engine neededUse when JSDOM lacks the required feature
Cypress Component TestingReal browser, Cypress already in useAlternative to Playwright CT

Pick one. Do not run RTL + Vitest AND Playwright CT in the same repo unless you have a clear lane separation.

Query Priority

React Testing Library exposes queries in three tiers — use top-down:

  1. Accessible to everyone: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue
  2. Semantic: getByAltText, getByTitle
  3. Test IDs (escape hatch): getByTestId
tsx
// Best
screen.getByRole("button", { name: /save/i });

// OK for inputs
screen.getByLabelText("Email");

// Last resort
screen.getByTestId("save-btn");

Variants:

  • getBy* — throws if no match
  • queryBy* — returns null (use for "assert absence")
  • findBy* — async, returns a Promise (use for elements that appear after async work)

User Interaction with userEvent

tsx
import userEvent from "@testing-library/user-event";

test("submits the form", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText("Email"), "user@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
});
  • Always await userEvent calls
  • Call userEvent.setup() once per test, reuse the returned user
  • userEvent simulates a real browser sequence; fireEvent dispatches a single synthetic event — prefer userEvent

Async Patterns

tsx
// Element that appears after async work
expect(await screen.findByText("Loaded")).toBeInTheDocument();

// Side effect assertion
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// Element that should disappear
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

Never setTimeout + assertion — flaky. Use the matchers above.

Network Mocking with MSW

Mock Service Worker mocks at the network layer. The component, hooks, and fetch library all behave exactly as in production.

Setup

ts
// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Configure onUnhandledRequest: "error" so any unmocked request fails the test loudly — silent passes are worse than red.

Per-test override

tsx
test("renders error on 500", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});

Provider Wrapping

Wrap providers once in a test-utils.tsx:

tsx
// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions,
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={lightTheme}>
        <MemoryRouter>{ui}</MemoryRouter>
      </ThemeProvider>
    </QueryClientProvider>,
    options,
  );
}

export * from "@testing-library/react";

Then import { renderWithProviders, screen } from "test-utils" in every test file.

Custom Hook Testing

tsx
import { renderHook, act } from "@testing-library/react";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter accepts initial value", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser fetches user data", async () => {
  // Instantiate QueryClient ONCE per test outside the wrapper so it survives re-renders.
  // Creating it inside the wrapper closure resets cache state on every render, producing flaky tests.
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
  • Wrap state-changing calls in act
  • Test through the hook's public API only
  • For hooks that use context, pass a wrapper

Accessibility Assertions

tsx
import { axe, toHaveNoViolations } from "jest-axe"; // or vitest-axe
expect.extend(toHaveNoViolations);

test("UserCard has no a11y violations", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

Run axe in component tests for every interactive component. Catches:

  • Missing labels on form inputs
  • Invalid ARIA usage
  • Poor color contrast (limited — JSDOM has no real CSS engine, so this works for inline styles only; visual contrast belongs in Playwright)
  • Missing alt text on images
  • Heading order violations

Cross-link: skills/accessibility/SKILL.md for the broader a11y testing playbook.

When NOT to Use Snapshot Tests

Snapshots of rendered output:

  • Break on every styling change
  • Get rubber-stamped during review
  • Test implementation detail (DOM structure), not behavior

Acceptable snapshot uses:

  • Pure data serialization functions (formatInvoice(invoice) -> stable string)
  • Generated config files (e.g., webpack config output)

For visual regression on components, use Playwright/Cypress screenshots or Percy/Chromatic — actual visual diffs, not DOM strings.

When to Reach for Playwright / Cypress

JSDOM (used by Vitest/Jest) cannot:

  • Render real layout (flexbox, grid, viewport queries)
  • Run native browser animation, CSS transitions
  • Test scrolling behavior, drag-and-drop, paste from clipboard
  • Handle iframes, popups, downloads, cross-origin flows
  • Run real network in a controlled environment with full DevTools support

For any of those, use Playwright Component Testing (component test in real browser) or full E2E. See e2e-testing skill.

Decision boundary:

  • A hook, a presentational component, a form with logic -> RTL
  • A component whose layout matters or that uses browser APIs not in JSDOM -> Playwright CT
  • A full user flow across multiple pages -> Playwright/Cypress E2E

Coverage Targets

LayerTarget
Pure utilities>=90%
Custom hooks>=85%
Presentational components>=80% — behavior, not lines
Container components>=70% — golden paths + error states
PagesE2E covered separately; smoke test minimum

Configure via vitest.config.ts / jest.config.js:

ts
// vitest.config.ts
test: {
  coverage: {
    provider: "v8",
    reporter: ["text", "html", "lcov"],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
}

Anti-Patterns

  • container.querySelector("...") — bypasses accessibility queries, lets tests pass when real users would fail
  • Asserting on number of renders — implementation detail
  • jest.mock("react", ...) — never mock React. Refactor the component instead
  • Mocking child components by default — tests the integration, not isolation. Mock only when the child has heavy side effects
  • Ignoring act() warnings — they signal real bugs (state update after unmount, missing async wrapping)
  • Sharing mutable state across tests — flakes when test order changes
  • Tests that pass with it.skip() removed — your test does not actually assert what you think

TDD Workflow

RED     -> Write failing test for the next requirement
GREEN   -> Write minimal component code to pass
REFACTOR -> Improve the component, tests stay green
REPEAT  -> Next requirement

For new components:

  1. Define the component's prop type and signature
  2. Write the first test for the simplest case
  3. Verify it fails for the right reason
  4. Implement just enough to pass
  5. Add the next test case
  6. Refactor when the third similar test reveals a pattern

Test Commands

bash
# Vitest
vitest                            # watch
vitest run                        # one-shot
vitest run --coverage             # with coverage
vitest run path/to/file.test.tsx  # single file

# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx

# CI mode
CI=true vitest run --coverage

Related

Examples

Form submission with MSW and userEvent

tsx
test("submits user form and shows success", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "alice@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});

Testing an error boundary

tsx
function Broken() {
  throw new Error("boom");
}

test("error boundary renders fallback", () => {
  // Suppress React's console.error noise for the expected throw, then restore so
  // the spy does not leak across tests and hide real errors elsewhere.
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("Something went wrong")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

Testing a Suspense boundary

tsx
test("shows loading then content", async () => {
  renderWithProviders(
    <Suspense fallback={<div>Loading...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("Loading...")).toBeInTheDocument();
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

按任务相关推荐

来自同一仓库

更多 Skills

全部 Skills
affaan-m
社区

docker-patterns

Docker and Docker Compose patterns for local development, hardened CLI installer harnesses, container security, networking, volumes, and multi-service orchestration. Use when creating or reviewing Dockerfiles and Compose services, testing installers across Linux distributions, or planning accurate native macOS and Windows validation.

1小时安装量
3
GitHub Stars
29.6万
最近更新
11月2日
affaan-m
社区

benchmark

Measure performance baselines and detect regressions across browser Core Web Vitals (LCP, INP, CLS, page weight), API endpoint latency percentiles, and build/test feedback times, with before/after comparison stored in git-tracked .ecc/benchmarks JSON. Use when checking page speed, responding to 'it feels slow' reports, verifying launch performance targets, or comparing stack alternatives.

1小时安装量
3
GitHub Stars
29.4万
最近更新
10月30日
affaan-m
社区

benchmark-methodology

Score a scoped competitor set into comparable profile cards: nine weighted dimensions (positioning, voice, visual craft, offer packaging, evidence, enterprise-readiness, thought leadership, pricing, client tension) with 1-5 evidence-anchored rubrics and a tension 2x2 plot. Use when benchmarking or scoring competitors, building a competitive comparison matrix, or grading rival positioning before assembling the report; runs after competitive-platform-analysis and before competitive-report-structur

1小时安装量
3
GitHub Stars
29.4万
最近更新
10月30日
affaan-m
社区

benchmark-optimization-loop

Convert 'make it faster' requests into a bounded measured optimization loop — baseline first, generate one-hypothesis variants, benchmark each against a correctness gate, and promote the fastest safe variant with reproducible commands. Use when asked to speed something up, try many variants, run recursive optimization, benchmark latency/throughput/cost, or pick the best implementation by repeated measured tests.

1小时安装量
3
GitHub Stars
29.4万
最近更新
10月30日