0
# Async Utilities
1
2
Wait for conditions or elements to appear/disappear in the DOM for testing asynchronous behavior.
3
4
## waitFor
5
6
Wait for a callback to succeed without throwing, with polling and MutationObserver optimization.
7
8
```typescript
9
function waitFor<T>(
10
callback: () => Promise<T> | T,
11
options?: waitForOptions
12
): Promise<T>;
13
14
interface waitForOptions {
15
container?: HTMLElement; // Element to observe for mutations (default: document)
16
timeout?: number; // Max wait time in ms (default: 1000)
17
interval?: number; // Polling interval in ms (default: 50)
18
onTimeout?: (error: Error) => Error; // Custom timeout error
19
mutationObserverOptions?: MutationObserverInit;
20
}
21
```
22
23
Usage:
24
```javascript
25
import {waitFor, screen} from '@testing-library/dom';
26
27
// Wait for element to appear
28
await waitFor(() => {
29
expect(screen.getByText('Loaded')).toBeInTheDocument();
30
});
31
32
// Wait for specific condition
33
await waitFor(() => {
34
const counter = screen.getByTestId('counter');
35
expect(counter.textContent).toBe('5');
36
});
37
38
// Custom timeout
39
await waitFor(
40
() => expect(screen.getByText('Slow')).toBeInTheDocument(),
41
{timeout: 5000}
42
);
43
44
// With MutationObserver optimization
45
await waitFor(
46
() => expect(screen.getByText('Content')).toBeInTheDocument(),
47
{container: screen.getByTestId('dynamic-content')}
48
);
49
50
// Custom error
51
await waitFor(
52
() => expect(screen.getByText('Ready')).toBeInTheDocument(),
53
{
54
timeout: 5000,
55
onTimeout: (error) => new Error(`Custom: ${error.message}`)
56
}
57
);
58
```
59
60
## waitForElementToBeRemoved
61
62
Wait for elements to be removed from the DOM. Throws immediately if elements not present.
63
64
```typescript
65
function waitForElementToBeRemoved<T>(
66
callback: T | (() => T),
67
options?: waitForOptions
68
): Promise<void>;
69
```
70
71
Usage:
72
```javascript
73
import {waitForElementToBeRemoved, screen} from '@testing-library/dom';
74
75
// Wait for specific element
76
const spinner = screen.getByRole('status', {name: /loading/i});
77
await waitForElementToBeRemoved(spinner);
78
79
// Using query function
80
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
81
82
// Multiple elements
83
const errors = screen.getAllByRole('alert');
84
await waitForElementToBeRemoved(errors);
85
86
// With timeout
87
await waitForElementToBeRemoved(
88
() => screen.queryByTestId('modal'),
89
{timeout: 3000}
90
);
91
```
92
93
## Find Queries (Built-in Async)
94
95
All `findBy*` and `findAllBy*` queries are async and use `waitFor` internally:
96
97
```javascript
98
// Using findBy (recommended for async content)
99
const element = await screen.findByText('Loaded content');
100
101
// Equivalent to:
102
const element = await waitFor(() => screen.getByText('Loaded content'));
103
104
// With options
105
const element = await screen.findByRole(
106
'alert',
107
{name: /error/i},
108
{timeout: 3000} // pass to waitForOptions
109
);
110
111
// findAllBy for multiple elements
112
const items = await screen.findAllByRole('listitem');
113
```
114
115
## Best Practices
116
117
### Use `findBy*` for async content
118
```javascript
119
// Good
120
const element = await screen.findByText('Async content');
121
122
// Works but less idiomatic
123
await waitFor(() => {
124
expect(screen.getByText('Async content')).toBeInTheDocument();
125
});
126
```
127
128
### Use `waitFor` for complex conditions
129
```javascript
130
// Multiple related assertions
131
await waitFor(() => {
132
expect(screen.getByText('Username')).toBeInTheDocument();
133
expect(screen.getByText('Email')).toBeInTheDocument();
134
expect(screen.getByText('Status: Active')).toBeInTheDocument();
135
});
136
137
// Complex logic
138
await waitFor(() => {
139
const items = screen.getAllByRole('listitem');
140
expect(items.length).toBe(5);
141
expect(items[0]).toHaveTextContent('First item');
142
});
143
```
144
145
### Use `waitForElementToBeRemoved` for removal
146
```javascript
147
// Good - explicit removal check
148
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
149
150
// Less clear
151
await waitFor(() => {
152
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
153
});
154
```
155
156
## Common Patterns
157
158
### Waiting after user interaction
159
```javascript
160
fireEvent.click(screen.getByRole('button', {name: /load/i}));
161
162
await waitFor(() => {
163
expect(screen.getByText('Data loaded')).toBeInTheDocument();
164
});
165
```
166
167
### API response handling
168
```javascript
169
// Submit form
170
fireEvent.submit(screen.getByRole('form'));
171
172
// Wait for loading to finish
173
await waitForElementToBeRemoved(() =>
174
screen.queryByRole('status', {name: /loading/i})
175
);
176
177
// Verify result
178
expect(screen.getByText('Success!')).toBeInTheDocument();
179
```
180
181
### Polling for state changes
182
```javascript
183
// Start process
184
fireEvent.click(screen.getByRole('button', {name: /start/i}));
185
186
// Poll for completion
187
await waitFor(
188
() => {
189
const progress = screen.getByTestId('progress');
190
expect(progress).toHaveTextContent('100%');
191
},
192
{interval: 200, timeout: 10000}
193
);
194
```
195
196
## Error Handling
197
198
```javascript
199
try {
200
await waitFor(
201
() => expect(screen.getByText('Never appears')).toBeInTheDocument(),
202
{timeout: 1000}
203
);
204
} catch (error) {
205
console.error('Element did not appear within 1 second');
206
}
207
208
// Custom timeout messages
209
await waitFor(
210
() => {
211
const element = screen.queryByTestId('status');
212
if (!element || element.textContent !== 'ready') {
213
throw new Error('Status is not ready');
214
}
215
},
216
{
217
timeout: 5000,
218
onTimeout: (error) => {
219
const status = screen.queryByTestId('status');
220
const currentStatus = status?.textContent || 'not found';
221
return new Error(
222
`Timeout waiting for ready status. Current: ${currentStatus}`
223
);
224
}
225
}
226
);
227
```
228
229