@testing-library/react-native 14.0.0-rc.0 → 14.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,587 @@
1
+ # Common Mistakes with React Native Testing Library
2
+
3
+ > **Note:** This guide is adapted from Kent C. Dodds' article ["Common mistakes with React Testing Library"](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) for React Native Testing Library v14. The original article focuses on web React, but the principles apply to React Native as well. This adaptation includes React Native-specific examples, async API usage (v14), and ARIA-compatible accessibility attributes.
4
+
5
+ React Native Testing Library guiding principle is:
6
+
7
+ > "The more your tests resemble the way your software is used, the more confidence they can give you."
8
+
9
+ This guide outlines some common mistakes people make when using React Native Testing Library and how to avoid them.
10
+
11
+ ## Using the wrong query
12
+
13
+ **Importance: high**
14
+
15
+ React Native Testing Library provides several query types. Here's the priority order:
16
+
17
+ 1. **Queries that reflect user experience:**
18
+ - `getByRole` - most accessible
19
+ - `getByLabelText` - accessible label
20
+ - `getByPlaceholderText` - `TextInput` placeholder text
21
+ - `getByText` - text content
22
+ - `getByDisplayValue` - `TextInput` input value
23
+
24
+ 2. **Semantic queries:**
25
+ - `getByTestId` - only if nothing else works
26
+
27
+ Here's an example of using the right query:
28
+
29
+ ```tsx
30
+ import { TextInput, View } from 'react-native';
31
+ import { render, screen } from '@testing-library/react-native';
32
+
33
+ test('finds input by label', async () => {
34
+ await render(
35
+ <View>
36
+ <TextInput aria-label="Username" placeholder="Enter username" value="" />
37
+ </View>,
38
+ );
39
+
40
+ // ✅ Good - uses accessible label
41
+ const input = screen.getByLabelText('Username');
42
+
43
+ // ✅ Also good - uses placeholder
44
+ const inputByPlaceholder = screen.getByPlaceholderText('Enter username');
45
+
46
+ // ❌ Bad - uses testID when accessible queries work
47
+ // const input = screen.getByTestId('username-input');
48
+ });
49
+ ```
50
+
51
+ ## Not using `*ByRole` query most of the time
52
+
53
+ **Importance: high**
54
+
55
+ `getByRole` is the most accessible query and should be your first choice. It queries elements by their semantic role:
56
+
57
+ ```tsx
58
+ import { Pressable, Text, TextInput, View } from 'react-native';
59
+ import { render, screen } from '@testing-library/react-native';
60
+
61
+ test('uses role queries', async () => {
62
+ await render(
63
+ <View>
64
+ <Pressable role="button">
65
+ <Text>Submit</Text>
66
+ </Pressable>
67
+ <TextInput role="searchbox" aria-label="Search" placeholder="Search..." />
68
+ </View>,
69
+ );
70
+
71
+ // ✅ Good - uses role query
72
+ const button = screen.getByRole('button', { name: 'Submit' });
73
+ const searchbox = screen.getByRole('searchbox', { name: 'Search' });
74
+
75
+ expect(button).toBeOnTheScreen();
76
+ expect(searchbox).toBeOnTheScreen();
77
+ });
78
+ ```
79
+
80
+ Common roles in React Native include:
81
+
82
+ - `button` - pressable elements
83
+ - `text` - static text
84
+ - `header` / `heading` - headers
85
+ - `searchbox` - search inputs
86
+ - `switch` - toggle switches
87
+ - `checkbox` - checkboxes
88
+ - `radio` - radio buttons
89
+ - And more...
90
+
91
+ Note: React Native supports both ARIA-compatible (`role`) and legacy (`accessibilityRole`) props. Prefer `role` for consistency with web standards.
92
+
93
+ ## Using the wrong assertion
94
+
95
+ **Importance: high**
96
+
97
+ React Native Testing Library provides built-in Jest matchers. Make sure you're using the right ones:
98
+
99
+ ```tsx
100
+ import { Pressable, Text } from 'react-native';
101
+ import { render, screen } from '@testing-library/react-native';
102
+
103
+ test('button is disabled', async () => {
104
+ await render(
105
+ <Pressable role="button" aria-disabled>
106
+ <Text>Submit</Text>
107
+ </Pressable>,
108
+ );
109
+
110
+ const button = screen.getByRole('button', { name: 'Submit' });
111
+
112
+ // ✅ Good - uses RNTL matcher
113
+ expect(button).toBeDisabled();
114
+
115
+ // ❌ Bad - doesn't use RNTL matcher
116
+ expect(button.props['aria-disabled']).toBe(true);
117
+ });
118
+ ```
119
+
120
+ Common matchers include:
121
+
122
+ - `toBeOnTheScreen()` - checks if element is rendered (replaces `toBeInTheDocument()`)
123
+ - `toBeDisabled()` - checks if element is disabled
124
+ - `toHaveTextContent()` - checks text content
125
+ - `toHaveAccessibleName()` - checks accessible name
126
+ - And more...
127
+
128
+ ## Using `query*` variants for anything except checking for non-existence
129
+
130
+ **Importance: high**
131
+
132
+ Use `queryBy*` only when checking that an element doesn't exist:
133
+
134
+ ```tsx
135
+ import { View, Text } from 'react-native';
136
+ import { render, screen } from '@testing-library/react-native';
137
+
138
+ test('checks non-existence', async () => {
139
+ await render(
140
+ <View>
141
+ <Text>Hello</Text>
142
+ </View>,
143
+ );
144
+
145
+ // ✅ Good - uses queryBy for non-existence check
146
+ expect(screen.queryByText('Goodbye')).not.toBeOnTheScreen();
147
+
148
+ // ❌ Bad - uses queryBy when element should exist
149
+ // const element = screen.queryByText('Hello');
150
+ // expect(element).toBeOnTheScreen();
151
+
152
+ // ✅ Good - uses getBy when element should exist
153
+ expect(screen.getByText('Hello')).toBeOnTheScreen();
154
+ });
155
+ ```
156
+
157
+ ## Using `waitFor` to wait for elements that can be queried with `find*`
158
+
159
+ **Importance: high**
160
+
161
+ Use `findBy*` queries instead of `waitFor` + `getBy*`:
162
+
163
+ ```tsx
164
+ import { View, Text } from 'react-native';
165
+ import { render, screen, waitFor } from '@testing-library/react-native';
166
+
167
+ test('waits for element', async () => {
168
+ const Component = () => {
169
+ const [show, setShow] = React.useState(false);
170
+
171
+ React.useEffect(() => {
172
+ setTimeout(() => setShow(true), 100);
173
+ }, []);
174
+
175
+ return <View>{show && <Text>Loaded</Text>}</View>;
176
+ };
177
+
178
+ await render(<Component />);
179
+
180
+ // ✅ Good - uses findBy query
181
+ const element = await screen.findByText('Loaded');
182
+ expect(element).toBeOnTheScreen();
183
+
184
+ // ❌ Bad - uses waitFor + getBy
185
+ // await waitFor(() => {
186
+ // expect(screen.getByText('Loaded')).toBeOnTheScreen();
187
+ // });
188
+ });
189
+ ```
190
+
191
+ ## Performing side-effects in `waitFor`
192
+
193
+ **Importance: high**
194
+
195
+ Don't perform side-effects in `waitFor` callbacks:
196
+
197
+ ```tsx
198
+ import { Pressable, Text, View } from 'react-native';
199
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react-native';
200
+
201
+ test('avoids side effects in waitFor', async () => {
202
+ const Component = () => {
203
+ const [count, setCount] = React.useState(0);
204
+ return (
205
+ <View>
206
+ <Pressable role="button" onPress={() => setCount(count + 1)}>
207
+ <Text>Increment</Text>
208
+ </Pressable>
209
+ <Text>Count: {count}</Text>
210
+ </View>
211
+ );
212
+ };
213
+
214
+ await render(<Component />);
215
+
216
+ const button = screen.getByRole('button');
217
+
218
+ // ❌ Bad - side effect in waitFor
219
+ // await waitFor(async () => {
220
+ // await fireEvent.press(button);
221
+ // expect(screen.getByText('Count: 1')).toBeOnTheScreen();
222
+ // });
223
+
224
+ // ✅ Good - side effect outside waitFor
225
+ await fireEvent.press(button);
226
+ await waitFor(() => {
227
+ expect(screen.getByText('Count: 1')).toBeOnTheScreen();
228
+ });
229
+ });
230
+ ```
231
+
232
+ ## Using `container` to query for elements
233
+
234
+ **Importance: high**
235
+
236
+ React Native Testing Library provides a `container` object that has a `queryAll` method, but you should avoid using it directly:
237
+
238
+ ```tsx
239
+ import { View, Text } from 'react-native';
240
+ import { render } from '@testing-library/react-native';
241
+
242
+ test('finds element incorrectly', async () => {
243
+ const { container } = await render(
244
+ <View>
245
+ <Text testID="message">Hello</Text>
246
+ </View>,
247
+ );
248
+
249
+ // ❌ Bad - using container.queryAll directly
250
+ const element = container.queryAll((node) => node.props.testID === 'message')[0];
251
+
252
+ // ✅ Good - use proper queries
253
+ // const element = screen.getByTestId('message');
254
+ });
255
+ ```
256
+
257
+ Instead, use the proper query methods from `screen` or the `render` result. The `container` is a low-level API that you rarely need.
258
+
259
+ ## Passing an empty callback to `waitFor`
260
+
261
+ **Importance: high**
262
+
263
+ Don't pass an empty callback to `waitFor`:
264
+
265
+ ```tsx
266
+ import { View } from 'react-native';
267
+ import { render, waitFor } from '@testing-library/react-native';
268
+
269
+ test('waits correctly', async () => {
270
+ await render(<View testID="test" />);
271
+
272
+ // ❌ Bad - empty callback
273
+ // await waitFor(() => {});
274
+
275
+ // ✅ Good - meaningful assertion
276
+ await waitFor(() => {
277
+ expect(screen.getByTestId('test')).toBeOnTheScreen();
278
+ });
279
+ });
280
+ ```
281
+
282
+ ## Not using `screen`
283
+
284
+ **Importance: medium**
285
+
286
+ You can get all the queries from the `render` result:
287
+
288
+ ```tsx
289
+ import { View, Text } from 'react-native';
290
+ import { render } from '@testing-library/react-native';
291
+
292
+ test('renders component', async () => {
293
+ const { getByText } = await render(
294
+ <View>
295
+ <Text>Hello</Text>
296
+ </View>,
297
+ );
298
+
299
+ expect(getByText('Hello')).toBeOnTheScreen();
300
+ });
301
+ ```
302
+
303
+ But you can also get them from the `screen` object:
304
+
305
+ ```tsx
306
+ import { View, Text } from 'react-native';
307
+ import { render, screen } from '@testing-library/react-native';
308
+
309
+ test('renders component', async () => {
310
+ await render(
311
+ <View>
312
+ <Text>Hello</Text>
313
+ </View>,
314
+ );
315
+
316
+ expect(screen.getByText('Hello')).toBeOnTheScreen();
317
+ });
318
+ ```
319
+
320
+ Using `screen` has several benefits:
321
+
322
+ 1. You don't need to destructure `getByText` from `render`
323
+ 2. It's more consistent with the Testing Library ecosystem
324
+
325
+ ## Wrapping things in `act` unnecessarily
326
+
327
+ **Importance: medium**
328
+
329
+ React Native Testing Library's `render`, `renderHook`, `userEvent`, and `fireEvent` are already wrapped in `act`, so you don't need to wrap them yourself:
330
+
331
+ ```tsx
332
+ import { Pressable, Text, View } from 'react-native';
333
+ import { render, fireEvent, screen } from '@testing-library/react-native';
334
+
335
+ test('updates on press', async () => {
336
+ const Component = () => {
337
+ const [count, setCount] = React.useState(0);
338
+ return (
339
+ <View>
340
+ <Pressable role="button" onPress={() => setCount(count + 1)}>
341
+ <Text>Count: {count}</Text>
342
+ </Pressable>
343
+ </View>
344
+ );
345
+ };
346
+
347
+ await render(<Component />);
348
+
349
+ const button = screen.getByRole('button');
350
+
351
+ // ✅ Good - fireEvent is already wrapped in act
352
+ await fireEvent.press(button);
353
+
354
+ expect(screen.getByText('Count: 1')).toBeOnTheScreen();
355
+
356
+ // ❌ Bad - unnecessary act wrapper
357
+ // await act(async () => {
358
+ // await fireEvent.press(button);
359
+ // });
360
+ });
361
+ ```
362
+
363
+ ## Not using User Event API
364
+
365
+ **Importance: medium**
366
+
367
+ `userEvent` provides a more realistic way to simulate user interactions:
368
+
369
+ ```tsx
370
+ import { Pressable, Text, TextInput, View } from 'react-native';
371
+ import { render, screen, userEvent } from '@testing-library/react-native';
372
+
373
+ test('uses userEvent', async () => {
374
+ const user = userEvent.setup();
375
+
376
+ const Component = () => {
377
+ const [value, setValue] = React.useState('');
378
+ return (
379
+ <View>
380
+ <TextInput aria-label="Name" value={value} onChangeText={setValue} />
381
+ <Pressable role="button" onPress={() => setValue('')}>
382
+ <Text>Clear</Text>
383
+ </Pressable>
384
+ </View>
385
+ );
386
+ };
387
+
388
+ await render(<Component />);
389
+
390
+ const input = screen.getByLabelText('Name');
391
+ const button = screen.getByRole('button', { name: 'Clear' });
392
+
393
+ // ✅ Good - uses userEvent for realistic interactions
394
+ await user.type(input, 'John');
395
+ expect(input).toHaveValue('John');
396
+
397
+ await user.press(button);
398
+ expect(input).toHaveValue('');
399
+ });
400
+ ```
401
+
402
+ `userEvent` methods are async and must be awaited. Available methods include:
403
+
404
+ - `press()` - simulates a press
405
+ - `longPress()` - simulates long press
406
+ - `type()` - simulates typing
407
+ - `clear()` - clears text input
408
+ - `paste()` - simulates pasting
409
+ - `scrollTo()` - simulates scrolling
410
+
411
+ ## Not querying by text
412
+
413
+ **Importance: medium**
414
+
415
+ In React Native, text is rendered in `<Text>` components. You should query by the text content that users see:
416
+
417
+ ```tsx
418
+ import { Text, View } from 'react-native';
419
+ import { render, screen } from '@testing-library/react-native';
420
+
421
+ test('finds text correctly', async () => {
422
+ await render(
423
+ <View>
424
+ <Text>Hello World</Text>
425
+ </View>,
426
+ );
427
+
428
+ // ✅ Good - queries by visible text
429
+ expect(screen.getByText('Hello World')).toBeOnTheScreen();
430
+
431
+ // ❌ Bad - queries by testID when text is available
432
+ // expect(screen.getByTestId('greeting')).toBeOnTheScreen();
433
+ });
434
+ ```
435
+
436
+ ## Not using Testing Library ESLint plugins
437
+
438
+ **Importance: medium**
439
+
440
+ There's an ESLint plugin for Testing Library: [`eslint-plugin-testing-library`](https://github.com/testing-library/eslint-plugin-testing-library). This plugin can help you avoid common mistakes and will automatically fix your code in many cases.
441
+
442
+ You can install it with:
443
+
444
+ ```bash
445
+ yarn add --dev eslint-plugin-testing-library
446
+ ```
447
+
448
+ And configure it in your `eslint.config.js` (flat config):
449
+
450
+ ```js
451
+ import testingLibrary from 'eslint-plugin-testing-library';
452
+
453
+ export default [testingLibrary.configs['flat/react']];
454
+ ```
455
+
456
+ Note: Unlike React Testing Library, React Native Testing Library has built-in Jest matchers, so you don't need `eslint-plugin-jest-dom`.
457
+
458
+ ## Using `cleanup`
459
+
460
+ **Importance: medium**
461
+
462
+ React Native Testing Library automatically cleans up after each test. You don't need to call `cleanup()` manually unless you're using the `pure` export (which doesn't include automatic cleanup).
463
+
464
+ If you want to disable automatic cleanup for a specific test, you can use:
465
+
466
+ ```tsx
467
+ import { render } from '@testing-library/react-native/pure';
468
+
469
+ test('does not cleanup', async () => {
470
+ // This test won't cleanup automatically
471
+ await render(<MyComponent />);
472
+ // ... your test
473
+ });
474
+ ```
475
+
476
+ But in most cases, you don't need to worry about cleanup at all - it's handled automatically.
477
+
478
+ ## Using `get*` variants as assertions
479
+
480
+ **Importance: low**
481
+
482
+ `getBy*` queries throw errors when elements aren't found, so they work as assertions. However, for better error messages, you might want to combine them with explicit matchers:
483
+
484
+ ```tsx
485
+ import { View, Text } from 'react-native';
486
+ import { render, screen } from '@testing-library/react-native';
487
+
488
+ test('uses getBy as assertion', async () => {
489
+ await render(
490
+ <View>
491
+ <Text>Hello</Text>
492
+ </View>,
493
+ );
494
+
495
+ // ✅ Good - getBy throws if not found, so it's an assertion
496
+ const element = screen.getByText('Hello');
497
+ expect(element).toBeOnTheScreen();
498
+
499
+ // ✅ Also good - more explicit
500
+ expect(screen.getByText('Hello')).toBeOnTheScreen();
501
+
502
+ // ❌ Bad - redundant assertion
503
+ // const element = screen.getByText('Hello');
504
+ // expect(element).not.toBeNull(); // getBy already throws if null
505
+ });
506
+ ```
507
+
508
+ ## Having multiple assertions in a single `waitFor` callback
509
+
510
+ **Importance: low**
511
+
512
+ Keep `waitFor` callbacks focused on a single assertion:
513
+
514
+ ```tsx
515
+ import { View, Text } from 'react-native';
516
+ import { render, screen, waitFor } from '@testing-library/react-native';
517
+
518
+ test('waits with single assertion', async () => {
519
+ const Component = () => {
520
+ const [count, setCount] = React.useState(0);
521
+
522
+ React.useEffect(() => {
523
+ setTimeout(() => setCount(1), 100);
524
+ }, []);
525
+
526
+ return (
527
+ <View>
528
+ <Text>Count: {count}</Text>
529
+ </View>
530
+ );
531
+ };
532
+
533
+ await render(<Component />);
534
+
535
+ // ✅ Good - single assertion per waitFor
536
+ await waitFor(() => {
537
+ expect(screen.getByText('Count: 1')).toBeOnTheScreen();
538
+ });
539
+
540
+ // If you need multiple assertions, do them after waitFor
541
+ expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');
542
+
543
+ // ❌ Bad - multiple assertions in waitFor
544
+ // await waitFor(() => {
545
+ // expect(screen.getByText('Count: 1')).toBeOnTheScreen();
546
+ // expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');
547
+ // });
548
+ });
549
+ ```
550
+
551
+ ## Using `wrapper` as the variable name
552
+
553
+ **Importance: low**
554
+
555
+ This is not really a "mistake" per se, but it's a common pattern that can be improved. When you use the `wrapper` option in `render`, you might be tempted to name your wrapper component `Wrapper`:
556
+
557
+ ```tsx
558
+ import { View } from 'react-native';
559
+ import { render, screen } from '@testing-library/react-native';
560
+
561
+ test('renders with wrapper', async () => {
562
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
563
+ <View testID="wrapper">{children}</View>
564
+ );
565
+
566
+ await render(<View testID="content">Content</View>, {
567
+ wrapper: Wrapper,
568
+ });
569
+
570
+ expect(screen.getByTestId('content')).toBeOnTheScreen();
571
+ });
572
+ ```
573
+
574
+ This works fine, but it's more conventional to name it something more descriptive like `ThemeProvider` or `AllTheProviders` (if you're wrapping with multiple providers). This makes it clearer what the wrapper is doing.
575
+
576
+ ## Summary
577
+
578
+ The key principles to remember:
579
+
580
+ 1. **Use the right query** - Prefer `getByRole` as your first choice, use `findBy*` for async elements, and `queryBy*` only for checking non-existence
581
+ 2. **Use proper assertions** - Use RNTL's built-in matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.) instead of asserting on props directly
582
+ 3. **Handle async operations correctly** - Always `await` `render()`, `renderHook`, `fireEvent`,and `userEvent` methods
583
+ 4. **Use `waitFor` correctly** - Avoid side-effects in callbacks, use `findBy*` instead when possible, and keep callbacks focused
584
+ 5. **Follow accessibility best practices** - Prefer ARIA attributes (`role`, `aria-label`) over `accessibility*` props
585
+ 6. **Organize code well** - Use `screen` over destructuring, prefer `userEvent` over `fireEvent`, and don't use `cleanup()`
586
+
587
+ By following these principles, your tests will be more maintainable, accessible, and reliable.