0
# Async Testing Utilities
1
2
Built-in utilities for handling asynchronous behavior, waiting for conditions, and testing time-dependent components in React Native applications.
3
4
## Capabilities
5
6
### WaitFor Utility
7
8
Wait for expectations to pass with configurable polling and timeout options.
9
10
```typescript { .api }
11
/**
12
* Wait for expectation to pass with polling
13
* @param expectation - Function that should eventually not throw
14
* @param options - Waiting configuration options
15
* @returns Promise that resolves with expectation result
16
*/
17
function waitFor<T>(
18
expectation: () => T,
19
options?: WaitForOptions
20
): Promise<T>;
21
22
interface WaitForOptions {
23
/** Timeout in milliseconds (default: from config.asyncUtilTimeout) */
24
timeout?: number;
25
26
/** Polling interval in milliseconds (default: 50) */
27
interval?: number;
28
29
/** Error object for stack traces */
30
stackTraceError?: Error;
31
32
/** Custom timeout error handler */
33
onTimeout?: (error: Error) => Error;
34
}
35
```
36
37
**Usage Examples:**
38
39
```typescript
40
import { render, screen, waitFor, fireEvent } from "@testing-library/react-native";
41
42
test("waiting for async state changes", async () => {
43
const AsyncComponent = () => {
44
const [loading, setLoading] = useState(true);
45
const [data, setData] = useState(null);
46
47
useEffect(() => {
48
setTimeout(() => {
49
setData("Loaded data");
50
setLoading(false);
51
}, 1000);
52
}, []);
53
54
return loading ? <Text>Loading...</Text> : <Text>{data}</Text>;
55
};
56
57
render(<AsyncComponent />);
58
59
// Initially shows loading
60
expect(screen.getByText("Loading...")).toBeOnTheScreen();
61
62
// Wait for data to load
63
await waitFor(() => {
64
expect(screen.getByText("Loaded data")).toBeOnTheScreen();
65
});
66
67
// Loading should be gone
68
expect(screen.queryByText("Loading...")).not.toBeOnTheScreen();
69
});
70
71
test("waiting with custom timeout", async () => {
72
const SlowComponent = () => {
73
const [message, setMessage] = useState("Initial");
74
75
useEffect(() => {
76
setTimeout(() => setMessage("Updated"), 2000);
77
}, []);
78
79
return <Text>{message}</Text>;
80
};
81
82
render(<SlowComponent />);
83
84
// Wait with extended timeout
85
await waitFor(
86
() => {
87
expect(screen.getByText("Updated")).toBeOnTheScreen();
88
},
89
{ timeout: 3000, interval: 100 }
90
);
91
});
92
93
test("waiting for user interactions to complete", async () => {
94
const InteractiveComponent = () => {
95
const [count, setCount] = useState(0);
96
const [processing, setProcessing] = useState(false);
97
98
const handlePress = async () => {
99
setProcessing(true);
100
// Simulate async operation
101
await new Promise(resolve => setTimeout(resolve, 500));
102
setCount(prev => prev + 1);
103
setProcessing(false);
104
};
105
106
return (
107
<View>
108
<Text>Count: {count}</Text>
109
<Text>Status: {processing ? "Processing" : "Ready"}</Text>
110
<Pressable testID="increment" onPress={handlePress}>
111
<Text>Increment</Text>
112
</Pressable>
113
</View>
114
);
115
};
116
117
render(<InteractiveComponent />);
118
119
const button = screen.getByTestId("increment");
120
121
// Initial state
122
expect(screen.getByText("Count: 0")).toBeOnTheScreen();
123
expect(screen.getByText("Status: Ready")).toBeOnTheScreen();
124
125
// Press button
126
fireEvent.press(button);
127
128
// Wait for processing to start
129
await waitFor(() => {
130
expect(screen.getByText("Status: Processing")).toBeOnTheScreen();
131
});
132
133
// Wait for processing to complete and count to update
134
await waitFor(() => {
135
expect(screen.getByText("Count: 1")).toBeOnTheScreen();
136
expect(screen.getByText("Status: Ready")).toBeOnTheScreen();
137
});
138
});
139
```
140
141
### WaitFor with Custom Error Handling
142
143
Advanced waitFor usage with custom error handling and debugging.
144
145
```typescript { .api }
146
/**
147
* Custom timeout error handler
148
* @param error - Original timeout error
149
* @returns Modified error with additional context
150
*/
151
type TimeoutErrorHandler = (error: Error) => Error;
152
```
153
154
**Usage Examples:**
155
156
```typescript
157
test("custom error handling", async () => {
158
const FailingComponent = () => <Text>This will never change</Text>;
159
160
render(<FailingComponent />);
161
162
// Custom error with debugging info
163
await expect(
164
waitFor(
165
() => {
166
expect(screen.getByText("Non-existent")).toBeOnTheScreen();
167
},
168
{
169
timeout: 1000,
170
onTimeout: (error) => {
171
const elements = screen.getAllByText(/./);
172
const elementTexts = elements.map(el =>
173
el.children.join(" ")
174
);
175
176
return new Error(
177
`${error.message}\n\nAvailable elements:\n${elementTexts.join("\n")}`
178
);
179
}
180
}
181
)
182
).rejects.toThrow(/Available elements:/);
183
});
184
185
test("stack trace preservation", async () => {
186
const stackTraceError = new Error();
187
188
render(<Text>Static text</Text>);
189
190
await expect(
191
waitFor(
192
() => {
193
expect(screen.getByText("Dynamic text")).toBeOnTheScreen();
194
},
195
{
196
timeout: 100,
197
stackTraceError // Preserves original call site in stack trace
198
}
199
)
200
).rejects.toThrow();
201
});
202
```
203
204
### WaitForElementToBeRemoved
205
206
Wait for elements to be removed from the component tree.
207
208
```typescript { .api }
209
/**
210
* Wait for element(s) to be removed from the tree
211
* @param callback - Function returning element(s) or element(s) directly
212
* @param options - Waiting configuration options
213
* @returns Promise that resolves when element(s) are removed
214
*/
215
function waitForElementToBeRemoved<T>(
216
callback: (() => T) | T,
217
options?: WaitForOptions
218
): Promise<void>;
219
```
220
221
**Usage Examples:**
222
223
```typescript
224
test("waiting for element removal", async () => {
225
const DismissibleComponent = () => {
226
const [visible, setVisible] = useState(true);
227
228
useEffect(() => {
229
const timer = setTimeout(() => setVisible(false), 1000);
230
return () => clearTimeout(timer);
231
}, []);
232
233
return visible ? (
234
<View testID="dismissible">
235
<Text>This will disappear</Text>
236
</View>
237
) : null;
238
};
239
240
render(<DismissibleComponent />);
241
242
// Element is initially present
243
const element = screen.getByTestId("dismissible");
244
expect(element).toBeOnTheScreen();
245
246
// Wait for element to be removed
247
await waitForElementToBeRemoved(element);
248
249
// Element should no longer exist
250
expect(screen.queryByTestId("dismissible")).not.toBeOnTheScreen();
251
});
252
253
test("waiting for multiple elements removal", async () => {
254
const MultiDismissComponent = () => {
255
const [items, setItems] = useState([1, 2, 3]);
256
257
useEffect(() => {
258
const timer = setTimeout(() => {
259
setItems(prev => prev.filter(item => item !== 2));
260
}, 500);
261
return () => clearTimeout(timer);
262
}, []);
263
264
return (
265
<View>
266
{items.map(item => (
267
<Text key={item} testID={`item-${item}`}>
268
Item {item}
269
</Text>
270
))}
271
</View>
272
);
273
};
274
275
render(<MultiDismissComponent />);
276
277
// All items initially present
278
expect(screen.getAllByText(/Item/)).toHaveLength(3);
279
280
// Wait for specific item to be removed using callback
281
await waitForElementToBeRemoved(() =>
282
screen.getByTestId("item-2")
283
);
284
285
// Only items 1 and 3 should remain
286
expect(screen.getAllByText(/Item/)).toHaveLength(2);
287
expect(screen.queryByTestId("item-2")).not.toBeOnTheScreen();
288
});
289
290
test("removal with loading states", async () => {
291
const LoadingComponent = () => {
292
const [loading, setLoading] = useState(true);
293
const [data, setData] = useState(null);
294
295
const loadData = async () => {
296
await new Promise(resolve => setTimeout(resolve, 800));
297
setData("Loaded content");
298
setLoading(false);
299
};
300
301
useEffect(() => {
302
loadData();
303
}, []);
304
305
if (loading) {
306
return (
307
<View testID="loading-spinner">
308
<Text>Loading...</Text>
309
</View>
310
);
311
}
312
313
return (
314
<View testID="content">
315
<Text>{data}</Text>
316
</View>
317
);
318
};
319
320
render(<LoadingComponent />);
321
322
// Loading spinner is present
323
const spinner = screen.getByTestId("loading-spinner");
324
expect(spinner).toBeOnTheScreen();
325
326
// Wait for loading spinner to be removed
327
await waitForElementToBeRemoved(spinner);
328
329
// Content should now be visible
330
expect(screen.getByTestId("content")).toBeOnTheScreen();
331
expect(screen.getByText("Loaded content")).toBeOnTheScreen();
332
});
333
```
334
335
### FindBy Queries (Async Queries)
336
337
All query methods have findBy variants that automatically wait for elements to appear.
338
339
```typescript { .api }
340
/**
341
* Async versions of getBy queries that wait for elements to appear
342
* These combine getBy queries with waitFor functionality
343
*/
344
345
// Text queries
346
function findByText(text: string | RegExp, options?: TextMatchOptions & WaitForOptions): Promise<ReactTestInstance>;
347
function findAllByText(text: string | RegExp, options?: TextMatchOptions & WaitForOptions): Promise<ReactTestInstance[]>;
348
349
// TestID queries
350
function findByTestId(testId: string | RegExp, options?: TestIdOptions & WaitForOptions): Promise<ReactTestInstance>;
351
function findAllByTestId(testId: string | RegExp, options?: TestIdOptions & WaitForOptions): Promise<ReactTestInstance[]>;
352
353
// Role queries
354
function findByRole(role: string, options?: RoleOptions & WaitForOptions): Promise<ReactTestInstance>;
355
function findAllByRole(role: string, options?: RoleOptions & WaitForOptions): Promise<ReactTestInstance[]>;
356
357
// Label text queries
358
function findByLabelText(text: string | RegExp, options?: LabelTextOptions & WaitForOptions): Promise<ReactTestInstance>;
359
function findAllByLabelText(text: string | RegExp, options?: LabelTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;
360
361
// Hint text queries
362
function findByHintText(text: string | RegExp, options?: HintTextOptions & WaitForOptions): Promise<ReactTestInstance>;
363
function findAllByHintText(text: string | RegExp, options?: HintTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;
364
365
// Placeholder text queries
366
function findByPlaceholderText(text: string | RegExp, options?: PlaceholderTextOptions & WaitForOptions): Promise<ReactTestInstance>;
367
function findAllByPlaceholderText(text: string | RegExp, options?: PlaceholderTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;
368
369
// Display value queries
370
function findByDisplayValue(value: string | RegExp, options?: DisplayValueOptions & WaitForOptions): Promise<ReactTestInstance>;
371
function findAllByDisplayValue(value: string | RegExp, options?: DisplayValueOptions & WaitForOptions): Promise<ReactTestInstance[]>;
372
```
373
374
**Usage Examples:**
375
376
```typescript
377
test("findBy queries for async elements", async () => {
378
const AsyncContentComponent = () => {
379
const [content, setContent] = useState(null);
380
381
useEffect(() => {
382
setTimeout(() => {
383
setContent("Dynamic content loaded");
384
}, 600);
385
}, []);
386
387
return (
388
<View>
389
{content ? (
390
<Text testID="dynamic-content">{content}</Text>
391
) : (
392
<Text>Loading content...</Text>
393
)}
394
</View>
395
);
396
};
397
398
render(<AsyncContentComponent />);
399
400
// Initially only loading text
401
expect(screen.getByText("Loading content...")).toBeOnTheScreen();
402
403
// Wait for dynamic content to appear
404
const dynamicText = await screen.findByText("Dynamic content loaded");
405
expect(dynamicText).toBeOnTheScreen();
406
407
// Alternative using testID
408
const dynamicElement = await screen.findByTestId("dynamic-content");
409
expect(dynamicElement).toBeOnTheScreen();
410
});
411
412
test("findBy with custom wait options", async () => {
413
const VerySlowComponent = () => {
414
const [visible, setVisible] = useState(false);
415
416
useEffect(() => {
417
setTimeout(() => setVisible(true), 2500);
418
}, []);
419
420
return visible ? <Text>Finally loaded</Text> : <Text>Still loading</Text>;
421
};
422
423
render(<VerySlowComponent />);
424
425
// Wait with extended timeout
426
const slowText = await screen.findByText("Finally loaded", {
427
timeout: 3000,
428
interval: 100
429
});
430
431
expect(slowText).toBeOnTheScreen();
432
});
433
434
test("findAll for multiple async elements", async () => {
435
const AsyncListComponent = () => {
436
const [items, setItems] = useState([]);
437
438
useEffect(() => {
439
const timer = setTimeout(() => {
440
setItems(["Item 1", "Item 2", "Item 3"]);
441
}, 500);
442
return () => clearTimeout(timer);
443
}, []);
444
445
return (
446
<View>
447
{items.map((item, index) => (
448
<Text key={index} testID={`list-item-${index}`}>
449
{item}
450
</Text>
451
))}
452
</View>
453
);
454
};
455
456
render(<AsyncListComponent />);
457
458
// Wait for all items to appear
459
const items = await screen.findAllByText(/Item \d/);
460
expect(items).toHaveLength(3);
461
462
// Alternative using testID pattern
463
const itemElements = await screen.findAllByTestId(/list-item-\d/);
464
expect(itemElements).toHaveLength(3);
465
});
466
```
467
468
### Act Utility
469
470
React act utility for properly wrapping state updates and effects in tests.
471
472
```typescript { .api }
473
/**
474
* React act utility for wrapping state updates
475
* Ensures all updates are flushed before assertions
476
* @param callback - Function containing state updates
477
* @returns Promise that resolves when updates are complete
478
*/
479
function act<T>(callback: () => T): Promise<T>;
480
function act<T>(callback: () => Promise<T>): Promise<T>;
481
482
/**
483
* Get current React act environment setting
484
* @returns Current act environment state
485
*/
486
function getIsReactActEnvironment(): boolean;
487
488
/**
489
* Set React act environment setting
490
* @param isReactActEnvironment - Whether to use act environment
491
*/
492
function setReactActEnvironment(isReactActEnvironment: boolean): void;
493
```
494
495
**Usage Examples:**
496
497
```typescript
498
import { render, screen, act, fireEvent } from "@testing-library/react-native";
499
500
test("using act for state updates", async () => {
501
const StateComponent = () => {
502
const [count, setCount] = useState(0);
503
504
return (
505
<View>
506
<Text>Count: {count}</Text>
507
<Pressable testID="increment" onPress={() => setCount(c => c + 1)}>
508
<Text>+</Text>
509
</Pressable>
510
</View>
511
);
512
};
513
514
render(<StateComponent />);
515
516
const button = screen.getByTestId("increment");
517
518
// Act is usually not needed with fireEvent as it's wrapped automatically
519
fireEvent.press(button);
520
expect(screen.getByText("Count: 1")).toBeOnTheScreen();
521
522
// Manual act usage for direct state updates (rarely needed)
523
await act(async () => {
524
// Direct component state manipulation
525
fireEvent.press(button);
526
fireEvent.press(button);
527
});
528
529
expect(screen.getByText("Count: 3")).toBeOnTheScreen();
530
});
531
532
test("act environment configuration", () => {
533
// Check current act environment
534
const currentEnv = getIsReactActEnvironment();
535
expect(typeof currentEnv).toBe("boolean");
536
537
// Temporarily disable act warnings
538
setReactActEnvironment(false);
539
expect(getIsReactActEnvironment()).toBe(false);
540
541
// Restore previous setting
542
setReactActEnvironment(currentEnv);
543
expect(getIsReactActEnvironment()).toBe(currentEnv);
544
});
545
```
546
547
### FlushMicroTasks Utility
548
549
Utility for flushing pending microtasks in test environments.
550
551
```typescript { .api }
552
/**
553
* Flush all pending microtasks
554
* Useful for ensuring all Promise.resolve() calls have completed
555
* @returns Promise that resolves when all microtasks are flushed
556
*/
557
function flushMicroTasks(): Promise<void>;
558
```
559
560
**Usage Examples:**
561
562
```typescript
563
import { render, screen, flushMicroTasks } from "@testing-library/react-native";
564
565
test("flushing microtasks", async () => {
566
const MicroTaskComponent = () => {
567
const [message, setMessage] = useState("Initial");
568
569
useEffect(() => {
570
Promise.resolve().then(() => {
571
setMessage("Updated via microtask");
572
});
573
}, []);
574
575
return <Text>{message}</Text>;
576
};
577
578
render(<MicroTaskComponent />);
579
580
// Initially shows initial message
581
expect(screen.getByText("Initial")).toBeOnTheScreen();
582
583
// Flush microtasks to process Promise.resolve()
584
await flushMicroTasks();
585
586
// Message should now be updated
587
expect(screen.getByText("Updated via microtask")).toBeOnTheScreen();
588
});
589
590
test("combining with act for complete updates", async () => {
591
const ComplexAsyncComponent = () => {
592
const [state, setState] = useState("start");
593
594
useEffect(() => {
595
// Microtask
596
Promise.resolve().then(() => setState("microtask"));
597
598
// Macrotask
599
setTimeout(() => setState("macrotask"), 0);
600
}, []);
601
602
return <Text>{state}</Text>;
603
};
604
605
render(<ComplexAsyncComponent />);
606
607
// Initial state
608
expect(screen.getByText("start")).toBeOnTheScreen();
609
610
// Flush microtasks first
611
await flushMicroTasks();
612
expect(screen.getByText("microtask")).toBeOnTheScreen();
613
614
// Then wait for macrotask
615
await waitFor(() => {
616
expect(screen.getByText("macrotask")).toBeOnTheScreen();
617
});
618
});
619
```
620
621
## Configuration and Best Practices
622
623
Global configuration affecting async utilities behavior.
624
625
```typescript { .api }
626
/**
627
* Configuration affecting async utilities
628
*/
629
interface Config {
630
/** Default timeout for waitFor and findBy queries (ms) */
631
asyncUtilTimeout: number;
632
633
/** Other config options... */
634
defaultIncludeHiddenElements: boolean;
635
concurrentRoot: boolean;
636
defaultDebugOptions?: Partial<DebugOptions>;
637
}
638
```
639
640
**Usage Examples:**
641
642
```typescript
643
import { configure, resetToDefaults } from "@testing-library/react-native";
644
645
test("configuring async timeouts", async () => {
646
// Set longer timeout for slow tests
647
configure({ asyncUtilTimeout: 5000 });
648
649
const VerySlowComponent = () => {
650
const [loaded, setLoaded] = useState(false);
651
652
useEffect(() => {
653
setTimeout(() => setLoaded(true), 4000);
654
}, []);
655
656
return loaded ? <Text>Finally ready</Text> : <Text>Loading...</Text>;
657
};
658
659
render(<VerySlowComponent />);
660
661
// This will use the configured 5 second timeout
662
await screen.findByText("Finally ready");
663
664
// Reset to defaults
665
resetToDefaults();
666
});
667
668
test("best practices for async testing", async () => {
669
const BestPracticeComponent = () => {
670
const [data, setData] = useState(null);
671
const [loading, setLoading] = useState(true);
672
const [error, setError] = useState(null);
673
674
const fetchData = async () => {
675
try {
676
setLoading(true);
677
setError(null);
678
679
// Simulate API call
680
await new Promise(resolve => setTimeout(resolve, 500));
681
682
if (Math.random() > 0.8) {
683
throw new Error("Random API error");
684
}
685
686
setData("Success data");
687
} catch (err) {
688
setError(err.message);
689
} finally {
690
setLoading(false);
691
}
692
};
693
694
useEffect(() => {
695
fetchData();
696
}, []);
697
698
if (loading) return <Text>Loading...</Text>;
699
if (error) return <Text>Error: {error}</Text>;
700
return <Text>Data: {data}</Text>;
701
};
702
703
render(<BestPracticeComponent />);
704
705
// Wait for loading to complete
706
await waitForElementToBeRemoved(() => screen.getByText("Loading..."));
707
708
// Check for either success or error state
709
await waitFor(() => {
710
const successElement = screen.queryByText(/Data:/);
711
const errorElement = screen.queryByText(/Error:/);
712
713
expect(successElement || errorElement).toBeTruthy();
714
});
715
});
716
```