mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
11 KiB
11 KiB
LLM Guidelines for React Native Testing Library
Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14.
Core APIs
render
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.
await render(<Component />);
screen.getByRole('button'); // Access queries via screen
renderHook
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
getByRoleas 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()- usescreenqueries instead - Query by visible text, not
testIDwhen 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)
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()-renderandfireEventhandle it internally
waitFor Usage
- Use
findBy*instead ofwaitFor+getBy*when waiting for elements - Never perform side-effects (like
fireEvent.press()) insidewaitForcallbacks - One assertion per
waitForcallback - Never pass empty callbacks - always include a meaningful assertion
- Place side-effects before
waitFor- perform actions, then wait for result
Code Organization
- Use
screeninstead of destructuring fromrender():screen.getByText()notconst { getByText } = render() - Prefer
userEventoverfireEventfor realistic interactions - Don't use
cleanup()- handled automatically - Name wrappers descriptively:
ThemeProvidernotWrapper - Install ESLint plugin:
eslint-plugin-testing-library
Quick Checklist
- ✅ Using
getByRoleas first choice? - ✅ Using
awaitfor all async operations? - ✅ Using
findBy*for async elements (notwaitFor+getBy*)? - ✅ Using
queryBy*only for non-existence? - ✅ Using RNTL matchers (
toBeOnTheScreen(),toBeDisabled(), etc.)? - ✅ Using
screennot destructuring fromrender()? - ✅ Avoiding side-effects in
waitFor? - ✅ Using
userEventwhen appropriate?
Example: Good Pattern
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
// ❌ 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.