From 3644c9c3afe4d3a010c5cccb250b1ec3da986741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 10 Feb 2026 13:08:41 +0000 Subject: [PATCH 1/2] chore: skill support for both v13 and v14 --- skills/react-native-testing/SKILL.md | 79 +-- .../references/anti-patterns.md | 96 ++- ...{api-reference.md => api-reference-v13.md} | 125 +++- .../references/api-reference-v14.md | 574 ++++++++++++++++++ 4 files changed, 785 insertions(+), 89 deletions(-) rename skills/react-native-testing/references/{api-reference.md => api-reference-v13.md} (84%) create mode 100644 skills/react-native-testing/references/api-reference-v14.md diff --git a/skills/react-native-testing/SKILL.md b/skills/react-native-testing/SKILL.md index 496a22c0..87e1fba7 100644 --- a/skills/react-native-testing/SKILL.md +++ b/skills/react-native-testing/SKILL.md @@ -1,34 +1,25 @@ --- name: react-native-testing description: > - Write tests using React Native Testing Library (RNTL) v13 (`@testing-library/react-native`). + 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 both React 18 (sync render) and React 19 compat (renderAsync/fireEventAsync). + 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 v13 Test Writing Guide +# RNTL Test Writing Guide -## Core Pattern +## Version Detection -```tsx -import { render, screen, userEvent } from '@testing-library/react-native'; - -jest.useFakeTimers(); // recommended when using userEvent - -test('description', async () => { - const user = userEvent.setup(); - render(); // sync in v13 (React 18) +Check `@testing-library/react-native` version in the user's `package.json`: - const button = screen.getByRole('button', { name: 'Submit' }); - await user.press(button); +- **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`) - expect(screen.getByText('Done')).toBeOnTheScreen(); -}); -``` +Use the version-specific reference for render patterns, fireEvent sync/async behavior, screen API, configuration, and dependencies. ## Query Priority @@ -36,14 +27,14 @@ Use in this order: `getByRole` > `getByLabelText` > `getByPlaceholderText` > `ge ## Query Variants -| Variant | Use case | Returns | Async | -| ------------- | ------------------------ | ------------------------------ | ----- | -| `getBy*` | Element must exist | `ReactTestInstance` (throws) | No | -| `getAllBy*` | Multiple must exist | `ReactTestInstance[]` (throws) | No | -| `queryBy*` | Check non-existence ONLY | `ReactTestInstance \| null` | No | -| `queryAllBy*` | Count elements | `ReactTestInstance[]` | No | -| `findBy*` | Wait for element | `Promise` | Yes | -| `findAllBy*` | Wait for multiple | `Promise` | Yes | +| 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` | Yes | +| `findAllBy*` | Wait for multiple | `Promise` | Yes | ## Interactions @@ -59,12 +50,12 @@ await user.paste(textInput, 'pasted text'); // paste into TextInput await user.scrollTo(scrollView, { y: 100 }); // scroll ``` -Use `fireEvent` only when `userEvent` doesn't support the event: +`fireEvent` — use only when `userEvent` doesn't support the event. See version-specific reference for sync/async behavior: ```tsx -fireEvent.press(element); // sync, onPress only -fireEvent.changeText(textInput, 'new text'); // sync, onChangeText only -fireEvent(element, 'blur'); // any event by name +fireEvent.press(element); +fireEvent.changeText(textInput, 'new text'); +fireEvent(element, 'blur'); ``` ## Assertions (Jest Matchers) @@ -103,22 +94,6 @@ Available automatically with any `@testing-library/react-native` import. 10. **Prefer ARIA props** (`role`, `aria-label`, `aria-disabled`) over legacy `accessibility*` props 11. **Use RNTL matchers** over raw prop assertions -## 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(); - await fireEventAsync.press(screen.getByRole('button')); - expect(screen.getByText('Result')).toBeOnTheScreen(); -}); -``` - -Use `rerenderAsync`/`unmountAsync` instead of `rerender`/`unmount` when using `renderAsync`. - ## `*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`. @@ -130,14 +105,6 @@ 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`) -## API Reference - -See [references/api-reference.md](references/api-reference.md) for complete API signatures and options for render, screen, queries, userEvent, fireEvent, Jest matchers, waitFor, renderHook, configuration, and accessibility helpers. - -## Anti-Patterns Reference - -See [references/anti-patterns.md](references/anti-patterns.md) for detailed examples of what NOT to do. - ## waitFor ```tsx @@ -184,3 +151,9 @@ function renderWithProviders(ui: React.ReactElement) { }); } ``` + +## 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 diff --git a/skills/react-native-testing/references/anti-patterns.md b/skills/react-native-testing/references/anti-patterns.md index 53939b26..016bffa5 100644 --- a/skills/react-native-testing/references/anti-patterns.md +++ b/skills/react-native-testing/references/anti-patterns.md @@ -1,17 +1,19 @@ -# RNTL v13 Anti-Patterns +# RNTL Anti-Patterns ## Table of Contents -- [Wrong query variant](#wrong-query-variant) -- [Not using \*ByRole](#not-using-byrole) -- [Wrong assertions](#wrong-assertions) -- [waitFor misuse](#waitfor-misuse) -- [Unnecessary act()](#unnecessary-act) -- [fireEvent instead of userEvent](#fireevent-instead-of-userevent) -- [Destructuring render](#destructuring-render) -- [Using UNSAFE_root](#using-unsafe_root) -- [Manual cleanup](#manual-cleanup) -- [Legacy accessibility props](#legacy-accessibility-props) +- 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 @@ -211,3 +213,75 @@ afterEach(() => { // GOOD: ARIA state props ``` + +## 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(); +expect(screen.getByText('Hello')).toBeOnTheScreen(); // may fail intermittently + +// GOOD: await render (v14) +await render(); +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(); + +// GOOD: use render in v14 +import { render } from '@testing-library/react-native'; +await render(); + +// 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(, { concurrentRoot: false }); + +// GOOD: just render without concurrentRoot +await render(); + +// BAD: using update() in v14 (removed) +screen.update(); + +// GOOD: use rerender in v14 +await screen.rerender(); +``` diff --git a/skills/react-native-testing/references/api-reference.md b/skills/react-native-testing/references/api-reference-v13.md similarity index 84% rename from skills/react-native-testing/references/api-reference.md rename to skills/react-native-testing/references/api-reference-v13.md index ca7a0bd6..bcde5125 100644 --- a/skills/react-native-testing/references/api-reference.md +++ b/skills/react-native-testing/references/api-reference-v13.md @@ -1,18 +1,46 @@ # 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 -- [render / renderAsync](#render--renderasync) -- [screen](#screen) -- [Queries](#queries) -- [User Event](#user-event) -- [Fire Event](#fire-event) -- [Jest Matchers](#jest-matchers) -- [Async Utilities](#async-utilities) -- [Other Helpers](#other-helpers) -- [renderHook / renderHookAsync](#renderhook--renderhookasync) -- [Configuration](#configuration) -- [Accessibility](#accessibility) +- 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(); // sync in v13 + + const button = screen.getByRole('button', { name: 'Submit' }); + await user.press(button); + + expect(screen.getByText('Done')).toBeOnTheScreen(); +}); +``` --- @@ -20,15 +48,20 @@ ```ts function render(component: React.Element, options?: RenderOptions): RenderResult; +``` + +`render` is **synchronous** in v13. Returns helpers and queries immediately. + +### renderAsync (v13.3+) + +```ts async function renderAsync( component: React.Element, options?: RenderAsyncOptions, ): Promise; ``` -`render` deeply renders the given React element and returns helpers to query the output. Use `screen` for queries instead of destructuring the result. - -`renderAsync` (v13.3+) is the async version for React 19 / Suspense. When using `renderAsync`, use `rerenderAsync` and `unmountAsync` instead of their sync versions. +Use `renderAsync` for React 19 or Suspense components. When using `renderAsync`, use `rerenderAsync` and `unmountAsync` instead of their sync counterparts. ### Options @@ -63,10 +96,10 @@ await renderAsync(); ```ts let screen: { ...queries; - rerender(element: React.Element): void; + rerender(element: React.Element): void; // sync rerenderAsync(element: React.Element): Promise; // v13.3+ - unmount(): void; - unmountAsync(): Promise; // v13.3+ + unmount(): void; // sync + unmountAsync(): Promise; // v13.3+ debug(options?: { message?: string; mapProps?: MapPropsFunction }): void; toJSON(): ReactTestRendererJSON | null; root: ReactTestInstance; // root host element @@ -78,13 +111,14 @@ The `screen` object provides queries and utilities for the currently rendered UI ### Methods -- **`rerender(element)`** — Re-render with new root element. Triggers lifecycle events. +- **`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. Usually not needed (auto-cleanup). +- **`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. +- **`root`** — The rendered root host element (`ReactTestInstance`). +- **`UNSAFE_root`** — Root composite element. Avoid; use proper queries instead. --- @@ -280,17 +314,19 @@ Scrolls a host `ScrollView` (including `FlatList`). Match scroll direction to `h --- -## Fire Event +## 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 +#### Convenience Methods ```ts fireEvent.press(element, ...data): void; @@ -298,10 +334,10 @@ fireEvent.changeText(element, ...data): void; fireEvent.scroll(element, ...data): void; ``` -### Async Variants (v13.3+, for React 19 / Suspense) +### fireEventAsync (v13.3+, for React 19 / Suspense) ```ts -async function fireEventAsync(element, eventName, ...data): Promise; +async function fireEventAsync(element: ReactTestInstance, eventName: string, ...data: unknown[]): Promise; fireEventAsync.press(element, ...data): Promise; fireEventAsync.changeText(element, ...data): Promise; fireEventAsync.scroll(element, ...data): Promise; @@ -405,6 +441,11 @@ expect(item.getByText('Title')).toBeOnTheScreen(); ### `act` +```ts +function act(callback: () => void): void; +function act(callback: () => Promise): Promise; +``` + Re-exported from `react-test-renderer`. Usually not needed — `render`, `fireEvent`, `userEvent`, and `waitFor` handle it internally. ### `cleanup` @@ -419,13 +460,18 @@ Unmounts rendered trees and clears `screen`. Automatic after each test (if test ## renderHook / renderHookAsync +### renderHook (sync) + ```ts function renderHook( 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 version (v13.3+) +```ts async function renderHookAsync( hookFn: (props?: Props) => Result, options?: { initialProps?: Props; wrapper?: React.ComponentType; concurrentRoot?: boolean }, @@ -495,3 +541,32 @@ Element is hidden when it or any ancestor has: - `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(); + 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. diff --git a/skills/react-native-testing/references/api-reference-v14.md b/skills/react-native-testing/references/api-reference-v14.md new file mode 100644 index 00000000..55e3351c --- /dev/null +++ b/skills/react-native-testing/references/api-reference-v14.md @@ -0,0 +1,574 @@ +# 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 + +```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(); // 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()` +- `await fireEvent.press(element)` (and all fireEvent variants) +- `await screen.rerender()` +- `await screen.unmount()` +- `await renderHook(() => useHook())` +- `await act(() => { ... })` (even with sync callbacks) + +--- + +## render + +```ts +async function render( + component: React.Element, + options?: RenderOptions, +): Promise; +``` + +`render` returns a `Promise` — always `await` it. + +There is no `renderAsync` in v14. The standard `render` is already async. + +### Options + +| Option | Type | Description | +| ---------------- | -------------------------- | ----------------------------------------------------- | +| `wrapper` | `React.ComponentType` | 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(); +expect(screen.getByRole('button', { name: 'start' })).toBeOnTheScreen(); + +// With wrapper +await render(, { + wrapper: ({ children }) => {children}, +}); +``` + +--- + +## screen + +```ts +let screen: { + ...queries; + rerender(element: React.Element): Promise; // async + unmount(): Promise; // 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 + +| Variant | Assertion | Return Type | Async | +| ------------- | ------------------ | ------------------------------------ | ----- | +| `getBy*` | Exactly one match | `HostElement` (throws if 0 or >1) | No | +| `getAllBy*` | At least one match | `HostElement[]` (throws if 0) | No | +| `queryBy*` | Zero or one match | `HostElement \| null` (throws if >1) | No | +| `queryAllBy*` | No assertion | `HostElement[]` (empty if 0) | No | +| `findBy*` | Exactly one match | `Promise` | Yes | +| `findAllBy*` | At least one match | `Promise` | 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; +}): 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`. + +```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 }): HostElement; +``` + +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 }): HostElement; +``` + +Matches `TextInput` by `placeholder` prop. + +#### `*ByText` + +```ts +getByText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement; +``` + +Matches by text content. Joins `` siblings to find matches (like RN runtime). + +#### `*ByDisplayValue` + +```ts +getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement; +``` + +Matches `TextInput` by current display value. + +#### `*ByHintText` + +```ts +getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement; +``` + +Matches by `accessibilityHint` prop. Also available as `getByA11yHint` / `getByAccessibilityHint`. + +#### `*ByTestId` (last resort) + +```ts +getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement; +``` + +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 }); +``` + +### `press(element)` + +```ts +await user.press(element): Promise; +``` + +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; +``` + +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; +``` + +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; +``` + +Clears `TextInput` content. Events: `focus` → `selectionChange` → `keyPress` → `change` → `changeText` → `selectionChange` → `endEditing` → `blur`. + +### `paste(element, text)` + +```ts +await user.paste(textInput, text: string): Promise; +``` + +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; +``` + +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( + element: HostElement, + eventName: string, + ...data: unknown[] +): Promise; +``` + +Traverses tree bottom-up looking for `onXxx` handler. Does **not** auto-pass event data. + +### Convenience Methods + +```ts +await fireEvent.press(element, ...data): Promise; +await fireEvent.changeText(element, ...data): Promise; +await fireEvent.scroll(element, ...data): Promise; +``` + +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()` | `(element: HostElement \| 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