Simple and complete React DOM testing utilities that encourage good testing practices
npx @tessl/cli install tessl/npm-testing-library--react@16.3.10
# React Testing Library
1
2
Testing 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.
3
4
## Package Information
5
6
- **Package Name**: @testing-library/react
7
- **Package Type**: npm
8
- **Language**: JavaScript/TypeScript
9
- **Installation**: `npm install --save-dev @testing-library/react @testing-library/dom`
10
11
## Peer Dependencies
12
13
- `react` (^18.0.0 || ^19.0.0)
14
- `react-dom` (^18.0.0 || ^19.0.0)
15
- `@testing-library/dom` (^10.0.0)
16
- `@types/react` (optional, ^18.0.0 || ^19.0.0)
17
- `@types/react-dom` (optional, ^18.0.0 || ^19.0.0)
18
19
## Core Imports
20
21
```typescript
22
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
23
import { renderHook, act } from '@testing-library/react';
24
```
25
26
**Pure version** (without auto-cleanup):
27
```typescript
28
import { render, screen } from '@testing-library/react/pure';
29
```
30
31
**CommonJS:**
32
```javascript
33
const { render, screen, fireEvent } = require('@testing-library/react');
34
```
35
36
## Quick Reference
37
38
### Essential Pattern
39
```typescript
40
// Render component
41
render(<Component />);
42
43
// Find element (prefer role queries)
44
const button = screen.getByRole('button', { name: /submit/i });
45
46
// Interact
47
fireEvent.click(button);
48
49
// Assert
50
expect(screen.getByText(/success/i)).toBeInTheDocument();
51
52
// Async wait
53
await screen.findByText(/loaded/i);
54
```
55
56
### Test Hook
57
```typescript
58
const { result } = renderHook(() => useCustomHook());
59
act(() => result.current.action());
60
expect(result.current.value).toBe(expected);
61
```
62
63
## Architecture
64
65
React Testing Library is built around several key concepts:
66
67
- **Rendering Utilities**: Core `render()` and `renderHook()` functions for mounting React components and hooks in a test environment
68
- **Query System**: Inherited from @testing-library/dom, provides accessible queries (getBy*, queryBy*, findBy*) that encourage testing from a user's perspective
69
- **Event System**: Enhanced `fireEvent` with React-specific behaviors for simulating user interactions
70
- **act() Wrapper**: Automatic wrapping of renders and updates in React's act() to ensure proper state updates
71
- **Auto-cleanup**: Automatic unmounting and cleanup between tests (can be disabled via pure import)
72
- **Configuration**: Global and per-test configuration for strict mode, queries, wrappers, and error handling
73
74
## Core Capabilities
75
76
### Rendering: [rendering.md](./rendering.md)
77
78
```typescript { .api }
79
function render(ui: React.ReactNode, options?: RenderOptions): RenderResult;
80
81
interface RenderOptions {
82
container?: HTMLElement; // Custom container (e.g., for <tbody>)
83
baseElement?: HTMLElement; // Base element for queries (defaults to container)
84
wrapper?: React.ComponentType<{ children: React.ReactNode }>; // Provider wrapper
85
hydrate?: boolean; // SSR hydration mode
86
legacyRoot?: boolean; // React 18 only, not supported in React 19+
87
queries?: Queries; // Custom query set
88
reactStrictMode?: boolean; // Enable StrictMode
89
onCaughtError?: (error: Error, errorInfo: { componentStack?: string }) => void; // React 19+ only
90
onRecoverableError?: (error: Error, errorInfo: { componentStack?: string }) => void; // React 18+
91
}
92
93
interface RenderResult {
94
container: HTMLElement; // DOM container element
95
baseElement: HTMLElement; // Base element for queries
96
rerender: (ui: React.ReactNode) => void; // Re-render with new UI
97
unmount: () => void; // Unmount and cleanup
98
debug: (element?, maxLength?, options?) => void; // Pretty-print DOM
99
asFragment: () => DocumentFragment; // Snapshot testing helper
100
// All query functions bound to baseElement:
101
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
102
getAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
103
queryByRole: (role: string, options?: ByRoleOptions) => HTMLElement | null;
104
queryAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
105
findByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement>;
106
findAllByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement[]>;
107
// Plus: getBy/queryBy/findBy LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestId
108
}
109
```
110
111
**Key Patterns:**
112
```typescript
113
// Basic render
114
const { container, rerender } = render(<App />);
115
116
// With providers
117
render(<App />, {
118
wrapper: ({ children }) => <Provider>{children}</Provider>
119
});
120
121
// Re-render with props
122
rerender(<App count={2} />);
123
```
124
125
### Querying: [queries.md](./queries.md)
126
127
**Query Priority:** Role > Label > Placeholder > Text > DisplayValue > AltText > Title > TestId
128
129
```typescript { .api }
130
// Synchronous queries
131
getByRole(role: string, options?: ByRoleOptions): HTMLElement;
132
getAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];
133
134
queryByRole(role: string, options?: ByRoleOptions): HTMLElement | null;
135
queryAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];
136
137
// Async queries (wait for element to appear)
138
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
139
findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;
140
141
// Available query types: Role, LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestId
142
```
143
144
**Common Patterns:**
145
```typescript
146
// Prefer role queries (most accessible)
147
screen.getByRole('button', { name: /submit/i });
148
screen.getByRole('heading', { level: 1 });
149
150
// Form inputs
151
screen.getByLabelText('Email');
152
screen.getByPlaceholderText('Enter email');
153
154
// Text content
155
screen.getByText(/hello world/i);
156
157
// Negative assertions
158
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
159
160
// Scoped queries
161
const modal = screen.getByRole('dialog');
162
within(modal).getByRole('button', { name: /close/i });
163
```
164
165
### Events: [events.md](./events.md)
166
167
```typescript { .api }
168
// Common events (automatically wrapped in act())
169
fireEvent.click(element, options?: MouseEventInit);
170
fireEvent.change(element, { target: { value: 'text' } });
171
fireEvent.submit(form);
172
fireEvent.focus(element);
173
fireEvent.blur(element);
174
fireEvent.keyDown(element, { key: 'Enter', code: 'Enter' });
175
```
176
177
**Patterns:**
178
```typescript
179
// Click interaction
180
fireEvent.click(screen.getByRole('button'));
181
182
// Input change
183
fireEvent.change(screen.getByLabelText('Email'), {
184
target: { value: 'user@example.com' }
185
});
186
187
// Keyboard
188
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
189
190
// Hover
191
fireEvent.mouseEnter(element);
192
fireEvent.mouseLeave(element);
193
```
194
195
### Async Operations: [async.md](./async.md)
196
197
```typescript { .api }
198
// Wait for condition
199
function waitFor<T>(callback: () => T | Promise<T>, options?: {
200
timeout?: number; // default: 1000ms
201
interval?: number; // default: 50ms
202
}): Promise<T>;
203
204
// Wait for removal
205
function waitForElementToBeRemoved<T>(
206
element: T | (() => T),
207
options?: { timeout?: number }
208
): Promise<void>;
209
210
// Async queries (shorthand for waitFor + getBy)
211
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
212
```
213
214
**Patterns:**
215
```typescript
216
// Wait for element to appear
217
const element = await screen.findByText(/loaded/i);
218
219
// Wait for condition
220
await waitFor(() => {
221
expect(screen.getByText(/success/i)).toBeInTheDocument();
222
}, { timeout: 3000 });
223
224
// Wait for disappearance
225
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
226
```
227
228
### Hook Testing: [hooks.md](./hooks.md)
229
230
```typescript { .api }
231
function renderHook<Result, Props>(
232
callback: (props: Props) => Result,
233
options?: RenderHookOptions<Props>
234
): RenderHookResult<Result, Props>;
235
236
interface RenderHookResult<Result, Props> {
237
result: { current: Result };
238
rerender: (props?: Props) => void;
239
unmount: () => void;
240
}
241
```
242
243
**Patterns:**
244
```typescript
245
// Test hook
246
const { result } = renderHook(() => useCounter(0));
247
248
// Trigger updates (wrap in act)
249
act(() => result.current.increment());
250
expect(result.current.count).toBe(1);
251
252
// With context
253
const { result } = renderHook(() => useUser(), {
254
wrapper: ({ children }) => <UserProvider>{children}</UserProvider>
255
});
256
257
// Re-render with props
258
const { result, rerender } = renderHook(
259
({ id }) => useFetch(id),
260
{ initialProps: { id: 1 } }
261
);
262
rerender({ id: 2 });
263
```
264
265
### Configuration: [configuration.md](./configuration.md)
266
267
```typescript { .api }
268
function configure(config: Partial<Config>): void;
269
270
interface Config {
271
reactStrictMode: boolean; // default: false
272
testIdAttribute: string; // default: 'data-testid'
273
asyncUtilTimeout: number; // default: 1000ms
274
}
275
```
276
277
**Setup Pattern:**
278
```typescript
279
// test-setup.js
280
import { configure } from '@testing-library/react';
281
282
configure({
283
reactStrictMode: true,
284
asyncUtilTimeout: 2000,
285
});
286
```
287
288
## Common Type Definitions
289
290
```typescript { .api }
291
interface ByRoleOptions {
292
name?: string | RegExp; // Accessible name filter
293
description?: string | RegExp; // Accessible description filter
294
hidden?: boolean; // Include hidden elements (default: false)
295
selected?: boolean; // Filter by selected state
296
checked?: boolean; // Filter by checked state
297
pressed?: boolean; // Filter by pressed state (toggle buttons)
298
current?: boolean | string; // Filter by current state (navigation)
299
expanded?: boolean; // Filter by expanded state
300
level?: number; // Filter by heading level (1-6)
301
exact?: boolean; // Enable exact matching (default: true)
302
normalizer?: (text: string) => string; // Custom text normalizer
303
queryFallbacks?: boolean; // Enable query fallbacks
304
}
305
306
interface MatcherOptions {
307
exact?: boolean; // Enable exact matching (default: true)
308
normalizer?: (text: string) => string; // Custom text normalizer
309
}
310
311
interface SelectorMatcherOptions extends MatcherOptions {
312
selector?: string; // CSS selector to filter results
313
}
314
315
interface Queries {
316
[key: string]: (...args: any[]) => any;
317
}
318
```
319
320
## Production Patterns
321
322
### Custom Render with Providers
323
```typescript
324
import { render, RenderOptions } from '@testing-library/react';
325
import { ThemeProvider } from './theme';
326
import { QueryClientProvider } from '@tanstack/react-query';
327
328
export function renderWithProviders(
329
ui: React.ReactElement,
330
options?: Omit<RenderOptions, 'wrapper'>
331
) {
332
return render(ui, {
333
wrapper: ({ children }) => (
334
<QueryClientProvider client={queryClient}>
335
<ThemeProvider>{children}</ThemeProvider>
336
</QueryClientProvider>
337
),
338
...options,
339
});
340
}
341
```
342
343
### Testing Async Data Fetching
344
```typescript
345
test('loads and displays data', async () => {
346
render(<DataComponent />);
347
348
expect(screen.getByText(/loading/i)).toBeInTheDocument();
349
350
const data = await screen.findByText(/data loaded/i);
351
expect(data).toBeInTheDocument();
352
353
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
354
});
355
```
356
357
### Testing Form Submission
358
```typescript
359
test('submits form with validation', async () => {
360
const handleSubmit = jest.fn();
361
render(<Form onSubmit={handleSubmit} />);
362
363
const emailInput = screen.getByLabelText(/email/i);
364
const submitButton = screen.getByRole('button', { name: /submit/i });
365
366
fireEvent.change(emailInput, { target: { value: 'user@example.com' } });
367
fireEvent.click(submitButton);
368
369
await waitFor(() => {
370
expect(handleSubmit).toHaveBeenCalledWith({
371
email: 'user@example.com'
372
});
373
});
374
});
375
```
376
377
### Testing Modal Interactions
378
```typescript
379
test('opens and closes modal', async () => {
380
render(<App />);
381
382
const openButton = screen.getByRole('button', { name: /open modal/i });
383
fireEvent.click(openButton);
384
385
const modal = await screen.findByRole('dialog');
386
expect(modal).toBeInTheDocument();
387
388
const closeButton = within(modal).getByRole('button', { name: /close/i });
389
fireEvent.click(closeButton);
390
391
await waitForElementToBeRemoved(modal);
392
});
393
```
394
395
## Best Practices
396
397
1. **Query Priority**: Use getByRole when possible for accessibility
398
2. **Async Queries**: Use findBy* for async elements, waitFor for complex conditions
399
3. **Negative Assertions**: Use queryBy* with .not.toBeInTheDocument()
400
4. **act() Wrapping**: render/fireEvent/waitFor handle this automatically
401
5. **screen vs destructuring**: Prefer screen for better error messages
402
6. **Scoped Queries**: Use within() to limit query scope
403
7. **Real User Behavior**: Test what users see and do, not implementation
404
405
## Entry Points
406
407
### Main Entry (`@testing-library/react`)
408
409
Default import with auto-cleanup and automatic act() environment setup. Use for standard test setups with Jest, Vitest, or similar test runners.
410
411
```typescript
412
import { render, screen } from '@testing-library/react';
413
// Auto-cleanup runs after each test
414
```
415
416
### Pure Entry (`@testing-library/react/pure`)
417
418
No auto-cleanup or automatic setup. Use when you need manual control over cleanup or test lifecycle.
419
420
```typescript
421
import { render, screen, cleanup } from '@testing-library/react/pure';
422
423
afterEach(() => {
424
cleanup(); // Manual cleanup
425
});
426
```
427
428
### Dont-Cleanup-After-Each Entry
429
430
Alternative method to disable auto-cleanup by importing this file before the main import. Sets `RTL_SKIP_AUTO_CLEANUP=true` environment variable.
431
432
```typescript
433
import '@testing-library/react/dont-cleanup-after-each';
434
import { render, screen, cleanup } from '@testing-library/react';
435
436
// Auto-cleanup is now disabled, manual cleanup required
437
afterEach(() => {
438
cleanup();
439
});
440
```
441
442
## Common Utility Functions
443
444
### screen Object
445
446
Pre-bound queries to document.body for convenient access without destructuring render results.
447
448
```typescript { .api }
449
const screen: {
450
// All query functions
451
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
452
// ... all other query functions
453
454
// Debug utilities
455
debug: (element?: HTMLElement, maxLength?: number) => void;
456
logTestingPlaygroundURL: (element?: HTMLElement) => void;
457
};
458
```
459
460
### within Function
461
462
Get queries bound to a specific element for scoped searches.
463
464
```typescript { .api }
465
function within(element: HTMLElement): {
466
getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
467
// ... all other query functions
468
};
469
```
470
471
### Debug Utilities
472
473
```typescript { .api }
474
function prettyDOM(
475
element?: HTMLElement,
476
maxLength?: number,
477
options?: any
478
): string;
479
480
function logRoles(container: HTMLElement): void;
481
```
482
483
### Lifecycle Management
484
485
```typescript { .api }
486
// Manual cleanup (auto-runs by default)
487
function cleanup(): void;
488
489
// Manual act() wrapping (usually automatic)
490
function act(callback: () => void | Promise<void>): Promise<void>;
491
```
492
493
**Note:** Most operations are automatically wrapped in act(), so manual use is rarely needed.
494