React Testing Library tests components the way users interact with them — screen.getByRole, screen.getByLabelText, and screen.getByText find elements by accessibility attributes rather than CSS selectors. userEvent.setup() simulates realistic browser events including keyboard navigation, focus, and pointer events. waitFor retries assertions until they pass or timeout — use findBy* queries as a shortcut for waitFor + getBy*. Mock Service Worker intercepts fetch and XHR at the network level with http.get/post handlers registered in setupServer. renderWithProviders wraps the component under test in all required React context providers. jest-axe checks rendered output against axe accessibility rules. Custom queries extend the default query set for domain-specific finders. Claude Code generates Testing Library test suites, renderWithProviders helpers, msw handlers, and integration test patterns for React applications.
CLAUDE.md for Testing Library
## Testing Library Stack
- Version: @testing-library/react >= 16, @testing-library/user-event >= 14
- Query priority: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId
- Async: findByRole (= waitFor + getByRole), waitFor(() => expect(...)), waitForElementToBeRemoved
- Events: const user = userEvent.setup() — use user.click/type/keyboard/selectOptions
- Mocking: msw 2.x — setupServer(http.get/post handlers) — server.use for test overrides
- Providers: renderWithProviders helper wrapping QueryClient, Router, Theme, Store
- A11y: jest-axe — await expect(container).toHaveNoViolations()
- Vitest: happy-dom or jsdom environment — @testing-library/jest-dom matchers
Test Setup
// vitest.config.ts — Testing Library with Vitest
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
css: true,
},
})
// src/test/setup.ts — global test setup
import "@testing-library/jest-dom"
import { cleanup } from "@testing-library/react"
import { afterEach, beforeAll, afterAll } from "vitest"
import { server } from "./msw/server"
// Clean DOM between tests
afterEach(() => cleanup())
// Start msw server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: "warn" }))
afterEach(() => server.resetHandlers()) // Reset per-test overrides
afterAll(() => server.close())
renderWithProviders Helper
// src/test/render.tsx — wraps component with all providers
import { render, type RenderOptions } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter, type MemoryRouterProps } from "react-router-dom"
import { ThemeProvider } from "@/components/ThemeProvider"
import { ToastProvider } from "@/components/ui/toast"
import type { ReactElement, ReactNode } from "react"
interface RenderWithProvidersOptions extends Omit<RenderOptions, "wrapper"> {
routerProps?: MemoryRouterProps
preloadedQueries?: Record<string, unknown>
}
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // Don't retry in tests — fail fast
gcTime: Infinity,
},
mutations: {
retry: false,
},
},
})
}
export function renderWithProviders(
ui: ReactElement,
{
routerProps = { initialEntries: ["/"] },
preloadedQueries = {},
...renderOptions
}: RenderWithProvidersOptions = {}
) {
const queryClient = createTestQueryClient()
// Pre-populate the query cache for snapshot tests
Object.entries(preloadedQueries).forEach(([key, data]) => {
queryClient.setQueryData(JSON.parse(key), data)
})
function Wrapper({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter {...routerProps}>
<ThemeProvider defaultTheme="light" storageKey="test-theme">
<ToastProvider>
{children}
</ToastProvider>
</ThemeProvider>
</MemoryRouter>
</QueryClientProvider>
)
}
return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
queryClient,
}
}
export { screen, waitFor, within, fireEvent, act } from "@testing-library/react"
export { userEvent } from "@testing-library/user-event"
MSW Handlers
// src/test/msw/handlers.ts — default API mock handlers
import { http, HttpResponse, delay } from "msw"
const mockOrders = [
{
id: "ord-abc12345",
status: "pending",
totalCents: 4997,
customerId: "cust-1",
items: [{ productId: "prod-1", name: "Widget Pro", quantity: 1, priceCents: 4997 }],
createdAt: "2024-01-15T10:30:00Z",
},
]
export const handlers = [
http.get("/api/orders", async ({ request }) => {
const url = new URL(request.url)
const customerId = url.searchParams.get("customerId")
const filtered = customerId
? mockOrders.filter(o => o.customerId === customerId)
: mockOrders
return HttpResponse.json({ orders: filtered, totalCount: filtered.length })
}),
http.get("/api/orders/:id", ({ params }) => {
const order = mockOrders.find(o => o.id === params.id)
if (!order) return HttpResponse.json({ error: "Not found" }, { status: 404 })
return HttpResponse.json(order)
}),
http.post("/api/orders", async ({ request }) => {
const body = await request.json() as { items: unknown[] }
const newOrder = {
id: "ord-new-1234",
status: "pending",
totalCents: 2000,
customerId: "cust-1",
items: body.items,
createdAt: new Date().toISOString(),
}
return HttpResponse.json(newOrder, { status: 201 })
}),
http.post("/api/orders/:id/cancel", ({ params }) => {
return HttpResponse.json({ id: params.id, status: "cancelled" })
}),
]
// src/test/msw/server.ts
import { setupServer } from "msw/node"
import { handlers } from "./handlers"
export const server = setupServer(...handlers)
Component Tests
// src/components/orders/__tests__/OrderCard.test.tsx
import { describe, it, expect, vi } from "vitest"
import { renderWithProviders, screen, userEvent } from "@/test/render"
import { OrderCard } from "../OrderCard"
const mockOrder = {
id: "ord-abc12345",
status: "pending" as const,
totalCents: 4997,
items: [{ productId: "prod-1", name: "Widget Pro", quantity: 1, priceCents: 4997 }],
createdAt: "2024-01-15T10:30:00Z",
}
describe("OrderCard", () => {
it("renders order details correctly", () => {
renderWithProviders(<OrderCard order={mockOrder} />)
expect(screen.getByText("#abc12345")).toBeInTheDocument()
expect(screen.getByText("pending")).toBeInTheDocument()
expect(screen.getByText("$49.97")).toBeInTheDocument()
expect(screen.getByText("Widget Pro")).toBeInTheDocument()
})
it("calls onCancel when cancel button is clicked", async () => {
const user = userEvent.setup()
const onCancel = vi.fn()
renderWithProviders(<OrderCard order={mockOrder} onCancel={onCancel} />)
await user.click(screen.getByRole("button", { name: /cancel order/i }))
// Confirm dialog appears
expect(screen.getByRole("dialog")).toBeInTheDocument()
expect(screen.getByText(/are you sure/i)).toBeInTheDocument()
await user.click(screen.getByRole("button", { name: /confirm/i }))
expect(onCancel).toHaveBeenCalledWith("ord-abc12345")
})
it("hides cancel button for delivered orders", () => {
renderWithProviders(
<OrderCard order={{ ...mockOrder, status: "delivered" }} />
)
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument()
})
})
Integration Tests with MSW
// src/components/orders/__tests__/OrdersPage.test.tsx
import { describe, it, expect } from "vitest"
import { http, HttpResponse } from "msw"
import { renderWithProviders, screen, waitFor, userEvent } from "@/test/render"
import { server } from "@/test/msw/server"
import { OrdersPage } from "../OrdersPage"
describe("OrdersPage", () => {
it("loads and displays orders", async () => {
renderWithProviders(<OrdersPage customerId="cust-1" />, {
routerProps: { initialEntries: ["/orders"] },
})
// Loading state
expect(screen.getByRole("status", { name: /loading/i })).toBeInTheDocument()
// Orders appear after fetch
await screen.findByText("#abc12345")
expect(screen.getByText("$49.97")).toBeInTheDocument()
expect(screen.queryByRole("status", { name: /loading/i })).not.toBeInTheDocument()
})
it("shows empty state when no orders", async () => {
server.use(
http.get("/api/orders", () =>
HttpResponse.json({ orders: [], totalCount: 0 })
)
)
renderWithProviders(<OrdersPage customerId="cust-empty" />)
await screen.findByText(/no orders yet/i)
})
it("shows error state on API failure", async () => {
server.use(
http.get("/api/orders", () =>
HttpResponse.json({ error: "Internal server error" }, { status: 500 })
)
)
renderWithProviders(<OrdersPage customerId="cust-1" />)
await screen.findByText(/something went wrong/i)
expect(screen.getByRole("button", { name: /try again/i })).toBeInTheDocument()
})
it("filters orders by status", async () => {
const user = userEvent.setup()
renderWithProviders(<OrdersPage customerId="cust-1" />)
await screen.findByText("#abc12345")
await user.selectOptions(
screen.getByRole("combobox", { name: /filter by status/i }),
"shipped"
)
await waitFor(() => {
expect(screen.queryByText("#abc12345")).not.toBeInTheDocument()
})
})
})
Accessibility Tests
// src/components/forms/__tests__/CreateOrderForm.a11y.test.tsx
import { describe, it, expect } from "vitest"
import { axe, toHaveNoViolations } from "jest-axe"
import { renderWithProviders } from "@/test/render"
import { CreateOrderForm } from "../CreateOrderForm"
expect.extend(toHaveNoViolations)
describe("CreateOrderForm accessibility", () => {
it("has no accessibility violations in default state", async () => {
const { container } = renderWithProviders(
<CreateOrderForm onSuccess={() => {}} />
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it("has no violations with validation errors shown", async () => {
const user = userEvent.setup()
const { container } = renderWithProviders(
<CreateOrderForm onSuccess={() => {}} />
)
// Trigger validation errors
await user.click(screen.getByRole("button", { name: /place order/i }))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
For the Playwright E2E testing alternative that tests the full browser stack including real HTTP calls, authentication flows, and cross-browser rendering rather than JSDOM-based component tests, see the Playwright guide for page object models and test fixtures. For Vitest unit testing patterns without the Testing Library DOM layer — for testing utilities, hooks, stores, and pure TypeScript functions, the Vitest configuration guide covers the setup. The Claude Skills 360 bundle includes Testing Library skill sets covering renderWithProviders, msw integration, and accessibility testing. Start with the free tier to try test generation.