mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-13 23:03:34 +08:00
* feat: expand Kiro adapter to full language coverage - Add 17 new agents (typescript, rust, kotlin, java, cpp, django, swift, fsharp, pytorch, mle, performance-optimizer) in both .md and .json formats - Add 25 new skills (rust, kotlin, java/spring, django, fastapi, nestjs, react, nextjs, cpp, swift, mle/pytorch, deep-research, strategic-compact, autonomous-loops, content-hash-cache-pattern) - Add 6 new language-specific steering files (rust, kotlin, java, cpp, php, ruby) - Add 3 new hooks (rust-check-on-edit, python-lint-on-edit, security-check-on-create) - Update README with expanded component inventory and documentation - Fix install.sh line endings for macOS compatibility Total Kiro components: 33 agents, 43 skills, 22 steering files, 13 hooks * fix: resolve P1/P2 violations in Kiro agents, skills, and steering - java-patterns.md: remove reference to non-existent quarkus-patterns skill - kotlin-patterns.md: fix insecure BuildConfig recommendation for secrets - swift-actor-persistence: fix Swift version claim (5.9+) and Dictionary crash - java-reviewer.md: add recursive framework detection + robust diff chain - kotlin-reviewer.md: replace unreliable diff detection with fallback chain - rust-reviewer.md: add diff fallback + make CI gating mandatory - jpa-patterns: add DISTINCT to fetch-join query to prevent duplicates - django-reviewer.md: add migration safety check, narrow save() rule, fix pytest-django behavior description * fix: resolve remaining violations in Kiro agents, skills, and docs Agents: - java-build-resolver.md: remove quarkus-patterns ref, fix 'Initialise' spelling - java-reviewer.json: remove quarkus-patterns ref from prompt - mle-reviewer.md, cpp-build-resolver.md, java-build-resolver.md, performance-optimizer.md: fix allowedTools 'read' -> 'fs_read' Hooks: - rust-check-on-edit: fix description to match askAgent behavior Skills: - content-hash-cache-pattern: hyphenate 'Content-Hash-Based' - cpp-testing: hyphenate 'real-time' - django-security: use placeholder secrets, fix CSRF_COOKIE_HTTPONLY=False - nestjs-patterns: add Logger to HttpExceptionFilter for non-Http errors - react-patterns: add React 19 compatibility note for useActionState - rust-patterns: remove edition-specific 'Rust 2024+' reference - springboot-patterns: cap exponential backoff, recommend Resilience4j - springboot-security: fix invalid @Query SQL injection example - swift-protocol-di-testing: add thread-safety doc comment to mock Docs: - README.md: fix Project Structure counts (33/43/22/13) * fix: sync README tree with counts, restore local diff in kotlin-reviewer, correct django FK index guidance - README.md: Project Structure tree now lists all 33 agents, 43 skills, 22 steering files, and 13 hooks (was showing old subset) - kotlin-reviewer.md: restore git diff --staged / git diff for local pre-commit review before falling back to HEAD~1 - django-reviewer.md: clarify that ForeignKey fields are indexed by default; only flag missing db_index on non-FK filter columns
424 lines
13 KiB
Markdown
424 lines
13 KiB
Markdown
---
|
|
name: react-testing
|
|
description: 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. Use when writing or fixing tests for React components, hooks, or pages.
|
|
origin: ECC
|
|
---
|
|
|
|
# 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
|
|
|
|
| Runner | When | Note |
|
|
|---|---|---|
|
|
| **Vitest** | Vite, Remix, modern setups | Faster, native ESM, Jest-compatible API |
|
|
| **Jest** | Next.js, CRA, established repos | Default for many React projects |
|
|
| **Playwright Component Testing** | Real browser engine needed | Use when JSDOM lacks the required feature |
|
|
| **Cypress Component Testing** | Real browser, Cypress already in use | Alternative 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](../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](../e2e-testing/SKILL.md).
|
|
|
|
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
|
|
|
|
| Layer | Target |
|
|
|---|---|
|
|
| Pure utilities | >=90% |
|
|
| Custom hooks | >=85% |
|
|
| Presentational components | >=80% — behavior, not lines |
|
|
| Container components | >=70% — golden paths + error states |
|
|
| Pages | E2E 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
|
|
|
|
- Rules: [rules/react/testing.md](../../rules/react/testing.md)
|
|
- Skills: [react-patterns](../react-patterns/SKILL.md), [accessibility](../accessibility/SKILL.md), [e2e-testing](../e2e-testing/SKILL.md), [tdd-workflow](../tdd-workflow/SKILL.md)
|
|
- Agents: `react-reviewer` (reviews test quality during code review), `tdd-guide` (enforces TDD process)
|
|
- Commands: `/react-test`, `/react-review`
|
|
|
|
## 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();
|
|
});
|
|
```
|