
React Native Testing
Ship React Native screens with RNTL tests that match accessibility semantics, async behavior, and v14 APIs instead of brittle anti-patterns.
Overview
react-native-testing is an agent skill for the Ship phase that catalogs React Native Testing Library anti-patterns and prescribes accessible, async-safe query and interaction patterns.
Install
npx skills add https://github.com/callstack/react-native-testing-library --skill react-native-testingWhat is this skill?
- Documents 12 RNTL anti-pattern categories from wrong getBy/queryBy/findBy usage through v14 async and removed APIs
- Promotes *ByRole with accessible names over testID and raw getByText for buttons and controls
- Shows findBy* and userEvent patterns instead of waitFor+getBy and fireEvent for async and interaction tests
- Calls out unnecessary act(), manual cleanup, UNSAFE_root, and legacy accessibility props
- Maps Pressable role=button examples to semantic queries aligned with how assistive tech sees the UI
- 12 documented anti-pattern categories in the skill table of contents
Adoption & trust: 2.4k installs on skills.sh; 3.4k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your React Native tests pass intermittently, throw on negative cases, or ignore accessibility because queries and waits do not match how RNTL v14 expects you to render and interact.
Who is it for?
Solo builders maintaining Jest + RNTL suites on React Native or Expo who want a concise anti-pattern checklist during test authoring or PR review.
Skip if: Teams that only need E2E on Detox or Maestro with no component tests, or greenfield apps with zero @testing-library/react-native setup.
When should I use this skill?
Writing or reviewing React Native component tests with @testing-library/react-native, or debugging flaky RN test failures and v14 API mismatches.
What do I get? / Deliverables
You refactor tests toward *ByRole, findBy*, userEvent, and v14-safe APIs so CI catches real UI regressions with clearer, accessibility-aligned failures.
- Refactored test snippets using *ByRole, findBy*, and userEvent
- Review notes mapping failures to the twelve anti-pattern sections
- CI-stable assertion patterns for presence and absence checks
Recommended Skills
Journey fit
How it compares
Use this procedural RNTL checker during Ship testing instead of copying Stack Overflow snippets that mix queryBy*, waitFor loops, and fireEvent.
Common Questions / FAQ
Who is react-native-testing for?
It is for indie and solo React Native developers and small teams who write component tests with React Native Testing Library and want agent-assisted reviews that enforce semantic queries and v14 conventions.
When should I use react-native-testing?
Use it in Ship → testing when adding or fixing RNTL specs, before a release test hardening pass, or while migrating tests after a @testing-library/react-native major upgrade.
Is react-native-testing safe to install?
It is documentation-style procedural knowledge with no runtime hooks described in the skill; review the Security Audits panel on this Prism page before adding any third-party skill package to your agent workflow.
SKILL.md
READMESKILL.md - React Native Testing
# RNTL Anti-Patterns ## Table of Contents - Wrong query variant - Not using \*ByRole - Wrong assertions - waitFor misuse - Unnecessary act() - fireEvent instead of userEvent - Destructuring render - Using UNSAFE_root - Manual cleanup - Legacy accessibility props - Forgetting to await (v14) - Using removed APIs (v14) ## Wrong query variant ```tsx // BAD: queryBy* when element should exist const button = screen.queryByRole('button'); expect(button).toBeOnTheScreen(); // GOOD: getBy* when element should exist const button = screen.getByRole('button'); expect(button).toBeOnTheScreen(); // BAD: getBy* for non-existence check (throws instead of failing gracefully) expect(screen.getByText('Error')).not.toBeOnTheScreen(); // GOOD: queryBy* for non-existence check expect(screen.queryByText('Error')).not.toBeOnTheScreen(); // BAD: waitFor + getBy* for async elements await waitFor(() => { expect(screen.getByText('Loaded')).toBeOnTheScreen(); }); // GOOD: findBy* for async elements expect(await screen.findByText('Loaded')).toBeOnTheScreen(); ``` ## Not using \*ByRole ```tsx // BAD: testID when accessible query works <Pressable testID="submit-btn" role="button"> <Text>Submit</Text> </Pressable>; screen.getByTestId('submit-btn'); // GOOD: query by role and accessible name screen.getByRole('button', { name: 'Submit' }); // BAD: getByText for a button (less semantic) screen.getByText('Submit'); // GOOD: getByRole with name (more semantic, tests accessibility) screen.getByRole('button', { name: 'Submit' }); ``` ## Wrong assertions ```tsx // BAD: asserting on props directly expect(button.props['aria-disabled']).toBe(true); expect(button.props.style.backgroundColor).toBe('red'); // GOOD: use RNTL matchers expect(button).toBeDisabled(); expect(button).toHaveStyle({ backgroundColor: 'red' }); // BAD: redundant null check (getBy already throws) const el = screen.getByText('Hello'); expect(el).not.toBeNull(); // GOOD: use toBeOnTheScreen expect(screen.getByText('Hello')).toBeOnTheScreen(); ``` ## waitFor misuse ```tsx // BAD: side-effect inside waitFor (press runs on every retry) await waitFor(() => { fireEvent.press(screen.getByRole('button')); expect(screen.getByText('Result')).toBeOnTheScreen(); }); // GOOD: side-effect outside, assertion inside fireEvent.press(screen.getByRole('button')); await waitFor(() => { expect(screen.getByText('Result')).toBeOnTheScreen(); }); // BETTER: use findBy* fireEvent.press(screen.getByRole('button')); expect(await screen.findByText('Result')).toBeOnTheScreen(); // BAD: empty waitFor callback await waitFor(() => {}); // BAD: multiple assertions in single waitFor await waitFor(() => { expect(screen.getByText('Title')).toBeOnTheScreen(); expect(screen.getByText('Subtitle')).toBeOnTheScreen(); }); // GOOD: one assertion per waitFor, rest after await waitFor(() => { expect(screen.getByText('Title')).toBeOnTheScreen(); }); expect(screen.getByText('Subtitle')).toBeOnTheScreen(); ``` ## Unnecessary act() ```tsx // BAD: wrapping render in act act(() => { render(<Component />); }); // GOOD: render handles act internally render(<Component />); // BAD: wrapping fireEvent in act act(() => { fireEvent.press(button); }); // GOOD: fireEvent handles act internally fireEvent.press(button); // BAD: wrapping userEvent in act await act(async () => { await user.press(button); }); // GOOD: userEvent handles act internally await user.press(button); ``` ## fireEvent instead of userEvent ```tsx // BAD: fireEvent.press (only fires onPress, no pressIn/pressOut) fireEvent.press(button); // GOOD: userEvent.press (full press lifecycle) const user = userEvent.setup(); await user.press(button); // BAD: fireEvent.changeText (sets text all at once, no focus/blur/keyPress) fireEvent.changeText(input, 'Hello'); // GOOD: user.type (char-by-char with full event sequence) await user.type(input, 'Hello'); ``` ## Destructuring render ```tsx // BAD: destructuring queries from render const { get