All skills
callstackincubator avatar

/react-native-testing

@23643af official

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

Use this Skill: https://skilld.dev/gh/callstackincubator/agent-skills/react-native-testing

This session only. Nothing lands on disk.

referencesanti-patterns.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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

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

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

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

// 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()

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

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

// BAD: destructuring queries from render
const { getByText, getByRole } = render(<Component />);
getByText('Hello');

// GOOD: use screen object
render(<Component />);
screen.getByText('Hello');

Using UNSAFE_root

// BAD: traversing the tree manually
const { UNSAFE_root } = render(<Component />);
const el = UNSAFE_root.findAll((node) => node.props.testID === 'foo')[0];

// GOOD: use proper queries
render(<Component />);
screen.getByTestId('foo');

Manual cleanup

// BAD: calling cleanup manually (it's automatic)
afterEach(() => {
  cleanup();
});

// GOOD: just don't - RNTL auto-cleans after each test

Legacy accessibility props

// BAD: legacy accessibility props
<Pressable accessibilityRole="button" accessibilityLabel="Submit">
  <Text>Submit</Text>
</Pressable>

// GOOD: ARIA-compatible props
<Pressable role="button" aria-label="Submit">
  <Text>Submit</Text>
</Pressable>

// BAD: legacy state props
<Pressable accessibilityState={{ disabled: true, checked: true }}>

// GOOD: ARIA state props
<Pressable aria-disabled aria-checked>

Forgetting to await (v14)

In RNTL v14, render, fireEvent, rerender, unmount, renderHook, and act are async. Forgetting await causes subtle bugs where tests pass but assertions run before operations complete.

// BAD: missing await on render (v14)
render(<Component />);
expect(screen.getByText('Hello')).toBeOnTheScreen(); // may fail intermittently

// GOOD: await render (v14)
await render(<Component />);
expect(screen.getByText('Hello')).toBeOnTheScreen();

// BAD: missing await on fireEvent (v14)
fireEvent.press(screen.getByRole('button'));
// state updates may not have flushed yet

// GOOD: await fireEvent (v14)
await fireEvent.press(screen.getByRole('button'));

// BAD: missing await on act (v14)
act(() => {
  result.current.increment();
});

// GOOD: await act (v14)
await act(() => {
  result.current.increment();
});

Using removed APIs (v14)

These APIs exist in v13 but are removed in v14. Using them will cause import or runtime errors.

// BAD: using renderAsync in v14 (removed — render is already async)
import { renderAsync } from '@testing-library/react-native';
await renderAsync(<Component />);

// GOOD: use render in v14
import { render } from '@testing-library/react-native';
await render(<Component />);

// BAD: using fireEventAsync in v14 (removed — fireEvent is already async)
import { fireEventAsync } from '@testing-library/react-native';
await fireEventAsync.press(button);

// GOOD: use fireEvent in v14
import { fireEvent } from '@testing-library/react-native';
await fireEvent.press(button);

// BAD: using UNSAFE_root in v14 (removed)
screen.UNSAFE_root;

// GOOD: use container or root in v14
screen.container;
screen.root;

// BAD: using concurrentRoot option in v14 (removed — always on)
render(<Component />, { concurrentRoot: false });

// GOOD: just render without concurrentRoot
await render(<Component />);

// BAD: using update() in v14 (removed)
screen.update(<Component newProp />);

// GOOD: use rerender in v14
await screen.rerender(<Component newProp />);

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides comprehensive documentation and best practices for writing React Native component tests using React Native Testing Library (RNTL) versions 13 and 14. It defines clear rules and query priorities to ensure accessible and reliable tests. No security risks or malicious patterns were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at 23643af. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 6 months ago
  • Testing
  • react-native
  • testing-library
  • jest
  • component-testing
  • rntl
  • react-native-testing
  • userevent
  • accessibility

README badge

README badge for callstackincubator/agent-skills/react-native-testing

Write tests for React Native components using React Native Testing Library v13 or v14, with queries, userEvent interactions, Jest matchers, and async patterns. Covers both sync (v13, React 18) and async (v14, React 19+) render APIs, query priorities, fireEvent versus userEvent, and waitFor patterns.

Generated from the current SKILL.md.

Does this skill cover both RNTL v13 and v14?
Yes. The skill detects your project's RNTL version from package.json and loads the appropriate reference (v13 for React 18+, sync render; v14 for React 19+, async render).
What's the difference between getBy*, queryBy*, and findBy*?
getBy* returns an element or throws (sync, element must exist); queryBy* returns element or null (sync, for non-existence checks only); findBy* returns a promise and waits for the element (async, for dynamic content).
Should I use userEvent or fireEvent?
Prefer userEvent—it simulates full user interactions (press, type, longPress) and is always async. Use fireEvent only for events userEvent doesn't support.
Can I wrap my test components with providers (theme, auth, etc.)?
Yes. Use the `wrapper` option in render() to pass a component that wraps children with any context providers or wrappers your tests need.
Does this skill work with TypeScript?
Yes. The skill covers both v13 and v14, which have TypeScript support built in via @testing-library/react-native type definitions.

Generated from the current SKILL.md. These answers refresh after source changes.