Simple and complete React DOM testing utilities that encourage good testing practices
npx @tessl/cli install tessl/npm-testing-library--react@16.3.1Testing utility for React emphasizing user-centric testing. Renders components in a test environment and provides queries for finding elements by accessible roles, labels, and text content.
npm install --save-dev @testing-library/react @testing-library/domreact (^18.0.0 || ^19.0.0)react-dom (^18.0.0 || ^19.0.0)@testing-library/dom (^10.0.0)@types/react (optional, ^18.0.0 || ^19.0.0)@types/react-dom (optional, ^18.0.0 || ^19.0.0)import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react';Pure version (without auto-cleanup):
import { render, screen } from '@testing-library/react/pure';CommonJS:
const { render, screen, fireEvent } = require('@testing-library/react');// Render component
render(<Component />);
// Find element (prefer role queries)
const button = screen.getByRole('button', { name: /submit/i });
// Interact
fireEvent.click(button);
// Assert
expect(screen.getByText(/success/i)).toBeInTheDocument();
// Async wait
await screen.findByText(/loaded/i);const { result } = renderHook(() => useCustomHook());
act(() => result.current.action());
expect(result.current.value).toBe(expected);React Testing Library is built around several key concepts:
render() and renderHook() functions for mounting React components and hooks in a test environmentfireEvent with React-specific behaviors for simulating user interactionsfunction render(ui: React.ReactNode, options?: RenderOptions): RenderResult;
interface RenderOptions {
container?: HTMLElement; // Custom container (e.g., for <tbody>)
baseElement?: HTMLElement; // Base element for queries (defaults to container)
wrapper?: React.ComponentType<{ children: React.ReactNode }>; // Provider wrapper
hydrate?: boolean; // SSR hydration mode
legacyRoot?: boolean; // React 18 only, not supported in React 19+
queries?: Queries; // Custom query set
reactStrictMode?: boolean; // Enable StrictMode
onCaughtError?: (error: Error, errorInfo: { componentStack?: string }) => void; // React 19+ only
onRecoverableError?: (error: Error, errorInfo: { componentStack?: string }) => void; // React 18+
}
interface RenderResult {
container: HTMLElement; // DOM container element
baseElement: HTMLElement; // Base element for queries
rerender: (ui: React.ReactNode) => void; // Re-render with new UI
unmount: () => void; // Unmount and cleanup
debug: (element?, maxLength?, options?) => void; // Pretty-print DOM
asFragment: () => DocumentFragment; // Snapshot testing helper
// All query functions bound to baseElement:
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
getAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
queryByRole: (role: string, options?: ByRoleOptions) => HTMLElement | null;
queryAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
findByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement>;
findAllByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement[]>;
// Plus: getBy/queryBy/findBy LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestId
}Key Patterns:
// Basic render
const { container, rerender } = render(<App />);
// With providers
render(<App />, {
wrapper: ({ children }) => <Provider>{children}</Provider>
});
// Re-render with props
rerender(<App count={2} />);Query Priority: Role > Label > Placeholder > Text > DisplayValue > AltText > Title > TestId
// Synchronous queries
getByRole(role: string, options?: ByRoleOptions): HTMLElement;
getAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];
queryByRole(role: string, options?: ByRoleOptions): HTMLElement | null;
queryAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];
// Async queries (wait for element to appear)
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;
// Available query types: Role, LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestIdCommon Patterns:
// Prefer role queries (most accessible)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
// Form inputs
screen.getByLabelText('Email');
screen.getByPlaceholderText('Enter email');
// Text content
screen.getByText(/hello world/i);
// Negative assertions
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Scoped queries
const modal = screen.getByRole('dialog');
within(modal).getByRole('button', { name: /close/i });// Common events (automatically wrapped in act())
fireEvent.click(element, options?: MouseEventInit);
fireEvent.change(element, { target: { value: 'text' } });
fireEvent.submit(form);
fireEvent.focus(element);
fireEvent.blur(element);
fireEvent.keyDown(element, { key: 'Enter', code: 'Enter' });Patterns:
// Click interaction
fireEvent.click(screen.getByRole('button'));
// Input change
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'user@example.com' }
});
// Keyboard
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
// Hover
fireEvent.mouseEnter(element);
fireEvent.mouseLeave(element);// Wait for condition
function waitFor<T>(callback: () => T | Promise<T>, options?: {
timeout?: number; // default: 1000ms
interval?: number; // default: 50ms
}): Promise<T>;
// Wait for removal
function waitForElementToBeRemoved<T>(
element: T | (() => T),
options?: { timeout?: number }
): Promise<void>;
// Async queries (shorthand for waitFor + getBy)
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;Patterns:
// Wait for element to appear
const element = await screen.findByText(/loaded/i);
// Wait for condition
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
}, { timeout: 3000 });
// Wait for disappearance
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));function renderHook<Result, Props>(
callback: (props: Props) => Result,
options?: RenderHookOptions<Props>
): RenderHookResult<Result, Props>;
interface RenderHookResult<Result, Props> {
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
}Patterns:
// Test hook
const { result } = renderHook(() => useCounter(0));
// Trigger updates (wrap in act)
act(() => result.current.increment());
expect(result.current.count).toBe(1);
// With context
const { result } = renderHook(() => useUser(), {
wrapper: ({ children }) => <UserProvider>{children}</UserProvider>
});
// Re-render with props
const { result, rerender } = renderHook(
({ id }) => useFetch(id),
{ initialProps: { id: 1 } }
);
rerender({ id: 2 });function configure(config: Partial<Config>): void;
interface Config {
reactStrictMode: boolean; // default: false
testIdAttribute: string; // default: 'data-testid'
asyncUtilTimeout: number; // default: 1000ms
}Setup Pattern:
// test-setup.js
import { configure } from '@testing-library/react';
configure({
reactStrictMode: true,
asyncUtilTimeout: 2000,
});interface ByRoleOptions {
name?: string | RegExp; // Accessible name filter
description?: string | RegExp; // Accessible description filter
hidden?: boolean; // Include hidden elements (default: false)
selected?: boolean; // Filter by selected state
checked?: boolean; // Filter by checked state
pressed?: boolean; // Filter by pressed state (toggle buttons)
current?: boolean | string; // Filter by current state (navigation)
expanded?: boolean; // Filter by expanded state
level?: number; // Filter by heading level (1-6)
exact?: boolean; // Enable exact matching (default: true)
normalizer?: (text: string) => string; // Custom text normalizer
queryFallbacks?: boolean; // Enable query fallbacks
}
interface MatcherOptions {
exact?: boolean; // Enable exact matching (default: true)
normalizer?: (text: string) => string; // Custom text normalizer
}
interface SelectorMatcherOptions extends MatcherOptions {
selector?: string; // CSS selector to filter results
}
interface Queries {
[key: string]: (...args: any[]) => any;
}import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider } from './theme';
import { QueryClientProvider } from '@tanstack/react-query';
export function renderWithProviders(
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
),
...options,
});
}test('loads and displays data', async () => {
render(<DataComponent />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const data = await screen.findByText(/data loaded/i);
expect(data).toBeInTheDocument();
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});test('submits form with validation', async () => {
const handleSubmit = jest.fn();
render(<Form onSubmit={handleSubmit} />);
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
fireEvent.change(emailInput, { target: { value: 'user@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com'
});
});
});test('opens and closes modal', async () => {
render(<App />);
const openButton = screen.getByRole('button', { name: /open modal/i });
fireEvent.click(openButton);
const modal = await screen.findByRole('dialog');
expect(modal).toBeInTheDocument();
const closeButton = within(modal).getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitForElementToBeRemoved(modal);
});@testing-library/react)Default import with auto-cleanup and automatic act() environment setup. Use for standard test setups with Jest, Vitest, or similar test runners.
import { render, screen } from '@testing-library/react';
// Auto-cleanup runs after each test@testing-library/react/pure)No auto-cleanup or automatic setup. Use when you need manual control over cleanup or test lifecycle.
import { render, screen, cleanup } from '@testing-library/react/pure';
afterEach(() => {
cleanup(); // Manual cleanup
});Alternative method to disable auto-cleanup by importing this file before the main import. Sets RTL_SKIP_AUTO_CLEANUP=true environment variable.
import '@testing-library/react/dont-cleanup-after-each';
import { render, screen, cleanup } from '@testing-library/react';
// Auto-cleanup is now disabled, manual cleanup required
afterEach(() => {
cleanup();
});Pre-bound queries to document.body for convenient access without destructuring render results.
const screen: {
// All query functions
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
// ... all other query functions
// Debug utilities
debug: (element?: HTMLElement, maxLength?: number) => void;
logTestingPlaygroundURL: (element?: HTMLElement) => void;
};Get queries bound to a specific element for scoped searches.
function within(element: HTMLElement): {
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
// ... all other query functions
};function prettyDOM(
element?: HTMLElement,
maxLength?: number,
options?: any
): string;
function logRoles(container: HTMLElement): void;// Manual cleanup (auto-runs by default)
function cleanup(): void;
// Manual act() wrapping (usually automatic)
function act(callback: () => void | Promise<void>): Promise<void>;Note: Most operations are automatically wrapped in act(), so manual use is rarely needed.