0
# Async Utilities
1
2
Wait for asynchronous changes with automatic act() wrapping.
3
4
## API
5
6
### waitFor Function
7
8
Waits for a condition to be met, repeatedly calling the callback until it succeeds or times out. Useful for waiting for asynchronous state updates, API calls, or side effects.
9
10
```typescript { .api }
11
/**
12
* Wait for a condition to be met by repeatedly calling the callback
13
* @param callback - Function to call repeatedly until it doesn't throw
14
* @param options - Configuration options
15
* @returns Promise resolving to callback's return value
16
* @throws When timeout is reached or onTimeout callback returns error
17
*/
18
function waitFor<T>(
19
callback: () => T | Promise<T>,
20
options?: {
21
/**
22
* Maximum time to wait in milliseconds (default: 1000)
23
*/
24
timeout?: number;
25
26
/**
27
* Time between callback calls in milliseconds (default: 50)
28
*/
29
interval?: number;
30
31
/**
32
* Custom error handler when timeout is reached
33
*/
34
onTimeout?: (error: Error) => Error;
35
36
/**
37
* Suppress timeout errors for debugging (default: false)
38
*/
39
mutationObserverOptions?: MutationObserverInit;
40
}
41
): Promise<T>;
42
```
43
44
### waitForElementToBeRemoved Function
45
46
Waits for an element to be removed from the DOM. Useful for testing loading states, modals closing, or elements being unmounted.
47
48
```typescript { .api }
49
/**
50
* Wait for element(s) to be removed from the DOM
51
* @param callback - Element, elements, or function returning element(s) to wait for removal
52
* @param options - Configuration options
53
* @returns Promise that resolves when element(s) are removed
54
* @throws When timeout is reached
55
*/
56
function waitForElementToBeRemoved<T>(
57
callback: T | (() => T),
58
options?: {
59
/**
60
* Maximum time to wait in milliseconds (default: 1000)
61
*/
62
timeout?: number;
63
64
/**
65
* Time between checks in milliseconds (default: 50)
66
*/
67
interval?: number;
68
69
/**
70
* Suppress timeout errors for debugging (default: false)
71
*/
72
mutationObserverOptions?: MutationObserverInit;
73
}
74
): Promise<void>;
75
```
76
77
### findBy* Queries
78
79
Async query variants that wait for elements to appear. These are convenience wrappers around `waitFor` + `getBy*` queries.
80
81
```typescript { .api }
82
/**
83
* Async queries that wait for elements to appear
84
* All getBy* queries have findBy* async equivalents
85
*/
86
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
87
findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;
88
89
findByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;
90
findAllByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;
91
92
findByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
93
findAllByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
94
95
findByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;
96
findAllByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;
97
98
findByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
99
findAllByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
100
101
findByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
102
findAllByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
103
104
findByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
105
findAllByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
106
107
findByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
108
findAllByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
109
```
110
111
## Common Patterns
112
113
### Wait for Element to Appear
114
```typescript
115
// Using findBy* (recommended for single element)
116
const message = await screen.findByText(/loaded/i);
117
118
// Using waitFor (for complex assertions)
119
await waitFor(() => {
120
expect(screen.getByText(/loaded/i)).toBeInTheDocument();
121
});
122
```
123
124
### Wait for Element to Disappear
125
```typescript
126
// Wait for removal
127
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
128
129
// Or use waitFor with queryBy
130
await waitFor(() => {
131
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
132
});
133
```
134
135
### Wait for Multiple Conditions
136
```typescript
137
await waitFor(() => {
138
expect(screen.getByText('Title')).toBeInTheDocument();
139
expect(screen.getByText('Content')).toBeInTheDocument();
140
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
141
});
142
```
143
144
### Custom Timeout
145
```typescript
146
await waitFor(
147
() => expect(screen.getByText('Slow load')).toBeInTheDocument(),
148
{ timeout: 5000 } // 5 seconds
149
);
150
```
151
152
## Testing Patterns
153
154
### API Data Loading
155
```typescript
156
test('loads user data', async () => {
157
render(<UserProfile userId="123" />);
158
159
// Initially loading
160
expect(screen.getByText(/loading/i)).toBeInTheDocument();
161
162
// Wait for data
163
const name = await screen.findByText(/john doe/i);
164
expect(name).toBeInTheDocument();
165
166
// Loading gone
167
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
168
});
169
```
170
171
### Async State Updates
172
```typescript
173
test('updates after async operation', async () => {
174
render(<AsyncCounter />);
175
176
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
177
178
// Wait for state update
179
await waitFor(() => {
180
expect(screen.getByText('Count: 1')).toBeInTheDocument();
181
});
182
});
183
```
184
185
### Modal Closing
186
```typescript
187
test('closes modal', async () => {
188
render(<App />);
189
190
const closeButton = screen.getByRole('button', { name: /close/i });
191
const modal = screen.getByRole('dialog');
192
193
fireEvent.click(closeButton);
194
195
await waitForElementToBeRemoved(modal);
196
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
197
});
198
```
199
200
### Form Validation
201
```typescript
202
test('shows validation after submit', async () => {
203
render(<Form />);
204
205
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
206
207
const error = await screen.findByRole('alert');
208
expect(error).toHaveTextContent('Email is required');
209
});
210
```
211
212
### Polling/Retries
213
```typescript
214
test('retries failed request', async () => {
215
render(<DataFetcher />);
216
217
// Shows initial error
218
expect(await screen.findByText(/error/i)).toBeInTheDocument();
219
220
// Retries and succeeds
221
const data = await screen.findByText(/success/i, {}, { timeout: 5000 });
222
expect(data).toBeInTheDocument();
223
});
224
```
225
226
## Advanced Patterns
227
228
### Wait for Multiple Elements
229
```typescript
230
test('loads all items', async () => {
231
render(<ItemList />);
232
233
const items = await screen.findAllByRole('listitem');
234
expect(items).toHaveLength(5);
235
});
236
```
237
238
### Async Callback Return Value
239
```typescript
240
test('returns value from waitFor', async () => {
241
render(<Component />);
242
243
const element = await waitFor(() => screen.getByText('Value'));
244
expect(element).toHaveAttribute('data-loaded', 'true');
245
});
246
```
247
248
### Custom Error Messages
249
```typescript
250
test('provides context on timeout', async () => {
251
render(<Component />);
252
253
await waitFor(
254
() => expect(screen.getByText('Expected')).toBeInTheDocument(),
255
{
256
timeout: 2000,
257
onTimeout: (error) => {
258
screen.debug();
259
return new Error(`Element not found after 2s: ${error.message}`);
260
},
261
}
262
);
263
});
264
```
265
266
### Waiting for Hook Updates
267
```typescript
268
test('hook updates async state', async () => {
269
const { result } = renderHook(() => useAsyncData());
270
271
expect(result.current.loading).toBe(true);
272
273
await waitFor(() => {
274
expect(result.current.loading).toBe(false);
275
});
276
277
expect(result.current.data).toBeDefined();
278
});
279
```
280
281
## findBy* vs waitFor
282
283
### Use findBy* when:
284
- Waiting for single element to appear
285
- Simple query without complex assertions
286
287
```typescript
288
const button = await screen.findByRole('button');
289
```
290
291
### Use waitFor when:
292
- Multiple assertions needed
293
- Complex conditions
294
- Checking element properties
295
296
```typescript
297
await waitFor(() => {
298
const button = screen.getByRole('button');
299
expect(button).toBeEnabled();
300
expect(button).toHaveTextContent('Submit');
301
});
302
```
303
304
## Common Pitfalls
305
306
### ❌ Wrong: Using getBy without waiting
307
```typescript
308
// Will fail if element not immediately present
309
fireEvent.click(button);
310
expect(screen.getByText('Success')).toBeInTheDocument(); // ❌
311
```
312
313
### ✅ Correct: Use findBy or waitFor
314
```typescript
315
fireEvent.click(button);
316
const success = await screen.findByText('Success'); // ✅
317
expect(success).toBeInTheDocument();
318
```
319
320
### ❌ Wrong: Return value instead of assertion
321
```typescript
322
await waitFor(() => element !== null); // ❌ Never retries
323
```
324
325
### ✅ Correct: Use assertions that throw
326
```typescript
327
await waitFor(() => {
328
expect(element).toBeInTheDocument(); // ✅ Throws until true
329
});
330
```
331
332
### ❌ Wrong: waitFor for synchronous state
333
```typescript
334
fireEvent.click(button);
335
await waitFor(() => { // ❌ Unnecessary
336
expect(screen.getByText('Count: 1')).toBeInTheDocument();
337
});
338
```
339
340
### ✅ Correct: Direct assertion for sync updates
341
```typescript
342
fireEvent.click(button);
343
expect(screen.getByText('Count: 1')).toBeInTheDocument(); // ✅
344
```
345
346
## Configuration
347
348
### Global Timeout
349
```typescript
350
import { configure } from '@testing-library/react';
351
352
configure({ asyncUtilTimeout: 2000 }); // 2 seconds default
353
```
354
355
### Per-Test Timeout
356
```typescript
357
test('slow operation', async () => {
358
await waitFor(
359
() => expect(screen.getByText('Done')).toBeInTheDocument(),
360
{ timeout: 10000 } // Override for this test
361
);
362
});
363
```
364
365
## Debugging
366
367
### Print State on Timeout
368
```typescript
369
await waitFor(
370
() => {
371
screen.debug(); // Print current DOM
372
expect(screen.getByText('Expected')).toBeInTheDocument();
373
},
374
{
375
onTimeout: (error) => {
376
screen.debug(); // Print final state
377
return error;
378
},
379
}
380
);
381
```
382
383
## Important Notes
384
385
### Automatic act() Wrapping
386
387
All async utilities automatically wrap operations in React's `act()`, ensuring state updates are properly flushed:
388
389
```typescript
390
// No manual act() needed
391
await waitFor(() => {
392
expect(screen.getByText('Updated')).toBeInTheDocument();
393
});
394
```
395
396
### Assertions in waitFor
397
398
The callback passed to `waitFor` should contain assertions or throw errors. `waitFor` will retry until the callback doesn't throw:
399
400
```typescript
401
// CORRECT: Assertion that throws
402
await waitFor(() => {
403
expect(element).toBeInTheDocument();
404
});
405
406
// WRONG: Always returns true, never retries
407
await waitFor(() => {
408
return element !== null;
409
});
410
```
411
412
### Default Timeouts
413
414
- Default timeout: 1000ms (1 second)
415
- Default interval: 50ms
416
- Can be configured globally via `configure()` or per-call via options
417
418
### waitFor vs findBy
419
420
Both work similarly, but have different use cases:
421
422
```typescript
423
// findBy: Convenient for single queries
424
const element = await screen.findByText('Hello');
425
426
// waitFor: Better for complex assertions
427
await waitFor(() => {
428
expect(screen.getByText('Hello')).toBeInTheDocument();
429
expect(screen.getByText('World')).toBeInTheDocument();
430
});
431
```
432
433
### Error Messages
434
435
When `waitFor` times out, it includes the last error thrown by the callback:
436
437
```typescript
438
// Timeout error will include "Expected element to be in document"
439
await waitFor(() => {
440
expect(screen.getByText('Missing')).toBeInTheDocument();
441
});
442
```
443
444
### MutationObserver
445
446
`waitFor` uses MutationObserver to efficiently wait for DOM changes. It will check the callback:
447
448
1. After every DOM mutation
449
2. At the specified interval (default 50ms)
450
3. Until timeout is reached (default 1000ms)
451
452
This makes it efficient for most async scenarios without constant polling.
453
454
## Best Practices
455
456
1. **Prefer findBy*** for simple waits
457
2. **Use waitFor** for complex conditions
458
3. **Set realistic timeouts** - default 1s usually sufficient
459
4. **Don't waitFor sync operations** - fireEvent updates are synchronous
460
5. **Assert in callbacks** - waitFor needs thrown errors to retry
461
6. **Use queryBy in waitFor** - for checking absence
462
7. **Avoid waitFor for everything** - only use when actually async
463