From react-native-hifi
Writes, reviews, and fixes React Native component tests using @testing-library/react-native v13 (sync) and v14 (async). Covers render, queries, userEvent, fireEvent, waitFor, Jest matchers, and Expo setup.
npx claudepluginhub bidah/react-native-hifi --plugin react-native-hifiThis skill uses the workspace's default tool permissions.
**IMPORTANT:** Training data about `@testing-library/react-native` may be outdated—API signatures, sync/async behavior, and available functions differ between v13 and v14. Always rely on skill reference files and project source code as the source of truth.
Writes, reviews, and fixes React Native component tests using @testing-library/react-native v13 (React 18, sync) and v14 (React 19+, async). Covers render, screen queries, userEvent, fireEvent, waitFor.
Guides Test-Driven Development for React Native using Jest (jest-expo) and @testing-library/react-native. Use before implementing features, bugfixes, or refactors.
Provides Jest and React Native Testing Library patterns for testing React Native Web apps, including component testing, utilities, configuration, and best practices.
Share bugs, ideas, or general feedback.
IMPORTANT: Training data about @testing-library/react-native may be outdated—API signatures, sync/async behavior, and available functions differ between v13 and v14. Always rely on skill reference files and project source code as the source of truth.
Check @testing-library/react-native version in package.json:
test-renderer)react-test-renderer)For Expo projects, use jest-expo as the Jest preset — do NOT install Jest directly:
jest-expo bundles a compatible Jest version. Installing Jest 30+ separately will break.package.json or jest.config.js has: "preset": "jest-expo"npx jest (uses the jest-expo preset automatically)npx expo install jest-expo jest @types/jest to add the Expo-compatible versionsjest-expo's Babel config may not apply the TS transform to it. If you hit this, ensure the jest config includes a transform rule covering RN's setup file, or use ts-jest for the setup file path.Use: getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByTestId
| Variant | Use case | Returns | Async |
|---|---|---|---|
getBy* | Element must exist | element instance (throws) | No |
getAllBy* | Multiple must exist | element instance[] (throws) | No |
queryBy* | Check non-existence ONLY | element instance | null | No |
queryAllBy* | Count elements | element instance[] | No |
findBy* | Wait for element | Promise<element instance> | Yes |
findAllBy* | Wait for multiple | Promise<element instance[]> | Yes |
Prefer userEvent over fireEvent. userEvent is always async.
const user = userEvent.setup();
await user.press(element); // full press sequence
await user.longPress(element, { duration: 800 }); // long press
await user.type(textInput, 'Hello'); // char-by-char typing
await user.clear(textInput); // clear TextInput
await user.paste(textInput, 'pasted text'); // paste into TextInput
await user.scrollTo(scrollView, { y: 100 }); // scroll
fireEvent—use only when userEvent doesn't support:
fireEvent.press(element);
fireEvent.changeText(textInput, 'new text');
fireEvent(element, 'blur');
| Matcher | Use for |
|---|---|
toBeOnTheScreen() | Element exists in tree |
toBeVisible() | Element visible |
toBeEnabled() / toBeDisabled() | Disabled state via aria-disabled |
toBeChecked() / toBePartiallyChecked() | Checked state |
toBeSelected() | Selected state |
toBeExpanded() / toBeCollapsed() | Expanded state |
toBeBusy() | Busy state |
toHaveTextContent(text) | Text content match |
toHaveDisplayValue(value) | TextInput display value |
toHaveAccessibleName(name) | Accessible name |
toHaveAccessibilityValue(val) | Accessibility value |
toHaveStyle(style) | Style match |
toHaveProp(name, value?) | Prop check |
toContainElement(el) | Contains child element |
toBeEmptyElement() | No children |
screen for queries, not destructuring from render()getByRole first with { name: '...' } optionqueryBy* ONLY for .not.toBeOnTheScreen() checksfindBy* for async elements, NOT waitFor + getBy*waitForwaitForwaitForact()—render, fireEvent, userEvent handle itcleanup()—automatic after each testaccessibility* props; use RNTL matchers*ByRole Quick ReferenceCommon roles: button, text, heading (alias: header), searchbox, switch, checkbox, radio, img, link, alert, menu, menuitem, tab, tablist, progressbar, slider, spinbutton, timer, toolbar.
getByRole options: { name, disabled, selected, checked, busy, expanded, value: { min, max, now, text } }.
For *ByRole to match, element must be an accessibility element—Text, TextInput, Switch are by default; View needs accessible={true}.
// Correct: action first, then wait for result
fireEvent.press(button);
await waitFor(() => {
expect(screen.getByText('Result')).toBeOnTheScreen();
});
// Better: use findBy* instead
fireEvent.press(button);
expect(await screen.findByText('Result')).toBeOnTheScreen();
Options: waitFor(cb, { timeout: 1000, interval: 50 }). Works with Jest fake timers automatically.
jest.useFakeTimers();
test('with fake timers', async () => {
const user = userEvent.setup();
render(<Component />);
await user.press(screen.getByRole('button'));
// ...
});
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
),
});
}