UNPKG

@testing-library/react-native

Version:

Simple and complete React Native testing utilities that encourage good testing practices.

229 lines (175 loc) 11.1 kB
# LLM Guidelines for React Native Testing Library Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14. ## Core APIs ### render ```tsx const result = await render(<Component />, options?); ``` | Option | Description | | --------- | ---------------------------------------------------------------- | | `wrapper` | React component to wrap the rendered component (e.g., providers) | | Return | Description | | --------------------- | ------------------------------------------------ | | `rerender(component)` | Re-render with a new component (async) | | `unmount()` | Unmount the rendered component (async) | | `toJSON()` | Get JSON representation for snapshots | | `debug(options?)` | Print the component tree to console | | `container` | Root host element of the rendered tree | | `root` | First child host element (your component's root) | ### screen **Prefer `screen`** over destructuring from `render()`. Provides all query methods after `render()` is called. ```tsx await render(<Component />); screen.getByRole('button'); // Access queries via screen ``` ### renderHook ```tsx const { result, rerender, unmount } = await renderHook(() => useMyHook(), options?); ``` | Option | Description | | -------------- | -------------------------------------------------- | | `initialProps` | Initial props passed to the hook | | `wrapper` | React component to wrap the hook (e.g., providers) | | Return | Description | | ------------------ | ------------------------------------- | | `result.current` | Current return value of the hook | | `rerender(props?)` | Re-render hook with new props (async) | | `unmount()` | Unmount the hook (async) | ## Query Selection - **Prefer `getByRole`** as first choice for querying elements - **Query priority**: `getByRole``getByLabelText``getByPlaceholderText``getByText``getByDisplayValue``getByTestId` (last resort) - **Use `findBy*`** for elements that appear asynchronously (after API calls, timeouts, state updates) - **Use `queryBy*` ONLY** for checking non-existence (with `.not.toBeOnTheScreen()`) - **Never use `getBy*`** for non-existence checks - **Avoid `container.queryAll()`** - use `screen` queries instead - **Query by visible text**, not `testID` when text is available ## Assertions - **Use RNTL matchers** - prefer semantic matchers over prop assertions - **Combine queries with matchers**: `expect(screen.getByText('Hello')).toBeOnTheScreen()` - **No redundant null checks** - `getBy*` already throws if not found ## Jest Matchers Reference | Matcher | Description | | --------------------------------- | ------------------------------------------------------------------------------------------- | | `toBeOnTheScreen()` | Element is present in the element tree | | `toBeVisible()` | Element is visible (checks style, `aria-hidden`, `accessibilityElementsHidden`, ancestors) | | `toBeEmptyElement()` | Element has no children or text content | | `toContainElement(element)` | Element contains another element | | `toBeEnabled()` | Element is not disabled (checks `aria-disabled`, `accessibilityState`, ancestors) | | `toBeDisabled()` | Element has `aria-disabled` or `accessibilityState={{ disabled: true }}` (checks ancestors) | | `toBeBusy()` | Element has `aria-busy` or `accessibilityState={{ busy: true }}` | | `toBeChecked()` | Element has `aria-checked` or `accessibilityState={{ checked: true }}` | | `toBePartiallyChecked()` | Element has `aria-checked="mixed"` or `accessibilityState={{ checked: 'mixed' }}` | | `toBeSelected()` | Element has `aria-selected` or `accessibilityState={{ selected: true }}` | | `toBeExpanded()` | Element has `aria-expanded` or `accessibilityState={{ expanded: true }}` | | `toBeCollapsed()` | Element has `aria-expanded={false}` or `accessibilityState={{ expanded: false }}` | | `toHaveTextContent(text)` | Element has matching text content | | `toHaveDisplayValue(value)` | TextInput has matching display value | | `toHaveAccessibleName(name?)` | Element has matching `aria-label`, `accessibilityLabel`, or text content | | `toHaveAccessibilityValue(value)` | Element has matching `aria-value*` or `accessibilityValue` | | `toHaveStyle(style)` | Element has matching style | | `toHaveProp(name, value?)` | Element has prop (use semantic matchers when possible) | ## User Interactions **Prefer `userEvent`** over `fireEvent` for realistic user interaction simulation. `userEvent` triggers the complete event sequence that real users would produce. ### userEvent (Preferred) ```tsx const user = userEvent.setup(); ``` | Method | Description | | ------------------------------------ | ----------------------------------------------------------------------------------- | | `user.press(element)` | Press an element (triggers `pressIn`, `pressOut`, `press`) | | `user.longPress(element, options?)` | Long press with optional `{ duration }` | | `user.type(element, text, options?)` | Type into TextInput (triggers `focus`, `keyPress`, `change`, `changeText` per char) | | `user.clear(element)` | Clear TextInput (select all + backspace) | | `user.paste(element, text)` | Paste text into TextInput | | `user.scrollTo(element, options)` | Scroll a ScrollView with `{ y }` or `{ x }` offset | ### fireEvent (Low-level) Use only when `userEvent` doesn't support the event or when you need direct control. | Method | Description | | ---------------------------------------- | --------------------------------------------- | | `fireEvent(element, eventName, ...data)` | Fire any event by name | | `fireEvent.press(element)` | Fire `onPress` only (no `pressIn`/`pressOut`) | | `fireEvent.changeText(element, text)` | Fire `onChangeText` directly | | `fireEvent.scroll(element, eventData)` | Fire `onScroll` with event data | ## Async/Await (v14) - **Always `await`**: `render()`, `fireEvent.*`, `renderHook()`, `userEvent.*` - **Make test functions `async`**: `test('name', async () => { ... })` - **Don't wrap in `act()`** - `render` and `fireEvent` handle it internally ## waitFor Usage - **Use `findBy*`** instead of `waitFor` + `getBy*` when waiting for elements - **Never perform side-effects** (like `fireEvent.press()`) inside `waitFor` callbacks - **One assertion per `waitFor`** callback - **Never pass empty callbacks** - always include a meaningful assertion - **Place side-effects before `waitFor`** - perform actions, then wait for result ## Code Organization - **Use `screen`** instead of destructuring from `render()`: `screen.getByText()` not `const { getByText } = render()` - **Prefer `userEvent`** over `fireEvent` for realistic interactions - **Don't use `cleanup()`** - handled automatically - **Name wrappers descriptively**: `ThemeProvider` not `Wrapper` - **Install ESLint plugin**: `eslint-plugin-testing-library` ## Quick Checklist - ✅ Using `getByRole` as first choice? - ✅ Using `await` for all async operations? - ✅ Using `findBy*` for async elements (not `waitFor` + `getBy*`)? - ✅ Using `queryBy*` only for non-existence? - ✅ Using RNTL matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.)? - ✅ Using `screen` not destructuring from `render()`? - ✅ Avoiding side-effects in `waitFor`? - ✅ Using `userEvent` when appropriate? ## Example: Good Pattern ```tsx import { render, screen } from '@testing-library/react-native'; import userEvent from '@testing-library/react-native'; import { Pressable, Text, TextInput, View } from 'react-native'; test('user can submit form', async () => { const user = userEvent.setup(); const Component = () => { const [name, setName] = React.useState(''); const [submitted, setSubmitted] = React.useState(false); return ( <View> <TextInput role="textbox" aria-label="Name" value={name} onChangeText={setName} /> <Pressable role="button" aria-label="Submit" onPress={() => setSubmitted(true)}> <Text>Submit</Text> </Pressable> {submitted && <Text role="alert">Form submitted!</Text>} </View> ); }; await render(<Component />); // ✅ getByRole as first choice const input = screen.getByRole('textbox', { name: 'Name' }); const button = screen.getByRole('button', { name: 'Submit' }); // ✅ userEvent for realistic interactions await user.type(input, 'John Doe'); await user.press(button); // ✅ findBy* for async elements const successMessage = await screen.findByRole('alert'); // ✅ RNTL matchers expect(successMessage).toBeOnTheScreen(); expect(successMessage).toHaveTextContent('Form submitted!'); }); ``` ## Example: Anti-Patterns ```tsx // ❌ Missing await test('bad', () => { render(<Component />); fireEvent.press(screen.getByText('Submit')); }); // ❌ getBy* for non-existence expect(screen.getByText('Error')).not.toBeOnTheScreen(); // ❌ waitFor + getBy* instead of findBy* await waitFor(() => { expect(screen.getByText('Loaded')).toBeOnTheScreen(); }); // ❌ Side-effect in waitFor await waitFor(async () => { await fireEvent.press(button); expect(screen.getByText('Result')).toBeOnTheScreen(); }); // ❌ accessibility* props instead of ARIA <Pressable accessibilityRole="button" accessibilityLabel="Submit" />; // ❌ Destructuring from render const { getByText } = await render(<Component />); ``` By following these guidelines, your tests will be more maintainable, accessible, and reliable.