0
# Async Testing
1
2
Utilities for testing asynchronous hook behavior, including waiting for updates, value changes, and condition fulfillment. These utilities are essential for testing hooks that perform async operations or have delayed effects.
3
4
## Capabilities
5
6
### waitFor
7
8
Waits for a condition to become true, with configurable timeout and polling interval. Useful for testing hooks that have async side effects or delayed updates.
9
10
```typescript { .api }
11
/**
12
* Wait for a condition to become true
13
* @param callback - Function that returns true when condition is met, or void (treated as true)
14
* @param options - Configuration for timeout and polling interval
15
* @returns Promise that resolves when condition is met
16
* @throws TimeoutError if condition is not met within timeout
17
*/
18
waitFor(callback: () => boolean | void, options?: WaitForOptions): Promise<void>;
19
20
interface WaitForOptions {
21
/** Polling interval in milliseconds (default: 50ms, false disables polling) */
22
interval?: number | false;
23
/** Timeout in milliseconds (default: 1000ms, false disables timeout) */
24
timeout?: number | false;
25
}
26
```
27
28
**Usage Examples:**
29
30
```typescript
31
import { renderHook } from "@testing-library/react-hooks";
32
import { useState, useEffect } from "react";
33
34
function useAsyncData(url: string) {
35
const [data, setData] = useState(null);
36
const [loading, setLoading] = useState(true);
37
const [error, setError] = useState(null);
38
39
useEffect(() => {
40
setLoading(true);
41
setError(null);
42
43
fetch(url)
44
.then(response => response.json())
45
.then(data => {
46
setData(data);
47
setLoading(false);
48
})
49
.catch(error => {
50
setError(error);
51
setLoading(false);
52
});
53
}, [url]);
54
55
return { data, loading, error };
56
}
57
58
test("async data loading", async () => {
59
// Mock fetch
60
global.fetch = jest.fn().mockResolvedValue({
61
json: () => Promise.resolve({ id: 1, name: "Test Data" })
62
});
63
64
const { result, waitFor } = renderHook(() => useAsyncData("/api/data"));
65
66
expect(result.current.loading).toBe(true);
67
expect(result.current.data).toBe(null);
68
69
// Wait for loading to complete
70
await waitFor(() => !result.current.loading);
71
72
expect(result.current.loading).toBe(false);
73
expect(result.current.data).toEqual({ id: 1, name: "Test Data" });
74
expect(result.current.error).toBe(null);
75
});
76
77
// Wait for specific condition
78
test("wait for specific value", async () => {
79
const { result, waitFor } = renderHook(() => useAsyncData("/api/data"));
80
81
// Wait for data to be loaded
82
await waitFor(() => result.current.data !== null);
83
84
expect(result.current.data).toBeTruthy();
85
});
86
87
// Custom timeout and interval
88
test("custom wait options", async () => {
89
const { result, waitFor } = renderHook(() => useAsyncData("/api/slow"));
90
91
// Wait with custom timeout and interval
92
await waitFor(
93
() => !result.current.loading,
94
{ timeout: 5000, interval: 100 }
95
);
96
97
expect(result.current.loading).toBe(false);
98
});
99
```
100
101
### waitForValueToChange
102
103
Waits for a selected value to change from its current value. Useful when you want to wait for a specific piece of state to update.
104
105
```typescript { .api }
106
/**
107
* Wait for a selected value to change from its current value
108
* @param selector - Function that returns the value to monitor
109
* @param options - Configuration for timeout and polling interval
110
* @returns Promise that resolves when the value changes
111
* @throws TimeoutError if value doesn't change within timeout
112
*/
113
waitForValueToChange(
114
selector: () => unknown,
115
options?: WaitForValueToChangeOptions
116
): Promise<void>;
117
118
interface WaitForValueToChangeOptions {
119
/** Polling interval in milliseconds (default: 50ms, false disables polling) */
120
interval?: number | false;
121
/** Timeout in milliseconds (default: 1000ms, false disables timeout) */
122
timeout?: number | false;
123
}
124
```
125
126
**Usage Examples:**
127
128
```typescript
129
function useCounter() {
130
const [count, setCount] = useState(0);
131
132
const incrementAsync = async () => {
133
await new Promise(resolve => setTimeout(resolve, 100));
134
setCount(prev => prev + 1);
135
};
136
137
return { count, incrementAsync };
138
}
139
140
test("wait for value to change", async () => {
141
const { result, waitForValueToChange } = renderHook(() => useCounter());
142
143
expect(result.current.count).toBe(0);
144
145
// Start async operation
146
result.current.incrementAsync();
147
148
// Wait for count to change
149
await waitForValueToChange(() => result.current.count);
150
151
expect(result.current.count).toBe(1);
152
});
153
154
// Wait for nested property changes
155
function useUserProfile(userId: string) {
156
const [profile, setProfile] = useState({ name: "", email: "", loading: true });
157
158
useEffect(() => {
159
fetchUserProfile(userId).then(data => {
160
setProfile({ ...data, loading: false });
161
});
162
}, [userId]);
163
164
return profile;
165
}
166
167
test("wait for nested property change", async () => {
168
const { result, waitForValueToChange } = renderHook(() =>
169
useUserProfile("123")
170
);
171
172
expect(result.current.loading).toBe(true);
173
174
// Wait for loading state to change
175
await waitForValueToChange(() => result.current.loading);
176
177
expect(result.current.loading).toBe(false);
178
expect(result.current.name).toBeTruthy();
179
});
180
```
181
182
### waitForNextUpdate
183
184
Waits for the next hook update/re-render, regardless of what causes it. This is useful when you know an update will happen but don't know exactly what will change.
185
186
```typescript { .api }
187
/**
188
* Wait for the next hook update/re-render
189
* @param options - Configuration for timeout
190
* @returns Promise that resolves on the next update
191
* @throws TimeoutError if no update occurs within timeout
192
*/
193
waitForNextUpdate(options?: WaitForNextUpdateOptions): Promise<void>;
194
195
interface WaitForNextUpdateOptions {
196
/** Timeout in milliseconds (default: 1000ms, false disables timeout) */
197
timeout?: number | false;
198
}
199
```
200
201
**Usage Examples:**
202
203
```typescript
204
function useWebSocket(url: string) {
205
const [data, setData] = useState(null);
206
const [connected, setConnected] = useState(false);
207
208
useEffect(() => {
209
const ws = new WebSocket(url);
210
211
ws.onopen = () => setConnected(true);
212
ws.onmessage = (event) => setData(JSON.parse(event.data));
213
ws.onclose = () => setConnected(false);
214
215
return () => ws.close();
216
}, [url]);
217
218
return { data, connected };
219
}
220
221
test("websocket updates", async () => {
222
const mockWebSocket = {
223
onopen: null,
224
onmessage: null,
225
onclose: null,
226
close: jest.fn()
227
};
228
229
global.WebSocket = jest.fn(() => mockWebSocket);
230
231
const { result, waitForNextUpdate } = renderHook(() =>
232
useWebSocket("ws://localhost:8080")
233
);
234
235
expect(result.current.connected).toBe(false);
236
237
// Simulate connection
238
mockWebSocket.onopen();
239
240
// Wait for next update (connection state change)
241
await waitForNextUpdate();
242
243
expect(result.current.connected).toBe(true);
244
245
// Simulate message
246
mockWebSocket.onmessage({ data: JSON.stringify({ message: "Hello" }) });
247
248
// Wait for next update (data change)
249
await waitForNextUpdate();
250
251
expect(result.current.data).toEqual({ message: "Hello" });
252
});
253
254
// With custom timeout
255
test("wait for update with timeout", async () => {
256
const { waitForNextUpdate } = renderHook(() => useWebSocket("ws://test"));
257
258
// Wait with longer timeout
259
await waitForNextUpdate({ timeout: 2000 });
260
});
261
```
262
263
### Error Handling in Async Utils
264
265
All async utilities can throw `TimeoutError` when operations don't complete within the specified timeout:
266
267
```typescript
268
import { renderHook } from "@testing-library/react-hooks";
269
270
function useNeverUpdating() {
271
const [value] = useState("static");
272
return value;
273
}
274
275
test("timeout error handling", async () => {
276
const { result, waitForValueToChange } = renderHook(() => useNeverUpdating());
277
278
// This will throw TimeoutError after 100ms
279
await expect(
280
waitForValueToChange(() => result.current, { timeout: 100 })
281
).rejects.toThrow("Timed out");
282
});
283
284
test("no timeout with false", async () => {
285
const { result, waitFor } = renderHook(() => useNeverUpdating());
286
287
// This would wait forever, but we'll resolve it manually
288
const waitPromise = waitFor(() => false, { timeout: false });
289
290
// In real tests, you'd trigger the condition to become true
291
// For this example, we'll just verify the promise doesn't reject immediately
292
const timeoutPromise = new Promise((_, reject) =>
293
setTimeout(() => reject(new Error("Should not timeout")), 100)
294
);
295
296
// The wait should not timeout
297
await expect(Promise.race([waitPromise, timeoutPromise])).rejects.toThrow("Should not timeout");
298
});
299
```
300
301
### Combining Async Utilities
302
303
You can combine multiple async utilities for complex testing scenarios:
304
305
```typescript
306
function useComplexAsync() {
307
const [step, setStep] = useState(1);
308
const [data, setData] = useState(null);
309
const [error, setError] = useState(null);
310
311
useEffect(() => {
312
if (step === 1) {
313
setTimeout(() => setStep(2), 100);
314
} else if (step === 2) {
315
fetchData()
316
.then(setData)
317
.catch(setError)
318
.finally(() => setStep(3));
319
}
320
}, [step]);
321
322
return { step, data, error };
323
}
324
325
test("complex async flow", async () => {
326
const { result, waitFor, waitForValueToChange, waitForNextUpdate } =
327
renderHook(() => useComplexAsync());
328
329
expect(result.current.step).toBe(1);
330
331
// Wait for step to change to 2
332
await waitForValueToChange(() => result.current.step);
333
expect(result.current.step).toBe(2);
334
335
// Wait for data or error
336
await waitFor(() =>
337
result.current.data !== null || result.current.error !== null
338
);
339
340
// Wait for final step
341
await waitForValueToChange(() => result.current.step);
342
expect(result.current.step).toBe(3);
343
});
344
```
345
346
## Types
347
348
```typescript { .api }
349
class TimeoutError extends Error {
350
constructor(util: Function, timeout: number);
351
}
352
```