
React Testing Library
- 1.2k installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
react-testing-library provides documented workflows for React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API. Use when writing React
About
The react-testing-library skill react Testing Library user-centric component testing with queries user-event simulation async utilities and accessibility-first API Use when writing React component tests selecting elements by role label text simulating user events or testing async UI behavior Keywords React Testing Library testing-library react user-event queries render React Testing Library Skill Quick Navigation Topic Link Queries references queries md references queries md User Events references user-events md references user-events md API references api md references api md Async references async md references async md Debugging references debugging md references debugging md Config references config md references config md Installation Install npm install save-dev testing-library react testing-library dom Recommended extras testing-library user-event and testing-library jest-dom React 19 requires v16 1 0 Core Philosophy The more your tests resemble the way your software is used the more confidence they can give you Avoid testing Internal state of components Internal methods Lifecycle methods Child component implementation details Test instead What users see and interact with B.
- Internal state of components
- Internal methods
- Lifecycle methods
- Child component implementation details
- What users see and interact with
React Testing Library by the numbers
- 1,228 all-time installs (skills.sh)
- +28 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #200 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
react-testing-library capabilities & compatibility
- Capabilities
- internal state of components · internal methods · lifecycle methods · child component implementation details · what users see and interact with
- Use cases
- documentation
What react-testing-library says it does
Recommended extras: `@testing-library/user-event` and `@testing-library/jest-dom`.
Semantic Queries ```ts // Images getByAltText("Company logo"); // Title attribute (less reliable) getByTitle("Close"); ``` ### 3.
npx skills add https://github.com/itechmeat/llm-code --skill react-testing-libraryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 22 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
How do I use react-testing-library for the task described in its SKILL.md triggers?
React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API. Use when writing React component tests, selecting elements by.
Who is it for?
Teams invoking react-testing-library when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API. Use when writing React component tests, selecting elements by role/label/text, simu
What you get
Step-by-step guidance grounded in react-testing-library documentation and reference files.
- React component test files
- Provider wrapper test utilities
Files
React Testing Library Skill
Quick Navigation
| Topic | Link |
|---|---|
| Queries | references/queries.md |
| User Events | references/user-events.md |
| API | references/api.md |
| Async | references/async.md |
| Debugging | references/debugging.md |
| Config | references/config.md |
---
Installation
Install: npm install --save-dev @testing-library/react @testing-library/dom. Recommended extras: @testing-library/user-event and @testing-library/jest-dom. React 19 requires v16.1.0+.
Core Philosophy
"The more your tests resemble the way your software is used, the more confidence they can give you."
Avoid testing:
- Internal state of components
- Internal methods
- Lifecycle methods
- Child component implementation details
Test instead:
- What users see and interact with
- Behavior from user's perspective
- Accessibility (queries by role, label)
---
Query Priority
Use queries in this order of preference:
1. Accessible to Everyone (Preferred)
// Best — by ARIA role
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// Form fields — by label
getByLabelText("Email");
// Non-interactive content — by text
getByText("Welcome back!");2. Semantic Queries
// Images
getByAltText("Company logo");
// Title attribute (less reliable)
getByTitle("Close");3. Test IDs (Escape Hatch)
// Only when other queries don't work
getByTestId("custom-element");---
Query Types
| Type | No Match | 1 Match | >1 Match | Async |
|---|---|---|---|---|
getBy... | throw | return | throw | No |
queryBy... | null | return | throw | No |
findBy... | throw | return | throw | Yes |
getAllBy... | throw | array | array | No |
queryAllBy... | [] | array | array | No |
findAllBy... | throw | array | array | Yes |
When to use:
getBy*— element existsqueryBy*— element may not exist (assertions likeexpect(...).not.toBeInTheDocument())findBy*— element appears asynchronously
---
Basic Test Pattern
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("shows greeting after login", async () => {
const user = userEvent.setup();
render(<App />);
// Act — simulate user interactions
await user.type(screen.getByLabelText(/username/i), "john");
await user.click(screen.getByRole("button", { name: /login/i }));
// Assert — verify outcome
expect(await screen.findByText(/welcome, john/i)).toBeInTheDocument();
});---
User Events
Always use @testing-library/user-event over fireEvent:
import userEvent from "@testing-library/user-event";
test("user interactions", async () => {
const user = userEvent.setup();
// Click
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element);
// Type
await user.type(input, "Hello");
await user.clear(input);
// Select
await user.selectOptions(select, ["option1", "option2"]);
// Keyboard
await user.keyboard("{Enter}");
await user.keyboard("[ShiftLeft>]a[/ShiftLeft]"); // Shift+A
// Clipboard
await user.copy();
await user.paste();
// Pointer
await user.hover(element);
await user.unhover(element);
});---
Async Patterns
waitFor — Retry Until Success
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});findBy — Built-in waitFor
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");waitForElementToBeRemoved
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));---
Common Patterns
Custom Render with Providers
// test-utils.tsx
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
function AllProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
const customRender = (ui, options) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };Testing Hooks
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Rerender with New Props
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();Query Within Container
import { within } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });---
Debugging
// Print entire DOM
screen.debug();
// Print specific element
screen.debug(screen.getByRole("button"));
// Log available roles
import { logRoles } from "@testing-library/react";
logRoles(container);
// With prettyDOM options
screen.debug(undefined, 10000); // max length---
jest-dom Matchers
import "@testing-library/jest-dom";
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveValue("input value");
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(element).toHaveFocus();
expect(element).toBeChecked();---
Configuration
import { configure } from "@testing-library/react";
configure({
// Custom test ID attribute
testIdAttribute: "data-my-test-id",
// Async timeout
asyncUtilTimeout: 5000,
// Default hidden
defaultHidden: true,
// Throw suggestions (debugging)
throwSuggestions: true,
});---
❌ Prohibitions (Anti-patterns)
// ❌ Don't query by class/id
container.querySelector(".my-class");
// ❌ Don't use container.firstChild
const { container } = render(<Component />);
expect(container.firstChild).toHaveClass("active");
// ❌ Don't use fireEvent when userEvent works
fireEvent.click(button); // Use userEvent.click instead
// ❌ Don't test implementation details
expect(component.state.loading).toBe(false);
// ❌ Don't use waitFor with findBy
await waitFor(() => screen.findByText("x")); // findBy already waits
// ❌ Don't assert inside waitFor callback (unless necessary)
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // OK - need to wait for call
});---
✅ Best Practices
// ✅ Use screen for all queries
import { render, screen } from "@testing-library/react";
render(<Component />);
screen.getByRole("button"); // Good
// ✅ Prefer userEvent over fireEvent
const user = userEvent.setup();
await user.click(button);
// ✅ Use findBy for async elements
const element = await screen.findByText("Loaded");
// ✅ Use queryBy for non-existence assertions
expect(screen.queryByText("Error")).not.toBeInTheDocument();
// ✅ Use within for scoped queries
const form = screen.getByRole("form");
within(form).getByLabelText("Email");
// ✅ Use accessible queries (role, label, text)
getByRole("button", { name: /submit/i });---
TextMatch Options
// Exact match (default)
getByText("Hello World");
// Substring match
getByText("llo Worl", { exact: false });
// Regex
getByText(/hello world/i);
// Custom function
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});---
Quick Reference
| Import | Usage |
|---|---|
render | Render component to DOM |
screen | Query the rendered DOM |
cleanup | Unmount components (auto in Jest) |
act | Wrap state updates |
renderHook | Test custom hooks |
within | Scope queries to element |
waitFor | Retry until assertion passes |
configure | Set global options |
userEvent.setup() | Create user event instance |
Links
React Testing Library API Reference
render()
import { render } from '@testing-library/react'
const result = render(ui, options?)Basic Usage
import { render, screen } from "@testing-library/react";
test("renders greeting", () => {
render(<Greeting name="World" />);
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
});Render Options
render(<Component />, {
container: document.body.appendChild(document.createElement("div")),
baseElement: document.body,
hydrate: false,
legacyRoot: false, // React 17 mode (not available in React 19+)
wrapper: AllProviders,
queries: { ...queries, ...customQueries },
reactStrictMode: true,
// React 19 error handlers
onCaughtError: (error, errorInfo) => {},
onRecoverableError: (error, errorInfo) => {},
});| Option | Description |
|---|---|
container | DOM element to render into |
baseElement | Element for queries (default: document.body) |
hydrate | Use ReactDOM.hydrate for SSR |
legacyRoot | Use React 17 rendering (not in React 19+) |
wrapper | Component to wrap around rendered element |
queries | Custom queries to use |
reactStrictMode | Enable React StrictMode |
onCaughtError | Callback for errors caught by Error Boundary |
onRecoverableError | Callback for errors React automatically recovers |
Render Result
const {
container, // DOM container element
baseElement, // Base element for queries
debug, // console.log(prettyDOM())
rerender, // Re-render with new props
unmount, // Unmount component
asFragment, // Get DocumentFragment snapshot
...queries // All query functions bound to baseElement
} = render(<Component />);React Error Handlers (v16.2.0+)
Handle errors in tests with React 19 error callbacks:
test("catches error boundary errors", () => {
const errors: Error[] = [];
render(<ComponentWithErrorBoundary />, {
onCaughtError: (error, errorInfo) => {
errors.push(error);
console.log("Caught:", error.message);
console.log("Component stack:", errorInfo.componentStack);
},
});
// Trigger error and verify
fireEvent.click(screen.getByRole("button", { name: /throw/i }));
expect(errors).toHaveLength(1);
});
test("handles recoverable errors", () => {
const recoverableErrors: Error[] = [];
render(<HydratedComponent />, {
hydrate: true,
onRecoverableError: (error, errorInfo) => {
recoverableErrors.push(error);
},
});
expect(recoverableErrors).toHaveLength(0);
});---
screen
Pre-bound queries for document.body:
import { render, screen } from "@testing-library/react";
render(<Component />);
// All queries available
screen.getByRole("button");
screen.queryByText("Loading");
screen.findByLabelText("Email");
// Debug
screen.debug();
screen.debug(screen.getByRole("button"));
// Playground URL
screen.logTestingPlaygroundURL();---
rerender()
Update props without remounting:
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();---
unmount()
const { unmount } = render(<Component />);
unmount();
// Component is now unmounted, container is empty---
asFragment()
Create snapshot of current DOM state:
const { asFragment } = render(<Component />);
const firstRender = asFragment();
fireEvent.click(screen.getByRole("button"));
// Snapshot diff
expect(firstRender).toMatchDiffSnapshot(asFragment());---
cleanup()
Unmounts all rendered components. Called automatically in Jest/Vitest.
import { cleanup, render } from "@testing-library/react";
afterEach(cleanup); // Usually not needed
// Or skip auto-cleanup
import "@testing-library/react/dont-cleanup-after-each";---
act()
Wrap state updates:
import { act } from "@testing-library/react";
await act(async () => {
// Trigger state updates
fireEvent.click(button);
await promise;
});Note: Most RTL methods handle act() automatically. Use explicitly only when needed.
---
renderHook()
Test custom hooks:
import { renderHook, act } from "@testing-library/react";
test("useCounter", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});With Initial Props
const { result, rerender } = renderHook((props) => useUser(props.id), { initialProps: { id: 1 } });
expect(result.current.name).toBe("User 1");
rerender({ id: 2 });
expect(result.current.name).toBe("User 2");With Wrapper
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>;
const { result } = renderHook(() => useAuth(), { wrapper });Result Object
const {
result, // { current: hookReturnValue }
rerender, // Re-run hook with new props
unmount, // Unmount hook
} = renderHook(() => useMyHook());---
configure()
Set global options:
import { configure } from "@testing-library/react";
configure({
testIdAttribute: "data-my-test-id",
asyncUtilTimeout: 5000,
defaultHidden: false,
throwSuggestions: true,
reactStrictMode: true,
});---
Custom Render Pattern
// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
const AllProviders = ({ children }) => (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, "wrapper">) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };Usage:
import { render, screen } from "./test-utils";
test("works with providers", () => {
render(<MyComponent />);
// Component has access to ThemeProvider and AuthProvider
});---
Jest Configuration
// jest.config.js
module.exports = {
testEnvironment: "jsdom", // Required for Jest 27+
setupFilesAfterEnv: ["<rootDir>/setupTests.js"],
moduleDirectories: ["node_modules", "utils"],
};// setupTests.js
import "@testing-library/jest-dom";---
Vitest Configuration
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
globals: true, // Enable auto-cleanup
setupFiles: "./setupTests.ts",
},
});// setupTests.ts
import "@testing-library/jest-dom";Async Utilities Reference
Overview
| Method | Use Case |
|---|---|
findBy* | Wait for element to appear |
waitFor | Wait for any condition |
waitForElementToBeRemoved | Wait for element to disappear |
Always use await with async utilities!
---
findBy Queries
findBy* = waitFor + getBy*
// Wait for element to appear
const button = await screen.findByRole("button", { name: /submit/i });
// With timeout
const element = await screen.findByText("Loaded", {}, { timeout: 5000 });Options
await screen.findByText(
'text', // query matcher
{ exact: false }, // query options
{ // waitFor options
timeout: 1000,
interval: 50,
onTimeout: (error) => error,
mutationObserverOptions: { ... },
}
)---
waitFor()
Retry callback until it succeeds or times out:
import { waitFor, screen } from "@testing-library/react";
// Wait for assertion
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// Wait for mock to be called
await waitFor(() => {
expect(mockFn).toHaveBeenCalled();
});
// Wait with custom timeout
await waitFor(() => expect(element).toBeVisible(), { timeout: 5000, interval: 100 });Options
| Option | Default | Description |
|---|---|---|
container | document | DOM container to observe |
timeout | 1000 | Max wait time (ms) |
interval | 50 | Retry interval (ms) |
onTimeout | - | Error transformer |
mutationObserverOptions | {subtree: true, childList: true, attributes: true, characterData: true} | MutationObserver config |
Common Patterns
// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
// Wait for API call
await waitFor(() => {
expect(mockApi).toHaveBeenCalledTimes(1);
});
// Wait for DOM change
await waitFor(() => {
expect(screen.getByRole("list").children).toHaveLength(5);
});⚠️ Anti-patterns
// ❌ Don't use waitFor with findBy (findBy already waits)
await waitFor(() => screen.findByText("Hello"));
// ✅ Just use findBy
const element = await screen.findByText("Hello");
// ❌ Don't wrap getBy in waitFor if element exists
await waitFor(() => screen.getByText("Static"));
// ✅ Use getBy directly
const element = screen.getByText("Static");
// ❌ Empty waitFor callback
await waitFor(() => {});
// ✅ Always assert something
await waitFor(() => expect(something).toBe(true));---
waitForElementToBeRemoved()
Wait for element to be removed from DOM:
import { waitForElementToBeRemoved, screen } from "@testing-library/react";
// Pass element directly
const loader = screen.getByText("Loading...");
await waitForElementToBeRemoved(loader);
// Pass callback
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// With timeout
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."), { timeout: 5000 });Important Notes
// ❌ Element must exist when calling
await waitForElementToBeRemoved(screen.queryByText("Not there"));
// Error: Element not found
// ✅ Element must exist initially
const element = screen.getByText("Loading...");
await waitForElementToBeRemoved(element);---
Async Test Patterns
Loading State
test("shows loading then data", async () => {
render(<DataFetcher />);
// Loading appears
expect(screen.getByText("Loading...")).toBeInTheDocument();
// Wait for data to load
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// Data appears
expect(screen.getByText("Data loaded")).toBeInTheDocument();
});Form Submission
test("submits form and shows success", async () => {
const user = userEvent.setup();
render(<ContactForm />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.click(screen.getByRole("button", { name: "Submit" }));
// Wait for success message
expect(await screen.findByText("Thank you!")).toBeInTheDocument();
});Multiple Async Operations
test("loads and updates data", async () => {
render(<Dashboard />);
// Wait for initial load
await screen.findByText("Dashboard");
// Trigger refresh
await userEvent.click(screen.getByRole("button", { name: "Refresh" }));
// Wait for loading indicator to appear and disappear
await waitFor(() => {
expect(screen.queryByText("Refreshing...")).not.toBeInTheDocument();
});
// Verify updated content
expect(screen.getByText("Updated")).toBeInTheDocument();
});Testing Error States
test("shows error on failure", async () => {
server.use(
rest.get("/api/data", (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<DataComponent />);
// Wait for error
expect(await screen.findByRole("alert")).toHaveTextContent("Error loading data");
});---
Timeout Configuration
Global Default
import { configure } from "@testing-library/react";
configure({
asyncUtilTimeout: 5000, // 5 seconds
});Per-Query
await screen.findByText("Slow content", {}, { timeout: 10000 });
await waitFor(() => expect(element).toBeVisible(), { timeout: 10000 });---
Best Practices
1. Use findBy for async elements
const element = await screen.findByText("Loaded");2. Use waitFor for assertions
await waitFor(() => expect(mockFn).toHaveBeenCalled());3. Don't mix waitFor with findBy
// ❌ Bad
await waitFor(() => screen.findByText("x"));
// ✅ Good
await screen.findByText("x");4. Always await async operations
// ❌ Missing await
screen.findByText("Hello");
// ✅ Correct
await screen.findByText("Hello");5. Use queryBy for disappearance checks
await waitFor(() => {
expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});Configuration Reference
configure()
import { configure } from "@testing-library/react";
configure({
testIdAttribute: "data-testid",
asyncUtilTimeout: 1000,
defaultHidden: false,
throwSuggestions: false,
getElementError: (message, container) => new Error(message),
// React Testing Library specific
reactStrictMode: false,
});---
Options
testIdAttribute
Custom attribute for getByTestId:
configure({ testIdAttribute: "data-my-test-id" });
// Now queries use data-my-test-id
// <div data-my-test-id="my-element">
screen.getByTestId("my-element");Default: 'data-testid'
---
asyncUtilTimeout
Global timeout for async utilities:
configure({ asyncUtilTimeout: 5000 }); // 5 seconds
// Affects findBy*, waitFor, waitForElementToBeRemoved
await screen.findByText("Slow content"); // waits up to 5sDefault: 1000 (1 second)
---
defaultHidden
Include hidden elements in getByRole by default:
configure({ defaultHidden: true });
// Now includes aria-hidden elements
screen.getByRole("button"); // includes hidden buttonsDefault: false
---
throwSuggestions (experimental)
Fail tests when better queries exist:
configure({ throwSuggestions: true });
// This will throw an error suggesting getByRole
screen.getByTestId("submit-button");
// Error: A better query is available: getByRole('button', { name: /submit/i })Disable per query:
screen.getByTestId("element", { suggest: false });Default: false
---
defaultIgnore
Elements to ignore in queries and error output:
configure({ defaultIgnore: "script, style, svg" });Default: 'script, style'
---
getElementError
Custom error formatting:
configure({
getElementError: (message, container) => {
const error = new Error([message, prettyDOM(container), "Custom debug info here"].join("\n\n"));
error.name = "TestingLibraryError";
return error;
},
});---
showOriginalStackTrace
Show full stack trace in waitFor errors:
configure({ showOriginalStackTrace: true });Default: false (cleaned up stack trace)
---
computedStyleSupportsPseudoElements
Enable pseudo-element support in getComputedStyle:
configure({ computedStyleSupportsPseudoElements: true });Set to true in real browsers, false for jsdom.
Default: false
---
React Testing Library Specific
reactStrictMode
Wrap renders in React StrictMode:
configure({ reactStrictMode: true });
// All renders now use StrictMode
render(<MyComponent />);
// Equivalent to: render(<StrictMode><MyComponent /></StrictMode>)Override per render:
render(<Component />, { reactStrictMode: false });Default: false
---
Setup File
Apply configuration globally:
// setupTests.ts
import { configure } from "@testing-library/react";
import "@testing-library/jest-dom";
configure({
testIdAttribute: "data-test-id",
asyncUtilTimeout: 3000,
});Jest config:
// jest.config.js
module.exports = {
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
};Vitest config:
// vitest.config.ts
export default defineConfig({
test: {
setupFiles: ["./setupTests.ts"],
},
});---
Environment Variables
DEBUG_PRINT_LIMIT
Max characters in debug output:
DEBUG_PRINT_LIMIT=20000 npm testDefault: 7000
COLORS
Enable/disable colored output:
COLORS=false npm testRTL_SKIP_AUTO_CLEANUP
Disable automatic cleanup:
RTL_SKIP_AUTO_CLEANUP=true npm testOr import:
import "@testing-library/react/dont-cleanup-after-each";---
Jest Configuration
jsdom Environment
// jest.config.js
module.exports = {
testEnvironment: "jsdom", // Required for Jest 27+
};Or per file:
/**
* @jest-environment jsdom
*/Module Resolution
// jest.config.js
module.exports = {
moduleDirectories: ["node_modules", "utils"],
moduleNameMapper: {
"^test-utils$": "<rootDir>/utils/test-utils",
},
};---
Vitest Configuration
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
globals: true, // Enables auto-cleanup
setupFiles: ["./setupTests.ts"],
},
});Manual Cleanup (if globals: false)
// setupTests.ts
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(cleanup);Debugging Reference
screen.debug()
Print current DOM state:
import { screen } from "@testing-library/react";
// Print entire document
screen.debug();
// Print specific element
screen.debug(screen.getByRole("button"));
// Print multiple elements
screen.debug(screen.getAllByRole("listitem"));
// With max length
screen.debug(undefined, 20000);
// With options
screen.debug(undefined, 10000, { highlight: false });---
prettyDOM()
Convert DOM to string:
import { prettyDOM } from "@testing-library/react";
const div = document.createElement("div");
div.innerHTML = "<h1>Hello</h1>";
console.log(prettyDOM(div));
// <div>
// <h1>Hello</h1>
// </div>
// With max length
prettyDOM(element, 5000);
// With options
prettyDOM(element, undefined, {
highlight: false,
filterNode: (node) => node.tagName !== "SCRIPT",
});Options
| Option | Description |
|---|---|
highlight | Syntax highlighting (default: true in node) |
filterNode | Function to exclude nodes |
---
logRoles()
Show all ARIA roles in DOM:
import { logRoles } from "@testing-library/react";
const { container } = render(<Navigation />);
logRoles(container);
// Output:
// navigation:
// <nav />
// --------------------------------------------------
// list:
// <ul />
// --------------------------------------------------
// listitem:
// <li />
// <li />Use when getByRole fails to help identify correct role.
---
screen.logTestingPlaygroundURL()
Generate Testing Playground link:
render(<MyComponent />);
// Log URL for entire document
screen.logTestingPlaygroundURL();
// https://testing-playground.com/#markup=...
// Log URL for specific element
screen.logTestingPlaygroundURL(screen.getByRole("form"));Open URL in browser to interactively find queries.
---
Automatic Error Logging
When getBy* fails, DOM is automatically logged:
Unable to find an element with the text: Goodbye.
Here is the state of your container:
<div>
<h1>Hello World</h1>
</div>Increase Output Length
# macOS/Linux
DEBUG_PRINT_LIMIT=20000 npm test
# Windows (with cross-env)
cross-env DEBUG_PRINT_LIMIT=20000 npm testDisable Colors
COLORS=false npm test---
Debugging Tips
1. Print Before Assertion
screen.debug();
expect(screen.getByText("Hello")).toBeInTheDocument();2. Use logRoles for Role Queries
const { container } = render(<MyComponent />);
logRoles(container);
// Then use correct role in getByRole()3. Check Element Properties
const button = screen.getByRole("button");
console.log({
text: button.textContent,
disabled: button.disabled,
visible: button.style.display,
classes: button.className,
});4. Inspect Async State
// Before async action
screen.debug()
await user.click(button)
// After async action
await waitFor(() => {
screen.debug() // See state at each retry
expect(...).toBe(...)
})5. Check What Queries Return
// See what's found
console.log(screen.queryAllByRole("button"));
// Check accessible name
const buttons = screen.getAllByRole("button");
buttons.forEach((b) => console.log(b.textContent, b.getAttribute("aria-label")));---
Common Debugging Scenarios
Can't Find Element
// 1. Print DOM
screen.debug();
// 2. Check all roles
logRoles(container);
// 3. Try different queries
screen.queryByText("...");
screen.queryByRole("...");
screen.queryByTestId("...");Element Not Visible
// Include hidden elements
screen.getByRole("button", { hidden: true });
// Check visibility
const el = screen.getByText("Hidden");
console.log(window.getComputedStyle(el).display);
console.log(window.getComputedStyle(el).visibility);Async Element Not Appearing
// Add debug in waitFor
await waitFor(
() => {
screen.debug();
expect(element).toBeInTheDocument();
},
{ timeout: 5000 }
);Multiple Elements Found
// See all matching elements
screen.debug(screen.getAllByRole("button"));
// Be more specific
screen.getByRole("button", { name: /submit/i });---
Testing Playground
Interactive tool at testing-playground.com:
1. Paste HTML markup 2. Click elements to see suggested queries 3. Get query recommendations based on priority
Browser Extension
Install "Testing Playground" Chrome extension:
- Inspect elements in DevTools
- Get query suggestions
- See accessibility tree
Queries Reference
Query Types Summary
| Type | No Match | 1 Match | >1 Match | Async |
|---|---|---|---|---|
getBy... | throw | return element | throw | No |
queryBy... | null | return element | throw | No |
findBy... | throw | return element | throw | Yes |
getAllBy... | throw | array | array | No |
queryAllBy... | [] | array | array | No |
findAllBy... | throw | array | array | Yes |
When to Use
- getBy\* — element should exist
- queryBy\* — element may not exist (use for negative assertions)
- findBy\* — element appears asynchronously
---
Query Priority (Best → Worst)
1. Accessible to Everyone (Preferred)
getByRole — Best choice for most queries
// Buttons
getByRole("button", { name: /submit/i });
// Form fields
getByRole("textbox", { name: /email/i });
getByRole("checkbox", { name: /remember me/i });
getByRole("combobox", { name: /country/i });
// Headings
getByRole("heading", { name: /welcome/i });
getByRole("heading", { level: 2 });
// Navigation
getByRole("link", { name: /home/i });
getByRole("navigation");
// Lists
getByRole("list");
getByRole("listitem");
// Dialogs
getByRole("dialog");
getByRole("alertdialog");Role Options:
getByRole("button", {
name: /submit/i, // accessible name (text, aria-label)
description: /text/, // aria-describedby content
hidden: true, // include hidden elements (default: false)
selected: true, // aria-selected state
checked: true, // checkbox/radio checked state
pressed: true, // toggle button pressed state
expanded: true, // aria-expanded state
current: "page", // aria-current value
busy: false, // aria-busy state
level: 2, // heading level (h1=1, h2=2, etc.)
value: { now: 50, min: 0, max: 100 }, // slider/spinbutton value
queryFallbacks: true, // include fallback roles
});getByLabelText — Best for form fields
getByLabelText("Username");
getByLabelText(/email/i);
getByLabelText("Password", { selector: "input" });getByPlaceholderText — When no label available
getByPlaceholderText("Enter email");getByText — For non-interactive content
getByText("Welcome back!");
getByText(/loading/i);
getByText((content, element) => content.startsWith("Hello"));getByDisplayValue — Current form value
getByDisplayValue("john@example.com");2. Semantic Queries
getByAltText — Images
getByAltText("Company logo");
getByAltText(/avatar/i);getByTitle — Title attribute (less reliable)
getByTitle("Close");3. Test IDs (Escape Hatch)
getByTestId — Last resort
getByTestId("submit-button");
getByTestId("custom-element");---
TextMatch
Queries accept strings, regex, or functions:
// Exact string
getByText("Hello World");
// Substring (case-insensitive)
getByText("hello", { exact: false });
// Regex
getByText(/hello world/i);
getByText(/^hello/i); // starts with
// Custom function
getByText((content, element) => {
return element.tagName === "SPAN" && content.includes("Hello");
});Options
getByText("text", {
exact: false, // substring match, case-insensitive
normalizer: (str) => str.trim().toLowerCase(), // custom normalizer
});
// Default normalizer options
import { getDefaultNormalizer } from "@testing-library/react";
getByText("text", {
normalizer: getDefaultNormalizer({ trim: false, collapseWhitespace: true }),
});---
Using screen
Always prefer screen over destructuring:
import { render, screen } from "@testing-library/react";
render(<MyComponent />);
// ✅ Recommended
screen.getByRole("button");
// ❌ Avoid (unless scoping)
const { getByRole } = render(<MyComponent />);---
Query Within Elements
import { within, screen } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });
// Alternative
const form = screen.getByRole("form");
within(form).getByLabelText("Email");---
Common Roles Reference
| Element | Default Role |
|---|---|
<button> | button |
<a href="..."> | link |
<input type="text"> | textbox |
<input type="checkbox"> | checkbox |
<input type="radio"> | radio |
<select> | combobox |
<ul>, <ol> | list |
<li> | listitem |
<table> | table |
<tr> | row |
<img> | img |
<h1>-<h6> | heading |
<nav> | navigation |
<main> | main |
<article> | article |
<dialog> | dialog |
<form> | form |
Note: <input type="password"> has no implicit role — use getByLabelText.
---
Performance Tip
getByRole can be slow on large DOMs. For performance-critical tests:
// Faster alternatives when accessibility isn't the focus
getByLabelText("Email"); // faster than getByRole('textbox', { name: 'Email' })
getByText("Submit"); // faster than getByRole('button', { name: 'Submit' })
// Or skip visibility checks
getByRole("button", { hidden: true });User Events Reference
Installation
npm install --save-dev @testing-library/user-eventSetup
Always use userEvent.setup() before render:
import userEvent from "@testing-library/user-event";
import { render, screen } from "@testing-library/react";
test("example", async () => {
const user = userEvent.setup();
render(<MyComponent />);
await user.click(screen.getByRole("button"));
});Setup with Custom Options
const user = userEvent.setup({
delay: null, // no delay between events (faster tests)
advanceTimers: jest.advanceTimersByTime, // for fake timers
pointerEventsCheck: 0, // disable pointer-events check
skipHover: true, // skip hover before click
});---
Why userEvent over fireEvent?
| fireEvent | userEvent |
|---|---|
| Triggers single event | Simulates full interaction |
| No visibility checks | Checks element is visible/enabled |
| Synchronous | Async (returns Promise) |
| Low-level | User-centric |
// fireEvent — just triggers click event
fireEvent.click(button);
// userEvent — hovers, focuses, triggers mousedown/up/click
await user.click(button);---
Convenience APIs
Click Events
// Single click
await user.click(element);
// Double click
await user.dblClick(element);
// Triple click (selects text)
await user.tripleClick(element);Hover
await user.hover(element);
await user.unhover(element);Keyboard Navigation
// Tab through elements
await user.tab();
await user.tab({ shift: true }); // Shift+Tab---
Utility APIs
type() — Input Text
await user.type(input, "Hello World");
// With options
await user.type(input, "text", {
skipClick: true, // don't click before typing
skipAutoClose: true, // don't release keys at end
initialSelectionStart: 0, // set cursor position
initialSelectionEnd: 5, // select text range
});clear() — Clear Input
await user.clear(input);
// Equivalent to: select all + deleteselectOptions() / deselectOptions()
// Select by value
await user.selectOptions(select, ["option1", "option2"]);
// Select by text content
await user.selectOptions(select, ["Apple", "Banana"]);
// Select by element
const option = screen.getByRole("option", { name: "Apple" });
await user.selectOptions(select, option);
// Deselect (multi-select only)
await user.deselectOptions(select, "option1");upload() — File Upload
const file = new File(["content"], "file.png", { type: "image/png" });
const input = screen.getByLabelText(/upload/i);
await user.upload(input, file);
expect(input.files[0]).toBe(file);
expect(input.files).toHaveLength(1);
// Multiple files
const files = [new File(["a"], "a.png", { type: "image/png" }), new File(["b"], "b.png", { type: "image/png" })];
await user.upload(input, files);---
Keyboard API
Basic Keys
// Press Enter
await user.keyboard("{Enter}");
// Press Tab
await user.keyboard("{Tab}");
// Press Escape
await user.keyboard("{Escape}");
// Press Backspace
await user.keyboard("{Backspace}");
// Press Delete
await user.keyboard("{Delete}");
// Arrow keys
await user.keyboard("{ArrowUp}");
await user.keyboard("{ArrowDown}");
await user.keyboard("{ArrowLeft}");
await user.keyboard("{ArrowRight}");Modifier Keys
// Hold Shift (key down, no release)
await user.keyboard("{Shift>}");
// Release Shift
await user.keyboard("{/Shift}");
// Shift + A (hold, press a, release)
await user.keyboard("{Shift>}A{/Shift}");
// Ctrl + A (select all)
await user.keyboard("{Control>}a{/Control}");
// Ctrl + C (copy)
await user.keyboard("{Control>}c{/Control}");
// Ctrl + V (paste)
await user.keyboard("{Control>}v{/Control}");Type Text
// Type literal text
await user.keyboard("Hello World");
// Special characters need escaping
await user.keyboard("Hello {{World}}"); // types "Hello {World}"
await user.keyboard("Hello [[World]]"); // types "Hello [World]"---
Pointer API
Basic Pointer Actions
// Click
await user.pointer({ keys: "[MouseLeft]", target: element });
// Right-click
await user.pointer({ keys: "[MouseRight]", target: element });
// Double-click
await user.pointer({ keys: "[MouseLeft][MouseLeft]", target: element });
// Move to element
await user.pointer({ target: element });Drag and Drop
await user.pointer([
{ keys: "[MouseLeft>]", target: source }, // Press down on source
{ target: destination }, // Move to destination
{ keys: "[/MouseLeft]" }, // Release
]);---
Clipboard API
// Copy selected text
await user.copy();
// Cut selected text
await user.cut();
// Paste from clipboard
await user.paste();
// Paste specific text
await user.paste("pasted text");---
Complete Example
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("form submission", async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Fill form
await user.type(screen.getByLabelText(/username/i), "john");
await user.type(screen.getByLabelText(/password/i), "secret123");
// Check remember me
await user.click(screen.getByRole("checkbox", { name: /remember/i }));
// Submit
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username: "john",
password: "secret123",
remember: true,
});
});---
Options Reference
| Option | Default | Description |
|---|---|---|
delay | 0 | Delay between events (ms) |
advanceTimers | - | Function to advance timers |
skipHover | false | Skip hover before click |
skipClick | false | Skip click before type |
skipAutoClose | false | Don't release keys at end |
pointerEventsCheck | 1 | Check pointer-events CSS |
applyAccept | true | Filter files by accept attr |
Related skills
How it compares
Use react-testing-library for unit and integration React component tests; use lobe-chat testing skill for agent runtime E2E with database mocks.
FAQ
What does react-testing-library do?
React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API. Use when writing React component tests, selecting elements by role/label/text, simu
When should I use react-testing-library?
React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API. Use when writing React component tests, selecting elements by role/label/text, simu
What are common prerequisites?
--- name: react-testing-library description: "React Testing Library: user-centric component testing with queries, user-event simulation, async utilities, and accessibility-first API.
Is React Testing Library safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.