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-v13.md

RNTL v13 API Reference

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

Test renderer: react-test-renderer Element type: ReactTestInstance

Table of Contents

  • Core Pattern

  • render / renderAsync

  • screen

  • Queries

  • User Event

  • Fire Event / fireEventAsync

  • Jest Matchers

  • Async Utilities

  • Other Helpers

  • renderHook / renderHookAsync

  • Configuration

  • Accessibility

  • React 19 Compatibility (v13.3+)

  • Available Legacy APIs


Core Pattern

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

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

test('description', async () => {
  const user = userEvent.setup();
  render(<Component />); // sync in v13

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

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

render / renderAsync

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

render is synchronous in v13. Returns helpers and queries immediately.

renderAsync (v13.3+)
async function renderAsync(
  component: React.Element<any>,
  options?: RenderAsyncOptions,
): Promise<RenderAsyncResult>;

Use renderAsync for React 19 or Suspense components. When using renderAsync, use rerenderAsync and unmountAsync instead of their sync counterparts.

Options
OptionTypeDescription
wrapperReact.ComponentType<any>Wraps tested component (useful for context providers)
concurrentRootbooleanSet false to disable concurrent rendering (default: true)
createNodeMock(element: React.Element) => unknownCustom mock refs for ReactTestRenderer.create()
unstable_validateStringsRenderedWithinTextbooleanExperimental: replicate RN Text string validation
Example
import { render, screen } from '@testing-library/react-native';

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

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

// Async (React 19 / Suspense)
await renderAsync(<SuspenseComponent />);

screen

let screen: {
  ...queries;
  rerender(element: React.Element<unknown>): void;          // sync
  rerenderAsync(element: React.Element<unknown>): Promise<void>;  // v13.3+
  unmount(): void;                                            // sync
  unmountAsync(): Promise<void>;                              // v13.3+
  debug(options?: { message?: string; mapProps?: MapPropsFunction }): void;
  toJSON(): ReactTestRendererJSON | null;
  root: ReactTestInstance;        // root host element
  UNSAFE_root: ReactTestInstance; // root composite element (avoid)
};

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 (sync). Triggers lifecycle events.

  • rerenderAsync(element) — Async version for React 19 / Suspense (v13.3+).

  • unmount() — Unmount the tree (sync). Usually not needed (auto-cleanup).

  • unmountAsync() — Async version for React 19 / Suspense (v13.3+).

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

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

  • root — The rendered root host element (ReactTestInstance).

  • UNSAFE_root — Root composite element. Avoid; use proper queries instead.


Queries

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

Query Variants
VariantAssertionReturn TypeAsync
getBy*Exactly one matchReactTestInstance (throws if 0 or >1)No
getAllBy*At least one matchReactTestInstance[] (throws if 0)No
queryBy*Zero or one matchReactTestInstance | null (throws if >1)No
queryAllBy*No assertionReactTestInstance[] (empty if 0)No
findBy*Exactly one matchPromise<ReactTestInstance>Yes
findAllBy*At least one matchPromise<ReactTestInstance[]>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;
}): ReactTestInstance;

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 }): ReactTestInstance;

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 }): ReactTestInstance;

Matches TextInput by placeholder prop.

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

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

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

Matches TextInput by current display value.

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

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

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

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

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

fireEvent (sync)
function fireEvent(element: ReactTestInstance, eventName: string, ...data: unknown[]): void;

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

Convenience Methods
fireEvent.press(element, ...data): void;
fireEvent.changeText(element, ...data): void;
fireEvent.scroll(element, ...data): void;
fireEventAsync (v13.3+, for React 19 / Suspense)
async function fireEventAsync(element: ReactTestInstance, eventName: string, ...data: unknown[]): Promise<unknown>;
fireEventAsync.press(element, ...data): Promise<unknown>;
fireEventAsync.changeText(element, ...data): Promise<unknown>;
fireEventAsync.scroll(element, ...data): Promise<unknown>;

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: ReactTestInstance | 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 / getQueriesForElement
function within(element: ReactTestInstance): 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();
act
function act(callback: () => void): void;
function act(callback: () => Promise<void>): Promise<void>;

Re-exported from react-test-renderer. Usually not needed — render, fireEvent, userEvent, and waitFor handle it internally.

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

renderHook (sync)
function renderHook<Result, Props>(
  hookFn: (props?: Props) => Result,
  options?: { initialProps?: Props; wrapper?: React.ComponentType; concurrentRoot?: boolean },
): { result: { current: Result }; rerender: (props: Props) => void; unmount: () => void };
renderHookAsync (v13.3+)
async function renderHookAsync<Result, Props>(
  hookFn: (props?: Props) => Result,
  options?: { initialProps?: Props; wrapper?: React.ComponentType; concurrentRoot?: boolean },
): Promise<{
  result: { current: Result };
  rerenderAsync: (props: Props) => Promise<void>;
  unmountAsync: () => Promise<void>;
}>;

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

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

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

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>;
    concurrentRoot: boolean; // default concurrent rendering (default: true)
  }>,
): void;

function resetToDefaults(): void;
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: ReactTestInstance | 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)


React 19 Compatibility (v13.3+)

For React 19 or Suspense, use async variants:

import { renderAsync, screen, fireEventAsync } from '@testing-library/react-native';

test('async component', async () => {
  await renderAsync(<SuspenseComponent />);
  await fireEventAsync.press(screen.getByRole('button'));
  expect(screen.getByText('Result')).toBeOnTheScreen();
});

Use rerenderAsync/unmountAsync instead of rerender/unmount when using renderAsync.


Available Legacy APIs

These are available in v13 but deprecated. Prefer standard alternatives:

  • update(element) — Alias for rerender. Use rerender() instead.

  • getQueriesForElement(element) — Alias for within. Use within() instead.

  • UNSAFE_getByType, UNSAFE_getAllByType, UNSAFE_queryByType, UNSAFE_queryAllByType — Query by React component type. Use proper queries instead.

  • UNSAFE_getByProps, UNSAFE_getAllByProps, UNSAFE_queryByProps, UNSAFE_queryAllByProps — Query by props. Use proper queries instead.

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