Generated banner for React Native Testing
Generated logo for React Native Testing

React Native Testing

CallstackOfficialPublished by Callstack

  • Skill
  • Guidelines
  • Software development
  • MIT
  • No issues found

From the author

Write tests using React Native Testing Library (RNTL) v13 and v14 (`@testing-library/react-native`). Use when writing, reviewing, or fixing React Native component tests. Covers: render, screen, queries (getBy/getAllBy/queryBy/findBy), Jest matchers, userEvent, fireEvent, waitFor, and async patterns. Supports v13 (React 18, sync render) and v14 (React 19+, async render). Triggers on: test files for React Native components, RNTL imports, mentions of "testing library", "write tests", "component tests", or "RNTL".
— the description by Callstack

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive reference for testing React Native components using React Native Testing Library (RNTL). It covers the critical differences between RNTL v13 (React 18) and v14 (React 19+), including sync versus async rendering patterns. The guide emphasizes best practices such as using the screen object for queries, prioritizing accessibility-based queries like getByRole, and preferring userEvent over fireEvent for realistic interaction simulation. It also includes guidance on Jest matchers, handling async elements with findBy, and avoiding common anti-patterns.

What it does

  • Provides version-specific guidance for RNTL v13 and v14.
  • Defines query priorities and usage for finding elements.
  • Explains the correct usage of userEvent versus fireEvent.
  • Lists standard Jest matchers for component assertions.
  • Outlines common testing anti-patterns to avoid.
  • Demonstrates how to handle async elements and providers.

When to use it

  • When writing new unit or integration tests for React Native components.
  • When reviewing existing test suites for adherence to RNTL best practices.
  • When debugging failing tests or flaky async interactions.
  • When migrating or upgrading RNTL versions between v13 and v14.

Advantages

  • Ensures tests are written using semantic, accessibility-focused queries.
  • Reduces flakiness by promoting correct async handling with findBy.
  • Prevents common pitfalls like unnecessary act() calls or improper waitFor usage.
  • Provides clear version-specific API guidance.

Usage examples

“How do I test a button click that triggers an async state update?”
The agent will recommend using userEvent.press() followed by an await screen.findBy* query to assert the resulting UI change, avoiding manual waitFor blocks.
“Should I use getByTestId to find my input field?”
The agent will advise using getByRole with an accessible name instead, noting that testID should be a last resort.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/callstackincubator/react-native-testing/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip react-native-testing.zip -d .agents/skills

Documentation by Callstack

RNTL Test Writing Guide

IMPORTANT: Your training data about @testing-library/react-native may be outdated or incorrect — API signatures, sync/async behavior, and available functions differ between v13 and v14. Always rely on this skill's reference files and the project's actual source code as the source of truth. Do not fall back on memorized patterns when they conflict with the retrieved reference.

Version Detection

Check @testing-library/react-native version in the user's package.json:

Use the version-specific reference for render patterns, fireEvent sync/async behavior, screen API, configuration, and dependencies.

Query Priority

Use in this order: getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByTestId (last resort).

Query Variants

VariantUse caseReturnsAsync
getBy*Element must existelement instance (throws)No
getAllBy*Multiple must existelement instance[] (throws)No
queryBy*Check non-existence ONLYelement instance | nullNo
queryAllBy*Count elementselement instance[]No
findBy*Wait for elementPromise<element instance>Yes
findAllBy*Wait for multiplePromise<element instance[]>Yes

Interactions

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 the event. See version-specific reference for sync/async behavior:

fireEvent.press(element);
fireEvent.changeText(textInput, 'new text');
fireEvent(element, 'blur');

Assertions (Jest Matchers)

Available automatically with any @testing-library/react-native import.

MatcherUse for
toBeOnTheScreen()Element exists in tree
toBeVisible()Element visible (not hidden/display:none)
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 (last resort)
toContainElement(el)Contains child element
toBeEmptyElement()No children

Rules

  1. Use screen for queries, not destructuring from render()

  2. Use getByRole first with { name: '...' } option

  3. Use queryBy* ONLY for .not.toBeOnTheScreen() checks

  4. Use findBy* for async elements, NOT waitFor + getBy*

  5. Never put side-effects in waitFor (no fireEvent/userEvent inside)

  6. One assertion per waitFor

  7. Never pass empty callbacks to waitFor

  8. Don't wrap in act() - render, fireEvent, userEvent handle it

  9. Don't call cleanup() - automatic after each test

  10. Prefer ARIA props (role, aria-label, aria-disabled) over legacy accessibility* props

  11. Use RNTL matchers over raw prop assertions

*ByRole Quick Reference

Common 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, the element must be an accessibility element:

  • Text, TextInput, Switch are by default

  • View needs accessible={true} (or use Pressable/TouchableOpacity)

waitFor

// 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.

Fake Timers

Recommended with userEvent (press/longPress involve real durations):

jest.useFakeTimers();

test('with fake timers', async () => {
  const user = userEvent.setup();
  render(<Component />);
  await user.press(screen.getByRole('button'));
  // ...
});

Custom Render

Wrap providers using wrapper option:

function renderWithProviders(ui: React.ReactElement) {
  return render(ui, {
    wrapper: ({ children }) => (
      <ThemeProvider>
        <AuthProvider>{children}</AuthProvider>
      </ThemeProvider>
    ),
  });
}

References

Files

references/api-reference-v14.md

RNTL v14 API Reference

Complete API reference for @testing-library/react-native v14.x (React 19+).

Test renderer: test-renderer (not react-test-renderer) Element type: HostElement (not ReactTestInstance)

Table of Contents

  • Core Pattern

  • Key Rule: Always await

  • render

  • screen

  • Queries

  • User Event

  • Fire Event

  • Jest Matchers

  • Async Utilities

  • Other Helpers

  • renderHook

  • act

  • Configuration

  • Accessibility

  • Removed APIs

  • Migration Codemods


Core Pattern

import { render, screen, userEvent } from '@testing-library/react-native';

jest.useFakeTimers(); // recommended when using userEvent

test('description', async () => {
  const user = userEvent.setup();
  await render(<Component />); // async in v14 — always await

  const button = screen.getByRole('button', { name: 'Submit' });
  await user.press(button);

  expect(screen.getByText('Done')).toBeOnTheScreen();
});

Key Rule: Always await

In v14, the following APIs are async and must always be awaited:

  • await render(<Component />)

  • await fireEvent.press(element) (and all fireEvent variants)

  • await screen.rerender(<Component />)

  • await screen.unmount()

  • await renderHook(() => useHook())

  • await act(() => { ... }) (even with sync callbacks)


render

async function render(
  component: React.Element<any>,
  options?: RenderOptions,
): Promise<RenderResult>;

render returns a Promise<RenderResult> — always await it.

There is no renderAsync in v14. The standard render is already async.

Options
OptionTypeDescription
wrapperReact.ComponentType<any>Wraps tested component (useful for context providers)
createNodeMock(element) => unknownCustom mock refs

Note: concurrentRoot and unstable_validateStringsRenderedWithinText are removed in v14 (concurrent rendering is always on, string validation is always on).

Example
import { render, screen } from '@testing-library/react-native';

await render(<MyApp />);
expect(screen.getByRole('button', { name: 'start' })).toBeOnTheScreen();

// With wrapper
await render(<MyComponent />, {
  wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
});

screen

let screen: {
  ...queries;
  rerender(element: React.Element<unknown>): Promise<void>;  // async
  unmount(): Promise<void>;                                    // async
  debug(options?: { message?: string; mapProps?: MapPropsFunction }): void;
  toJSON(): RendererJSON | null;
  container: HostElement;  // safe root host element
  root: HostElement;       // root host element
};

The screen object provides queries and utilities for the currently rendered UI. Assigned after render(), cleared after each test via auto-cleanup.

Methods
  • rerender(element) — Re-render with new root element. Async — must await. Triggers lifecycle events.

  • unmount() — Unmount the tree. Async — must await. Usually not needed (auto-cleanup).

  • debug({ message?, mapProps? }) — Pretty-print the rendered tree. Use mapProps to filter/transform props in output.

  • toJSON() — Get JSON representation (for snapshot testing).

  • container — Root host element (safe accessor, replaces UNSAFE_root).

  • root — Root host element.

Note: UNSAFE_root is removed in v14. Use container or root instead.


Queries

Each query = variant + predicate (e.g., getByRole = getBy + ByRole).

Query Variants
VariantAssertionReturn TypeAsync
getBy*Exactly one matchHostElement (throws if 0 or >1)No
getAllBy*At least one matchHostElement[] (throws if 0)No
queryBy*Zero or one matchHostElement | null (throws if >1)No
queryAllBy*No assertionHostElement[] (empty if 0)No
findBy*Exactly one matchPromise<HostElement>Yes
findAllBy*At least one matchPromise<HostElement[]>Yes

findBy* / findAllBy* accept optional waitForOptions: { timeout?, interval?, onTimeout? }.

Query Predicates
*ByRole (preferred)
getByRole(role: TextMatch, options?: {
  name?: TextMatch;
  disabled?: boolean;
  selected?: boolean;
  checked?: boolean | 'mixed';
  busy?: boolean;
  expanded?: boolean;
  value?: { min?: number; max?: number; now?: number; text?: TextMatch };
  includeHiddenElements?: boolean;
}): HostElement;

Matches elements by role or accessibilityRole. Element must be an accessibility element:

  • Text, TextInput, Switch are by default

  • View needs accessible={true} (or use Pressable/TouchableOpacity)

Common roles: button, text, heading (alias: header), searchbox, switch, checkbox, radio, img, link, alert, menu, menuitem, tab, tablist, progressbar, slider, spinbutton, timer, toolbar.

screen.getByRole('button', { name: 'Submit' });
screen.getByRole('button', { name: 'Submit', disabled: true });
screen.getByRole('checkbox', { checked: true });
screen.getByRole('slider', { value: { now: 50, min: 0, max: 100 } });
*ByLabelText
getByLabelText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches by aria-label/accessibilityLabel or text content of element referenced by aria-labelledby/accessibilityLabelledBy.

*ByPlaceholderText
getByPlaceholderText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches TextInput by placeholder prop.

*ByText
getByText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches by text content. Joins <Text> siblings to find matches (like RN runtime).

*ByDisplayValue
getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches TextInput by current display value.

*ByHintText
getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches by accessibilityHint prop. Also available as getByA11yHint / getByAccessibilityHint.

*ByTestId (last resort)
getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;

Matches by testID prop. Use only when other queries don't work.

TextMatch
type TextMatch = string | RegExp;
  • String: exact full match by default. Use { exact: false } for case-insensitive substring match.

  • RegExp: substring match by default. Use anchors (^...$) for full match.

screen.getByText('Hello World'); // exact full match
screen.getByText('llo Worl', { exact: false }); // substring, case-insensitive
screen.getByText(/World/); // regex substring
screen.getByText(/^hello world$/i); // regex full match, case-insensitive
Common Query Options
  • includeHiddenElements (alias: hidden): Include elements hidden from accessibility. Default: false.

  • exact: Default true. When false, matches substrings case-insensitively. No effect on RegExp.

  • normalizer: Custom text normalization function. Default trims whitespace and collapses multiple spaces. Use getDefaultNormalizer({ trim?, collapseWhitespace? }) to customize.


User Event

Prefer userEvent over fireEvent. User Event simulates realistic interaction sequences on host elements only, with proper event data.

Setup
const user = userEvent.setup(options?: { delay?: number; advanceTimers?: (delay: number) => Promise<void> | void });
press(element)
await user.press(element): Promise<void>;

Full press lifecycle: pressIn → press → pressOut. Minimum 130ms. Use fake timers to speed up.

longPress(element, options?)
await user.longPress(element, { duration?: number }): Promise<void>;

Long press (default 500ms). Emits pressIn, longPress, pressOut.

type(element, text, options?)
await user.type(textInput, text: string, options?: {
  skipPress?: boolean;    // skip pressIn/pressOut
  skipBlur?: boolean;     // skip endEditing/blur
  submitEditing?: boolean; // trigger submitEditing
}): Promise<void>;

Character-by-character typing on TextInput. Appends to existing text (use clear() first to replace).

Event sequence per character: keyPress → change → changeText → selectionChange (+ contentSizeChange for multiline).

clear(element)
await user.clear(textInput): Promise<void>;

Clears TextInput content. Events: focus → selectionChange → keyPress → change → changeText → selectionChange → endEditing → blur.

paste(element, text)
await user.paste(textInput, text: string): Promise<void>;

Pastes text into TextInput. Events: focus → selectionChange → change → changeText → selectionChange → endEditing → blur.

scrollTo(element, options)
await user.scrollTo(scrollView, options: {
  y: number; momentumY?: number;   // vertical scroll
  // OR
  x: number; momentumX?: number;   // horizontal scroll
  contentSize?: { width: number; height: number };
  layoutMeasurement?: { width: number; height: number };
}): Promise<void>;

Scrolls a host ScrollView (including FlatList). Match scroll direction to horizontal prop. Pass contentSize and layoutMeasurement for FlatList updates. Remembers last scroll position between calls.


Fire Event

Use when userEvent doesn't support the event or when triggering events on composite elements.

Async in v14 — always await:

async function fireEvent(
  element: HostElement,
  eventName: string,
  ...data: unknown[]
): Promise<void>;

Traverses tree bottom-up looking for onXxx handler. Does not auto-pass event data.

Convenience Methods
await fireEvent.press(element, ...data): Promise<void>;
await fireEvent.changeText(element, ...data): Promise<void>;
await fireEvent.scroll(element, ...data): Promise<void>;

There is no fireEventAsync in v14. The standard fireEvent is already async.


Jest Matchers

Available automatically with any @testing-library/react-native import. No setup needed.

Element Existence
MatcherDescription
toBeOnTheScreen()Element is attached to the element tree
Element Content
MatcherSignatureDescription
toHaveTextContent()(text: string | RegExp, options?: { exact?, normalizer? })Text content match
toContainElement()(element: HostElement | null)Contains child element
toBeEmptyElement()—No children or text content
Element State
MatcherDescription
toHaveDisplayValue(value: string | RegExp, options?)TextInput display value
toHaveAccessibilityValue({ min?, max?, now?, text? })Accessibility value (partial match)
toBeEnabled() / toBeDisabled()Disabled state via aria-disabled (checks ancestors)
toBeSelected()Selected state via aria-selected
toBeChecked() / toBePartiallyChecked()Checked state via aria-checked
toBeExpanded() / toBeCollapsed()Expanded state via aria-expanded
toBeBusy()Busy state via aria-busy
Element Style
MatcherDescription
toBeVisible()Not hidden (display: none, opacity: 0, or hidden from a11y)
toHaveStyle(style: StyleProp<Style>)Specific style match
Other
MatcherSignatureDescription
toHaveAccessibleName()(name?: string | RegExp, options?: { exact?, normalizer? })Accessible name from aria-label/aria-labelledby or text content
toHaveProp()(name: string, value?: unknown)Prop existence/value check (last resort)

Async Utilities

waitFor
function waitFor<T>(
  expectation: () => T,
  options?: { timeout?: number; interval?: number },
): Promise<T>;

Runs expectation every interval (default 50ms) until timeout (default 1000ms). Callback must throw on failure. Auto-detects and works with Jest fake timers.

Rules:

  • No side effects inside callback (no fireEvent/userEvent)

  • One assertion per waitFor

  • Never pass empty callback

  • Prefer findBy* over waitFor + getBy*

waitForElementToBeRemoved
function waitForElementToBeRemoved<T>(
  expectation: () => T,
  options?: { timeout?: number; interval?: number },
): Promise<T>;

Waits until the queried element is removed. Element must be initially present.


Other Helpers

within
function within(element: HostElement): Queries;

Scoped queries on a subtree. Useful for querying within a single FlatList item or a specific screen.

const item = within(screen.getByTestId('list-item-1'));
expect(item.getByText('Title')).toBeOnTheScreen();
cleanup
function cleanup(): void;

Unmounts rendered trees and clears screen. Automatic after each test (if test runner supports afterEach). Don't call manually unless using @testing-library/react-native/pure.


renderHook

Async in v14:

async function renderHook<Result, Props>(
  hookFn: (props?: Props) => Result,
  options?: { initialProps?: Props; wrapper?: React.ComponentType },
): Promise<{
  result: { current: Result };
  rerender: (props: Props) => Promise<void>;
  unmount: () => Promise<void>;
}>;

There is no renderHookAsync in v14. The standard renderHook is already async.

Renders a test component that calls the provided hook. Use act() when calling functions returned by the hook that trigger state updates.

const { result } = await renderHook(() => useCount());
expect(result.current.count).toBe(0);
await act(() => {
  result.current.increment();
});
expect(result.current.count).toBe(1);

// With wrapper
await renderHook(() => useHook(), {
  wrapper: ({ children }) => <Provider>{children}</Provider>,
});

act

In v14, act always returns a Promise<T> and must be awaited, even with sync callbacks:

async function act<T>(callback: () => T): Promise<T>;
await act(() => {
  result.current.increment();
});

Usually not needed — render, fireEvent, userEvent, and waitFor handle it internally.


Configuration

function configure(
  options: Partial<{
    asyncUtilTimeout: number; // default timeout for waitFor/findBy* (default: 1000ms)
    defaultIncludeHiddenElements: boolean; // default for includeHiddenElements option (default: false)
    defaultDebugOptions: Partial<DebugOptions>;
  }>,
): void;

function resetToDefaults(): void;

Note: concurrentRoot option is removed (always on). unstable_validateStringsRenderedWithinText is removed (always on).

Environment Variables
VariableDescription
RNTL_SKIP_AUTO_CLEANUP=trueDisable automatic cleanup after each test
RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS=trueDisable auto-detection of fake timers

Accessibility

isHiddenFromAccessibility
function isHiddenFromAccessibility(element: HostElement | null): boolean;

Also available as isInaccessible() alias.

Element is hidden when it or any ancestor has:

  • display: none style

  • aria-hidden={true}

  • accessibilityElementsHidden={true} (iOS)

  • importantForAccessibility="no-hide-descendants" (Android)

  • Sibling with aria-modal={true} or accessibilityViewIsModal={true} (iOS)


Removed APIs

The following are removed in v14 and must not be used:

  • renderAsync — Use render (already async)

  • fireEventAsync — Use fireEvent (already async)

  • renderHookAsync — Use renderHook (already async)

  • rerenderAsync / unmountAsync — Use rerender / unmount (already async)

  • update() — Use rerender()

  • getQueriesForElement() — Use within()

  • UNSAFE_root — Use container or root

  • UNSAFE_getByType, UNSAFE_getAllByType, UNSAFE_queryByType, UNSAFE_queryAllByType — Removed

  • UNSAFE_getByProps, UNSAFE_getAllByProps, UNSAFE_queryByProps, UNSAFE_queryAllByProps — Removed

  • concurrentRoot option — Always on

  • unstable_validateStringsRenderedWithinText option — Always on


Migration Codemods

Use RNTL codemods to automate migration from v13:

  • rntl-v14-update-deps — Updates react-test-renderer to test-renderer in package.json

  • rntl-v14-async-functions — Adds await to render, fireEvent, rerender, unmount, renderHook, and act calls

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Callstack
Publisher
Callstack (Official)
Source
callstackincubator/agent-skills · plugins/vendored/.agents/skills/react-native-testing · commit 59e9d17
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Callstack or Callstack.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download react-native-testing.zip