references/anti-patterns.md
# 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 { getByText, getByRole } = render(<Component />);
getByText('Hello');
// GOOD: use screen object
render(<Component />);
screen.getByText('Hello');
```
## Using UNSAFE_root
```tsx
// 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
```tsx
// BAD: calling cleanup manually (it's automatic)
afterEach(() => {
cleanup();
});
// GOOD: just don't - RNTL auto-cleans after each test
```
## Legacy accessibility props
```tsx
// 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.
```tsx
// 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.
```tsx
// 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 />);
```
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
```tsx
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
```ts
function render(component: React.Element<any>, options?: RenderOptions): RenderResult;
```
`render` is **synchronous** in v13. Returns helpers and queries immediately.
### renderAsync (v13.3+)
```ts
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
| Option | Type | Description |
| -------------------------------------------- | ------------------------------------- | ------------------------------------------------------------- |
| `wrapper` | `React.ComponentType<any>` | Wraps tested component (useful for context providers) |
| `concurrentRoot` | `boolean` | Set `false` to disable concurrent rendering (default: `true`) |
| `createNodeMock` | `(element: React.Element) => unknown` | Custom mock refs for `ReactTestRenderer.create()` |
| `unstable_validateStringsRenderedWithinText` | `boolean` | Experimental: replicate RN `Text` string validation |
### Example
```tsx
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
```ts
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
| Variant | Assertion | Return Type | Async |
| ------------- | ------------------ | ------------------------------------------ | ----- |
| `getBy*` | Exactly one match | `ReactTestInstance` (throws if 0 or >1) | No |
| `getAllBy*` | At least one match | `ReactTestInstance[]` (throws if 0) | No |
| `queryBy*` | Zero or one match | `ReactTestInstance \| null` (throws if >1) | No |
| `queryAllBy*` | No assertion | `ReactTestInstance[]` (empty if 0) | No |
| `findBy*` | Exactly one match | `Promise<ReactTestInstance>` | Yes |
| `findAllBy*` | At least one match | `Promise<ReactTestInstance[]>` | Yes |
`findBy*` / `findAllBy*` accept optional `waitForOptions: { timeout?, interval?, onTimeout? }`.
### Query Predicates
#### `*ByRole` (preferred)
```ts
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`.
```tsx
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`
```ts
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`
```ts
getByPlaceholderText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): ReactTestInstance;
```
Matches `TextInput` by `placeholder` prop.
#### `*ByText`
```ts
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`
```ts
getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): ReactTestInstance;
```
Matches `TextInput` by current display value.
#### `*ByHintText`
```ts
getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): ReactTestInstance;
```
Matches by `accessibilityHint` prop. Also available as `getByA11yHint` / `getByAccessibilityHint`.
#### `*ByTestId` (last resort)
```ts
getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): ReactTestInstance;
```
Matches by `testID` prop. Use only when other queries don't work.
### TextMatch
```ts
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.
```tsx
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
```ts
const user = userEvent.setup(options?: { delay?: number; advanceTimers?: (delay: number) => Promise<void> | void });
```
### `press(element)`
```ts
await user.press(element): Promise<void>;
```
Full press lifecycle: `pressIn` → `press` → `pressOut`. Minimum 130ms. Use fake timers to speed up.
### `longPress(element, options?)`
```ts
await user.longPress(element, { duration?: number }): Promise<void>;
```
Long press (default 500ms). Emits `pressIn`, `longPress`, `pressOut`.
### `type(element, text, options?)`
```ts
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)`
```ts
await user.clear(textInput): Promise<void>;
```
Clears `TextInput` content. Events: `focus` → `selectionChange` → `keyPress` → `change` → `changeText` → `selectionChange` → `endEditing` → `blur`.
### `paste(element, text)`
```ts
await user.paste(textInput, text: string): Promise<void>;
```
Pastes text into `TextInput`. Events: `focus` → `selectionChange` → `change` → `changeText` → `selectionChange` → `endEditing` → `blur`.
### `scrollTo(element, options)`
```ts
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)
```ts
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
```ts
fireEvent.press(element, ...data): void;
fireEvent.changeText(element, ...data): void;
fireEvent.scroll(element, ...data): void;
```
### fireEventAsync (v13.3+, for React 19 / Suspense)
```ts
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
| Matcher | Description |
| ------------------- | --------------------------------------- |
| `toBeOnTheScreen()` | Element is attached to the element tree |
### Element Content
| Matcher | Signature | Description |
| --------------------- | ------------------------------------------------------------- | --------------------------- |
| `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
| Matcher | Description |
| ------------------------------------------------------- | ----------------------------------------------------- |
| `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
| Matcher | Description |
| -------------------------------------- | --------------------------------------------------------------- |
| `toBeVisible()` | Not hidden (`display: none`, `opacity: 0`, or hidden from a11y) |
| `toHaveStyle(style: StyleProp<Style>)` | Specific style match |
### Other
| Matcher | Signature | Description |
| ------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------- |
| `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`
```ts
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`
```ts
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`
```ts
function within(element: ReactTestInstance): Queries;
```
Scoped queries on a subtree. Useful for querying within a single `FlatList` item or a specific screen.
```tsx
const item = within(screen.getByTestId('list-item-1'));
expect(item.getByText('Title')).toBeOnTheScreen();
```
### `act`
```ts
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`
```ts
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)
```ts
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+)
```ts
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.
```tsx
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
```ts
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
| Variable | Description |
| ---------------------------------------- | ----------------------------------------- |
| `RNTL_SKIP_AUTO_CLEANUP=true` | Disable automatic cleanup after each test |
| `RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS=true` | Disable auto-detection of fake timers |
---
## Accessibility
### `isHiddenFromAccessibility`
```ts
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:
```tsx
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.
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:** `TestInstance` (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
```tsx
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
```ts
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
| Option | Type | Description |
| ---------------- | -------------------------- | ----------------------------------------------------- |
| `wrapper` | `React.ComponentType<any>` | Wraps tested component (useful for context providers) |
| `createNodeMock` | `(element) => unknown` | Custom mock refs |
Note: `concurrentRoot` and `unstable_validateStringsRenderedWithinText` are removed in v14 (concurrent rendering is always on, string validation is always on).
### Example
```tsx
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
```ts
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: TestInstance; // safe root host element
root: TestInstance; // 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
| Variant | Assertion | Return Type | Async |
| ------------- | ------------------ | ------------------------------------- | ----- |
| `getBy*` | Exactly one match | `TestInstance` (throws if 0 or >1) | No |
| `getAllBy*` | At least one match | `TestInstance[]` (throws if 0) | No |
| `queryBy*` | Zero or one match | `TestInstance \| null` (throws if >1) | No |
| `queryAllBy*` | No assertion | `TestInstance[]` (empty if 0) | No |
| `findBy*` | Exactly one match | `Promise<TestInstance>` | Yes |
| `findAllBy*` | At least one match | `Promise<TestInstance[]>` | Yes |
`findBy*` / `findAllBy*` accept optional `waitForOptions: { timeout?, interval?, onTimeout? }`.
### Query Predicates
#### `*ByRole` (preferred)
```ts
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;
}): TestInstance;
```
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`.
```tsx
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`
```ts
getByLabelText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches by `aria-label`/`accessibilityLabel` or text content of element referenced by `aria-labelledby`/`accessibilityLabelledBy`.
When multiple elements are referenced, their text content is joined with spaces in the referenced order and matched as a single label.
#### `*ByPlaceholderText`
```ts
getByPlaceholderText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches `TextInput` by `placeholder` prop.
#### `*ByText`
```ts
getByText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches by text content. Joins `<Text>` siblings to find matches (like RN runtime).
#### `*ByDisplayValue`
```ts
getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches `TextInput` by current display value.
#### `*ByHintText`
```ts
getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches by `accessibilityHint` prop. Also available as `getByA11yHint` / `getByAccessibilityHint`.
#### `*ByTestId` (last resort)
```ts
getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```
Matches by `testID` prop. Use only when other queries don't work.
### TextMatch
```ts
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.
```tsx
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
```ts
const user = userEvent.setup(options?: { delay?: number; advanceTimers?: (delay: number) => Promise<void> | void });
```
### `press(element)`
```ts
await user.press(element): Promise<void>;
```
Full press lifecycle: `pressIn` → `press` → `pressOut`. Minimum 130ms. Use fake timers to speed up.
### `longPress(element, options?)`
```ts
await user.longPress(element, { duration?: number }): Promise<void>;
```
Long press (default 500ms). Emits `pressIn`, `longPress`, `pressOut`.
### `type(element, text, options?)`
```ts
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)`
```ts
await user.clear(textInput): Promise<void>;
```
Clears `TextInput` content. Events: `focus` → `selectionChange` → `keyPress` → `change` → `changeText` → `selectionChange` → `endEditing` → `blur`.
### `paste(element, text)`
```ts
await user.paste(textInput, text: string): Promise<void>;
```
Pastes text into `TextInput`. Events: `focus` → `selectionChange` → `change` → `changeText` → `selectionChange` → `endEditing` → `blur`.
### `scrollTo(element, options)`
```ts
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`:
```ts
async function fireEvent(
instance: TestInstance,
eventName: string,
...data: unknown[]
): Promise<void>;
```
Traverses tree bottom-up looking for `onXxx` handler. Does **not** auto-pass event data.
### Convenience Methods
```ts
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
| Matcher | Description |
| ------------------- | --------------------------------------- |
| `toBeOnTheScreen()` | Element is attached to the element tree |
### Element Content
| Matcher | Signature | Description |
| --------------------- | ------------------------------------------------------------- | --------------------------- |
| `toHaveTextContent()` | `(text: string \| RegExp, options?: { exact?, normalizer? })` | Text content match |
| `toContainElement()` | `(instance: TestInstance \| null)` | Contains child element |
| `toBeEmptyElement()` | — | No children or text content |
### Element State
| Matcher | Description |
| ------------------------------------------------------- | ----------------------------------------------------- |
| `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
| Matcher | Description |
| -------------------------------------- | --------------------------------------------------------------- |
| `toBeVisible()` | Not hidden (`display: none`, `opacity: 0`, or hidden from a11y) |
| `toHaveStyle(style: StyleProp<Style>)` | Specific style match |
### Other
| Matcher | Signature | Description |
| ------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------- |
| `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`
```ts
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`
```ts
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`
```ts
function within(instance: TestInstance): Queries;
```
Scoped queries on a subtree. Useful for querying within a single `FlatList` item or a specific screen.
```tsx
const item = within(screen.getByTestId('list-item-1'));
expect(item.getByText('Title')).toBeOnTheScreen();
```
### `cleanup`
```ts
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:
```ts
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.
```tsx
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:
```ts
async function act<T>(callback: () => T): Promise<T>;
```
```tsx
await act(() => {
result.current.increment();
});
```
Usually not needed — `render`, `fireEvent`, `userEvent`, and `waitFor` handle it internally.
---
## Configuration
```ts
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
| Variable | Description |
| ---------------------------------------- | ----------------------------------------- |
| `RNTL_SKIP_AUTO_CLEANUP=true` | Disable automatic cleanup after each test |
| `RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS=true` | Disable auto-detection of fake timers |
---
## Accessibility
### `isHiddenFromAccessibility`
```ts
function isHiddenFromAccessibility(instance: TestInstance | 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
SKILL.md
---
name: react-native-testing
description: >
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".
---
# 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`:
- **v14.x** → load [references/api-reference-v14.md](references/api-reference-v14.md) (React 19+, async APIs, `test-renderer`)
- **v13.x** → load [references/api-reference-v13.md](references/api-reference-v13.md) (React 18+, sync APIs, `react-test-renderer`)
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
| 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 |
## Interactions
Prefer `userEvent` over `fireEvent`. userEvent is always async.
```tsx
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:
```tsx
fireEvent.press(element);
fireEvent.changeText(textInput, 'new text');
fireEvent(element, 'blur');
```
## Assertions (Jest Matchers)
Available automatically with any `@testing-library/react-native` import.
| Matcher | Use 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
```tsx
// 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):
```tsx
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:
```tsx
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
),
});
}
```
## References
- [v13 API Reference](references/api-reference-v13.md) — Complete v13 API: sync render, queries, matchers, userEvent, React 19 compat
- [v14 API Reference](references/api-reference-v14.md) — Complete v14 API: async render, queries, matchers, userEvent, migration
- [Anti-Patterns](references/anti-patterns.md) — Common mistakes to avoid