Search across all documentation pages
Test component rendering, user interactions, conditional UI, and dynamic content with confidence.
Quick-reference recipe card -- copy-paste ready.
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
// Test rendering with props
render(<Alert severity="warning" message="Disk full" />);
expect(screen.getByRole("alert")).toHaveTextContent("Disk full");
// Test user interactions
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /delete/i }));
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
// Test conditional rendering
render(<Banner show={false} />);
expect(screen.queryByRole("banner")).not.toBeInTheDocument();
// Test lists
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);
expect(within(items[0]).getByText("First")).toBeInTheDocument();When to reach for this: Whenever you build a component that renders props, handles interactions, or shows conditional UI.
// src/components/todo-list.tsx
"use client";
import { useState } from "react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
export function TodoList
// src/components/todo-list.test.tsx
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { TodoList } from "./todo-list";
// Mock crypto.randomUUID for deterministic IDs
beforeEach(() => {
What this demonstrates:
queryBywithin()aria-label for targeted delete button queriesrender() call creates a fresh DOM tree -- tests are isolated by defaultwithin() scopes queries to a specific container element, useful for testing individual list itemsqueryBy returns null instead of throwing, making it ideal for asserting absencegetAllBy returns an array of matching elements -- use .toHaveLength() to assert list sizesSnapshot testing (use sparingly):
it("matches snapshot", () => {
const { container } = render(
<Alert severity="error" message="Something broke" />
);
expect(container.firstChild).toMatchSnapshot();
});
// Snapshots are useful for detecting unintended UI changes
Inline snapshots for small output:
it("renders correct class names", () => {
render(<Badge variant="success">OK</Badge>);
expect(screen.getByText("OK").className).toMatchInlineSnapshot(
`"badge badge-success"`
);
});Testing with rerender for prop changes:
it("updates when severity changes", () => {
const { rerender } = render(<Alert severity="info" message="Note" />);
expect(screen.getByRole("alert")).toHaveClass("alert-info");
// Type-safe test factories for complex props
function createTodo(overrides: Partial<Todo> = {}): Todo {
return {
id: crypto.randomUUID(),
text: "Default todo",
completed: false,
...overrides,
};
Testing implementation details -- Checking internal state, instance methods, or CSS class names ties tests to code structure. Fix: Assert what the user sees -- text content, visibility, enabled/disabled state.
Over-relying on snapshots -- Large snapshots are hard to review and get rubber-stamped on update. Fix: Use snapshots only for small, stable output. Prefer explicit assertions.
Forgetting to await userEvent -- All userEvent methods return promises in v14+. Fix: Always await them or the test may pass before the interaction completes.
Not using within() for list items -- screen.getByText("Delete") matches the first delete button on the page. Fix: Scope to the specific list item with within() or use unique aria-label attributes.
Testing too many things in one test -- Tests that add, toggle, and delete in a single it block are hard to debug when they fail. Fix: Write focused tests for each behavior.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Storybook + Chromatic | You need visual regression testing alongside component stories | You only need behavioral tests |
| Playwright Component Testing | You need real browser rendering (canvas, complex CSS) | Fast unit-level component tests are sufficient |
| Snapshot testing only | Guarding against unintended changes in stable, small components | Components change frequently or snapshots are large |
Use queryBy which returns null instead of throwing:
render(<Banner show={false} />);
expect(screen.queryByRole("banner")).not.toBeInTheDocument();Use within() to scope queries to a container:
const items = screen.getAllByRole("listitem");
expect(within(items[0]).getByText("First")).toBeInTheDocument();toHaveTextContent or toBeInTheDocument.Use rerender from the render result:
const { rerender } = render(<Alert severity="info" message="Note" />);
rerender(<Alert severity="error" message="Note" />);
expect(screen.getByRole(screen.getByText("Delete") match the wrong button?If multiple elements contain the same text, getByText matches the first one. Use within() to scope to a specific parent, or use unique aria-label attributes.
crypto.randomUUID() for deterministic test IDs?let counter = 0;
vi.stubGlobal("crypto", {
randomUUID: () => `test-id-${++counter}`,
});it block?await userEvent methods?In userEvent v14+, all methods are async. Without await, interactions may not complete before assertions run, causing false positives or flaky tests.
function createTodo(overrides: Partial<Todo> = {}): Todo {
return {
id: crypto.randomUUID(),
text: "Default todo",
completed: false,
...overrides,
};
Yes. Modern Testing Library auto-cleans up after each test by unmounting components and removing DOM nodes. You do not need to call cleanup() manually.
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);.snap file.