mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
chore: bundle agent docs (#1908)
This commit is contained in:
committed by
GitHub
parent
1e0a97beca
commit
45cc32de75
@@ -46,6 +46,25 @@ jobs:
|
||||
- name: Typecheck React 19.2-only tests
|
||||
run: yarn typecheck:react-19_2
|
||||
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Docs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Node.js and deps
|
||||
uses: ./.github/actions/setup-deps
|
||||
|
||||
- name: Check generated package docs
|
||||
run: yarn docs:check
|
||||
|
||||
- name: Setup Node.js and website deps
|
||||
uses: ./.github/actions/setup-website-deps
|
||||
|
||||
- name: Validate website docs
|
||||
run: yarn --cwd website validate
|
||||
|
||||
typecheck-rn-0-85:
|
||||
runs-on: ubuntu-latest
|
||||
name: Typecheck → RN 0.85
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"hooks": {
|
||||
"before:init": ["yarn typecheck", "yarn test", "yarn lint"],
|
||||
"before:init": ["yarn docs:check", "yarn typecheck", "yarn test", "yarn lint"],
|
||||
"after:bump": "yarn build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
},
|
||||
|
||||
@@ -12,11 +12,6 @@
|
||||
[![MIT License][license-badge]][license]
|
||||
[![Sponsored by Callstack][callstack-badge]][callstack]
|
||||
|
||||
> [!WARNING]
|
||||
> **Beta Version:** This version (v14) is currently in beta. APIs and behavior may change before the stable release. Please report any issues you encounter.
|
||||
>
|
||||
> For stable version (v13) see [README-v13.md](./README-v13.md).
|
||||
|
||||
## The problem
|
||||
|
||||
You want to write maintainable tests for your React Native components. Your tests should avoid implementation details and focus on giving you confidence. They should remain maintainable so refactors (changes to implementation but not functionality) don't break your tests and slow you and your team down.
|
||||
@@ -34,7 +29,7 @@ This project is inspired by [React Testing Library](https://github.com/testing-l
|
||||
Open a Terminal in your project's folder and run:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @testing-library/react-native@alpha
|
||||
npm install --save-dev @testing-library/react-native
|
||||
```
|
||||
|
||||
This library has a `peerDependencies` listing for [Test Renderer](https://github.com/mdjastrzebski/test-renderer). Make sure to install it as a dev dependency:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# React Native Testing Library package docs
|
||||
|
||||
These markdown files are bundled with the npm package for coding agents. They describe the installed package version.
|
||||
|
||||
Start with [LLM Guidelines](./guides/llm-guidelines.md) for the agent setup snippet and quick rules, or use the included page list below to load specific references.
|
||||
|
||||
## Included pages
|
||||
|
||||
- [LLM Guidelines](./guides/llm-guidelines.md) - Quick rules for agents writing RNTL tests.
|
||||
- [Quick Start](./guides/quick-start.md) - Installation and setup basics.
|
||||
- [How to Query](./guides/how-to-query.md) - Query priority and selection guidance.
|
||||
- [Common Mistakes](./guides/common-mistakes.md) - Common anti-patterns and preferred alternatives.
|
||||
- [Troubleshooting](./guides/troubleshooting.md) - Common integration and runtime issues.
|
||||
- [Testing Environment](./guides/testing-environment.md) - How RNTL simulates React Native under tests.
|
||||
- [Understanding act](./guides/understanding-act.md) - How act warnings happen and how to resolve them.
|
||||
- [Migration to 14.x](./guides/migration-v14.md) - Breaking changes and upgrade steps for RNTL v14.
|
||||
- [API Overview](./api/overview.md) - Top-level API map.
|
||||
- [render](./api/render.md) - Rendering components in tests.
|
||||
- [screen](./api/screen.md) - Recommended global query surface.
|
||||
- [Queries](./api/queries.md) - Query variants and predicates.
|
||||
- [Jest Matchers](./api/jest-matchers.md) - Built-in RNTL assertions.
|
||||
- [User Event](./api/user-event.md) - Realistic user interactions.
|
||||
- [Fire Event](./api/fire-event.md) - Low-level event triggering.
|
||||
- [Async Utilities](./api/async-utilities.md) - Async queries and wait helpers.
|
||||
- [renderHook](./api/render-hook.md) - Testing custom hooks.
|
||||
- [Configuration](./api/configuration.md) - Runtime configuration options.
|
||||
- [Accessibility](./api/accessibility.md) - Accessibility helpers and hidden elements.
|
||||
- [Other Helpers](./api/other-helpers.md) - within, act, cleanup, and related helpers.
|
||||
- [Custom Render](./cookbook/custom-render.md) - Reusable render wrappers.
|
||||
- [Async Events](./cookbook/async-events.md) - Testing async interactions.
|
||||
- [Network Requests](./cookbook/network-requests.md) - Testing components that make network requests.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Accessibility
|
||||
|
||||
## `isHiddenFromAccessibility`
|
||||
|
||||
```ts
|
||||
function isHiddenFromAccessibility(instance: TestInstance | null): boolean {}
|
||||
```
|
||||
|
||||
Also available as `isInaccessible()` alias for React Testing Library compatibility.
|
||||
|
||||
Checks if given element is hidden from assistive technology, e.g. screen readers.
|
||||
|
||||
> [!NOTE]
|
||||
> Like [`isInaccessible`](https://testing-library.com/docs/dom-testing-library/api-accessibility/#isinaccessible) function from DOM Testing Library this function considers both accessibility elements and presentational elements (regular `View`s) to be accessible, unless they are hidden in terms of host platform.
|
||||
>
|
||||
> This covers only part of [ARIA notion of Accessibility Tree](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), as ARIA excludes both hidden and presentational elements from the Accessibility Tree.
|
||||
|
||||
For the scope of this function, element is inaccessible when it, or any of its ancestors, meets any of the following conditions:
|
||||
|
||||
- it has `display: none` style
|
||||
- it has [`aria-hidden`](https://reactnative.dev/docs/accessibility#aria-hidden) prop set to `true`
|
||||
- it has [`accessibilityElementsHidden`](https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios) prop set to `true`
|
||||
- it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants`
|
||||
- it has sibling host element with either [`aria-modal`](https://reactnative.dev/docs/accessibility#aria-modal-ios) or [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true`
|
||||
|
||||
Specifying `accessible={false}`, `role="none"`, `accessibilityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Async utilities
|
||||
|
||||
## `findBy*` queries
|
||||
|
||||
The `findBy*` queries are used to find elements that are not instantly available but will be added as a result of some asynchronous action. Learn more details [here](./queries.md#find-by).
|
||||
|
||||
## `waitFor`
|
||||
|
||||
```tsx
|
||||
function waitFor<T>(
|
||||
expectation: () => T,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
interval?: number;
|
||||
onTimeout?: (error: Error) => Error;
|
||||
},
|
||||
): Promise<T>;
|
||||
```
|
||||
|
||||
Waits for the `expectation` callback to pass. `waitFor` runs the callback multiple times until timeout is reached, as specified by the `timeout` and `interval` options. The callback must throw an error when the expectation is not met. Returning any value, including a falsy one, is treated as meeting the expectation, and the callback result is returned to the caller.
|
||||
|
||||
```tsx
|
||||
await waitFor(() => expect(mockFunction).toHaveBeenCalledWith());
|
||||
```
|
||||
|
||||
`waitFor` executes the `expectation` callback every `interval` (default: 50 ms) until `timeout` (default: 1000 ms) is reached. Execution stops as soon as the callback doesn't throw an error, and the callback's return value is returned to the caller. If timeout is reached, `waitFor` re-throws the final error thrown by `expectation`.
|
||||
|
||||
```tsx
|
||||
// ❌ `waitFor` will return immediately because callback does not throw
|
||||
await waitFor(() => false);
|
||||
```
|
||||
|
||||
`waitFor` is an async function so you need to `await` the result to pause test execution.
|
||||
|
||||
```jsx
|
||||
// ❌ missing `await`: `waitFor` will just return Promise that will be rejected when the timeout is reached
|
||||
waitFor(() => expect(1).toBe(2));
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You can enforce awaiting `waitFor` by using the [await-async-utils](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md) rule from [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library).
|
||||
|
||||
Since `waitFor` runs the `expectation` callback multiple times, [avoid performing side effects](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#performing-side-effects-in-waitfor) in `waitFor`.
|
||||
|
||||
```jsx
|
||||
await waitFor(async () => {
|
||||
// ❌ button will be pressed on each waitFor iteration
|
||||
await fireEvent.press(screen.getByText('press me'));
|
||||
expect(mockOnPress).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Avoiding side effects in `expectation` callback can be partially enforced with the [`no-wait-for-side-effects` rule](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md).
|
||||
|
||||
Use a [single assertion per `waitFor`](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#having-multiple-assertions-in-a-single-waitfor-callback) for consistency and faster failing tests. For multiple assertions, use separate `waitFor` calls. Often you won't need to wrap the second assertion in `waitFor` since the first one waits for the asynchronous change.
|
||||
|
||||
`waitFor` checks whether Jest fake timers are enabled and adapts its behavior in such case. The following snippet is a simplified version of how it behaves when fake timers are enabled:
|
||||
|
||||
```tsx
|
||||
let fakeTimeRemaining = timeout;
|
||||
let lastError;
|
||||
|
||||
while (fakeTimeRemaining > 0) {
|
||||
fakeTimeRemaining = fakeTimeRemaining - interval;
|
||||
jest.advanceTimersByTime(interval);
|
||||
try {
|
||||
// resolve
|
||||
return expectation();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
// reject
|
||||
throw lastError;
|
||||
```
|
||||
|
||||
In the following example we test that a function is called after 10 seconds using fake timers. With fake timers, the test doesn't depend on real time passing, making it faster and more reliable. We don't need to advance fake timers through Jest's API because `waitFor` handles this.
|
||||
|
||||
```tsx
|
||||
// in component
|
||||
setTimeout(() => {
|
||||
someFunction();
|
||||
}, 10000);
|
||||
|
||||
// in test
|
||||
jest.useFakeTimers();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(someFunction).toHaveBeenCalledWith();
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you receive warnings related to `act()` function consult our [Understanding Act](../guides/understanding-act.md) function document.
|
||||
|
||||
### Options
|
||||
|
||||
- `timeout`: How long to wait for, in ms. Defaults to 1000 ms (configured by `asyncUtilTimeout` option).
|
||||
- `interval`: How often to check, in ms. Defaults to 50 ms.
|
||||
- `onTimeout`: Callback to transform the error before it's thrown. Useful for debugging, e.g., `onTimeout: () => { screen.debug(); }`.
|
||||
|
||||
## `waitForElementToBeRemoved`
|
||||
|
||||
```ts
|
||||
function waitForElementToBeRemoved<T>(
|
||||
expectation: () => T,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
interval?: number;
|
||||
onTimeout?: (error: Error) => Error;
|
||||
},
|
||||
): Promise<T>;
|
||||
```
|
||||
|
||||
Waits for non-deterministic periods of time until queried element is removed or times out. `waitForElementToBeRemoved` periodically calls `expectation` every `interval` milliseconds to determine whether the element has been removed or not.
|
||||
|
||||
```jsx
|
||||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
|
||||
|
||||
test('waiting for an Banana to be removed', async () => {
|
||||
await render(<Banana />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText('Banana ready'));
|
||||
});
|
||||
```
|
||||
|
||||
This method expects that the element is initially present in the render tree and then is removed from it. If the element is not present when you call this method it throws an error.
|
||||
|
||||
You can use any of `getBy`, `getAllBy`, `queryBy` and `queryAllBy` queries for `expectation` parameter.
|
||||
|
||||
> [!NOTE]
|
||||
> If you receive warnings related to `act()` function consult our [Understanding Act](../guides/understanding-act.md) function document.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Configuration
|
||||
|
||||
## `configure`
|
||||
|
||||
```ts
|
||||
type Config = {
|
||||
/** Default timeout, in ms, for `waitFor` and `findBy*` queries. */
|
||||
asyncUtilTimeout: number;
|
||||
|
||||
/** Default value for `includeHiddenElements` query option. */
|
||||
defaultIncludeHiddenElements: boolean;
|
||||
|
||||
/** Default options for `debug` helper. */
|
||||
defaultDebugOptions?: Partial<DebugOptions>;
|
||||
};
|
||||
|
||||
type ConfigAliasOptions = {
|
||||
/** RTL-compatibility alias for `defaultIncludeHiddenElements`. */
|
||||
defaultHidden: boolean;
|
||||
};
|
||||
|
||||
function configure(options: Partial<Config & ConfigAliasOptions>) {}
|
||||
```
|
||||
|
||||
### `asyncUtilTimeout` option
|
||||
|
||||
Default timeout, in ms, for async helper functions (`waitFor`, `waitForElementToBeRemoved`) and `findBy*` queries. Defaults to 1000 ms.
|
||||
|
||||
### `defaultIncludeHiddenElements` option
|
||||
|
||||
Default value for [includeHiddenElements](./queries.md#includehiddenelements-option) query option for all queries. The default value is set to `false`, so all queries will not match [elements hidden from accessibility](#ishiddenfromaccessibility). This is because the users of the app would not be able to see such elements.
|
||||
|
||||
This option is also available as `defaultHidden` alias for compatibility with [React Testing Library](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaulthidden).
|
||||
|
||||
### `defaultDebugOptions` option
|
||||
|
||||
Default [debug options](#debug) to be used when calling `debug()`. These default options will be overridden by the ones you specify directly when calling `debug()`.
|
||||
|
||||
## `resetToDefaults()`
|
||||
|
||||
```ts
|
||||
function resetToDefaults() {}
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
### `RNTL_SKIP_AUTO_CLEANUP`
|
||||
|
||||
Set to `true` to disable automatic `cleanup()` after each test. It works the same as importing `react-native-testing-library/dont-cleanup-after-each` or using `react-native-testing-library/pure`.
|
||||
|
||||
```shell
|
||||
$ RNTL_SKIP_AUTO_CLEANUP=true jest
|
||||
```
|
||||
|
||||
### `RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS`
|
||||
|
||||
Set to `true` to disable auto-detection of fake timers. This might be useful in rare cases when you want to use non-Jest fake timers. See [issue #886](https://github.com/callstack/react-native-testing-library/issues/886) for more details.
|
||||
|
||||
```shell
|
||||
$ RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS=true jest
|
||||
```
|
||||
@@ -0,0 +1,165 @@
|
||||
# Fire Event API
|
||||
|
||||
## `fireEvent`
|
||||
|
||||
> [!NOTE]
|
||||
> For common events like `press` or `type`, use the [User Event API](./user-event.md). It simulates events more realistically by emitting a sequence of events with proper event objects that mimic React Native runtime behavior.
|
||||
>
|
||||
> Use Fire Event for cases not supported by User Event and for triggering event handlers on composite components.
|
||||
|
||||
```ts
|
||||
function fireEvent(instance: TestInstance, eventName: string, ...data: unknown[]): Promise<unknown>;
|
||||
```
|
||||
|
||||
The `fireEvent` API triggers event handlers on both host and composite components. It traverses the component tree bottom-up from the passed element to find an enabled event handler named `onXxx` where `xxx` is the event name.
|
||||
|
||||
Unlike User Event, this API does not automatically pass event object to event handler, this is responsibility of the user to construct such object.
|
||||
|
||||
The base `fireEvent(instance, eventName, ...data)` API can pass multiple custom arguments to the handler. Convenience helpers such as `fireEvent.press` and `fireEvent.scroll` are different: they create a default event object and accept one optional object to merge into it.
|
||||
|
||||
This function uses async `act` internally to execute all pending React updates during event handling.
|
||||
|
||||
```jsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react-native';
|
||||
|
||||
test('fire changeText event', async () => {
|
||||
const onEventMock = jest.fn();
|
||||
await render(
|
||||
// MyComponent renders TextInput which has a placeholder 'Enter details'
|
||||
// and with `onChangeText` bound to handleChangeText
|
||||
<MyComponent handleChangeText={onEventMock} />,
|
||||
);
|
||||
|
||||
await fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab');
|
||||
expect(onEventMock).toHaveBeenCalledWith('ab');
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> `fireEvent` performs checks that should prevent events firing on disabled elements.
|
||||
|
||||
An example using `fireEvent` with native events that aren't already aliased by the `fireEvent` api.
|
||||
|
||||
```jsx
|
||||
import { TextInput, View } from 'react-native';
|
||||
import { fireEvent, render, screen } from '@testing-library/react-native';
|
||||
|
||||
const onBlurMock = jest.fn();
|
||||
|
||||
await render(
|
||||
<View>
|
||||
<TextInput placeholder="my placeholder" onBlur={onBlurMock} />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// you can omit the `on` prefix
|
||||
await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
|
||||
```
|
||||
|
||||
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`.
|
||||
|
||||
### `fireEvent.press`
|
||||
|
||||
> [!NOTE]
|
||||
> Use the User Event [`press()`](./user-event.md#press) helper instead. It simulates press interactions more realistically, including pressable support.
|
||||
|
||||
```tsx
|
||||
fireEvent.press: (
|
||||
instance: TestInstance,
|
||||
eventProps?: Record<string, unknown>,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Builds a press event object, merges `eventProps` into it, and invokes the `press` handler on the element or nearest eligible parent.
|
||||
|
||||
```jsx
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { render, screen, fireEvent } from '@testing-library/react-native';
|
||||
|
||||
const onPressMock = jest.fn();
|
||||
const eventData = {
|
||||
nativeEvent: {
|
||||
pageX: 20,
|
||||
pageY: 30,
|
||||
},
|
||||
};
|
||||
|
||||
await render(
|
||||
<View>
|
||||
<TouchableOpacity onPress={onPressMock}>
|
||||
<Text>Press me</Text>
|
||||
</TouchableOpacity>
|
||||
</View>,
|
||||
);
|
||||
|
||||
await fireEvent.press(screen.getByText('Press me'), eventData);
|
||||
expect(onPressMock).toHaveBeenCalledWith(expect.objectContaining(eventData));
|
||||
```
|
||||
|
||||
### `fireEvent.changeText`
|
||||
|
||||
> [!NOTE]
|
||||
> Use the User Event [`type()`](./user-event.md#type) helper instead. It simulates text change interactions more realistically, including key-by-key typing, element focus, and other editing events.
|
||||
|
||||
```tsx
|
||||
fireEvent.changeText: (
|
||||
instance: TestInstance,
|
||||
text: string,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Invokes `changeText` event handler on the element or parent element in the tree.
|
||||
|
||||
```jsx
|
||||
import { View, TextInput } from 'react-native';
|
||||
import { render, screen, fireEvent } from '@testing-library/react-native';
|
||||
|
||||
const onChangeTextMock = jest.fn();
|
||||
const CHANGE_TEXT = 'content';
|
||||
|
||||
await render(
|
||||
<View>
|
||||
<TextInput placeholder="Enter data" onChangeText={onChangeTextMock} />
|
||||
</View>,
|
||||
);
|
||||
|
||||
await fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT);
|
||||
```
|
||||
|
||||
### `fireEvent.scroll`
|
||||
|
||||
> [!NOTE]
|
||||
> Prefer [`user.scrollTo`](./user-event.md#scrollto) over `fireEvent.scroll` for `ScrollView`, `FlatList`, and `SectionList` components. User Event simulates events more realistically based on React Native runtime behavior.
|
||||
|
||||
```tsx
|
||||
fireEvent.scroll: (
|
||||
instance: TestInstance,
|
||||
eventProps?: Record<string, unknown>,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Builds a scroll event object, merges `eventProps` into it, and invokes the `scroll` handler on the element or nearest eligible parent.
|
||||
|
||||
#### On a `ScrollView`
|
||||
|
||||
```jsx
|
||||
import { ScrollView, Text } from 'react-native';
|
||||
import { render, screen, fireEvent } from '@testing-library/react-native';
|
||||
|
||||
const onScrollMock = jest.fn();
|
||||
const eventData = {
|
||||
nativeEvent: {
|
||||
contentOffset: {
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await render(
|
||||
<ScrollView testID="scroll-view" onScroll={onScrollMock}>
|
||||
<Text>Content</Text>
|
||||
</ScrollView>,
|
||||
);
|
||||
|
||||
await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData);
|
||||
```
|
||||
@@ -0,0 +1,198 @@
|
||||
# Jest matchers
|
||||
|
||||
This guide covers the built-in Jest matchers. These matchers make your tests easier to read and work better with accessibility features.
|
||||
|
||||
## Setup
|
||||
|
||||
No setup needed. Matchers are available when you import from `@testing-library/react-native`.
|
||||
|
||||
## Checking element existence
|
||||
|
||||
### `toBeOnTheScreen()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeOnTheScreen();
|
||||
```
|
||||
|
||||
Checks if an element is attached to the element tree. If you have a reference to an element and it gets unmounted during the test, this assertion will fail.
|
||||
|
||||
## Element Content
|
||||
|
||||
### `toHaveTextContent()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveTextContent(
|
||||
text: string | RegExp,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Checks if an element has the specified text content. Accepts `string` or `RegExp`, with optional [text match options](./queries.md#text-match-options) like `exact` and `normalizer`.
|
||||
|
||||
### `toContainElement()`
|
||||
|
||||
```ts
|
||||
expect(container).toContainElement(
|
||||
instance: TestInstance | null,
|
||||
)
|
||||
```
|
||||
|
||||
Checks if a container element contains another element.
|
||||
|
||||
### `toBeEmptyElement()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeEmptyElement();
|
||||
```
|
||||
|
||||
Checks if an element has no child elements or text content.
|
||||
|
||||
## Checking element state
|
||||
|
||||
### `toHaveDisplayValue()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveDisplayValue(
|
||||
value: string | RegExp,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Checks if a `TextInput` has the specified display value. Accepts `string` or `RegExp`, with optional [text match options](./queries.md#text-match-options) like `exact` and `normalizer`.
|
||||
|
||||
### `toHaveAccessibilityValue()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveAccessibilityValue(
|
||||
value: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
now?: number;
|
||||
text?: string | RegExp;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Checks if an element has the specified accessible value.
|
||||
|
||||
The matcher reads accessibility values from `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext`, and `accessibilityValue` props. It only checks the values you specify, so the element can have other accessibility value entries and still match.
|
||||
|
||||
For the `text` entry, you can use a string or `RegExp`.
|
||||
|
||||
### `toBeEnabled()` / `toBeDisabled`
|
||||
|
||||
```ts
|
||||
expect(element).toBeEnabled();
|
||||
expect(element).toBeDisabled();
|
||||
```
|
||||
|
||||
Checks if an element is enabled or disabled from `aria-disabled` or `accessibilityState.disabled` props. An element is disabled if it or any ancestor is disabled.
|
||||
|
||||
> [!NOTE]
|
||||
> These matchers are opposites. Both are available so you can avoid double negations like `expect(element).not.toBeDisabled()`.
|
||||
|
||||
### `toBeSelected()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeSelected();
|
||||
```
|
||||
|
||||
Checks if an element is selected from `aria-selected` or `accessibilityState.selected` props.
|
||||
|
||||
### `toBeChecked()` / `toBePartiallyChecked()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeChecked();
|
||||
expect(element).toBePartiallyChecked();
|
||||
```
|
||||
|
||||
Checks if an element is checked or partially checked from `aria-checked` or `accessibilityState.checked` props.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> - `toBeChecked()` only works on `Switch` host elements and elements with `checkbox`, `radio`, or `switch` role.
|
||||
> - `toBePartiallyChecked()` only works on elements with `checkbox` role.
|
||||
|
||||
### `toBeExpanded()` / `toBeCollapsed()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeExpanded();
|
||||
expect(element).toBeCollapsed();
|
||||
```
|
||||
|
||||
Checks if an element is expanded or collapsed from `aria-expanded` or `accessibilityState.expanded` props.
|
||||
|
||||
> [!NOTE]
|
||||
> These matchers are opposites for expandable elements (those with explicit `aria-expanded` or `accessibilityState.expanded` props). For non-expandable elements, neither matcher will pass.
|
||||
|
||||
### `toBeBusy()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeBusy();
|
||||
```
|
||||
|
||||
Checks if an element is busy from `aria-busy` or `accessibilityState.busy` props.
|
||||
|
||||
## Checking element style
|
||||
|
||||
### `toBeVisible()`
|
||||
|
||||
```ts
|
||||
expect(element).toBeVisible();
|
||||
```
|
||||
|
||||
Checks if an element is visible.
|
||||
|
||||
An element is invisible if it or any ancestor has `display: none` or `opacity: 0` styles, or if it's hidden from accessibility.
|
||||
|
||||
### `toHaveStyle()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveStyle(
|
||||
style: StyleProp<Style>,
|
||||
)
|
||||
```
|
||||
|
||||
Checks if an element has specific styles.
|
||||
|
||||
## Other matchers
|
||||
|
||||
### `toHaveAccessibleName()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveAccessibleName(
|
||||
name?: string | RegExp,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Checks if an element has the specified accessible name. Accepts `string` or `RegExp`, with optional [text match options](./queries.md#text-match-options) like `exact` and `normalizer`.
|
||||
|
||||
The accessible name comes from `aria-labelledby`, `accessibilityLabelledBy`, `aria-label`, and `accessibilityLabel` props. For `Image` elements, the `alt` prop is also used. If none are present, the element's text content is used.
|
||||
|
||||
When `accessibilityLabelledBy` references multiple elements with an array, their text content is joined with spaces in the referenced order and matched as a single accessible name. `aria-labelledby` follows React Native's single `nativeID` value behavior.
|
||||
|
||||
Without a `name` parameter (or with `undefined`), it only checks whether the element has any accessible name.
|
||||
|
||||
### `toHaveProp()`
|
||||
|
||||
```ts
|
||||
expect(element).toHaveProp(
|
||||
name: string,
|
||||
value?: unknown,
|
||||
)
|
||||
```
|
||||
|
||||
Checks if an element has a prop. Without a `value` (or with `undefined`), it only checks if the prop exists. With a `value`, it checks if the prop's value matches.
|
||||
|
||||
> [!NOTE]
|
||||
> Use this matcher as a last resort when other matchers don't fit your needs.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Other helpers
|
||||
|
||||
## `within`
|
||||
|
||||
```jsx
|
||||
function within(instance: TestInstance): Queries {}
|
||||
```
|
||||
|
||||
`within` performs [queries](./queries.md) scoped to given element.
|
||||
|
||||
> [!NOTE]
|
||||
> Please note that additional `render` specific operations like `rerender`, `unmount`, `debug`, `toJSON` are _not_ included.
|
||||
|
||||
```jsx
|
||||
const detailsScreen = within(screen.getByHintText('Details Screen'));
|
||||
expect(detailsScreen.getByText('Some Text')).toBeOnTheScreen();
|
||||
expect(detailsScreen.getByDisplayValue('Some Value')).toBeOnTheScreen();
|
||||
expect(detailsScreen.queryByLabelText('Some Label')).toBeOnTheScreen();
|
||||
await expect(detailsScreen.findByHintText('Some Label')).resolves.toBeOnTheScreen();
|
||||
```
|
||||
|
||||
Use cases for scoped queries include:
|
||||
|
||||
- queries scoped to a single item inside a FlatList containing many items
|
||||
- queries scoped to a single screen in tests involving screen transitions (e.g. with react-navigation)
|
||||
|
||||
## `act`
|
||||
|
||||
```ts
|
||||
function act<T>(callback: () => T | Promise<T>): Promise<T>;
|
||||
```
|
||||
|
||||
Wraps code that causes React state updates to ensure all updates are processed before assertions. By default any `render`, `rerender`, `fireEvent`, and `waitFor` calls are wrapped by this function, so there is no need to wrap it manually.
|
||||
|
||||
**In v14, `act` is now async by default and always returns a Promise**. This works with async React features like `Suspense` boundaries and the `use()` hook. All pending React updates are executed before the Promise resolves.
|
||||
|
||||
```ts
|
||||
import { act } from '@testing-library/react-native';
|
||||
|
||||
it('should update state', async () => {
|
||||
await act(() => {
|
||||
setState('new value');
|
||||
});
|
||||
expect(state).toBe('new value');
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: Even if your callback is synchronous, you should still use `await act(...)` as `act` now always returns a Promise.
|
||||
|
||||
Consult our [Understanding Act function](../guides/understanding-act.md) document for more understanding of its intricacies.
|
||||
|
||||
## `cleanup`
|
||||
|
||||
```ts
|
||||
function cleanup(): Promise<void>;
|
||||
```
|
||||
|
||||
Unmounts React trees that were mounted with `render` and clears `screen` variable that holds latest `render` output.
|
||||
|
||||
> [!INFO]
|
||||
> Please note that this is done automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.
|
||||
|
||||
For example, if you're using the `jest` testing framework, then you would need to use the `afterEach` hook like so:
|
||||
|
||||
```jsx
|
||||
import { cleanup, render } from '@testing-library/react-native/pure';
|
||||
import { View } from 'react-native';
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('renders a view', async () => {
|
||||
await render(<View />);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
The `afterEach(cleanup)` call also works in `describe` blocks:
|
||||
|
||||
```jsx
|
||||
describe('when logged in', () => {
|
||||
afterEach(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('renders the user', async () => {
|
||||
await render(<SiteHeader />);
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Failing to call `cleanup` when you've called `render` could result in a memory leak and tests which are not "idempotent" (which can lead to difficult to debug errors in your tests).
|
||||
@@ -0,0 +1,18 @@
|
||||
# API Overview
|
||||
|
||||
React Native Testing Library consists of following APIs:
|
||||
|
||||
- [`render` function](./render.md) - render your UI components for testing purposes
|
||||
- [`screen` object](./screen.md) - access rendered UI:
|
||||
- [Queries](./queries.md) - find relevant components by various predicates: role, text, test ids, etc
|
||||
- Lifecycle methods: [`rerender`](./screen.md#rerender), [`unmount`](./screen.md#unmount)
|
||||
- Helpers: [`debug`](./screen.md#debug), [`toJSON`](./screen.md#tojson), [`root`](./screen.md#root)
|
||||
- [Jest matchers](./jest-matchers.md) - validate assumptions about your UI
|
||||
- [User Event](./user-event.md) - simulate common user interactions like [`press`](./user-event.md#press) or [`type`](./user-event.md#type) in a realistic way
|
||||
- [Fire Event](./fire-event.md) - simulate any component event in a simplified way purposes
|
||||
- Misc APIs:
|
||||
- [`renderHook` function](./render-hook.md) - render hooks for testing
|
||||
- [Async utils](./async-utilities.md): `findBy*` queries, `waitFor`, `waitForElementToBeRemoved`
|
||||
- [Configuration](./configuration.md): `configure`, `resetToDefaults`
|
||||
- [Accessibility](./accessibility.md): `isHiddenFromAccessibility`
|
||||
- [Other](./other-helpers.md): `within`, `act`, `cleanup`
|
||||
@@ -0,0 +1,500 @@
|
||||
# Queries
|
||||
|
||||
Queries are one of the main building blocks for the React Native Testing Library. They enable you to find relevant elements in the element tree, which represents your application's user interface when running under tests.
|
||||
|
||||
## Accessing queries
|
||||
|
||||
All queries described below are accessible in two main ways: through the `screen` object or by capturing the `render` function call result.
|
||||
|
||||
### Using `screen` object
|
||||
|
||||
```tsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('accessing queries using "screen" object', async () => {
|
||||
await render(...);
|
||||
|
||||
screen.getByRole("button", { name: "Start" });
|
||||
})
|
||||
```
|
||||
|
||||
Use the `screen` object exported by `@testing-library/react-native` to access queries. This object contains all available query methods bound to the most recently rendered UI.
|
||||
|
||||
### Using `render` result
|
||||
|
||||
```tsx
|
||||
import { render } from '@testing-library/react-native';
|
||||
|
||||
test('accessing queries using "render" result', async () => {
|
||||
const { getByRole } = await render(...);
|
||||
getByRole("button", { name: "Start" });
|
||||
})
|
||||
```
|
||||
|
||||
You can also capture query functions from the `render` function return value. This provides the same query functions as the `screen` object.
|
||||
|
||||
## Query parts
|
||||
|
||||
Each query is composed of two parts: variant and predicate, which are separated by the `by` word in the middle of the name.
|
||||
|
||||
Consider the following query:
|
||||
|
||||
```
|
||||
getByRole()
|
||||
```
|
||||
|
||||
For this query, `getBy*` is the query variant, and `*ByRole` is the predicate.
|
||||
|
||||
## Query variant
|
||||
|
||||
The query variants describe the expected number (and timing) of matching elements, so they differ in their return type.
|
||||
|
||||
| Variant | Assertion | Return type | Is Async? |
|
||||
| ------------------------------------------ | ----------------------------- | ------------------------------------- | --------- |
|
||||
| [`getBy*`](./queries.md#get-by) | Exactly one matching element | `TestInstance` | No |
|
||||
| [`getAllBy*`](./queries.md#get-all-by) | At least one matching element | `Array<TestInstance>` | No |
|
||||
| [`queryBy*`](./queries.md#query-by) | Zero or one matching element | <code>TestInstance | null</code> | No |
|
||||
| [`queryAllBy*`](./queries.md#query-all-by) | No assertion | `Array<TestInstance>` | No |
|
||||
| [`findBy*`](./queries.md#find-by) | Exactly one matching element | `Promise<TestInstance>` | Yes |
|
||||
| [`findAllBy*`](./queries.md#find-all-by) | At least one matching element | `Promise<Array<TestInstance>>` | Yes |
|
||||
|
||||
Queries work as implicit assertions on the number of matching elements and will throw an error when the assertion fails.
|
||||
|
||||
### `getBy*` queries
|
||||
|
||||
```ts
|
||||
getByX(...): TestInstance
|
||||
```
|
||||
|
||||
`getBy*` queries return the single matching element for a query, and throw an error if no elements match or if more than one match is found. If you need to find more than one element, then use `getAllBy`.
|
||||
|
||||
### `getAllBy*` queries
|
||||
|
||||
```ts
|
||||
getAllByX(...): TestInstance[]
|
||||
```
|
||||
|
||||
`getAllBy*` queries return an array of all matching elements for a query and throw an error if no elements match.
|
||||
|
||||
### `queryBy*` queries
|
||||
|
||||
```ts
|
||||
queryByX(...): TestInstance | null
|
||||
```
|
||||
|
||||
`queryBy*` queries return the first matching node for a query, or `null` if no elements match. Use these to assert that an element is not present. They throw if more than one match is found (use `queryAllBy` instead).
|
||||
|
||||
### `queryAllBy*` queries
|
||||
|
||||
```ts
|
||||
queryAllByX(...): TestInstance[]
|
||||
```
|
||||
|
||||
`queryAllBy*` queries return an array of all matching nodes for a query and return an empty array (`[]`) when no elements match.
|
||||
|
||||
### `findBy*` queries
|
||||
|
||||
```ts
|
||||
findByX(
|
||||
...,
|
||||
waitForOptions?: {
|
||||
timeout?: number,
|
||||
interval?: number,
|
||||
},
|
||||
): Promise<TestInstance>
|
||||
```
|
||||
|
||||
`findBy*` queries return a promise which resolves when a matching element is found. The promise is rejected if no elements match or if more than one match is found after a default timeout of 1000 ms. If you need to find more than one element use `findAllBy*` queries.
|
||||
|
||||
### `findAllBy*` queries
|
||||
|
||||
```ts
|
||||
findAllByX(
|
||||
...,
|
||||
waitForOptions?: {
|
||||
timeout?: number,
|
||||
interval?: number,
|
||||
},
|
||||
): Promise<TestInstance[]>
|
||||
```
|
||||
|
||||
`findAllBy*` queries return a promise which resolves to an array of matching elements. The promise is rejected if no elements match after a default timeout of 1000 ms.
|
||||
|
||||
> [!INFO]
|
||||
> `findBy*` and `findAllBy*` queries accept optional `waitForOptions` object arguments, which can contain `timeout`, `interval` and `onTimeout` properties which have the same meaning as respective options for [`waitFor`](./async-utilities.md#waitfor) function.
|
||||
|
||||
> [!INFO]
|
||||
> In cases when your `findBy*` and `findAllBy*` queries throw when unable to find matching elements, it is helpful to pass `onTimeout: () => { screen.debug(); }` callback using the `waitForOptions` parameter.
|
||||
|
||||
## Query predicates
|
||||
|
||||
_Note: most methods like this one return a [`TestInstance`](https://github.com/mdjastrzebski/test-renderer#test-instance) with following properties that you may be interested in:_
|
||||
|
||||
```typescript
|
||||
type TestInstance = {
|
||||
type: string;
|
||||
props: { [propName: string]: any };
|
||||
parent: TestInstance | null;
|
||||
children: Array<TestInstance | string>;
|
||||
};
|
||||
```
|
||||
|
||||
### `*ByRole`
|
||||
|
||||
> getByRole, getAllByRole, queryByRole, queryAllByRole, findByRole, findAllByRole
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` with matching `role` or `accessibilityRole` prop.
|
||||
|
||||
> [!INFO]
|
||||
> In order for `*ByRole` queries to match an element it needs to be considered an accessibility element:
|
||||
>
|
||||
> 1. `Text`, `TextInput` and `Switch` elements are these by default.
|
||||
> 2. `View` elements need an explicit [`accessible`](https://reactnative.dev/docs/accessibility#accessible) prop set to `true`
|
||||
> 3. Some React Native composite components like `Pressable` & `TouchableOpacity` render host `View` element with `accessible` prop already set.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(
|
||||
<Pressable accessibilityRole="button" disabled>
|
||||
<Text>Hello</Text>
|
||||
</Pressable>,
|
||||
);
|
||||
const element = screen.getByRole('button');
|
||||
const element2 = screen.getByRole('button', { name: 'Hello' });
|
||||
const element3 = screen.getByRole('button', { name: 'Hello', disabled: true });
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
- `name`: Finds an element with given `role`/`accessibilityRole` and an accessible name (= accessability label or text content).
|
||||
|
||||
- `disabled`: You can filter elements by their disabled state (coming either from `aria-disabled` prop or `accessbilityState.disabled` prop). The possible values are `true` or `false`. Querying `disabled: false` will also match elements with `disabled: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details).
|
||||
- See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `disabled` state.
|
||||
- This option can alternatively be expressed using the [`toBeEnabled()` / `toBeDisabled()`](./jest-matchers.md#tobeenabled) Jest matchers.
|
||||
|
||||
- `selected`: You can filter elements by their selected state (coming either from `aria-selected` prop or `accessbilityState.selected` prop). The possible values are `true` or `false`. Querying `selected: false` will also match elements with `selected: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details).
|
||||
- See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `selected` state.
|
||||
- This option can alternatively be expressed using the [`toBeSelected()`](./jest-matchers.md#tobeselected) Jest matcher.
|
||||
|
||||
* `checked`: You can filter elements by their checked state (coming either from `aria-checked` prop or `accessbilityState.checked` prop). The possible values are `true`, `false`, or `"mixed"`.
|
||||
- See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `checked` state.
|
||||
- This option can alternatively be expressed using the [`toBeChecked()` / `toBePartiallyChecked()`](./jest-matchers.md#tobechecked) Jest matchers.
|
||||
|
||||
* `busy`: You can filter elements by their busy state (coming either from `aria-busy` prop or `accessbilityState.busy` prop). The possible values are `true` or `false`. Querying `busy: false` will also match elements with `busy: undefined` (see the [wiki](https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State) for more details).
|
||||
- See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `busy` state.
|
||||
- This option can alternatively be expressed using the [`toBeBusy()`](./jest-matchers.md#tobebusy) Jest matcher.
|
||||
|
||||
* `expanded`: You can filter elements by their expanded state (coming either from `aria-expanded` prop or `accessbilityState.expanded` prop). The possible values are `true` or `false`.
|
||||
- See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `expanded` state.
|
||||
- This option can alternatively be expressed using the [`toBeExpanded()` / `toBeCollapsed()`](./jest-matchers.md#tobeexpanded) Jest matchers.
|
||||
|
||||
* `value`: Filter elements by their accessibility value, based on either `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` or `accessibilityValue` props. Accessiblity value conceptually consists of numeric `min`, `max` and `now` entries, as well as string `text` entry.
|
||||
- See React Native [accessibilityValue](https://reactnative.dev/docs/accessibility#accessibilityvalue) docs to learn more about the accessibility value concept.
|
||||
- This option can alternatively be expressed using the [`toHaveAccessibilityValue()`](./jest-matchers.md#tohaveaccessibilityvalue) Jest matcher.
|
||||
|
||||
### `*ByLabelText`
|
||||
|
||||
> getByLabelText, getAllByLabelText, queryByLabelText, queryAllByLabelText, findByLabelText, findAllByLabelText
|
||||
|
||||
```ts
|
||||
getByLabelText(
|
||||
text: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
},
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` with matching label:
|
||||
|
||||
- either by matching [`aria-label`](https://reactnative.dev/docs/accessibility#aria-label)/[`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
|
||||
- or by matching text content of view referenced by [`aria-labelledby`](https://reactnative.dev/docs/accessibility#aria-labelledby-android)/[`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
|
||||
- or by matching the [`alt`](https://reactnative.dev/docs/image#alt) prop on `Image` elements
|
||||
|
||||
When `accessibilityLabelledBy` references multiple elements with an array, their text content is joined with spaces in the referenced order and matched as a single label. `aria-labelledby` follows React Native's single `nativeID` value behavior.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByLabelText('my-label');
|
||||
```
|
||||
|
||||
### `*ByPlaceholderText`
|
||||
|
||||
> getByPlaceholderText, getAllByPlaceholderText, queryByPlaceholderText, queryAllByPlaceholderText, findByPlaceholderText, findAllByPlaceholderText
|
||||
|
||||
```ts
|
||||
getByPlaceholderText(
|
||||
text: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
}
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` for a `TextInput` with a matching placeholder – may be a string or regular expression.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByPlaceholderText('username');
|
||||
```
|
||||
|
||||
### `*ByDisplayValue`
|
||||
|
||||
> getByDisplayValue, getAllByDisplayValue, queryByDisplayValue, queryAllByDisplayValue, findByDisplayValue, findAllByDisplayValue
|
||||
|
||||
```ts
|
||||
getByDisplayValue(
|
||||
value: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
},
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` for a `TextInput` with a matching display value – may be a string or regular expression.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByDisplayValue('username');
|
||||
```
|
||||
|
||||
### `*ByText`
|
||||
|
||||
> getByText, getAllByText, queryByText, queryAllByText, findByText, findAllByText
|
||||
|
||||
```ts
|
||||
getByText(
|
||||
text: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
}
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` with matching text – may be a string or regular expression.
|
||||
|
||||
This method will join `<Text>` siblings to find matches, similarly to [how React Native handles these components](https://reactnative.dev/docs/text#containers). This will allow for querying for strings that will be visually rendered together, but may be semantically separate React components.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByText('banana');
|
||||
```
|
||||
|
||||
### `*ByHintText`
|
||||
|
||||
> getByA11yHint, getAllByA11yHint, queryByA11yHint, queryAllByA11yHint, findByA11yHint, findAllByA11yHint
|
||||
> getByAccessibilityHint, getAllByAccessibilityHint, queryByAccessibilityHint, queryAllByAccessibilityHint, findByAccessibilityHint, findAllByAccessibilityHint
|
||||
> getByHintText, getAllByHintText, queryByHintText, queryAllByHintText, findByHintText, findAllByHintText
|
||||
|
||||
```ts
|
||||
getByHintText(
|
||||
hint: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
},
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` with matching `accessibilityHint` prop.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByHintText('Plays a song');
|
||||
```
|
||||
|
||||
> [!INFO]
|
||||
> Please consult [Apple guidelines on how `accessibilityHint` should be used](https://developer.apple.com/documentation/objectivec/nsobject/1615093-accessibilityhint).
|
||||
|
||||
### `*ByTestId`
|
||||
|
||||
> getByTestId, getAllByTestId, queryByTestId, queryAllByTestId, findByTestId, findAllByTestId
|
||||
|
||||
```ts
|
||||
getByTestId(
|
||||
testId: TextMatch,
|
||||
options?: {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
includeHiddenElements?: boolean;
|
||||
},
|
||||
): TestInstance;
|
||||
```
|
||||
|
||||
Returns a `TestInstance` with matching `testID` prop. `testID` – may be a string or a regular expression.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
await render(<MyComponent />);
|
||||
const element = screen.getByTestId('unique-id');
|
||||
```
|
||||
|
||||
> [!INFO]
|
||||
> Following [the guiding principles](https://testing-library.com/docs/guiding-principles), use this only when other queries don't work for your use case. `testID` attributes don't resemble how your software is used and should be avoided when possible. They're useful for end-to-end testing on real devices, e.g. with Detox. Learn more from the blog post ["Making your UI tests resilient to change"](https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change).
|
||||
|
||||
### Common options
|
||||
|
||||
Usually query first argument can be a **string** or a **regex**. All queries take at least the [`hidden`](#hidden-option) option as an optionnal second argument and some queries accept more options which change string matching behaviour. See [TextMatch](#textmatch) for more info.
|
||||
|
||||
#### `includeHiddenElements` option
|
||||
|
||||
All queries have the `includeHiddenElements` option which affects whether [elements hidden from accessibility](./accessibility.md#ishiddenfromaccessibility) are matched by the query. By default queries will not match hidden elements, because the users of the app would not be able to see such elements.
|
||||
|
||||
You can configure the default value with the [`configure` function](./configuration.md#configure).
|
||||
|
||||
This option is also available as `hidden` alias for compatibility with [React Testing Library](https://testing-library.com/docs/queries/byrole#hidden).
|
||||
|
||||
**Examples**
|
||||
|
||||
```tsx
|
||||
await render(<Text style={{ display: 'none' }}>Hidden from accessibility</Text>);
|
||||
|
||||
// Exclude hidden elements
|
||||
expect(
|
||||
screen.queryByText('Hidden from accessibility', {
|
||||
includeHiddenElements: false,
|
||||
}),
|
||||
).not.toBeOnTheScreen();
|
||||
|
||||
// Include hidden elements
|
||||
expect(
|
||||
screen.getByText('Hidden from accessibility', { includeHiddenElements: true }),
|
||||
).toBeOnTheScreen();
|
||||
```
|
||||
|
||||
## TextMatch type
|
||||
|
||||
```ts
|
||||
type TextMatch = string | RegExp;
|
||||
```
|
||||
|
||||
Most of the query APIs take a `TextMatch` as an argument, which means the argument can be either a _string_ or _regex_.
|
||||
|
||||
### Examples
|
||||
|
||||
Given the following render:
|
||||
|
||||
```jsx
|
||||
await render(<Text>Hello World</Text>);
|
||||
```
|
||||
|
||||
Will **find a match**:
|
||||
|
||||
```js
|
||||
// Matching a string:
|
||||
screen.getByText('Hello World'); // full string match
|
||||
screen.getByText('llo Worl', { exact: false }); // substring match
|
||||
screen.getByText('hello world', { exact: false }); // ignore case-sensitivity
|
||||
|
||||
// Matching a regex:
|
||||
screen.getByText(/World/); // substring match
|
||||
screen.getByText(/world/i); // substring match, ignore case
|
||||
screen.getByText(/^hello world$/i); // full string match, ignore case-sensitivity
|
||||
screen.getByText(/Hello W?oRlD/i); // advanced regex
|
||||
```
|
||||
|
||||
Will **NOT find a match**
|
||||
|
||||
```js
|
||||
// substring does not match
|
||||
screen.getByText('llo Worl');
|
||||
// full string does not match
|
||||
screen.getByText('Goodbye World');
|
||||
|
||||
// case-sensitive regex with different case
|
||||
screen.getByText(/hello world/);
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
#### Precision
|
||||
|
||||
```typescript
|
||||
type TextMatchOptions = {
|
||||
exact?: boolean;
|
||||
normalizer?: (text: string) => string;
|
||||
};
|
||||
```
|
||||
|
||||
Queries that take a `TextMatch` also accept an object as the second argument that can contain options that affect the precision of string matching:
|
||||
|
||||
- `exact`: Defaults to `true`; matches full strings, case-sensitive. When false, matches substrings and is not case-sensitive.
|
||||
- `exact` has no effect on regex argument.
|
||||
- In most cases using a `regex` instead of a string gives you more control over fuzzy matching and should be preferred over `{ exact: false }`.
|
||||
- `normalizer`: An optional function which overrides normalization behavior. See [Normalization](#normalization).
|
||||
|
||||
`exact` option defaults to `true` but if you want to search for a text slice or make text matching case-insensitive you can override it. That being said we advise you to use regex in more complex scenarios.
|
||||
|
||||
#### Normalization
|
||||
|
||||
Before running any matching logic against text, it is automatically normalized. By default, normalization consists of trimming whitespace from the start and end of text, and collapsing multiple adjacent whitespace characters into a single space.
|
||||
|
||||
If you want to prevent that normalization, or provide alternative normalization (e.g. to remove Unicode control characters), you can provide a `normalizer` function in the options object. This function will be given a string and is expected to return a normalized version of that string.
|
||||
|
||||
> [!INFO]
|
||||
> Specifying a value for `normalizer` replaces the built-in normalization, but you can call `getDefaultNormalizer` to obtain a built-in normalizer, either to adjust that normalization or to call it from your own normalizer.
|
||||
|
||||
`getDefaultNormalizer` take options object which allows the selection of behaviour:
|
||||
|
||||
- `trim`: Defaults to `true`. Trims leading and trailing whitespace.
|
||||
- `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs repeated spaces) into a single space.
|
||||
|
||||
##### Normalization Examples
|
||||
|
||||
To perform a match against text without trimming:
|
||||
|
||||
```typescript
|
||||
screen.getByText('text', {
|
||||
normalizer: getDefaultNormalizer({ trim: false }),
|
||||
});
|
||||
```
|
||||
|
||||
To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior:
|
||||
|
||||
```typescript
|
||||
screen.getByText('text', {
|
||||
normalizer: (str) => getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''),
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
# `renderHook` function
|
||||
|
||||
## `renderHook`
|
||||
|
||||
```ts
|
||||
async function renderHook<Result, Props>(
|
||||
hookFn: (props: Props) => Result,
|
||||
options?: RenderHookOptions<Props>,
|
||||
): Promise<RenderHookResult<Result, Props>>;
|
||||
```
|
||||
|
||||
Renders a test component that calls the provided `callback` (and any hooks it uses) on each render. Returns a Promise that resolves to a [`RenderHookResult`](#renderhookresult) object.
|
||||
|
||||
**This is the recommended default API** for testing hooks. It uses async `act` internally to ensure all pending React updates are executed during rendering. This makes it compatible with async React features like `Suspense` boundaries and the `use()` hook.
|
||||
|
||||
- **Returns a Promise**: Should be awaited
|
||||
- **Async methods**: Both `rerender` and `unmount` return Promises and should be awaited
|
||||
- **Suspense support**: Compatible with `Suspense` boundaries and `use()` hook
|
||||
|
||||
```ts
|
||||
import { renderHook, act } from '@testing-library/react-native';
|
||||
import { useCount } from '../useCount';
|
||||
|
||||
it('should increment count', async () => {
|
||||
const { result } = await renderHook(() => useCount());
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
await act(() => {
|
||||
// Note that you should wrap the calls to functions your hook returns with `act` if they trigger an update of your hook's state to ensure pending useEffects are run before your next assertion.
|
||||
result.current.increment();
|
||||
});
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// useCount.js
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useCount = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
const increment = () => setCount((previousCount) => previousCount + 1);
|
||||
|
||||
return { count, increment };
|
||||
};
|
||||
```
|
||||
|
||||
The `renderHook` function accepts the following arguments:
|
||||
|
||||
**Callback**: A function called on each render of the test component. This function should call one or more hooks for testing.
|
||||
|
||||
The callback receives `props` from the `initialProps` option, or from a subsequent `rerender` call if provided.
|
||||
|
||||
### `options`
|
||||
|
||||
A `RenderHookOptions<Props>` object with the following properties:
|
||||
|
||||
#### `initialProps`
|
||||
|
||||
The initial values to pass as `props` to the `callback` function of `renderHook`. The `Props` type is determined by the type passed to or inferred by the `renderHook` call.
|
||||
|
||||
#### `wrapper`
|
||||
|
||||
A React component that wraps the test component. Use this to add context providers so hooks can access them with `useContext`.
|
||||
|
||||
### Result
|
||||
|
||||
```ts
|
||||
interface RenderHookResult<Result, Props> {
|
||||
result: { current: Result };
|
||||
rerender: (props: Props) => Promise<void>;
|
||||
unmount: () => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
The `renderHook` function returns a Promise that resolves to an object with the following properties:
|
||||
|
||||
#### `result`
|
||||
|
||||
The `current` value contains whatever the `callback` returned from `renderHook`. The `Result` type is determined by the type passed to or inferred by the `renderHook` call.
|
||||
|
||||
**Note:** When using React Suspense, `result.current` will be `null` while the hook is suspended.
|
||||
|
||||
#### `rerender`
|
||||
|
||||
An async function that rerenders the test component and recalculates hooks. If `newProps` are passed, they replace the `callback` function's `initialProps` for subsequent rerenders. The `Props` type is determined by the type passed to or inferred by the `renderHook` call.
|
||||
|
||||
**Note**: This method returns a Promise and should be awaited.
|
||||
|
||||
#### `unmount`
|
||||
|
||||
An async function to unmount the test component. This is commonly used to trigger cleanup effects for `useEffect` hooks.
|
||||
|
||||
**Note**: This method returns a Promise and should be awaited.
|
||||
|
||||
### Examples
|
||||
|
||||
Additional examples of using `renderHook`:
|
||||
|
||||
#### With `initialProps`
|
||||
|
||||
```ts
|
||||
import { useState, useEffect } from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-native';
|
||||
|
||||
const useCount = (initialCount: number) => {
|
||||
const [count, setCount] = useState(initialCount);
|
||||
const increment = () => setCount((previousCount) => previousCount + 1);
|
||||
|
||||
useEffect(() => {
|
||||
setCount(initialCount);
|
||||
}, [initialCount]);
|
||||
|
||||
return { count, increment };
|
||||
};
|
||||
|
||||
it('should increment count', async () => {
|
||||
const { result, rerender } = await renderHook((initialCount: number) => useCount(initialCount), {
|
||||
initialProps: 1,
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
|
||||
await act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(2);
|
||||
await rerender(5);
|
||||
expect(result.current.count).toBe(5);
|
||||
});
|
||||
```
|
||||
|
||||
#### With `wrapper`
|
||||
|
||||
```tsx
|
||||
it('should use context value', async () => {
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <Context.Provider value="provided">{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
const { result } = await renderHook(() => useHook(), { wrapper: Wrapper });
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
#### With React Suspense
|
||||
|
||||
```tsx
|
||||
import { renderHook, act } from '@testing-library/react-native';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
function useSuspendingHook(promise: Promise<string>) {
|
||||
return React.use(promise);
|
||||
}
|
||||
|
||||
it('handles hook with suspense', async () => {
|
||||
let resolvePromise: (value: string) => void;
|
||||
const promise = new Promise<string>((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
|
||||
const { result } = await renderHook(useSuspendingHook, {
|
||||
initialProps: promise,
|
||||
wrapper: ({ children }) => (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>{children}</React.Suspense>
|
||||
),
|
||||
});
|
||||
|
||||
// Initially suspended, result should not be available
|
||||
expect(result.current).toBeNull();
|
||||
|
||||
await act(() => resolvePromise('resolved'));
|
||||
expect(result.current).toBe('resolved');
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# `render` API
|
||||
|
||||
## `render` function
|
||||
|
||||
```ts
|
||||
async function render<T>(
|
||||
element: React.ReactElement<T>,
|
||||
options?: RenderOptions,
|
||||
): Promise<RenderResult>;
|
||||
```
|
||||
|
||||
The `render` function is the entry point for writing React Native Testing Library tests. It deeply renders the given React element and returns helpers to query the output. The function is async and uses async `act` internally, so all pending React updates run before it resolves. This works with async React features like `Suspense` boundaries and the `use()` hook.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('basic test', async () => {
|
||||
await render(<MyApp />);
|
||||
expect(screen.getAllByRole('button', { name: 'start' })).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
> When using React context providers like Redux Provider, you'll likely want to wrap the rendered component with them. In such cases, create your own custom `render` method. [Follow this guide on how to set it up](https://testing-library.com/docs/react-testing-library/setup#custom-render).
|
||||
|
||||
### Options
|
||||
|
||||
You can customize the `render` method by passing options as the second argument:
|
||||
|
||||
#### `wrapper`
|
||||
|
||||
```ts
|
||||
wrapper?: React.ComponentType<any>,
|
||||
```
|
||||
|
||||
Wraps the tested component in an additional wrapper component. Use this to create custom render functions for common React Context providers.
|
||||
|
||||
> [!NOTE] Text string validation
|
||||
> Test Renderer enforces React Native's requirement that text strings must be rendered within a `<Text>` component. If you render a `string` value under components other than `<Text>` (e.g., under `<View>`), it throws an `Invariant Violation: Text strings must be rendered within a <Text> component` error. This matches React Native's runtime behavior.
|
||||
>
|
||||
> This validation is always enabled and cannot be disabled. Your tests will catch the same text rendering errors that would occur in production.
|
||||
|
||||
### Result
|
||||
|
||||
The `render` function returns a promise that resolves to the same queries and utilities as the [`screen`](./screen.md) object. Use `screen` for queries and the lifecycle methods from the render result when needed.
|
||||
|
||||
See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details.
|
||||
|
||||
> [!NOTE] Type information
|
||||
> Query results and element references use the `TestInstance` type from [Test Renderer](https://github.com/mdjastrzebski/test-renderer). If you need to type element variables, import this type directly from `test-renderer`.
|
||||
@@ -0,0 +1,188 @@
|
||||
# `screen` object
|
||||
|
||||
```ts
|
||||
let screen: {
|
||||
...queries;
|
||||
rerender(element: React.Element<unknown>): Promise<void>;
|
||||
unmount(): Promise<void>;
|
||||
debug(options?: DebugOptions): void
|
||||
toJSON(): JsonElement | null;
|
||||
container: TestInstance;
|
||||
root: TestInstance | null;
|
||||
};
|
||||
```
|
||||
|
||||
The `screen` object provides access to queries and utilities for the currently rendered UI.
|
||||
|
||||
This object is assigned after the `render` call and cleared after each test by calling [`cleanup`](./other-helpers.md#cleanup). If no `render` call has been made in a given test, then it holds a special object and throws a helpful error on each property and method access.
|
||||
|
||||
### `...queries`
|
||||
|
||||
The main feature of `screen` is its queries for finding elements in the view hierarchy.
|
||||
|
||||
See [Queries](./queries.md) for a complete list.
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('example', async () => {
|
||||
await render(<MyComponent />);
|
||||
const buttonStart = screen.getByRole('button', { name: 'start' });
|
||||
});
|
||||
```
|
||||
|
||||
### `rerender`
|
||||
|
||||
```ts
|
||||
function rerender(element: React.Element<unknown>): Promise<void>;
|
||||
```
|
||||
|
||||
Re-render the in-memory tree with a new root element. This simulates a React update render at the root. If the new element has the same type (and `key`) as the previous element, the tree will be updated; otherwise, it will re-mount a new tree, in both cases triggering the appropriate lifecycle events.
|
||||
|
||||
This method is async and uses async `act` internally to execute all pending React updates during updating. This works with async React features like `Suspense` boundaries and the `use()` hook.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('async rerender test', async () => {
|
||||
await render(<MyComponent initialData="first" />);
|
||||
|
||||
await screen.rerender(<MyComponent initialData="updated" />);
|
||||
expect(screen.getByText('updated')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
### `unmount`
|
||||
|
||||
```ts
|
||||
function unmount(): Promise<void>;
|
||||
```
|
||||
|
||||
Unmount the in-memory tree, triggering the appropriate lifecycle events.
|
||||
|
||||
This method is async and uses async `act` internally to execute all pending React updates during unmounting. This works with async React features like `Suspense` boundaries and the `use()` hook.
|
||||
|
||||
> [!NOTE]
|
||||
> Usually you should not need to call `unmount` as it is done automatically if your test runner supports `afterEach` hook (like Jest, mocha, Jasmine).
|
||||
|
||||
### `debug`
|
||||
|
||||
```ts
|
||||
function debug(options?: { message?: string; mapProps?: MapPropsFunction }): void;
|
||||
```
|
||||
|
||||
Pretty prints deeply rendered component passed to `render`.
|
||||
|
||||
#### `message` option
|
||||
|
||||
You can provide a message that will be printed on top.
|
||||
|
||||
```jsx
|
||||
await render(<Component />);
|
||||
screen.debug({ message: 'optional message' });
|
||||
```
|
||||
|
||||
logs optional message and colored JSX:
|
||||
|
||||
```jsx
|
||||
optional message
|
||||
|
||||
<View
|
||||
onPress={[Function bound fn]}
|
||||
>
|
||||
<Text>Press me</Text>
|
||||
</View>
|
||||
```
|
||||
|
||||
#### `mapProps` option
|
||||
|
||||
```ts
|
||||
function debug({ mapProps: (props) => ({}) });
|
||||
```
|
||||
|
||||
You can use the `mapProps` option to transform the props that will be printed :
|
||||
|
||||
```jsx
|
||||
await render(<View style={{ backgroundColor: 'red' }} />);
|
||||
screen.debug({ mapProps: ({ style, ...props }) => ({ props }) });
|
||||
```
|
||||
|
||||
This will log the rendered JSX without the `style` props.
|
||||
|
||||
The `children` prop cannot be filtered out so the following will print all rendered components with all props but `children` filtered out.
|
||||
|
||||
This option can be used to target specific props when debugging a query (for instance, keeping only the `children` prop when debugging a `getByText` query).
|
||||
|
||||
You can also transform prop values so that they are more readable (e.g., flatten styles).
|
||||
|
||||
```ts
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
screen.debug({
|
||||
mapProps: ({ style, ...props }) => ({ style: StyleSheet.flatten(style), ...props }),
|
||||
});
|
||||
```
|
||||
|
||||
Or remove props that have little value when debugging tests, e.g. path prop for svgs
|
||||
|
||||
```ts
|
||||
screen.debug({ mapProps: ({ path, ...props }) => ({ ...props }) });
|
||||
```
|
||||
|
||||
### `toJSON`
|
||||
|
||||
```ts
|
||||
function toJSON(): JsonElement | null;
|
||||
```
|
||||
|
||||
Get the rendered component JSON representation, e.g. for snapshot testing.
|
||||
|
||||
### `container`
|
||||
|
||||
```ts
|
||||
const container: TestInstance;
|
||||
```
|
||||
|
||||
Returns a pseudo-element container whose children are the elements you asked to render. This is the root container element from [Test Renderer](https://github.com/mdjastrzebski/test-renderer).
|
||||
|
||||
The `container` provides access to the entire rendered tree. Use it to query or manipulate the rendered output, similar to how `container` works in [React Testing Library](https://testing-library.com/docs/react-testing-library/other#container-1).
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('example', async () => {
|
||||
await render(<MyComponent />);
|
||||
// container contains the entire rendered tree
|
||||
const container = screen.container;
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
### `root`
|
||||
|
||||
```ts
|
||||
const root: TestInstance | null;
|
||||
```
|
||||
|
||||
Returns the rendered root [host element](../guides/testing-environment.md#host-and-composite-components), or `null` if nothing was rendered. This is the first child of the `container`, which represents the actual root element you rendered.
|
||||
|
||||
This API is useful for component tests where you need to access the root host view without using `*ByTestId` queries or similar methods.
|
||||
|
||||
> [!NOTE]
|
||||
> In rare cases where your root element is a `React.Fragment` with multiple children, the `container` will have more than one child, and `root` will return only the first one. In such cases, use `container.children` to access all rendered elements.
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('example', async () => {
|
||||
await render(
|
||||
<View testID="root-view">
|
||||
<Text>Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
// root is the View element you rendered
|
||||
expect(screen.root.props.testID).toBe('root-view');
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,295 @@
|
||||
# User Event interactions
|
||||
|
||||
## Comparison with Fire Event API
|
||||
|
||||
Fire Event is our original event simulation API. It can invoke **any event handler** declared on **either host or composite elements**. Suppose the element does not have `onEventName` event handler for the passed `eventName` event, or the element is disabled. In that case, Fire Event will traverse up the component tree, looking for an event handler on both host and composite elements along the way. By default, it will **not pass any event data**, but the user might provide it in the last argument.
|
||||
|
||||
In contrast, User Event provides realistic event simulation for user interactions like `press` or `type`. Each interaction will trigger a **sequence of events** corresponding to React Native runtime behavior. These events will be invoked **only on host elements**, and **will automatically receive event data** corresponding to each event.
|
||||
|
||||
If User Event supports a given interaction, prefer it over the Fire Event counterpart. It makes tests more realistic and reliable. When User Event doesn't support the event or you need to invoke event handlers on composite elements, use Fire Event.
|
||||
|
||||
## `setup()`
|
||||
|
||||
```ts
|
||||
userEvent.setup(options?: {
|
||||
delay?: number;
|
||||
advanceTimers?: (delay: number) => Promise<void> | void;
|
||||
})
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
```
|
||||
|
||||
Creates a User Event object instance, which can be used to trigger events.
|
||||
|
||||
### Options
|
||||
|
||||
- `delay` controls the default delay between subsequent events, e.g., keystrokes.
|
||||
- `advanceTimers` is a time advancement utility function that should be used for fake timers. The default setup handles both real timers and Jest fake timers.
|
||||
|
||||
## `press()`
|
||||
|
||||
```ts
|
||||
press(
|
||||
instance: TestInstance,
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.press(element);
|
||||
```
|
||||
|
||||
Simulates a press on any pressable element, e.g. `Pressable`, `TouchableOpacity`, `Text`, `TextInput`, etc. Unlike `fireEvent.press()`, which only calls the `onPress` prop, this function simulates the entire press interaction by reproducing the event sequence emitted by React Native runtime. It triggers additional events like `pressIn` and `pressOut`.
|
||||
|
||||
This event will take a minimum of 130 ms to run due to the internal React Native logic. Consider using fake timers to speed up test execution for tests involving `press` and `longPress` interactions.
|
||||
|
||||
## `longPress()`
|
||||
|
||||
```ts
|
||||
longPress(
|
||||
instance: TestInstance,
|
||||
options?: { duration?: number }
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.longPress(element);
|
||||
```
|
||||
|
||||
Simulates a long press user interaction. In React Native, the `longPress` event is emitted when the press duration exceeds the long press threshold (by default, 500 ms). In other aspects, this action behaves similarly to regular `press` action, e.g., by emitting `pressIn` and `pressOut` events. The press duration is customizable through the options, which is useful when using the `delayLongPress` prop.
|
||||
|
||||
This event will, by default, take 500 ms to run. Due to internal React Native logic, it will take at least 130 ms regardless of the duration option passed. Consider using fake timers to speed up test execution for tests involving `press` and `longPress` interactions.
|
||||
|
||||
### Options
|
||||
|
||||
- `duration` - duration of the press in milliseconds. The default value is 500 ms.
|
||||
|
||||
## `type()`
|
||||
|
||||
```ts
|
||||
type(
|
||||
instance: TestInstance,
|
||||
text: string,
|
||||
options?: {
|
||||
skipPress?: boolean;
|
||||
skipBlur?: boolean;
|
||||
submitEditing?: boolean;
|
||||
}
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.type(textInput, 'Hello world!');
|
||||
```
|
||||
|
||||
Simulates focusing on a `TextInput` element, typing `text` one character at a time, and leaving the element.
|
||||
|
||||
This function supports only host `TextInput` elements. Passing other element types will result in throwing an error.
|
||||
|
||||
> [!NOTE]
|
||||
> This function will add text to the text already present in the text input (as specified by `value` or `defaultValue` props). To replace existing text, use [`clear()`](#clear) helper first.
|
||||
|
||||
### Options
|
||||
|
||||
- `skipPress` - if true, `pressIn` and `pressOut` events will not be triggered.
|
||||
- `skipBlur` - if true, `endEditing` and `blur` events will not be triggered when typing is complete.
|
||||
- `submitEditing` - if true, `submitEditing` event will be triggered after typing the text.
|
||||
|
||||
### Sequence of events
|
||||
|
||||
The sequence of events depends on the `multiline` prop and the passed options.
|
||||
|
||||
Events will not be emitted if the `editable` prop is set to `false`.
|
||||
|
||||
**Entering the element**:
|
||||
|
||||
- `pressIn` (optional)
|
||||
- `focus`
|
||||
- `pressOut` (optional)
|
||||
|
||||
The `pressIn` and `pressOut` events are sent by default but can be skipped by passing the `skipPress: true` option.
|
||||
|
||||
**Typing (for each character)**:
|
||||
|
||||
- `keyPress`
|
||||
- `change`
|
||||
- `changeText`
|
||||
- `selectionChange`
|
||||
- `contentSizeChange` (only multiline)
|
||||
|
||||
**Leaving the element**:
|
||||
|
||||
- `submitEditing` (optional)
|
||||
- `endEditing`
|
||||
- `blur`
|
||||
|
||||
The `submitEditing` event is skipped by default. It can sent by setting the `submitEditing: true` option.
|
||||
The `endEditing` and `blur` events can be skipped by passing the `skipBlur: true` option.
|
||||
|
||||
## `clear()`
|
||||
|
||||
```ts
|
||||
clear(
|
||||
instance: TestInstance,
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.clear(textInput);
|
||||
```
|
||||
|
||||
Simulates clearing the content of a `TextInput` element.
|
||||
|
||||
This function supports only host `TextInput` elements. Passing other element types will result in throwing an error.
|
||||
|
||||
### Sequence of events
|
||||
|
||||
Events will not be emitted if the `editable` prop is set to `false`.
|
||||
|
||||
**Entering the element**:
|
||||
|
||||
- `focus`
|
||||
|
||||
**Selecting all content**:
|
||||
|
||||
- `selectionChange`
|
||||
|
||||
**Pressing backspace**:
|
||||
|
||||
- `keyPress`
|
||||
- `change`
|
||||
- `changeText`
|
||||
- `selectionChange`
|
||||
|
||||
**Leaving the element**:
|
||||
|
||||
- `endEditing`
|
||||
- `blur`
|
||||
|
||||
## `paste()`
|
||||
|
||||
```ts
|
||||
paste(
|
||||
instance: TestInstance,
|
||||
text: string,
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.paste(textInput, 'Text to paste');
|
||||
```
|
||||
|
||||
Simulates pasting text into a `TextInput` element.
|
||||
|
||||
This function supports only host `TextInput` elements. Passing other element types will result in throwing an error.
|
||||
|
||||
### Sequence of events
|
||||
|
||||
Events will not be emitted if the `editable` prop is set to `false`.
|
||||
|
||||
**Entering the element**:
|
||||
|
||||
- `focus`
|
||||
|
||||
**Selecting all content**:
|
||||
|
||||
- `selectionChange`
|
||||
|
||||
**Pasting the text**:
|
||||
|
||||
- `change`
|
||||
- `changeText`
|
||||
- `selectionChange`
|
||||
- `contentSizeChange` (only multiline)
|
||||
|
||||
**Leaving the element**:
|
||||
|
||||
- `endEditing`
|
||||
- `blur`
|
||||
|
||||
## `scrollTo()` \
|
||||
|
||||
```ts
|
||||
scrollTo(
|
||||
instance: TestInstance,
|
||||
options: {
|
||||
y: number;
|
||||
momentumY?: number;
|
||||
contentSize?: { width: number; height: number };
|
||||
layoutMeasurement?: { width: number; height: number };
|
||||
} | {
|
||||
x: number;
|
||||
momentumX?: number;
|
||||
contentSize?: { width: number; height: number };
|
||||
layoutMeasurement?: { width: number; height: number };
|
||||
}
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup();
|
||||
await user.scrollTo(scrollView, { y: 100, momentumY: 200 });
|
||||
```
|
||||
|
||||
Simulates scrolling a host `ScrollView` element.
|
||||
|
||||
This function supports only host `ScrollView` elements, passing other element types will result in an error. Note that `FlatList` is accepted as it renders to a host `ScrollView` element.
|
||||
|
||||
Scroll interaction should match the `ScrollView` element direction:
|
||||
|
||||
- for a vertical scroll view (default or `horizontal={false}`), you should pass only the `y` option (and optionally also `momentumY`).
|
||||
- for a horizontal scroll view (`horizontal={true}`), you should pass only the `x` option (and optionally `momentumX`).
|
||||
|
||||
Each scroll interaction consists of a mandatory drag scroll part, which simulates the user dragging the scroll view with his finger (the `y` or `x` option). This may optionally be followed by a momentum scroll movement, which simulates the inertial movement of scroll view content after the user lifts his finger (`momentumY` or `momentumX` options).
|
||||
|
||||
### Options
|
||||
|
||||
- `y` - target vertical drag scroll offset
|
||||
- `x` - target horizontal drag scroll offset
|
||||
- `momentumY` - target vertical momentum scroll offset
|
||||
- `momentumX` - target horizontal momentum scroll offset
|
||||
- `contentSize` - passed to `ScrollView` events and enabling `FlatList` updates
|
||||
- `layoutMeasurement` - passed to `ScrollView` events and enabling `FlatList` updates
|
||||
|
||||
User Event will generate several intermediate scroll steps to simulate user scroll interaction. You should not rely on exact number or values of these scrolls steps as they might be change in the future version.
|
||||
|
||||
This function will remember where the last scroll ended, so subsequent scroll interaction will starts from that position. The initial scroll position will be assumed to be `{ y: 0, x: 0 }`.
|
||||
|
||||
To simulate a `FlatList` (and other controls based on `VirtualizedList`) scrolling, you should pass the `contentSize` and `layoutMeasurement` options, which enable the underlying logic to update the currently visible window.
|
||||
|
||||
### Sequence of events
|
||||
|
||||
The sequence of events depends on whether the scroll includes an optional momentum scroll component.
|
||||
|
||||
**Drag scroll**:
|
||||
|
||||
- `contentSizeChange`
|
||||
- `scrollBeginDrag`
|
||||
- `scroll` (multiple events)
|
||||
- `scrollEndDrag`
|
||||
|
||||
**Momentum scroll (optional)**:
|
||||
|
||||
- `momentumScrollBegin`
|
||||
- `scroll` (multiple events)
|
||||
- `momentumScrollEnd`
|
||||
@@ -0,0 +1,147 @@
|
||||
# Async Events
|
||||
|
||||
## Summary
|
||||
|
||||
In RNTL v14, all tests are async since `render()`, `fireEvent()`, and other core APIs return Promises. Beyond the basic async APIs, there are additional async utilities for handling events that complete over time:
|
||||
|
||||
1. **Waiting for elements to appear**: Use `findBy*` queries when elements appear after some delay (e.g., after data fetching).
|
||||
2. **Waiting for conditions**: Use `waitFor()` to wait for arbitrary conditions to be met.
|
||||
3. **Waiting for elements to disappear**: Use `waitForElementToBeRemoved()` when elements should be removed after some action.
|
||||
|
||||
These utilities help you write reliable tests that properly handle timing in your application.
|
||||
|
||||
### Example
|
||||
|
||||
Consider a test for a user signing in with correct credentials:
|
||||
|
||||
```javascript
|
||||
test('User can sign in with correct credentials', async () => {
|
||||
// Typical test setup
|
||||
const user = userEvent.setup();
|
||||
await render(<App />);
|
||||
|
||||
// No need to use async here, components are already rendered
|
||||
expect(screen.getByRole('header', { name: 'Sign in to Hello World App!' })).toBeOnTheScreen();
|
||||
|
||||
// Using await as User Event requires it
|
||||
await user.type(screen.getByLabelText('Username'), 'admin');
|
||||
await user.type(screen.getByLabelText('Password'), 'admin1');
|
||||
await user.press(screen.getByRole('button', { name: 'Sign In' }));
|
||||
|
||||
// Using await as sign in operation is asynchronous
|
||||
expect(await screen.findByRole('header', { name: 'Welcome admin!' })).toBeOnTheScreen();
|
||||
|
||||
// Follow-up assertions do not need to be async, as we already waited for sign in operation to complete
|
||||
expect(
|
||||
screen.queryByRole('header', { name: 'Sign in to Hello World App' }),
|
||||
).not.toBeOnTheScreen();
|
||||
expect(screen.queryByLabelText('Username')).not.toBeOnTheScreen();
|
||||
expect(screen.queryByLabelText('Password')).not.toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
## Async utilities
|
||||
|
||||
There are several asynchronous utilities you might use in your tests.
|
||||
|
||||
### `findBy*` queries
|
||||
|
||||
The most common are the [`findBy*` queries](../api/queries.md#find-by). These are useful when waiting for a matching element to appear. They can be understood as a [`getBy*` queries](../api/queries.md#get-by) used in conjunction with a [`waitFor` function](../api/async-utilities.md#waitfor).
|
||||
|
||||
They accept the same predicates as `getBy*` queries like `findByRole`, `findByTest`, etc. They also have a multiple elements variant called [`findAllBy*`](../api/queries.md#find-all-by).
|
||||
|
||||
```typescript
|
||||
function findByRole: (
|
||||
role: TextMatch,
|
||||
queryOptions?: {
|
||||
// Query specific options
|
||||
}
|
||||
waitForOptions?: {
|
||||
timeout?: number;
|
||||
interval?: number;
|
||||
// ..
|
||||
}
|
||||
): Promise<TestInstance>;
|
||||
```
|
||||
|
||||
Each query has a default `timeout` value of 1000 ms and a default `interval` of 50 ms. Custom timeout and check intervals can be specified if needed, as shown below:
|
||||
|
||||
#### Example
|
||||
|
||||
```typescript
|
||||
const button = await screen.findByRole(
|
||||
'button',
|
||||
{ name: 'Start' },
|
||||
{ timeout: 1000, interval: 50 },
|
||||
);
|
||||
```
|
||||
|
||||
Alternatively, a default global `timeout` value can be set using the [`configure` function](../api/configuration.md#configure):
|
||||
|
||||
```typescript
|
||||
configure({ asyncUtilTimeout: timeout });
|
||||
```
|
||||
|
||||
### `waitFor` function
|
||||
|
||||
The `waitFor` function is another option, serving as a lower-level utility in more advanced cases.
|
||||
|
||||
```typescript
|
||||
function waitFor<T>(
|
||||
expectation: () => T,
|
||||
options?: {
|
||||
timeout: number;
|
||||
interval: number;
|
||||
},
|
||||
): Promise<T>;
|
||||
```
|
||||
|
||||
It accepts an `expectation` to be validated and repeats the check every defined interval until it no longer throws an error. Similarly to `findBy*` queries they accept `timeout` and `interval` options and have the same default values of 1000ms for timeout, and a checking interval of 50 ms.
|
||||
|
||||
#### Example
|
||||
|
||||
```typescript
|
||||
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1));
|
||||
```
|
||||
|
||||
If you want to use it with `getBy*` queries, use the `findBy*` queries instead, as they essentially do the same, but offer better developer experience.
|
||||
|
||||
### `waitForElementToBeRemoved` function
|
||||
|
||||
A specialized function, [`waitForElementToBeRemoved`](../api/async-utilities.md#waitforelementtoberemoved), is used to verify that a matching element was present but has since been removed.
|
||||
|
||||
```typescript
|
||||
function waitForElementToBeRemoved<T>(
|
||||
expectation: () => T,
|
||||
options?: {
|
||||
timeout: number;
|
||||
interval: number;
|
||||
},
|
||||
): Promise<T> {}
|
||||
```
|
||||
|
||||
This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true (not throw an error), only to turn invalid (start throwing errors) on subsequent runs. It operates using the same `timeout` and `interval` parameters as `findBy*` queries and `waitFor`.
|
||||
|
||||
#### Example
|
||||
|
||||
```typescript
|
||||
await waitForElementToBeRemoved(() => getByText('Hello World'));
|
||||
```
|
||||
|
||||
## Fake Timers
|
||||
|
||||
Asynchronous tests can take long to execute due to the delays introduced by asynchronous operations. To mitigate this, fake timers can be used. These are particularly useful when delays are mere waits, such as the 130 milliseconds wait introduced by the UserEvent `press()` event due to React Native runtime behavior or simulated 1000 wait in a API call mock. Fake timers allow for precise fast-forwarding through these wait periods.
|
||||
|
||||
Here are the basics of using [Jest fake timers](https://jestjs.io/docs/timer-mocks):
|
||||
|
||||
- Enable fake timers with: `jest.useFakeTimers()`
|
||||
- Disable fake timers with: `jest.useRealTimers()`
|
||||
- Advance fake timers forward with: `jest.advanceTimersByTime(interval)`
|
||||
- Run **all timers** to completion with: `jest.runAllTimers()`
|
||||
- Run **currently pending timers** to completion with: `jest.runOnlyPendingTimers()`
|
||||
|
||||
Be cautious when running all timers to completion as it might create an infinite loop if these timers schedule follow-up timers. In such cases, it's safer to use `jest.runOnlyPendingTimers()` to avoid ending up in an infinite loop of scheduled tasks.
|
||||
|
||||
You can use both built-in Jest fake timers, as well as [Sinon.JS fake timers](https://sinonjs.org/releases/latest/fake-timers/).
|
||||
|
||||
Note: you do not need to advance timers by hand when using User Event API, as it's automatically.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Custom `render` function
|
||||
|
||||
### Summary
|
||||
|
||||
RNTL exposes the `render` function as the primary entry point for tests. If you make complex, repeating setups for your tests, consider creating a custom render function. The idea is to encapsulate common setup steps and test wiring inside a render function suitable for your tests.
|
||||
|
||||
### Example
|
||||
|
||||
```tsx title=test-utils.ts
|
||||
// ...
|
||||
|
||||
interface RenderWithProvidersProps {
|
||||
user?: User | null;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export async function renderWithProviders<T>(
|
||||
ui: React.ReactElement<T>,
|
||||
options?: RenderWithProvidersProps,
|
||||
) {
|
||||
return await render(
|
||||
<UserProvider.Provider value={options?.user ?? null}>
|
||||
<ThemeProvider.Provider value={options?.theme ?? 'light'}>{ui}</ThemeProvider.Provider>
|
||||
</UserProvider.Provider>,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title=custom-render/index.test.tsx
|
||||
import { screen } from '@testing-library/react-native';
|
||||
import { renderWithProviders } from '../test-utils';
|
||||
// ...
|
||||
|
||||
test('renders WelcomeScreen with user', async () => {
|
||||
await renderWithProviders(<WelcomeScreen />, { user: { name: 'Jar-Jar' } });
|
||||
expect(screen.getByText(/hello Jar-Jar/i)).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
test('renders WelcomeScreen without user', async () => {
|
||||
await renderWithProviders(<WelcomeScreen />, { user: null });
|
||||
expect(screen.getByText(/hello stranger/i)).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
Example [full source code](https://github.com/callstack/react-native-testing-library/tree/main/examples/cookbook/custom-render).
|
||||
|
||||
### More info
|
||||
|
||||
#### Additional params
|
||||
|
||||
A custom render function might accept additional parameters to allow for setting up different start conditions for a test, e.g., the initial state for global state management.
|
||||
|
||||
```tsx title=SomeScreen.test.tsx
|
||||
test('renders SomeScreen for logged in user', async () => {
|
||||
await renderScreen(<SomeScreen />, { state: loggedInState });
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
#### Multiple functions
|
||||
|
||||
Depending on the situation, you may declare more than one custom render function. For example, you have one function for testing application flows and a second for testing individual screens.
|
||||
|
||||
```tsx title=test-utils.tsx
|
||||
function renderNavigator(ui, options);
|
||||
function renderScreen(ui, options);
|
||||
```
|
||||
|
||||
#### Async setup
|
||||
|
||||
Since `render` is async, your custom render function should be marked as `async` and use `await render()`. This pattern also makes it easy to add additional async setup if needed:
|
||||
|
||||
```tsx title=SomeScreen.test.tsx
|
||||
async function renderWithData<T>(ui: React.ReactElement<T>) {
|
||||
const data = await fetchTestData();
|
||||
return await render(<DataProvider value={data}>{ui}</DataProvider>);
|
||||
}
|
||||
|
||||
test('renders SomeScreen', async () => {
|
||||
await renderWithData(<SomeScreen />);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,375 @@
|
||||
# Network Requests
|
||||
|
||||
## Introduction
|
||||
|
||||
Mocking network requests is an essential part of testing React Native applications. By mocking
|
||||
network
|
||||
requests, you can control the data that is returned from the server and test how your application
|
||||
behaves in different scenarios, such as when the request is successful or when it fails.
|
||||
|
||||
In this guide, we will show you how to mock network requests and guard your test suits from unwanted
|
||||
and unmocked/unhandled network requests
|
||||
|
||||
> [!INFO]
|
||||
> To simulate a real-world scenario, we will use the [Random User Generator API](https://randomuser.me/) that provides random user data.
|
||||
|
||||
## Phonebook Example
|
||||
|
||||
Let's assume we have a simple phonebook application that
|
||||
uses [`fetch`](https://reactnative.dev/docs/network#using-fetch) for fetching Data from a server.
|
||||
In our case, we have a list of contacts and favorites that we want to display in our application.
|
||||
|
||||
This is how the root of the application looks like:
|
||||
|
||||
```tsx title=network-requests/Phonebook.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { User } from './types';
|
||||
import ContactsList from './components/ContactsList';
|
||||
import FavoritesList from './components/FavoritesList';
|
||||
import getAllContacts from './api/getAllContacts';
|
||||
import getAllFavorites from './api/getAllFavorites';
|
||||
|
||||
export default () => {
|
||||
const [usersData, setUsersData] = useState<User[]>([]);
|
||||
const [favoritesData, setFavoritesData] = useState<User[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const _getAllContacts = async () => {
|
||||
const _data = await getAllContacts();
|
||||
setUsersData(_data);
|
||||
};
|
||||
const _getAllFavorites = async () => {
|
||||
const _data = await getAllFavorites();
|
||||
setFavoritesData(_data);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await Promise.all([_getAllContacts(), _getAllFavorites()]);
|
||||
} catch (e) {
|
||||
const message = isErrorWithMessage(e) ? e.message : 'Something went wrong';
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <Text>An error occurred: {error}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FavoritesList users={favoritesData} />
|
||||
<ContactsList users={usersData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
We fetch the contacts from the server using the `getAllFavorites` function that utilizes `fetch`.
|
||||
|
||||
```tsx title=network-requests/api/getAllContacts.ts
|
||||
import { User } from '../types';
|
||||
|
||||
export default async (): Promise<User[]> => {
|
||||
const res = await fetch('https://randomuser.me/api/?results=25');
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error fetching contacts`);
|
||||
}
|
||||
const json = await res.json();
|
||||
return json.results;
|
||||
};
|
||||
```
|
||||
|
||||
We have similar function for fetching the favorites, but this time limiting the results to 10.
|
||||
|
||||
```tsx title=network-requests/api/getAllFavorites.ts
|
||||
import { User } from '../types';
|
||||
|
||||
export default async (): Promise<User[]> => {
|
||||
const res = await fetch('https://randomuser.me/api/?results=10');
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error fetching favorites`);
|
||||
}
|
||||
const json = await res.json();
|
||||
return json.results;
|
||||
};
|
||||
```
|
||||
|
||||
Our `FavoritesList` component is a simple component that displays the list of favorite contacts and
|
||||
their avatars horizontally.
|
||||
|
||||
```tsx title=network-requests/components/FavoritesList.tsx
|
||||
import {FlatList, Image, StyleSheet, Text, View} from 'react-native';
|
||||
import React, {useCallback} from 'react';
|
||||
import type {ListRenderItem} from '@react-native/virtualized-lists';
|
||||
import {User} from '../types';
|
||||
|
||||
export default ({users}: { users: User[] }) => {
|
||||
const renderItem: ListRenderItem<User> = useCallback(({item: {picture}}) => {
|
||||
return (
|
||||
<View style={styles.userContainer}>
|
||||
<Image
|
||||
source={{uri: picture.thumbnail}}
|
||||
style={styles.userImage}
|
||||
accessibilityLabel={'favorite-contact-avatar'}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (users.length === 0) return (
|
||||
<View style={styles.loaderContainer}>
|
||||
<Text>Figuring out your favorites...</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<Text>⭐My Favorites</Text>
|
||||
<FlatList<User>
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={users}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item, index) => `${index}-${item.id.value}`}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// Looking for styles?
|
||||
// Check examples/cookbook/app/advanced/components/FavoritesList.tsx
|
||||
const styles =
|
||||
...
|
||||
```
|
||||
|
||||
Our `ContactsList` component is similar to the `FavoritesList` component, but it displays the list
|
||||
of
|
||||
all contacts vertically.
|
||||
|
||||
```tsx title=network-requests/components/ContactsList.tsx
|
||||
import { FlatList, Image, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { ListRenderItem } from '@react-native/virtualized-lists';
|
||||
import { User } from '../types';
|
||||
|
||||
export default ({ users }: { users: User[] }) => {
|
||||
const renderItem: ListRenderItem<User> = useCallback(
|
||||
({ item: { name, email, picture, cell }, index }) => {
|
||||
const { title, first, last } = name;
|
||||
const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff';
|
||||
return (
|
||||
<View style={[{ backgroundColor }, styles.userContainer]}>
|
||||
<Image source={{ uri: picture.thumbnail }} style={styles.userImage} />
|
||||
<View>
|
||||
<Text>
|
||||
Name: {title} {first} {last}
|
||||
</Text>
|
||||
<Text>Email: {email}</Text>
|
||||
<Text>Mobile: {cell}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (users.length === 0) return <FullScreenLoader />;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<FlatList<User>
|
||||
data={users}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item, index) => `${index}-${item.id.value}`}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// Looking for styles or FullScreenLoader component?
|
||||
// Check examples/cookbook/app/advanced/components/ContactsList.tsx
|
||||
const FullScreenLoader = () => ...
|
||||
const styles = ...
|
||||
```
|
||||
|
||||
## Start testing with a simple test
|
||||
|
||||
In our initial test we would like to test if the `PhoneBook` component renders the `FavoritesList`
|
||||
and `ContactsList` components correctly.
|
||||
We will need to mock the network requests and their corresponding responses to ensure that the component behaves as
|
||||
expected. To mock the network requests we will use [MSW (Mock Service Worker)](https://mswjs.io/docs/getting-started).
|
||||
|
||||
> [!NOTE]
|
||||
> We recommend using the Mock Service Worker (MSW) library to declaratively mock API communication in your tests instead of stubbing `fetch`, or relying on third-party adapters.
|
||||
|
||||
> [!INFO]
|
||||
> You can install MSW by running `npm install msw --save-dev` or `yarn add msw --dev`.
|
||||
> More info regarding installation can be found in [MSW's getting started guide](https://mswjs.io/docs/getting-started#step-1-install).
|
||||
>
|
||||
> Please make sure you're also aware of [MSW's setup guide](https://mswjs.io/docs/integrations/react-native).
|
||||
> Please be minded that the MSW's setup guide is potentially incomplete and might contain discrepancies/missing pieces.
|
||||
|
||||
```tsx title=network-requests/Phonebook.test.tsx
|
||||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import PhoneBook from '../PhoneBook';
|
||||
import { User } from '../types';
|
||||
import {http, HttpResponse} from "msw";
|
||||
import {setupServer} from "msw/node";
|
||||
|
||||
// Define request handlers and response resolvers for random user API.
|
||||
// By default, we always return the happy path response.
|
||||
const handlers = [
|
||||
http.get('https://randomuser.me/api/*', () => {
|
||||
return HttpResponse.json(DATA);
|
||||
}),
|
||||
];
|
||||
|
||||
// Setup a request interception server with the given request handlers.
|
||||
const server = setupServer(...handlers);
|
||||
|
||||
// Enable API mocking via Mock Service Worker (MSW)
|
||||
beforeAll(() => server.listen());
|
||||
// Reset any runtime request handlers we may add during the tests
|
||||
afterEach(() => server.resetHandlers());
|
||||
// Disable API mocking after the tests are done
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('PhoneBook', () => {
|
||||
it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => {
|
||||
await render(<PhoneBook />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
|
||||
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
|
||||
expect(await screen.findByText('Email: ida.kristensen@example.com')).toBeOnTheScreen();
|
||||
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
|
||||
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
|
||||
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
const DATA: { results: User[] } = {
|
||||
results: [
|
||||
{
|
||||
name: {
|
||||
title: 'Mrs',
|
||||
first: 'Ida',
|
||||
last: 'Kristensen',
|
||||
},
|
||||
email: 'ida.kristensen@example.com',
|
||||
id: {
|
||||
name: 'CPR',
|
||||
value: '250562-5730',
|
||||
},
|
||||
picture: {
|
||||
large: 'https://randomuser.me/api/portraits/women/26.jpg',
|
||||
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
|
||||
},
|
||||
cell: '123-4567-890',
|
||||
},
|
||||
// For brevity, we have omitted the rest of the users, you can still find them in
|
||||
// examples/cookbook/app/network-requests/__tests__/test-utils.ts
|
||||
...
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
> [!INFO]
|
||||
> More info regarding how to describe the network using request handlers, intercepting a request and handling its response can be found in the [MSW's documentation](https://mswjs.io/docs/getting-started#step-2-describe).
|
||||
|
||||
## Testing error handling
|
||||
|
||||
As we are dealing with network requests, and things can go wrong, we should also cover the case when
|
||||
the API request fails. In this case, we would like to test how our application behaves when the API request fails.
|
||||
|
||||
> [!INFO]
|
||||
> The nature of the network can be highly dynamic, which makes it challenging to describe it completely in a fixed list of request handlers.
|
||||
> MSW provides us the means to override any particular network behavior using the designated `.use()` API.
|
||||
> More info can be found in [MSW's Network behavior overrides documentation](https://mswjs.io/docs/best-practices/network-behavior-overrides)
|
||||
|
||||
```tsx title=network-requests/Phonebook.test.tsx
|
||||
...
|
||||
|
||||
const mockServerFailureForGetAllContacts = () => {
|
||||
server.use(
|
||||
http.get('https://randomuser.me/api/', ({ request }) => {
|
||||
// Construct a URL instance out of the intercepted request.
|
||||
const url = new URL(request.url);
|
||||
// Read the "results" URL query parameter using the "URLSearchParams" API.
|
||||
const resultsLength = url.searchParams.get('results');
|
||||
// Simulate a server error for the get all contacts request.
|
||||
// We check if the "results" query parameter is set to "25"
|
||||
// to know it's the correct request to mock, in our case get all contacts.
|
||||
if (resultsLength === '25') {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
}
|
||||
// Return the default response for all other requests that match URL and verb. (in our case get favorites)
|
||||
return HttpResponse.json(DATA);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
describe('PhoneBook', () => {
|
||||
...
|
||||
it('fails to fetch all contacts and renders error message', async () => {
|
||||
mockServerFailureForGetAllContacts();
|
||||
await render(<PhoneBook />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
|
||||
expect(
|
||||
await screen.findByText(/an error occurred: error fetching contacts/i),
|
||||
).toBeOnTheScreen();
|
||||
});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## Global guarding against unwanted API requests
|
||||
|
||||
As mistakes may happen, we might forget to mock a network request in one of our tests in the future.
|
||||
To prevent us from happening, and alert when a certain network request is left unhandled, you may choose to
|
||||
move MSW's server management from `PhoneBook.test.tsx` to Jest's setup file via [`setupFilesAfterEnv`](https://jestjs.io/docs/configuration#setupfilesafterenv-array).
|
||||
|
||||
```tsx title=examples/cookbook/jest-setup.ts
|
||||
// Enable API mocking via Mock Service Worker (MSW)
|
||||
beforeAll(() => server.listen());
|
||||
// Reset any runtime request handlers we may add during the tests
|
||||
afterEach(() => server.resetHandlers());
|
||||
// Disable API mocking after the tests are done
|
||||
afterAll(() => server.close());
|
||||
|
||||
// ... rest of your setup file
|
||||
```
|
||||
|
||||
This setup will ensure you have the MSW server running before any test suite starts and stops it after all tests are done.
|
||||
Which will result in a warning in the console if you forget to mock an API request in your test suite.
|
||||
|
||||
```bash
|
||||
[MSW] Warning: intercepted a request without a matching request handler:
|
||||
• GET https://randomuser.me/api/?results=25?results=25
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Testing a component that makes network requests in combination with MSW takes some initial preparation to configure and describe the overridden networks.
|
||||
We can achieve that by using MSW's request handlers and intercepting APIs.
|
||||
|
||||
Once up and running we gain full grip over the network requests, their responses, statuses.
|
||||
Doing so is crucial to be able to test how our application behaves in different
|
||||
scenarios, such as when the request is successful or when it fails.
|
||||
|
||||
When global configuration is in place, MSW's will also warn us when an unhandled network requests has occurred throughout a test suite.
|
||||
|
||||
## Further Reading and Alternatives
|
||||
|
||||
Explore more advanced scenarios for mocking network requests with MSW:
|
||||
|
||||
- MSW's Basics - [Intercepting requests](https://mswjs.io/docs/basics/intercepting-requests) and/or [Mocking responses](https://mswjs.io/docs/basics/mocking-responses)
|
||||
- MSW's Network behavior - how to describe [REST](https://mswjs.io/docs/network-behavior/rest) and/or [GraphQL](https://mswjs.io/docs/network-behavior/graphql) APIs
|
||||
@@ -0,0 +1,587 @@
|
||||
# Common Mistakes with React Native Testing Library
|
||||
|
||||
> **Note:** This guide is adapted from Kent C. Dodds' article ["Common mistakes with React Testing Library"](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) for React Native Testing Library v14. The original article focuses on web React, but the principles apply to React Native as well. This adaptation includes React Native-specific examples, async API usage (v14), and ARIA-compatible accessibility attributes.
|
||||
|
||||
React Native Testing Library guiding principle is:
|
||||
|
||||
> "The more your tests resemble the way your software is used, the more confidence they can give you."
|
||||
|
||||
This guide outlines some common mistakes people make when using React Native Testing Library and how to avoid them.
|
||||
|
||||
## Using the wrong query
|
||||
|
||||
**Importance: high**
|
||||
|
||||
React Native Testing Library provides several query types. Here's the priority order:
|
||||
|
||||
1. **Queries that reflect user experience:**
|
||||
- `getByRole` - most accessible
|
||||
- `getByLabelText` - accessible label
|
||||
- `getByPlaceholderText` - `TextInput` placeholder text
|
||||
- `getByText` - text content
|
||||
- `getByDisplayValue` - `TextInput` input value
|
||||
|
||||
2. **Semantic queries:**
|
||||
- `getByTestId` - only if nothing else works
|
||||
|
||||
Here's an example of using the right query:
|
||||
|
||||
```tsx
|
||||
import { TextInput, View } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('finds input by label', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<TextInput aria-label="Username" placeholder="Enter username" value="" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ✅ Good - uses accessible label
|
||||
const input = screen.getByLabelText('Username');
|
||||
|
||||
// ✅ Also good - uses placeholder
|
||||
const inputByPlaceholder = screen.getByPlaceholderText('Enter username');
|
||||
|
||||
// ❌ Bad - uses testID when accessible queries work
|
||||
// const input = screen.getByTestId('username-input');
|
||||
});
|
||||
```
|
||||
|
||||
## Not using `*ByRole` query most of the time
|
||||
|
||||
**Importance: high**
|
||||
|
||||
`getByRole` is the most accessible query and should be your first choice. It queries elements by their semantic role:
|
||||
|
||||
```tsx
|
||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('uses role queries', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<Pressable role="button">
|
||||
<Text>Submit</Text>
|
||||
</Pressable>
|
||||
<TextInput role="searchbox" aria-label="Search" placeholder="Search..." />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ✅ Good - uses role query
|
||||
const button = screen.getByRole('button', { name: 'Submit' });
|
||||
const searchbox = screen.getByRole('searchbox', { name: 'Search' });
|
||||
|
||||
expect(button).toBeOnTheScreen();
|
||||
expect(searchbox).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
Common roles in React Native include:
|
||||
|
||||
- `button` - pressable elements
|
||||
- `text` - static text
|
||||
- `header` / `heading` - headers
|
||||
- `searchbox` - search inputs
|
||||
- `switch` - toggle switches
|
||||
- `checkbox` - checkboxes
|
||||
- `radio` - radio buttons
|
||||
- And more...
|
||||
|
||||
Note: React Native supports both ARIA-compatible (`role`) and legacy (`accessibilityRole`) props. Prefer `role` for consistency with web standards.
|
||||
|
||||
## Using the wrong assertion
|
||||
|
||||
**Importance: high**
|
||||
|
||||
React Native Testing Library provides built-in Jest matchers. Make sure you're using the right ones:
|
||||
|
||||
```tsx
|
||||
import { Pressable, Text } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('button is disabled', async () => {
|
||||
await render(
|
||||
<Pressable role="button" aria-disabled>
|
||||
<Text>Submit</Text>
|
||||
</Pressable>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Submit' });
|
||||
|
||||
// ✅ Good - uses RNTL matcher
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
// ❌ Bad - doesn't use RNTL matcher
|
||||
expect(button.props['aria-disabled']).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
Common matchers include:
|
||||
|
||||
- `toBeOnTheScreen()` - checks if element is rendered (replaces `toBeInTheDocument()`)
|
||||
- `toBeDisabled()` - checks if element is disabled
|
||||
- `toHaveTextContent()` - checks text content
|
||||
- `toHaveAccessibleName()` - checks accessible name
|
||||
- And more...
|
||||
|
||||
## Using `query*` variants for anything except checking for non-existence
|
||||
|
||||
**Importance: high**
|
||||
|
||||
Use `queryBy*` only when checking that an element doesn't exist:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('checks non-existence', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<Text>Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ✅ Good - uses queryBy for non-existence check
|
||||
expect(screen.queryByText('Goodbye')).not.toBeOnTheScreen();
|
||||
|
||||
// ❌ Bad - uses queryBy when element should exist
|
||||
// const element = screen.queryByText('Hello');
|
||||
// expect(element).toBeOnTheScreen();
|
||||
|
||||
// ✅ Good - uses getBy when element should exist
|
||||
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
## Using `waitFor` to wait for elements that can be queried with `find*`
|
||||
|
||||
**Importance: high**
|
||||
|
||||
Use `findBy*` queries instead of `waitFor` + `getBy*`:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen, waitFor } from '@testing-library/react-native';
|
||||
|
||||
test('waits for element', async () => {
|
||||
const Component = () => {
|
||||
const [show, setShow] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => setShow(true), 100);
|
||||
}, []);
|
||||
|
||||
return <View>{show && <Text>Loaded</Text>}</View>;
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
// ✅ Good - uses findBy query
|
||||
const element = await screen.findByText('Loaded');
|
||||
expect(element).toBeOnTheScreen();
|
||||
|
||||
// ❌ Bad - uses waitFor + getBy
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByText('Loaded')).toBeOnTheScreen();
|
||||
// });
|
||||
});
|
||||
```
|
||||
|
||||
## Performing side-effects in `waitFor`
|
||||
|
||||
**Importance: high**
|
||||
|
||||
Don't perform side-effects in `waitFor` callbacks:
|
||||
|
||||
```tsx
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react-native';
|
||||
|
||||
test('avoids side effects in waitFor', async () => {
|
||||
const Component = () => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
return (
|
||||
<View>
|
||||
<Pressable role="button" onPress={() => setCount(count + 1)}>
|
||||
<Text>Increment</Text>
|
||||
</Pressable>
|
||||
<Text>Count: {count}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// ❌ Bad - side effect in waitFor
|
||||
// await waitFor(async () => {
|
||||
// await fireEvent.press(button);
|
||||
// expect(screen.getByText('Count: 1')).toBeOnTheScreen();
|
||||
// });
|
||||
|
||||
// ✅ Good - side effect outside waitFor
|
||||
await fireEvent.press(button);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Count: 1')).toBeOnTheScreen();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Using `container` to query for elements
|
||||
|
||||
**Importance: high**
|
||||
|
||||
React Native Testing Library provides a `container` object that has a `queryAll` method, but you should avoid using it directly:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render } from '@testing-library/react-native';
|
||||
|
||||
test('finds element incorrectly', async () => {
|
||||
const { container } = await render(
|
||||
<View>
|
||||
<Text testID="message">Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ❌ Bad - using container.queryAll directly
|
||||
const element = container.queryAll((node) => node.props.testID === 'message')[0];
|
||||
|
||||
// ✅ Good - use proper queries
|
||||
// const element = screen.getByTestId('message');
|
||||
});
|
||||
```
|
||||
|
||||
Instead, use the proper query methods from `screen` or the `render` result. The `container` is a low-level API that you rarely need.
|
||||
|
||||
## Passing an empty callback to `waitFor`
|
||||
|
||||
**Importance: high**
|
||||
|
||||
Don't pass an empty callback to `waitFor`:
|
||||
|
||||
```tsx
|
||||
import { View } from 'react-native';
|
||||
import { render, waitFor } from '@testing-library/react-native';
|
||||
|
||||
test('waits correctly', async () => {
|
||||
await render(<View testID="test" />);
|
||||
|
||||
// ❌ Bad - empty callback
|
||||
// await waitFor(() => {});
|
||||
|
||||
// ✅ Good - meaningful assertion
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test')).toBeOnTheScreen();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Not using `screen`
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
You can get all the queries from the `render` result:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render } from '@testing-library/react-native';
|
||||
|
||||
test('renders component', async () => {
|
||||
const { getByText } = await render(
|
||||
<View>
|
||||
<Text>Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
expect(getByText('Hello')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
But you can also get them from the `screen` object:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('renders component', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<Text>Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
Using `screen` has several benefits:
|
||||
|
||||
1. You don't need to destructure `getByText` from `render`
|
||||
2. It's more consistent with the Testing Library ecosystem
|
||||
|
||||
## Wrapping things in `act` unnecessarily
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
React Native Testing Library's `render`, `renderHook`, `userEvent`, and `fireEvent` are already wrapped in `act`, so you don't need to wrap them yourself:
|
||||
|
||||
```tsx
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { render, fireEvent, screen } from '@testing-library/react-native';
|
||||
|
||||
test('updates on press', async () => {
|
||||
const Component = () => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
return (
|
||||
<View>
|
||||
<Pressable role="button" onPress={() => setCount(count + 1)}>
|
||||
<Text>Count: {count}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// ✅ Good - fireEvent is already wrapped in act
|
||||
await fireEvent.press(button);
|
||||
|
||||
expect(screen.getByText('Count: 1')).toBeOnTheScreen();
|
||||
|
||||
// ❌ Bad - unnecessary act wrapper
|
||||
// await act(async () => {
|
||||
// await fireEvent.press(button);
|
||||
// });
|
||||
});
|
||||
```
|
||||
|
||||
## Not using User Event API
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
`userEvent` provides a more realistic way to simulate user interactions:
|
||||
|
||||
```tsx
|
||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
||||
import { render, screen, userEvent } from '@testing-library/react-native';
|
||||
|
||||
test('uses userEvent', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const Component = () => {
|
||||
const [value, setValue] = React.useState('');
|
||||
return (
|
||||
<View>
|
||||
<TextInput aria-label="Name" value={value} onChangeText={setValue} />
|
||||
<Pressable role="button" onPress={() => setValue('')}>
|
||||
<Text>Clear</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
const input = screen.getByLabelText('Name');
|
||||
const button = screen.getByRole('button', { name: 'Clear' });
|
||||
|
||||
// ✅ Good - uses userEvent for realistic interactions
|
||||
await user.type(input, 'John');
|
||||
expect(input).toHaveValue('John');
|
||||
|
||||
await user.press(button);
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
```
|
||||
|
||||
`userEvent` methods are async and must be awaited. Available methods include:
|
||||
|
||||
- `press()` - simulates a press
|
||||
- `longPress()` - simulates long press
|
||||
- `type()` - simulates typing
|
||||
- `clear()` - clears text input
|
||||
- `paste()` - simulates pasting
|
||||
- `scrollTo()` - simulates scrolling
|
||||
|
||||
## Not querying by text
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
In React Native, text is rendered in `<Text>` components. You should query by the text content that users see:
|
||||
|
||||
```tsx
|
||||
import { Text, View } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('finds text correctly', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<Text>Hello World</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ✅ Good - queries by visible text
|
||||
expect(screen.getByText('Hello World')).toBeOnTheScreen();
|
||||
|
||||
// ❌ Bad - queries by testID when text is available
|
||||
// expect(screen.getByTestId('greeting')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
## Not using Testing Library ESLint plugins
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
There's an ESLint plugin for Testing Library: [`eslint-plugin-testing-library`](https://github.com/testing-library/eslint-plugin-testing-library). This plugin can help you avoid common mistakes and will automatically fix your code in many cases.
|
||||
|
||||
You can install it with:
|
||||
|
||||
```bash
|
||||
yarn add --dev eslint-plugin-testing-library
|
||||
```
|
||||
|
||||
And configure it in your `eslint.config.js` (flat config):
|
||||
|
||||
```js
|
||||
import testingLibrary from 'eslint-plugin-testing-library';
|
||||
|
||||
export default [testingLibrary.configs['flat/react']];
|
||||
```
|
||||
|
||||
Note: Unlike React Testing Library, React Native Testing Library has built-in Jest matchers, so you don't need `eslint-plugin-jest-dom`.
|
||||
|
||||
## Using `cleanup`
|
||||
|
||||
**Importance: medium**
|
||||
|
||||
React Native Testing Library automatically cleans up after each test. You don't need to call `cleanup()` manually unless you're using the `pure` export (which doesn't include automatic cleanup).
|
||||
|
||||
If you want to disable automatic cleanup for a specific test, you can use:
|
||||
|
||||
```tsx
|
||||
import { render } from '@testing-library/react-native/pure';
|
||||
|
||||
test('does not cleanup', async () => {
|
||||
// This test won't cleanup automatically
|
||||
await render(<MyComponent />);
|
||||
// ... your test
|
||||
});
|
||||
```
|
||||
|
||||
But in most cases, you don't need to worry about cleanup at all - it's handled automatically.
|
||||
|
||||
## Using `get*` variants as assertions
|
||||
|
||||
**Importance: low**
|
||||
|
||||
`getBy*` queries throw errors when elements aren't found, so they work as assertions. However, for better error messages, you might want to combine them with explicit matchers:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('uses getBy as assertion', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<Text>Hello</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// ✅ Good - getBy throws if not found, so it's an assertion
|
||||
const element = screen.getByText('Hello');
|
||||
expect(element).toBeOnTheScreen();
|
||||
|
||||
// ✅ Also good - more explicit
|
||||
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
||||
|
||||
// ❌ Bad - redundant assertion
|
||||
// const element = screen.getByText('Hello');
|
||||
// expect(element).not.toBeNull(); // getBy already throws if null
|
||||
});
|
||||
```
|
||||
|
||||
## Having multiple assertions in a single `waitFor` callback
|
||||
|
||||
**Importance: low**
|
||||
|
||||
Keep `waitFor` callbacks focused on a single assertion:
|
||||
|
||||
```tsx
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen, waitFor } from '@testing-library/react-native';
|
||||
|
||||
test('waits with single assertion', async () => {
|
||||
const Component = () => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => setCount(1), 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Count: {count}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
// ✅ Good - single assertion per waitFor
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Count: 1')).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
// If you need multiple assertions, do them after waitFor
|
||||
expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');
|
||||
|
||||
// ❌ Bad - multiple assertions in waitFor
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByText('Count: 1')).toBeOnTheScreen();
|
||||
// expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');
|
||||
// });
|
||||
});
|
||||
```
|
||||
|
||||
## Using `wrapper` as the variable name
|
||||
|
||||
**Importance: low**
|
||||
|
||||
This is not really a "mistake" per se, but it's a common pattern that can be improved. When you use the `wrapper` option in `render`, you might be tempted to name your wrapper component `Wrapper`:
|
||||
|
||||
```tsx
|
||||
import { View } from 'react-native';
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('renders with wrapper', async () => {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<View testID="wrapper">{children}</View>
|
||||
);
|
||||
|
||||
await render(<View testID="content">Content</View>, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('content')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
This works fine, but it's more conventional to name it something more descriptive like `ThemeProvider` or `AllTheProviders` (if you're wrapping with multiple providers). This makes it clearer what the wrapper is doing.
|
||||
|
||||
## Summary
|
||||
|
||||
The key principles to remember:
|
||||
|
||||
1. **Use the right query** - Prefer `getByRole` as your first choice, use `findBy*` for async elements, and `queryBy*` only for checking non-existence
|
||||
2. **Use proper assertions** - Use RNTL's built-in matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.) instead of asserting on props directly
|
||||
3. **Handle async operations correctly** - Always `await` `render()`, `renderHook`, `fireEvent`,and `userEvent` methods
|
||||
4. **Use `waitFor` correctly** - Avoid side-effects in callbacks, use `findBy*` instead when possible, and keep callbacks focused
|
||||
5. **Follow accessibility best practices** - Prefer ARIA attributes (`role`, `aria-label`) over `accessibility*` props
|
||||
6. **Organize code well** - Use `screen` over destructuring, prefer `userEvent` over `fireEvent`, and don't use `cleanup()`
|
||||
|
||||
By following these principles, your tests will be more maintainable, accessible, and reliable.
|
||||
@@ -0,0 +1,125 @@
|
||||
# How should I query?
|
||||
|
||||
React Native Testing Library provides various query types for finding views in tests. The number of queries can be confusing. This guide helps you pick the right queries for your test scenarios.
|
||||
|
||||
## Query parts
|
||||
|
||||
Each query is composed of two parts: variant and predicate, which are separated by the `by` word in the middle of the query.
|
||||
|
||||
Consider the following query:
|
||||
|
||||
```ts
|
||||
getByRole();
|
||||
```
|
||||
|
||||
For this query, `getBy*` is the query variant, and `*ByRole` is the predicate.
|
||||
|
||||
## Query variant
|
||||
|
||||
The query variants describe the expected number (and timing) of matching elements, so they differ in their return type.
|
||||
|
||||
| Variant | Assertion | Return type | Is Async? |
|
||||
| ----------------------------------------------- | ----------------------------- | ------------------------------------- | --------- |
|
||||
| [`getBy*`](../api/queries.md#get-by) | Exactly one matching element | `TestInstance` | No |
|
||||
| [`getAllBy*`](../api/queries.md#get-all-by) | At least one matching element | `Array<TestInstance>` | No |
|
||||
| [`queryBy*`](../api/queries.md#query-by) | Zero or one matching element | <code>TestInstance | null</code> | No |
|
||||
| [`queryAllBy*`](../api/queries.md#query-all-by) | No assertion | `Array<TestInstance>` | No |
|
||||
| [`findBy*`](../api/queries.md#find-by) | Exactly one matching element | `Promise<TestInstance>` | Yes |
|
||||
| [`findAllBy*`](../api/queries.md#find-all-by) | At least one matching element | `Promise<Array<TestInstance>>` | Yes |
|
||||
|
||||
Queries work as implicit assertions on the number of matching elements and will throw an error when the assertion fails.
|
||||
|
||||
### Idiomatic query variants
|
||||
|
||||
Idiomatic query variants clarify test intent and the expected number of matching elements. They will also throw helpful errors if assertions fail to help diagnose the issues.
|
||||
|
||||
Here are general guidelines for picking idiomatic query variants:
|
||||
|
||||
1. Use `getBy*` in the most common case when you expect a **single matching element**. Use other queries only in more specific cases.
|
||||
2. Use `findBy*` when an element is not yet in the element tree, but you expect it to be there as a **result of some asynchronous action**.
|
||||
3. Use `getAllBy*` (and `findAllBy*` for async) if you expect **more than one matching element**, e.g. in a list.
|
||||
4. Use `queryBy*` only when element **should not exist** to use it together with e.g. [`not.toBeOnTheScreen()`](../api/jest-matchers.md#tobeonthescreen) matcher.
|
||||
|
||||
Avoid using `queryAllBy*` in regular tests, as it provides no assertions on the number of matching elements. You may still find it useful when building reusable custom testing tools.
|
||||
|
||||
## Query predicate
|
||||
|
||||
The query predicate describes how you decide whether to match the given element.
|
||||
|
||||
| Predicate | Supported elements | Inspected props |
|
||||
| ------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| [`*ByRole`](../api/queries.md#by-role) | all host elements | `role`, `accessibilityRole`,<br /> optional: accessible name, accessibility state and value |
|
||||
| [`*ByLabelText`](../api/queries.md#by-label-text) | all host elements | `aria-label`, `aria-labelledby`,<br /> `accessibilityLabel`, `accessibilityLabelledBy`,<br /> `alt` (for `Image`) |
|
||||
| [`*ByDisplayValue`](../api/queries.md#by-display-value) | `TextInput` | `value`, `defaultValue` |
|
||||
| [`*ByPlaceholderText`](../api/queries.md#by-placeholder-text) | `TextInput` | `placeholder` |
|
||||
| [`*ByText`](../api/queries.md#by-text) | `Text` | `children` (text content) |
|
||||
| [`*ByHintText`](../api/queries.md#by-hint-text) | all host elements | `accessibilityHint` |
|
||||
| [`*ByTestId`](../api/queries.md#by-test-id) | all host elements | `testID` |
|
||||
|
||||
### Idiomatic query predicates
|
||||
|
||||
Choosing the right query predicate helps express test intent and makes tests resemble how users interact with your code (components, screens, etc.), following our [Guiding Principles](https://testing-library.com/docs/guiding-principles). Most predicates also promote proper accessibility props, which add a semantic layer on top of an element tree composed primarily of [`View`](https://reactnative.dev/docs/view) elements.
|
||||
|
||||
Use query predicates in the following order of priority:
|
||||
|
||||
### 1. By Role query
|
||||
|
||||
The [`*ByRole`](../api/queries.md#by-role) predicate starts with the semantic role of the element and can be narrowed down with additional options. React Native has two role systems: the web/ARIA-compatible one based on [`role`](https://reactnative.dev/docs/accessibility#role) prop and the traditional one based on [`accessibilityRole`](https://reactnative.dev/docs/accessibility#accessibilityrole) prop. You can use either.
|
||||
|
||||
In most cases, you need to set accessibility roles explicitly (or your component library can set some of them for you). These roles allow assistive technologies (like screen readers) and testing code to understand your view hierarchy better.
|
||||
|
||||
Some frequently used roles include:
|
||||
|
||||
- `alert` - important text to be presented to the user, e.g., error message
|
||||
- `button`
|
||||
- `checkbox` & `switch` - on/off controls
|
||||
- `heading` (`header`) - header for content section, e.g., the title of navigation bar
|
||||
- `img` (`image`)
|
||||
- `link`
|
||||
- `menu` & `menuitem`
|
||||
- `progressbar`
|
||||
- `radiogroup` & `radio`
|
||||
- `searchbox` (`search`)
|
||||
- `slider` (`adjustable`)
|
||||
- `summary`
|
||||
- `tablist` & `tab`
|
||||
- `text` - static text that cannot change
|
||||
- `toolbar` - container for action buttons
|
||||
|
||||
#### Name option
|
||||
|
||||
Frequently, you will want to add the [`name`](../api/queries.md#by-role-options) option, which will match both the element's role and its accessible name (= element's accessibility label or text content).
|
||||
|
||||
Here are a couple of examples:
|
||||
|
||||
- start button: `getByRole("button", { name: "Start" })`
|
||||
- silent mode switch: `getByRole("switch", { name: "Silent Mode" })`
|
||||
- screen header: `getByRole("header", { name: "Settings" })`
|
||||
- undo menu item: `getByRole("menuitem", { name: "Undo" })`
|
||||
- error messages: `getByRole("alert", { name: /Not logged in/ })`
|
||||
|
||||
### 2. Text input queries
|
||||
|
||||
Querying [`TextInput`](https://reactnative.dev/docs/textinput) elements presents a unique challenge as there is no separate role for `TextInput` elements. There is a `searchbox`/`search` role, which can be assigned to `TextInput`, but it should be only used in the context of search inputs, leaving other text inputs without a role to query with.
|
||||
|
||||
Therefore, you can use the following queries to find relevant text inputs:
|
||||
|
||||
1. [`*ByLabelText`](../api/queries.md#by-label-text) - will match the accessibility label of the element. This query will match any host elements, including `TextInput` elements.
|
||||
2. [`*ByPlaceholderText`](../api/queries.md#by-placeholder-text) - will match the placeholder of `TextInput` element. This query will match only `TextInput` elements.
|
||||
3. [`*ByDisplayValue`](../api/queries.md#by-display-value) - will the current (or default) value of `TextInput` element. This query will match only `TextInput` elements.
|
||||
|
||||
### 3. Other accessible queries
|
||||
|
||||
These queries reflect the apps' user experience, both visual and through assistive technologies (e.g. screen reader).
|
||||
|
||||
These queries include:
|
||||
|
||||
- [`*ByText`](../api/queries.md#by-text) - will match the text content of the element. This query will match only `Text` elements.
|
||||
- [`*ByLabelText`](../api/queries.md#by-label-text) - will match the accessibility label of the element.
|
||||
- [`*ByHintText`](../api/queries.md#by-hint-text) - will match the accessibility hint of the element.
|
||||
|
||||
### 4. Test ID query
|
||||
|
||||
As a final predicate, you can use the `testID` prop to find relevant views. Using the [`*ByTestId`](../api/queries.md#by-test-id) predicate offers the most flexibility, but at the same time, it does not represent the user experience, as users are not aware of test IDs.
|
||||
|
||||
Note that using test IDs is common in end-to-end testing due to various issues with querying views through other means **in that specific context**. For integration and component tests, use the recommended RNTL queries to make tests more reliable and resilient.
|
||||
@@ -0,0 +1,247 @@
|
||||
# LLM Guidelines for React Native Testing Library
|
||||
|
||||
Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14.
|
||||
|
||||
## Agent docs in the package
|
||||
|
||||
RNTL ships package-specific documentation for coding agents in `node_modules/@testing-library/react-native/docs/`.
|
||||
Add this snippet to your project's `AGENTS.md` or `CLAUDE.md` file so agents use the docs for the installed package version:
|
||||
|
||||
```md
|
||||
<!-- BEGIN:react-native-testing-library-agent-rules -->
|
||||
|
||||
# React Native Testing Library in this project
|
||||
|
||||
This project uses `@testing-library/react-native`. Its APIs and testing conventions can differ from your training data.
|
||||
Before writing or changing RNTL tests, read the relevant guide in
|
||||
`node_modules/@testing-library/react-native/docs/`, starting with
|
||||
`node_modules/@testing-library/react-native/docs/guides/llm-guidelines.md`.
|
||||
Prefer those package docs over stale assumptions, and follow deprecation notices.
|
||||
|
||||
<!-- END:react-native-testing-library-agent-rules -->
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
|
||||
### render
|
||||
|
||||
```tsx
|
||||
const result = await render(<Component />, options?);
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| --------- | ---------------------------------------------------------------- |
|
||||
| `wrapper` | React component to wrap the rendered component (e.g., providers) |
|
||||
|
||||
| Return | Description |
|
||||
| --------------------- | ------------------------------------------------ |
|
||||
| `rerender(component)` | Re-render with a new component (async) |
|
||||
| `unmount()` | Unmount the rendered component (async) |
|
||||
| `toJSON()` | Get JSON representation for snapshots |
|
||||
| `debug(options?)` | Print the component tree to console |
|
||||
| `container` | Root host element of the rendered tree |
|
||||
| `root` | First child host element (your component's root) |
|
||||
|
||||
### screen
|
||||
|
||||
**Prefer `screen`** over destructuring from `render()`. Provides all query methods after `render()` is called.
|
||||
|
||||
```tsx
|
||||
await render(<Component />);
|
||||
screen.getByRole('button'); // Access queries via screen
|
||||
```
|
||||
|
||||
### renderHook
|
||||
|
||||
```tsx
|
||||
const { result, rerender, unmount } = await renderHook(() => useMyHook(), options?);
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| `initialProps` | Initial props passed to the hook |
|
||||
| `wrapper` | React component to wrap the hook (e.g., providers) |
|
||||
|
||||
| Return | Description |
|
||||
| ------------------ | ------------------------------------- |
|
||||
| `result.current` | Current return value of the hook |
|
||||
| `rerender(props?)` | Re-render hook with new props (async) |
|
||||
| `unmount()` | Unmount the hook (async) |
|
||||
|
||||
## Query Selection
|
||||
|
||||
- **Prefer `getByRole`** as first choice for querying elements
|
||||
- **Query priority**: `getByRole` → `getByLabelText` → `getByPlaceholderText` → `getByText` → `getByDisplayValue` → `getByTestId` (last resort)
|
||||
- **Use `findBy*`** for elements that appear asynchronously (after API calls, timeouts, state updates)
|
||||
- **Use `queryBy*` ONLY** for checking non-existence (with `.not.toBeOnTheScreen()`)
|
||||
- **Never use `getBy*`** for non-existence checks
|
||||
- **Avoid `container.queryAll()`** - use `screen` queries instead
|
||||
- **Query by visible text**, not `testID` when text is available
|
||||
|
||||
## Assertions
|
||||
|
||||
- **Use RNTL matchers** - prefer semantic matchers over prop assertions
|
||||
- **Combine queries with matchers**: `expect(screen.getByText('Hello')).toBeOnTheScreen()`
|
||||
- **No redundant null checks** - `getBy*` already throws if not found
|
||||
|
||||
## Jest Matchers Reference
|
||||
|
||||
| Matcher | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `toBeOnTheScreen()` | Element is present in the element tree |
|
||||
| `toBeVisible()` | Element is visible (checks style, `aria-hidden`, `accessibilityElementsHidden`, ancestors) |
|
||||
| `toBeEmptyElement()` | Element has no children or text content |
|
||||
| `toContainElement(element)` | Element contains another element |
|
||||
| `toBeEnabled()` | Element is not disabled (checks `aria-disabled`, `accessibilityState`, ancestors) |
|
||||
| `toBeDisabled()` | Element has `aria-disabled` or `accessibilityState={{ disabled: true }}` (checks ancestors) |
|
||||
| `toBeBusy()` | Element has `aria-busy` or `accessibilityState={{ busy: true }}` |
|
||||
| `toBeChecked()` | Element has `aria-checked` or `accessibilityState={{ checked: true }}` |
|
||||
| `toBePartiallyChecked()` | Element has `aria-checked="mixed"` or `accessibilityState={{ checked: 'mixed' }}` |
|
||||
| `toBeSelected()` | Element has `aria-selected` or `accessibilityState={{ selected: true }}` |
|
||||
| `toBeExpanded()` | Element has `aria-expanded` or `accessibilityState={{ expanded: true }}` |
|
||||
| `toBeCollapsed()` | Element has `aria-expanded={false}` or `accessibilityState={{ expanded: false }}` |
|
||||
| `toHaveTextContent(text)` | Element has matching text content |
|
||||
| `toHaveDisplayValue(value)` | TextInput has matching display value |
|
||||
| `toHaveAccessibleName(name?)` | Element has matching `aria-label`, `accessibilityLabel`, or text content |
|
||||
| `toHaveAccessibilityValue(value)` | Element has matching `aria-value*` or `accessibilityValue` |
|
||||
| `toHaveStyle(style)` | Element has matching style |
|
||||
| `toHaveProp(name, value?)` | Element has prop (use semantic matchers when possible) |
|
||||
|
||||
## User Interactions
|
||||
|
||||
**Prefer `userEvent`** over `fireEvent` for realistic user interaction simulation. `userEvent` triggers the complete event sequence that real users would produce.
|
||||
|
||||
### userEvent (Preferred)
|
||||
|
||||
```tsx
|
||||
const user = userEvent.setup();
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `user.press(element)` | Press an element (triggers `pressIn`, `pressOut`, `press`) |
|
||||
| `user.longPress(element, options?)` | Long press with optional `{ duration }` |
|
||||
| `user.type(element, text, options?)` | Type into TextInput (triggers `focus`, `keyPress`, `change`, `changeText` per char) |
|
||||
| `user.clear(element)` | Clear TextInput (select all + backspace) |
|
||||
| `user.paste(element, text)` | Paste text into TextInput |
|
||||
| `user.scrollTo(element, options)` | Scroll a ScrollView with `{ y }` or `{ x }` offset |
|
||||
|
||||
### fireEvent (Low-level)
|
||||
|
||||
Use only when `userEvent` doesn't support the event or when you need direct control.
|
||||
|
||||
| Method | Description |
|
||||
| ---------------------------------------- | --------------------------------------------- |
|
||||
| `fireEvent(element, eventName, ...data)` | Fire any event by name |
|
||||
| `fireEvent.press(element)` | Fire `onPress` only (no `pressIn`/`pressOut`) |
|
||||
| `fireEvent.changeText(element, text)` | Fire `onChangeText` directly |
|
||||
| `fireEvent.scroll(element, eventData)` | Fire `onScroll` with event data |
|
||||
|
||||
## Async/Await (v14)
|
||||
|
||||
- **Always `await`**: `render()`, `fireEvent.*`, `renderHook()`, `userEvent.*`
|
||||
- **Make test functions `async`**: `test('name', async () => { ... })`
|
||||
- **Don't wrap in `act()`** - `render` and `fireEvent` handle it internally
|
||||
|
||||
## waitFor Usage
|
||||
|
||||
- **Use `findBy*`** instead of `waitFor` + `getBy*` when waiting for elements
|
||||
- **Never perform side-effects** (like `fireEvent.press()`) inside `waitFor` callbacks
|
||||
- **One assertion per `waitFor`** callback
|
||||
- **Never pass empty callbacks** - always include a meaningful assertion
|
||||
- **Place side-effects before `waitFor`** - perform actions, then wait for result
|
||||
|
||||
## Code Organization
|
||||
|
||||
- **Use `screen`** instead of destructuring from `render()`: `screen.getByText()` not `const { getByText } = render()`
|
||||
- **Prefer `userEvent`** over `fireEvent` for realistic interactions
|
||||
- **Don't use `cleanup()`** - handled automatically
|
||||
- **Name wrappers descriptively**: `ThemeProvider` not `Wrapper`
|
||||
- **Install ESLint plugin**: `eslint-plugin-testing-library`
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
- ✅ Using `getByRole` as first choice?
|
||||
- ✅ Using `await` for all async operations?
|
||||
- ✅ Using `findBy*` for async elements (not `waitFor` + `getBy*`)?
|
||||
- ✅ Using `queryBy*` only for non-existence?
|
||||
- ✅ Using RNTL matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.)?
|
||||
- ✅ Using `screen` not destructuring from `render()`?
|
||||
- ✅ Avoiding side-effects in `waitFor`?
|
||||
- ✅ Using `userEvent` when appropriate?
|
||||
|
||||
## Example: Good Pattern
|
||||
|
||||
```tsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
import userEvent from '@testing-library/react-native';
|
||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
||||
|
||||
test('user can submit form', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const Component = () => {
|
||||
const [name, setName] = React.useState('');
|
||||
const [submitted, setSubmitted] = React.useState(false);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextInput role="textbox" aria-label="Name" value={name} onChangeText={setName} />
|
||||
<Pressable role="button" aria-label="Submit" onPress={() => setSubmitted(true)}>
|
||||
<Text>Submit</Text>
|
||||
</Pressable>
|
||||
{submitted && <Text role="alert">Form submitted!</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<Component />);
|
||||
|
||||
// ✅ getByRole as first choice
|
||||
const input = screen.getByRole('textbox', { name: 'Name' });
|
||||
const button = screen.getByRole('button', { name: 'Submit' });
|
||||
|
||||
// ✅ userEvent for realistic interactions
|
||||
await user.type(input, 'John Doe');
|
||||
await user.press(button);
|
||||
|
||||
// ✅ findBy* for async elements
|
||||
const successMessage = await screen.findByRole('alert');
|
||||
|
||||
// ✅ RNTL matchers
|
||||
expect(successMessage).toBeOnTheScreen();
|
||||
expect(successMessage).toHaveTextContent('Form submitted!');
|
||||
});
|
||||
```
|
||||
|
||||
## Example: Anti-Patterns
|
||||
|
||||
```tsx
|
||||
// ❌ Missing await
|
||||
test('bad', () => {
|
||||
render(<Component />);
|
||||
fireEvent.press(screen.getByText('Submit'));
|
||||
});
|
||||
|
||||
// ❌ getBy* for non-existence
|
||||
expect(screen.getByText('Error')).not.toBeOnTheScreen();
|
||||
|
||||
// ❌ waitFor + getBy* instead of findBy*
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Loaded')).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
// ❌ Side-effect in waitFor
|
||||
await waitFor(async () => {
|
||||
await fireEvent.press(button);
|
||||
expect(screen.getByText('Result')).toBeOnTheScreen();
|
||||
});
|
||||
|
||||
// ❌ accessibility* props instead of ARIA
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Submit" />;
|
||||
|
||||
// ❌ Destructuring from render
|
||||
const { getByText } = await render(<Component />);
|
||||
```
|
||||
|
||||
By following these guidelines, your tests will be more maintainable, accessible, and reliable.
|
||||
@@ -0,0 +1,551 @@
|
||||
import { PackageManagerTabs } from '@rspress/core/theme';
|
||||
|
||||
# Migration to 14.x
|
||||
|
||||
This guide describes the migration to React Native Testing Library version 14 from version 13.x.
|
||||
|
||||
## Overview
|
||||
|
||||
RNTL v14 drops support for React 18 and adopts React 19's async rendering model. Here's what changed:
|
||||
|
||||
- React 19.0.0+ and React Native 0.78+ are now required
|
||||
- Node.js `^22.13.0 || >=24` is now required
|
||||
- `render`, `renderHook`, `fireEvent`, and `act` are now async
|
||||
- Switched from deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html) to [Test Renderer](https://github.com/mdjastrzebski/test-renderer)
|
||||
- Removed deprecated APIs: `update`, `getQueriesForElement`, `UNSAFE_root`, `concurrentRoot` option, `createNodeMock` option
|
||||
- Reintroduced `container` API, which is now safe to use
|
||||
|
||||
> [!INFO] React 18 Users
|
||||
> If you need to support React 18, please continue using RNTL v13.x.
|
||||
|
||||
## Quick Migration
|
||||
|
||||
We provide codemods to automate most of the migration:
|
||||
|
||||
**Step 1: Update dependencies**
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-update-deps --target .\nnpm install',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-update-deps --target .\nyarn install',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps --target .\npnpm install',
|
||||
bun: 'bunx codemod@latest rntl-v14-update-deps --target .\nbun install',
|
||||
}}
|
||||
/>
|
||||
|
||||
**Step 2: Update test code to async**
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
bun: 'bunx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
}}
|
||||
/>
|
||||
|
||||
After running the codemods, review the changes and run your tests.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Supported React, React Native, and Node.js versions
|
||||
|
||||
**This version requires React 19+, React Native 0.78+, and Node.js `^22.13.0 || >=24`.** If you need to support React 18, please use the latest v13.x version.
|
||||
|
||||
| RNTL Version | React Version | React Native Version | Node.js Version |
|
||||
| ------------ | ------------- | -------------------- | ------------------ |
|
||||
| v14.x | >= 19.0.0 | >= 0.78 | ^22.13.0 \|\| >=24 |
|
||||
| v13.x | >= 18.0.0 | >= 0.71 | >= 18 |
|
||||
|
||||
### Test Renderer replaces React Test Renderer
|
||||
|
||||
In v14, React Native Testing Library uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer) instead of the deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html). Test Renderer works with React 19 and has better TypeScript support.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- The underlying renderer is now Test Renderer instead of React Test Renderer
|
||||
- This is mostly an internal change; your tests should work without modifications in most cases
|
||||
- Type definitions now use [`TestInstance`](https://github.com/mdjastrzebski/test-renderer#test-instance) from Test Renderer instead of `ReactTestInstance`
|
||||
- Test Renderer 1.x is required as a peer dependency. Use the recommended Test Renderer version for your React 19 minor version.
|
||||
|
||||
#### Recommended Test Renderer versions
|
||||
|
||||
Choose the Test Renderer version that matches your React 19 minor version:
|
||||
|
||||
| React version | Recommended Test Renderer version | Notable React features |
|
||||
| ------------- | --------------------------------- | ------------------------------------------------- |
|
||||
| `19.2` | `test-renderer@1.2` | `<Activity />`, `useEffectEvent` |
|
||||
| `19.1` | `test-renderer@1.1` | Owner Stack support, updated `useId()` format |
|
||||
| `19.0` | `test-renderer@1.0` | Actions, `useActionState`, `useOptimistic`, `use` |
|
||||
|
||||
Using an older Test Renderer line can prevent RNTL from supporting newer React 19 features in your tests. Using a newer Test Renderer line than your React version can produce peer dependency warnings, or an install error with `npm`.
|
||||
|
||||
See the [Test Renderer React 19 compatibility lines](https://github.com/mdjastrzebski/test-renderer#react-19-compatibility-lines) for the latest recommendations.
|
||||
|
||||
**Migration:**
|
||||
|
||||
#### 1. Update dependencies
|
||||
|
||||
Run codemod for updating dependencies:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-update-deps\nnpm install',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-update-deps\nyarn install',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps\npnpm install',
|
||||
bun: 'bunx codemod@latest rntl-v14-update-deps\nbun install',
|
||||
}}
|
||||
/>
|
||||
|
||||
##### Manual changes
|
||||
|
||||
Remove React Test Renderer and its type definitions from your dev dependencies, and add the Test Renderer line that matches your React minor version:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npm uninstall react-test-renderer @types/react-test-renderer\nnpm install -D test-renderer@1.2',
|
||||
yarn: 'yarn remove react-test-renderer @types/react-test-renderer\nyarn add -D test-renderer@1.2',
|
||||
pnpm: 'pnpm remove react-test-renderer @types/react-test-renderer\npnpm add -D test-renderer@1.2',
|
||||
bun: 'bun remove react-test-renderer @types/react-test-renderer\nbun add -D test-renderer@1.2',
|
||||
}}
|
||||
/>
|
||||
|
||||
The commands above use `test-renderer@1.2` for React 19.2. Use `test-renderer@1.1` for React 19.1, or `test-renderer@1.0` for React 19.0.
|
||||
|
||||
#### 2. Update type imports (if needed)
|
||||
|
||||
If you were directly importing types from React Test Renderer, you may need to update your imports:
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
import type { ReactTestInstance } from 'react-test-renderer';
|
||||
|
||||
// After (v14)
|
||||
import type { TestInstance } from 'test-renderer';
|
||||
```
|
||||
|
||||
**Note:** Most users won't need to update type imports, as React Native Testing Library now exports the necessary types directly.
|
||||
|
||||
See the [Test Renderer documentation](https://github.com/mdjastrzebski/test-renderer) for more.
|
||||
|
||||
### Async APIs by Default
|
||||
|
||||
With React 18 support dropped, RNTL v14 uses React 19's async rendering model. The following functions are now async by default:
|
||||
|
||||
- `render()` → returns `Promise<RenderResult>`
|
||||
- `rerender()` and `unmount()` → return `Promise<void>`
|
||||
- `renderHook()` → returns `Promise<RenderHookResult>`
|
||||
- `fireEvent()` and helpers (`press`, `changeText`, `scroll`) → return `Promise<void>`
|
||||
- `act()` → always returns `Promise<T>`
|
||||
|
||||
> [!TIP] Already using async APIs?
|
||||
> If you adopted the async APIs introduced in RNTL v13.3 (`renderAsync`, `fireEventAsync`, `renderHookAsync`), rename them to their non-async counterparts (`render`, `fireEvent`, `renderHook`). The async versions have been removed since the standard APIs are now async by default.
|
||||
|
||||
#### `render` is now async
|
||||
|
||||
In v14, `render` is async by default and returns a Promise. This allows proper support for `Suspense` boundaries and the `use()` hook.
|
||||
|
||||
**Before (v13):**
|
||||
|
||||
```ts
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should render component', () => {
|
||||
render(<MyComponent />);
|
||||
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
**After (v14):**
|
||||
|
||||
```ts
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should render component', async () => {
|
||||
await render(<MyComponent />);
|
||||
expect(screen.getByText('Hello')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
See the [`render` API documentation](../api/render.md).
|
||||
|
||||
#### `renderHook` is now async
|
||||
|
||||
In v14, `renderHook` is async by default and returns a Promise.
|
||||
|
||||
**Before (v13):**
|
||||
|
||||
```ts
|
||||
import { renderHook } from '@testing-library/react-native';
|
||||
|
||||
it('should test hook', () => {
|
||||
const { result, rerender } = renderHook(() => useMyHook());
|
||||
|
||||
rerender(newProps);
|
||||
unmount();
|
||||
});
|
||||
```
|
||||
|
||||
**After (v14):**
|
||||
|
||||
```ts
|
||||
import { renderHook } from '@testing-library/react-native';
|
||||
|
||||
it('should test hook', async () => {
|
||||
const { result, rerender } = await renderHook(() => useMyHook());
|
||||
|
||||
await rerender(newProps);
|
||||
await unmount();
|
||||
});
|
||||
```
|
||||
|
||||
See the [`renderHook` API documentation](../api/render-hook.md).
|
||||
|
||||
#### `fireEvent` is now async
|
||||
|
||||
In v14, `fireEvent` and its helpers (`press`, `changeText`, `scroll`) are async by default and return a Promise.
|
||||
|
||||
**Before (v13):**
|
||||
|
||||
```ts
|
||||
import { fireEvent, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should press button', () => {
|
||||
render(<MyComponent />);
|
||||
fireEvent.press(screen.getByText('Press me'));
|
||||
expect(onPress).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
**After (v14):**
|
||||
|
||||
```ts
|
||||
import { fireEvent, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should press button', async () => {
|
||||
await render(<MyComponent />);
|
||||
await fireEvent.press(screen.getByText('Press me'));
|
||||
expect(onPress).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
`fireEvent.press()` and `fireEvent.scroll()` now create default synthetic native event objects. If you pass event props, they are deep-merged into the default event object:
|
||||
|
||||
```ts
|
||||
await fireEvent.press(screen.getByText('Press me'), {
|
||||
nativeEvent: { pageX: 20, pageY: 30 },
|
||||
});
|
||||
|
||||
expect(onPress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nativeEvent: expect.objectContaining({ pageX: 20, pageY: 30 }),
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
If your v13 assertions expected the handler to receive exactly the object you passed to `fireEvent.press()` or `fireEvent.scroll()`, update them to use partial matching.
|
||||
|
||||
#### `act` is now async
|
||||
|
||||
In v14, `act` is async by default and always returns a Promise. You should always `await` the result of `act()`.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `act` now always returns `Promise<T>` instead of `T | Thenable<T>`
|
||||
- `act` should always be awaited
|
||||
|
||||
> [!NOTE]
|
||||
> The transition to async `act` may prevent testing very short transient states, as awaiting `act` will flush all pending updates before returning.
|
||||
|
||||
**Before (v13):**
|
||||
|
||||
```ts
|
||||
import { act } from '@testing-library/react-native';
|
||||
|
||||
it('should update state', () => {
|
||||
act(() => {
|
||||
setState('new value');
|
||||
});
|
||||
expect(state).toBe('new value');
|
||||
});
|
||||
```
|
||||
|
||||
**After (v14):**
|
||||
|
||||
```ts
|
||||
import { act } from '@testing-library/react-native';
|
||||
|
||||
it('should update state', async () => {
|
||||
await act(() => {
|
||||
setState('new value');
|
||||
});
|
||||
expect(state).toBe('new value');
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: Even if your callback is synchronous, you should still use `await act(...)` as `act` now always returns a Promise.
|
||||
|
||||
See the [`act` API documentation](../api/other-helpers.md#act).
|
||||
|
||||
#### Why async APIs?
|
||||
|
||||
The async APIs properly handle `Suspense` boundaries and the `use()` hook, and ensure all pending React updates complete before assertions run. This matches React 19's async rendering model.
|
||||
|
||||
### Removed APIs
|
||||
|
||||
#### `update` alias removed
|
||||
|
||||
The `update` alias for `rerender` has been removed. Use `rerender` instead:
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
screen.update(<MyComponent />);
|
||||
const { update } = render(<MyComponent />);
|
||||
update(<MyComponent newProp />);
|
||||
|
||||
// After (v14)
|
||||
await screen.rerender(<MyComponent />);
|
||||
const { rerender } = await render(<MyComponent />);
|
||||
await rerender(<MyComponent newProp />);
|
||||
```
|
||||
|
||||
#### `getQueriesForElement` export removed
|
||||
|
||||
The `getQueriesForElement` export alias for `within` has been removed. Use `within` instead:
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
import { getQueriesForElement } from '@testing-library/react-native';
|
||||
|
||||
const queries = getQueriesForElement(element);
|
||||
|
||||
// After (v14)
|
||||
import { within } from '@testing-library/react-native';
|
||||
|
||||
const queries = within(element);
|
||||
```
|
||||
|
||||
**Note:** `getQueriesForElement` was just an alias for `within`, so the functionality is identical - only the import needs to change.
|
||||
|
||||
#### `UNSAFE_root` removed
|
||||
|
||||
`UNSAFE_root` has been removed. Use `container` to access the pseudo-element container, or `root` to access the first rendered host element:
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
const unsafeRoot = screen.UNSAFE_root;
|
||||
|
||||
// After (v14)
|
||||
const container = screen.container; // pseudo-element container
|
||||
const root = screen.root; // first rendered host element
|
||||
```
|
||||
|
||||
#### Legacy `UNSAFE_*` queries removed
|
||||
|
||||
The legacy `UNSAFE_getAllByType`, `UNSAFE_getByType`, `UNSAFE_getAllByProps`, and `UNSAFE_getByProps` queries have been removed. These queries could return composite (user-defined) components, which is no longer supported with [Test Renderer](https://github.com/mdjastrzebski/test-renderer) as it only renders host elements.
|
||||
|
||||
If you were using these legacy queries, you should refactor your tests to use the standard queries (`getByRole`, `getByText`, `getByTestId`, etc.) which target host elements.
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
const buttons = screen.UNSAFE_getAllByType(Button);
|
||||
const input = screen.UNSAFE_getByProps({ placeholder: 'Enter text' });
|
||||
|
||||
// After (v14)
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const input = screen.getByPlaceholderText('Enter text');
|
||||
```
|
||||
|
||||
#### `concurrentRoot` option removed
|
||||
|
||||
The `concurrentRoot` option has been removed from both `render` options and `configure` function. In v14, concurrent rendering is always enabled, since it's the standard rendering mode for React 19 and React Native's New Architecture.
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
render(<MyComponent />, { concurrentRoot: true }); // Enable concurrent mode
|
||||
render(<MyComponent />, { concurrentRoot: false }); // Disable concurrent mode
|
||||
configure({ concurrentRoot: false }); // Disable globally
|
||||
|
||||
// After (v14)
|
||||
await render(<MyComponent />); // Always uses concurrent rendering
|
||||
```
|
||||
|
||||
**Migration:** Remove any `concurrentRoot` options from your `render` calls and `configure` function. If you were setting `concurrentRoot: true`, just remove the option. If you were setting `concurrentRoot: false` to disable concurrent rendering, this is no longer supported in v14.
|
||||
|
||||
#### `createNodeMock` option removed
|
||||
|
||||
The `createNodeMock` render option has been removed. It was previously passed through to React Test Renderer, but the v14 Test Renderer integration does not support this option.
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
render(<MyComponent />, {
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === TextInput) {
|
||||
return { focus: jest.fn() };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
// After (v14)
|
||||
await render(<MyComponent />);
|
||||
```
|
||||
|
||||
If you used `createNodeMock` to mock imperative refs, prefer testing user-visible behavior and mock the component or native module at the boundary where the ref behavior is introduced.
|
||||
|
||||
### `container` API reintroduced
|
||||
|
||||
In v14, the `container` API has been reintroduced and is now safe to use. Previously, `container` was renamed to `UNSAFE_root` in v12 due to behavioral differences from React Testing Library's `container`. Now `container` returns a pseudo-element container whose children are the elements you rendered, consistent with React Testing Library's behavior.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `screen.container` is now available and safe to use
|
||||
- `container` returns a pseudo-element container from Test Renderer
|
||||
- The container's children are the elements you rendered
|
||||
- `UNSAFE_root` has been removed
|
||||
|
||||
**Before (v13):**
|
||||
|
||||
```ts
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should access root', () => {
|
||||
render(<MyComponent />);
|
||||
// UNSAFE_root was the only way to access the container
|
||||
const root = screen.UNSAFE_root;
|
||||
});
|
||||
```
|
||||
|
||||
**After (v14):**
|
||||
|
||||
```ts
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
it('should access container', async () => {
|
||||
await render(<MyComponent />);
|
||||
// container is now safe and available
|
||||
const container = screen.container;
|
||||
// root is the first child of container
|
||||
const root = screen.root;
|
||||
});
|
||||
```
|
||||
|
||||
See the [`screen` API documentation](../api/screen.md#container).
|
||||
|
||||
### Text string validation enforced by default
|
||||
|
||||
In v14, Test Renderer enforces React Native's requirement that text strings must be rendered within a `<Text>` component. The `unstable_validateStringsRenderedWithinText` option has been removed from `RenderOptions` since this validation is now always on.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- Text string validation is now always enabled and cannot be disabled
|
||||
- The `unstable_validateStringsRenderedWithinText` option has been removed
|
||||
- Tests will now throw `Invariant Violation: Text strings must be rendered within a <Text> component` errors when attempting to render strings outside of `<Text>` components, matching React Native's runtime behavior
|
||||
|
||||
**Migration:**
|
||||
|
||||
If you were using `unstable_validateStringsRenderedWithinText: true` in your render options, you can simply remove this option as the validation is now always enabled:
|
||||
|
||||
```ts
|
||||
// Before (v13)
|
||||
render(<MyComponent />, {
|
||||
unstable_validateStringsRenderedWithinText: true,
|
||||
});
|
||||
|
||||
// After (v14)
|
||||
await render(<MyComponent />);
|
||||
// Validation is now always enabled
|
||||
```
|
||||
|
||||
If you were relying on the previous behavior where strings could be rendered outside of `<Text>` components, you'll need to fix your components to wrap strings in `<Text>` components, as this matches React Native's actual runtime behavior.
|
||||
|
||||
### Hidden Suspense and Activity content
|
||||
|
||||
When React keeps previously rendered content hidden, such as suspended content or React 19.2 `<Activity mode="hidden">`, RNTL now applies React Native-like hidden props to the affected instances. In practice, hidden instances receive `display: 'none'`, preserving existing styles by appending the hidden style.
|
||||
|
||||
This makes visibility queries and matchers behave closer to runtime behavior:
|
||||
|
||||
- default queries do not match hidden instances
|
||||
- queries with `{ includeHiddenElements: true }` can still find hidden instances
|
||||
- `toBeVisible()` treats these instances as hidden
|
||||
|
||||
### Accessible name changes
|
||||
|
||||
Accessible name calculation has been updated to better match React Native behavior:
|
||||
|
||||
- `aria-labelledby` and `accessibilityLabelledBy` still take precedence over explicit labels
|
||||
- `aria-label` and `accessibilityLabel` take precedence over text content
|
||||
- `Image` `alt` is used as an accessible label
|
||||
- a root `TextInput` can use its `placeholder` as its accessible name
|
||||
- child accessible names are concatenated with spaces
|
||||
- child `TextInput` placeholders are not used when computing a parent's accessible name
|
||||
|
||||
This affects `getByRole(..., { name })` and `toHaveAccessibleName()`. If a v13 role query matched because descendant text or label queries were used as a fallback, update the test to match the element's computed accessible name directly or query the descendant element instead.
|
||||
|
||||
### Unknown options now warn
|
||||
|
||||
RNTL now logs a warning when unknown options are passed to `configure`, `render`, `renderHook`, or `userEvent.setup`. This helps catch stale v13 options and misspellings during migration.
|
||||
|
||||
For example, after removing `concurrentRoot`, `createNodeMock`, or `unstable_validateStringsRenderedWithinText`, make sure those options are not still being passed through shared test helpers.
|
||||
|
||||
## Codemods
|
||||
|
||||
Two codemods are available to automate the migration. Both are safe to run multiple times - they only transform code that hasn't been migrated yet.
|
||||
|
||||
### `rntl-v14-update-deps`
|
||||
|
||||
Updates your `package.json`:
|
||||
|
||||
- Removes React Test Renderer (`react-test-renderer` and `@types/react-test-renderer`)
|
||||
- Adds Test Renderer (`test-renderer`) using the current recommended 1.x line
|
||||
- Updates `@testing-library/react-native` to the v14 version
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-update-deps --target .\nnpm install',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-update-deps --target .\nyarn install',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps --target .\npnpm install',
|
||||
bun: 'bunx codemod@latest rntl-v14-update-deps --target .\nbun install',
|
||||
}}
|
||||
/>
|
||||
|
||||
### `rntl-v14-async-functions`
|
||||
|
||||
Transforms test files:
|
||||
|
||||
- Adds `await` to `render()`, `act()`, `renderHook()`, `fireEvent()` calls
|
||||
- Makes test functions async when needed
|
||||
- Handles `screen.rerender()`, `screen.unmount()`, and renderer methods
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
bun: 'bunx codemod@latest rntl-v14-async-functions --target ./src',
|
||||
}}
|
||||
/>
|
||||
|
||||
#### Custom render functions
|
||||
|
||||
If you have custom render helpers (like `renderWithProviders`), you can specify them using the `customRenderFunctions` parameter. The codemod will then also transform calls to these functions:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npx codemod@latest rntl-v14-async-functions \\\n --target ./src \\\n --param customRenderFunctions="renderWithProviders,renderWithTheme"',
|
||||
yarn: 'yarn dlx codemod@latest rntl-v14-async-functions \\\n --target ./src \\\n --param customRenderFunctions="renderWithProviders,renderWithTheme"',
|
||||
pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions \\\n --target ./src \\\n --param customRenderFunctions="renderWithProviders,renderWithTheme"',
|
||||
bun: 'bunx codemod@latest rntl-v14-async-functions \\\n --target ./src \\\n --param customRenderFunctions="renderWithProviders,renderWithTheme"',
|
||||
}}
|
||||
/>
|
||||
|
||||
This will add `await` to your custom render calls and make the containing test functions async, just like it does for the standard `render` function.
|
||||
|
||||
#### Limitations
|
||||
|
||||
- Helper functions defined in test files are not transformed by default
|
||||
- Namespace imports (`import * as RNTL`) are not handled
|
||||
|
||||
## Full Changelog
|
||||
|
||||
https://github.com/callstack/react-native-testing-library/compare/v13.3.3...v14.0.0
|
||||
@@ -0,0 +1,62 @@
|
||||
import { PackageManagerTabs } from '@rspress/core/theme';
|
||||
|
||||
# Quick Start
|
||||
|
||||
## Installation
|
||||
|
||||
Open a Terminal in your project's folder and run:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npm install -D @testing-library/react-native',
|
||||
yarn: 'yarn add -D @testing-library/react-native',
|
||||
pnpm: 'pnpm add -D @testing-library/react-native',
|
||||
bun: 'bun add -D @testing-library/react-native',
|
||||
}}
|
||||
/>
|
||||
|
||||
This library has a peer dependency on [Test Renderer](https://github.com/mdjastrzebski/test-renderer). Make sure to install it:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npm install -D test-renderer',
|
||||
yarn: 'yarn add -D test-renderer',
|
||||
pnpm: 'pnpm add -D test-renderer',
|
||||
bun: 'bun add -D test-renderer',
|
||||
}}
|
||||
/>
|
||||
|
||||
Test Renderer has better compatibility with React 19 and improved type safety compared to the deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html).
|
||||
|
||||
### Jest matchers
|
||||
|
||||
RNTL automatically extends Jest with React Native-specific matchers. The only thing you need to do is to import anything from `@testing-library/react-native` which you already need to do to access the `render` function.
|
||||
|
||||
### ESLint plugin
|
||||
|
||||
Set up [`eslint-plugin-testing-library`](https://github.com/testing-library/eslint-plugin-testing-library) to avoid common Testing Library mistakes and bad practices.
|
||||
|
||||
Install the plugin (assuming you already have `eslint` installed & configured):
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npm install -D eslint-plugin-testing-library',
|
||||
yarn: 'yarn add -D eslint-plugin-testing-library',
|
||||
pnpm: 'pnpm add -D eslint-plugin-testing-library',
|
||||
bun: 'bun add -D eslint-plugin-testing-library',
|
||||
}}
|
||||
/>
|
||||
|
||||
Then, add this to your ESLint config (e.g., `.eslintrc.js`). Extend the `react` plugin:
|
||||
|
||||
```js title=.eslintrc.js
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
// Test files only
|
||||
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
extends: ['plugin:testing-library/react'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# Testing environment
|
||||
|
||||
> [!INFO]
|
||||
> This document is intended for a more advanced audience who want to understand the internals of our testing environment better, e.g., to contribute to the codebase. You should be able to write integration or component tests without reading this.
|
||||
|
||||
React Native Testing Library lets you write integration and component tests for your React Native app or library. While the JSX code in tests closely resembles your React Native app, the underlying environment differs. This document describes the key elements of our testing environment and highlights things to be aware of when writing advanced tests or diagnosing issues.
|
||||
|
||||
## React renderers
|
||||
|
||||
React allows you to write declarative code using JSX, write function or class components, or use hooks like `useState`. You need to use a renderer to output the results of your components. Every React app uses some renderer:
|
||||
|
||||
- React Native is a renderer for mobile apps,
|
||||
- React DOM is a renderer for web apps,
|
||||
- There are other more [specialized renderers](https://github.com/chentsulin/awesome-react-renderer) that can e.g., render to console or HTML canvas.
|
||||
|
||||
When you run your tests in the React Native Testing Library, somewhat contrary to what the name suggests, they are actually **not** using React Native renderer. This is because this renderer needs to be run on an iOS or Android operating system, so it would need to run on a device or simulator.
|
||||
|
||||
## Test Renderer
|
||||
|
||||
Instead, RNTL uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer), a modern, actively maintained renderer that renders to pure JavaScript objects without access to mobile OS and runs in a Node.js environment using Jest (or any other JavaScript test runner). Test Renderer replaces the deprecated `react-test-renderer` package and has better compatibility with React 19 and improved type safety.
|
||||
|
||||
Using Test Renderer has trade-offs:
|
||||
|
||||
Benefits:
|
||||
|
||||
- Tests run on most CIs (Linux, etc) without a mobile device or emulator
|
||||
- Faster test execution
|
||||
- Light runtime environment
|
||||
|
||||
Limitations:
|
||||
|
||||
- Tests don't execute native code
|
||||
- Tests are unaware of view state managed by native components, e.g., focus, unmanaged text boxes, etc.
|
||||
- Assertions don't operate on native view hierarchy
|
||||
- Runtime behaviors are simulated, sometimes imperfectly
|
||||
|
||||
It's worth noting that the React Testing Library (web one) works a bit differently. While RTL also runs in Jest, it has access to a simulated browser DOM environment from the `jsdom` package, which allows it to use a regular React DOM renderer. Unfortunately, there is no similar React Native runtime environment package. This is probably because while the browser environment is well-defined and highly standardized, the React Native environment constantly evolves in sync with the evolution of underlying OS-es. Maintaining such an environment would require duplicating countless React Native behaviors and keeping them in sync as React Native develops.
|
||||
|
||||
## Element tree
|
||||
|
||||
Calling the `render()` function creates an element tree. This is done internally by invoking the `createRoot()` function from Test Renderer. The output tree represents your React Native component tree, containing only host elements. Each node of that tree corresponds to a host component that would have a counterpart in the native view hierarchy.
|
||||
|
||||
These tree elements are represented by `TestInstance` type from Test Renderer:
|
||||
|
||||
```tsx
|
||||
interface TestInstance {
|
||||
type: ElementType;
|
||||
props: { [propName: string]: any };
|
||||
parent: TestInstance | null;
|
||||
children: Array<TestInstance | string>;
|
||||
|
||||
// Other props and methods
|
||||
}
|
||||
```
|
||||
|
||||
For more details, see the [Test Renderer documentation](https://github.com/mdjastrzebski/test-renderer).
|
||||
|
||||
## Host and composite components
|
||||
|
||||
To understand RNTL's element tree, it's important to know the difference between host and composite components in React Native:
|
||||
|
||||
- [Host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components) have direct counterparts in the native view tree. Typical examples are `<View>`, `<Text>`, `<TextInput>`, and `<Image>` from React Native. You can think of these as an analog of `<div>`, `<span>` etc on the Web. You can also create custom host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler.
|
||||
- [Composite components](https://reactnative.dev/architecture/glossary#react-composite-components) are React code organization units that exist only on the JavaScript side of your app. Typical examples are components you create (function and class components), components imported from React Native (`View`, `Text`, etc.), or 3rd party packages.
|
||||
|
||||
That might initially sound confusing since we put React Native's `View` in both categories. There are two `View` components: composite and host. The relation between them is as follows:
|
||||
|
||||
- Composite `View` is the type imported from the `react-native` package. It is a JavaScript component that renders the host `View` as its only child.
|
||||
- Host `View`, which you do not render directly. React Native takes the props you pass to the composite `View`, does some processing on them and passes them to the host `View`.
|
||||
|
||||
In a full React tree, this would look like:
|
||||
|
||||
```jsx
|
||||
* <View> (composite)
|
||||
* <View> (host)
|
||||
* children prop passed in JSX
|
||||
```
|
||||
|
||||
A similar relation exists between other composite and host pairs: e.g. `Text`, `TextInput`, and `Image` components.
|
||||
|
||||
Not all React Native components are organized this way, e.g., when you use `Pressable` (or `TouchableOpacity`), there is no host `Pressable`, but composite `Pressable` is rendering a host `View` with specific props being set:
|
||||
|
||||
```jsx
|
||||
* <Pressable> (composite)
|
||||
* <View accessible={true} {...}> (host)
|
||||
* children prop passed in JSX
|
||||
```
|
||||
|
||||
### Host-only element tree
|
||||
|
||||
In RNTL v14, [Test Renderer](https://github.com/mdjastrzebski/test-renderer) only exposes host elements in the element tree. Composite components aren't visible in the tree—you only see their host element output. This aligns with Testing Library's philosophy: tests should focus on what users can see and interact with (host elements), not implementation details (composite components).
|
||||
|
||||
For a `TestInstance`, the `type` prop is always a string value representing the host component name, e.g., `"View"`, `"Text"`, `"TextInput"`.
|
||||
|
||||
## Tree nodes
|
||||
|
||||
RNTL v14 queries and the element tree only expose host elements. Tests assert on what users can see and interact with. Host elements represent the actual UI controls users interact with, while composite components exist purely in the JavaScript domain.
|
||||
|
||||
### Understanding props
|
||||
|
||||
When asserting props on host elements, you're verifying what actually reaches the native view. This is important because composite components may process, transform, or even forget to pass props to their host children.
|
||||
|
||||
```jsx
|
||||
function ForgotToPassPropsButton({ title, onPress, style }) {
|
||||
return (
|
||||
<Pressable>
|
||||
<Text>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, the component accepts `onPress` and `style` props but doesn't pass them to host views, so they won't affect the user interface. By testing host elements, RNTL helps you catch these issues: if a prop doesn't reach a host element, users won't see or interact with it.
|
||||
|
||||
## Tree navigation
|
||||
|
||||
> [!CAUTION]
|
||||
> You should avoid navigating over the element tree, as this makes your testing code fragile and may result in false positives. This section is more relevant for people who want to contribute to our codebase.
|
||||
|
||||
You can navigate the tree of host elements using `parent` or `children` props of a `TestInstance`. Be careful when doing this, as the tree structure for third-party components can change independently from your code and cause unexpected test failures.
|
||||
|
||||
## Queries
|
||||
|
||||
All Testing Library queries return host components to encourage the best practices described above. Since v14, RNTL uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer), which only renders host elements, making it impossible to query composite components directly.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Troubleshooting
|
||||
|
||||
This guide describes common issues found by users when integrating React Native Test Library to their projects:
|
||||
|
||||
## Example repository
|
||||
|
||||
We maintain an [example repository](https://github.com/callstack/react-native-testing-library/tree/main/examples/basic) with a React Native Testing Library setup using TypeScript.
|
||||
|
||||
If something doesn't work in your setup, check this repository for configuration examples.
|
||||
|
||||
## Undefined component error
|
||||
|
||||
> Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
|
||||
|
||||
This frequently happens when you mock a complex module incorrectly, e.g.:
|
||||
|
||||
```ts
|
||||
jest.mock('@react-navigation/native', () => {
|
||||
return {
|
||||
useNavigation: jest.fn(),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
The above mock will mock `useNavigation` hook as intended, but at the same time all other exports from `@react-navigation/native` package are now `undefined`. If you want to use `NavigationContainer` component from the same package it will be `undefined` and result in the error above.
|
||||
|
||||
To mock only part of a package, re-export all other exports using `jest.requireActual`:
|
||||
|
||||
```ts
|
||||
jest.mock('@react-navigation/native', () => {
|
||||
return {
|
||||
...jest.requireActual('@react-navigation/native'),
|
||||
useNavigation: jest.fn(),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
That way the mock will re-export all of the `@react-navigation/native` members and overwrite only the `useNavigation` hook.
|
||||
|
||||
Alternatively, you can use `jest.spyOn` to mock package exports selectively.
|
||||
|
||||
### Mocking React Native
|
||||
|
||||
When mocking the `react-native` package, don't mock the whole package at once, as this has issues with `jest.requireActual`. Mock specific library paths inside the package instead, e.g.:
|
||||
|
||||
```ts title=jest-setup.ts
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter');
|
||||
```
|
||||
|
||||
## Act warnings
|
||||
|
||||
When writing tests you may encounter warnings connected with `act()` function. There are two kinds of these warnings:
|
||||
|
||||
- sync `act()` warning - `Warning: An update to Component inside a test was not wrapped in act(...)`
|
||||
- async `act()` warning - `Warning: You called act(async () => ...) without await`
|
||||
|
||||
You can read more about `act()` function in our [understanding `act` function guide](./understanding-act.md).
|
||||
|
||||
Normally, you should not encounter sync `act()` warnings, but if that happens this probably indicate an issue with your test and should be investigated.
|
||||
|
||||
In case of async `act()` function this might happen more or less randomly, especially if your components contain async logic. So far this warning does not seem to affect test correctness.
|
||||
@@ -0,0 +1,207 @@
|
||||
# Understanding `act` function
|
||||
|
||||
When writing RNTL tests, cryptic [`act()`](https://react.dev/link/wrap-tests-with-act) function errors logged to console often confuse developers. This article explains the purpose and behavior of `act()` so you can write tests with more confidence.
|
||||
|
||||
## `act` warning
|
||||
|
||||
Let's start with a typical `act()` warning logged to console:
|
||||
|
||||
```
|
||||
An update to Root inside a test was not wrapped in act(...).
|
||||
|
||||
When testing, code that causes React state updates should be wrapped into act(...):
|
||||
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
});
|
||||
/* assert on the output */
|
||||
|
||||
This ensures that you're testing the behavior the user would see in the browser.
|
||||
Learn more at https://react.dev/link/wrap-tests-with-act
|
||||
```
|
||||
|
||||
## Understanding `act`
|
||||
|
||||
### Responsibility
|
||||
|
||||
This function is intended only for using in automated tests and works only in development mode. Attempting to use it in production build will throw an error.
|
||||
|
||||
The responsibility for `act` function is to make React renders and updates work in tests in a similar way they work in real application by grouping and executing related units of interaction (e.g. renders, effects, etc) together.
|
||||
|
||||
Let's demonstrate this with a small experiment. First, define a function component that uses `useEffect`:
|
||||
|
||||
```jsx
|
||||
function TestComponent() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
setCount((c) => c + 1);
|
||||
}, []);
|
||||
|
||||
return <Text>Count {count}</Text>;
|
||||
}
|
||||
```
|
||||
|
||||
In the following tests we will directly use [Test Renderer](https://github.com/mdjastrzebski/test-renderer) instead of RNTL `render` function to render our component for tests. In order to expose familiar queries like `getByText` we will use `within` function from RNTL.
|
||||
|
||||
```jsx
|
||||
import { createRoot } from 'test-renderer';
|
||||
import { within } from '@testing-library/react-native';
|
||||
|
||||
test('render without act', () => {
|
||||
const renderer = createRoot();
|
||||
renderer.render(<TestComponent />);
|
||||
|
||||
// Bind RNTL queries for root element.
|
||||
const view = within(renderer.container);
|
||||
expect(view.getByText('Count 0')).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
When testing without `act` wrapping the render call, the assertion runs just after rendering but before `useEffect` effects are applied. This isn't what we expected.
|
||||
|
||||
```jsx
|
||||
import { createRoot } from 'test-renderer';
|
||||
import { act, within } from '@testing-library/react-native';
|
||||
|
||||
test('render with act', async () => {
|
||||
const renderer = createRoot();
|
||||
await act(() => {
|
||||
renderer.render(<TestComponent />);
|
||||
});
|
||||
|
||||
// Bind RNTL queries for root element.
|
||||
const view = within(renderer.container);
|
||||
expect(view.getByText('Count 1')).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: In v14, `act` is now async by default and always returns a Promise. You should always use `await act(...)`.
|
||||
|
||||
When wrapping rendering call with `act` we see that the changes caused by `useEffect` hook have been applied as we would expect.
|
||||
|
||||
### When to use act
|
||||
|
||||
The name `act` comes from [Arrange-Act-Assert](http://wiki.c2.com/?ArrangeActAssert) unit testing pattern. Which means it's related to part of the test when we execute some actions on the component tree.
|
||||
|
||||
So far we learned that `act` function allows tests to wait for all pending React interactions to be applied before we make our assertions. When using `act` we get guarantee that any state updates will be executed as well as any enqueued effects will be executed.
|
||||
|
||||
Therefore, we should use `act` whenever there is some action that causes element tree to render, particularly:
|
||||
|
||||
- initial render call - `renderer.render` call
|
||||
- re-rendering of component - `renderer.render` call with updated element
|
||||
- triggering any event handlers that cause component tree render
|
||||
|
||||
Thankfully, for these basic cases RNTL has got you covered as our `render`, `rerender` and `fireEvent` methods already wrap their calls in `act` so that you do not have to do it explicitly. In v14, these functions are all async and should be awaited.
|
||||
|
||||
Note that `act` calls can be safely nested and internally form a stack of calls.
|
||||
|
||||
### Implementation
|
||||
|
||||
The `act` implementation is defined in the [ReactAct.js source file](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js) inside React repository. RNTL v14 requires React 19+, which provides the `act` function directly via `React.act`.
|
||||
|
||||
RNTL exports `act` for convenience as defined in the [act.ts source file](https://github.com/callstack/react-native-testing-library/blob/main/src/act.ts). In v14, `act` is async by default and always returns a Promise. This works with async React features like `Suspense` boundaries and the `use()` hook. The underlying implementation wraps React's `act` function to ensure consistent async behavior.
|
||||
|
||||
**Important**: You should always use `act` exported from `@testing-library/react-native` rather than the one from `react`. The RNTL version automatically ensures async behavior, whereas using `React.act` directly could still trigger synchronous act behavior if used improperly, leading to subtle test issues.
|
||||
|
||||
## Asynchronous code
|
||||
|
||||
In v14, `act` is always async and returns a Promise. While the callback you pass to `act` can be synchronous (dealing with things like synchronous effects or mocks using already resolved promises), the `act` function itself should always be awaited. However, not all component code is synchronous. Frequently our components or mocks contain some asynchronous behaviours like `setTimeout` calls or network calls.
|
||||
|
||||
### Handling asynchronous operations
|
||||
|
||||
When the callback passed to `act` contains asynchronous operations, the Promise returned by `act` will resolve only after those operations complete.
|
||||
|
||||
Here's a simple example with a component using `setTimeout` to simulate asynchronous behavior:
|
||||
|
||||
```jsx
|
||||
function TestAsyncComponent() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setCount((c) => c + 1);
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
return <Text>Count {count}</Text>;
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
import { render, screen } from '@testing-library/react-native';
|
||||
|
||||
test('render async natively', async () => {
|
||||
await render(<TestAsyncComponent />);
|
||||
expect(screen.getByText('Count 0')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
If we test our component in a native way without handling its asynchronous behaviour we will end up with an act warning. This is because the `setTimeout` callback will trigger a state update after the test has finished.
|
||||
|
||||
### Solution with fake timers
|
||||
|
||||
Use Jest's fake timers:
|
||||
|
||||
```jsx
|
||||
test('render with fake timers', async () => {
|
||||
jest.useFakeTimers();
|
||||
await render(<TestAsyncComponent />);
|
||||
|
||||
await act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(screen.getByText('Count 1')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: In v14, both `render` and `act` are async by default, so you should await them.
|
||||
|
||||
That way we can wrap `jest.runAllTimers()` call which triggers the `setTimeout` updates inside an `act` call, hence resolving the act warning.
|
||||
|
||||
### Solution with real timers
|
||||
|
||||
With real timers, things get more complex. Start with a simple solution: wrap an async `act()` call for the expected duration of component updates:
|
||||
|
||||
```jsx
|
||||
test('render with real timers - sleep', async () => {
|
||||
await render(<TestAsyncComponent />);
|
||||
await act(() => {
|
||||
await sleep(100); // Wait a bit longer than setTimeout in `TestAsyncComponent`
|
||||
});
|
||||
|
||||
expect(screen.getByText('Count 1')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
This works correctly as we use an explicit async `act()` call that resolves the console error. However, it relies on our knowledge of exact implementation details which is a bad practice.
|
||||
|
||||
A better solution uses `waitFor` to wait for the desired state:
|
||||
|
||||
```jsx
|
||||
test('render with real timers - waitFor', async () => {
|
||||
await render(<TestAsyncComponent />);
|
||||
|
||||
await waitFor(() => screen.getByText('Count 1'));
|
||||
expect(screen.getByText('Count 1')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
This also works correctly, because `waitFor` call executes async `act()` call internally.
|
||||
|
||||
The above code can be simplified using `findBy` query:
|
||||
|
||||
```jsx
|
||||
test('render with real timers - findBy', async () => {
|
||||
await render(<TestAsyncComponent />);
|
||||
|
||||
expect(await screen.findByText('Count 1')).toBeOnTheScreen();
|
||||
});
|
||||
```
|
||||
|
||||
This also works since `findByText` internally calls `waitFor` which uses async `act()`.
|
||||
|
||||
Note that all of the above examples are async tests using & awaiting async `act()` function call.
|
||||
|
||||
## References
|
||||
|
||||
- [React `act` implementation source](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js)
|
||||
- [React testing documentation](https://react.dev/link/wrap-tests-with-act)
|
||||
+6
-3
@@ -28,6 +28,8 @@
|
||||
"test:ci": "jest --maxWorkers=2",
|
||||
"test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true",
|
||||
"test:codemods": "node scripts/test-codemods.mjs",
|
||||
"docs:generate": "node scripts/generate-package-docs.mjs",
|
||||
"docs:check": "yarn docs:generate && git diff --exit-code -- docs",
|
||||
"typecheck": "tsc",
|
||||
"typecheck:react-19_2": "tsc -p tsconfig.react-19_2.json",
|
||||
"lint": "eslint src --cache",
|
||||
@@ -35,8 +37,8 @@
|
||||
"prettier:fix": "prettier --write .",
|
||||
"validate": "yarn typecheck && yarn test && yarn lint && yarn prettier",
|
||||
"validate:examples": "yarn --cwd examples/basic validate && yarn --cwd examples/cookbook validate",
|
||||
"validate:all": "yarn validate && yarn validate:examples && yarn --cwd website validate && yarn --cwd experiments-app validate",
|
||||
"validate:fix": "yarn prettier:fix && yarn lint --fix && yarn typecheck && yarn test -u",
|
||||
"validate:all": "yarn validate && yarn validate:examples && yarn docs:check && yarn --cwd website validate && yarn --cwd experiments-app validate",
|
||||
"validate:fix": "yarn prettier:fix && yarn lint --fix && yarn typecheck && yarn test -u && docs:generate",
|
||||
"build:js": "babel src --out-dir build --extensions \".js,.ts,.jsx,.tsx\" --source-maps --ignore \"**/__tests__/**\"",
|
||||
"build:ts": "tsc --build tsconfig.release.json",
|
||||
"build": "yarn clean && yarn build:js && yarn build:ts",
|
||||
@@ -50,7 +52,8 @@
|
||||
"matchers.d.ts",
|
||||
"pure.js",
|
||||
"pure.d.ts",
|
||||
"dont-cleanup-after-each.js"
|
||||
"dont-cleanup-after-each.js",
|
||||
"docs/"
|
||||
],
|
||||
"dependencies": {
|
||||
"jest-matcher-utils": "^30.3.0",
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const packageJsonPath = path.join(repoRoot, 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
const packageMajor = packageJson.version.match(/^(\d+)\./)?.[1];
|
||||
const docsVersion = packageMajor ? `${packageMajor}.x` : undefined;
|
||||
const sourceRoot = docsVersion ? path.join(repoRoot, 'website/docs', docsVersion) : undefined;
|
||||
|
||||
if (!sourceRoot || !fs.existsSync(sourceRoot)) {
|
||||
throw new Error(`Could not find website docs for package version ${packageJson.version}`);
|
||||
}
|
||||
|
||||
const generatedRoot = path.join(repoRoot, 'docs');
|
||||
const generatedPaths = ['README.md', 'llms.txt', 'llms-full.txt', 'api', 'guides', 'cookbook'];
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'LLM Guidelines',
|
||||
source: 'docs/guides/llm-guidelines.mdx',
|
||||
output: 'guides/llm-guidelines.md',
|
||||
description: 'Quick rules for agents writing RNTL tests.',
|
||||
},
|
||||
{
|
||||
title: 'Quick Start',
|
||||
source: 'docs/start/quick-start.mdx',
|
||||
output: 'guides/quick-start.md',
|
||||
description: 'Installation and setup basics.',
|
||||
},
|
||||
{
|
||||
title: 'How to Query',
|
||||
source: 'docs/guides/how-to-query.mdx',
|
||||
output: 'guides/how-to-query.md',
|
||||
description: 'Query priority and selection guidance.',
|
||||
},
|
||||
{
|
||||
title: 'Common Mistakes',
|
||||
source: 'docs/guides/common-mistakes.mdx',
|
||||
output: 'guides/common-mistakes.md',
|
||||
description: 'Common anti-patterns and preferred alternatives.',
|
||||
},
|
||||
{
|
||||
title: 'Troubleshooting',
|
||||
source: 'docs/guides/troubleshooting.mdx',
|
||||
output: 'guides/troubleshooting.md',
|
||||
description: 'Common integration and runtime issues.',
|
||||
},
|
||||
{
|
||||
title: 'Testing Environment',
|
||||
source: 'docs/advanced/testing-env.mdx',
|
||||
output: 'guides/testing-environment.md',
|
||||
description: 'How RNTL simulates React Native under tests.',
|
||||
},
|
||||
{
|
||||
title: 'Understanding act',
|
||||
source: 'docs/advanced/understanding-act.mdx',
|
||||
output: 'guides/understanding-act.md',
|
||||
description: 'How act warnings happen and how to resolve them.',
|
||||
},
|
||||
{
|
||||
title: 'Migration to 14.x',
|
||||
source: 'docs/start/migration-v14.mdx',
|
||||
output: 'guides/migration-v14.md',
|
||||
description: 'Breaking changes and upgrade steps for RNTL v14.',
|
||||
},
|
||||
{
|
||||
title: 'API Overview',
|
||||
source: 'docs/api.md',
|
||||
output: 'api/overview.md',
|
||||
description: 'Top-level API map.',
|
||||
},
|
||||
{
|
||||
title: 'render',
|
||||
source: 'docs/api/render.mdx',
|
||||
output: 'api/render.md',
|
||||
description: 'Rendering components in tests.',
|
||||
},
|
||||
{
|
||||
title: 'screen',
|
||||
source: 'docs/api/screen.mdx',
|
||||
output: 'api/screen.md',
|
||||
description: 'Recommended global query surface.',
|
||||
},
|
||||
{
|
||||
title: 'Queries',
|
||||
source: 'docs/api/queries.mdx',
|
||||
output: 'api/queries.md',
|
||||
description: 'Query variants and predicates.',
|
||||
},
|
||||
{
|
||||
title: 'Jest Matchers',
|
||||
source: 'docs/api/jest-matchers.mdx',
|
||||
output: 'api/jest-matchers.md',
|
||||
description: 'Built-in RNTL assertions.',
|
||||
},
|
||||
{
|
||||
title: 'User Event',
|
||||
source: 'docs/api/events/user-event.mdx',
|
||||
output: 'api/user-event.md',
|
||||
description: 'Realistic user interactions.',
|
||||
},
|
||||
{
|
||||
title: 'Fire Event',
|
||||
source: 'docs/api/events/fire-event.mdx',
|
||||
output: 'api/fire-event.md',
|
||||
description: 'Low-level event triggering.',
|
||||
},
|
||||
{
|
||||
title: 'Async Utilities',
|
||||
source: 'docs/api/misc/async.mdx',
|
||||
output: 'api/async-utilities.md',
|
||||
description: 'Async queries and wait helpers.',
|
||||
},
|
||||
{
|
||||
title: 'renderHook',
|
||||
source: 'docs/api/misc/render-hook.mdx',
|
||||
output: 'api/render-hook.md',
|
||||
description: 'Testing custom hooks.',
|
||||
},
|
||||
{
|
||||
title: 'Configuration',
|
||||
source: 'docs/api/misc/config.mdx',
|
||||
output: 'api/configuration.md',
|
||||
description: 'Runtime configuration options.',
|
||||
},
|
||||
{
|
||||
title: 'Accessibility',
|
||||
source: 'docs/api/misc/accessibility.mdx',
|
||||
output: 'api/accessibility.md',
|
||||
description: 'Accessibility helpers and hidden elements.',
|
||||
},
|
||||
{
|
||||
title: 'Other Helpers',
|
||||
source: 'docs/api/misc/other.mdx',
|
||||
output: 'api/other-helpers.md',
|
||||
description: 'within, act, cleanup, and related helpers.',
|
||||
},
|
||||
{
|
||||
title: 'Custom Render',
|
||||
source: 'cookbook/basics/custom-render.md',
|
||||
output: 'cookbook/custom-render.md',
|
||||
description: 'Reusable render wrappers.',
|
||||
},
|
||||
{
|
||||
title: 'Async Events',
|
||||
source: 'cookbook/basics/async-events.md',
|
||||
output: 'cookbook/async-events.md',
|
||||
description: 'Testing async interactions.',
|
||||
},
|
||||
{
|
||||
title: 'Network Requests',
|
||||
source: 'cookbook/advanced/network-requests.md',
|
||||
output: 'cookbook/network-requests.md',
|
||||
description: 'Testing components that make network requests.',
|
||||
},
|
||||
];
|
||||
|
||||
for (const generatedPath of generatedPaths) {
|
||||
fs.rmSync(path.join(generatedRoot, generatedPath), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(generatedRoot, { recursive: true });
|
||||
|
||||
const writtenPages = [];
|
||||
|
||||
for (const page of pages) {
|
||||
const sourcePath = path.join(sourceRoot, page.source);
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`Missing source doc: ${path.relative(repoRoot, sourcePath)}`);
|
||||
}
|
||||
|
||||
const outputPath = path.join(generatedRoot, page.output);
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
const content = normalizeMarkdown(fs.readFileSync(sourcePath, 'utf8'), page.output);
|
||||
fs.writeFileSync(outputPath, content);
|
||||
writtenPages.push(page);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(generatedRoot, 'README.md'), buildReadme(writtenPages));
|
||||
|
||||
execFileSync(
|
||||
'yarn',
|
||||
['prettier', '--write', 'docs/README.md', 'docs/api', 'docs/guides', 'docs/cookbook'],
|
||||
{ cwd: repoRoot, stdio: 'inherit' },
|
||||
);
|
||||
|
||||
console.log(`Generated package docs from website/docs/${docsVersion}`);
|
||||
|
||||
function normalizeMarkdown(input, output) {
|
||||
let markdown = input.replace(/\r\n/g, '\n');
|
||||
|
||||
markdown = markdown.replace(/^---\n[\s\S]*?\n---\n+/, '');
|
||||
markdown = markdown.replace(/\{#([^}]+)\}/g, '');
|
||||
markdown = markdown.replace(/<span style=\{\{[^}]+}}\>(.*?)<\/span>/g, '**$1**');
|
||||
markdown = normalizeAdmonitions(markdown);
|
||||
markdown = rewriteInternalLinks(markdown, output);
|
||||
|
||||
return `${markdown.trim()}\n`;
|
||||
}
|
||||
|
||||
function normalizeAdmonitions(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const start = lines[index].match(/^:::(note|info|warning|tip|danger|caution)\s*(.*)$/);
|
||||
|
||||
if (!start) {
|
||||
result.push(lines[index]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = [];
|
||||
index += 1;
|
||||
|
||||
while (index < lines.length && lines[index].trim() !== ':::') {
|
||||
body.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
result.push(admonition(start[1].toUpperCase(), start[2], body.join('\n')));
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
function admonition(kind, title, body) {
|
||||
const label = title.trim() ? `[!${kind}] ${title.trim()}` : `[!${kind}]`;
|
||||
const quotedBody = body
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => (line ? `> ${line}` : '>'))
|
||||
.join('\n');
|
||||
|
||||
return `> ${label}\n${quotedBody}`;
|
||||
}
|
||||
|
||||
function rewriteInternalLinks(markdown, output) {
|
||||
const linkTargets = new Map([
|
||||
['docs/api/events/user-event', 'api/user-event.md'],
|
||||
['docs/api/events/fire-event', 'api/fire-event.md'],
|
||||
['docs/api/misc/async', 'api/async-utilities.md'],
|
||||
['docs/api/misc/render-hook', 'api/render-hook.md'],
|
||||
['docs/api/misc/config', 'api/configuration.md'],
|
||||
['docs/api/misc/accessibility', 'api/accessibility.md'],
|
||||
['docs/api/misc/other', 'api/other-helpers.md'],
|
||||
['docs/api/render', 'api/render.md'],
|
||||
['docs/api/screen', 'api/screen.md'],
|
||||
['docs/api/queries', 'api/queries.md'],
|
||||
['docs/api/jest-matchers', 'api/jest-matchers.md'],
|
||||
['docs/api', 'api/overview.md'],
|
||||
['docs/guides/how-to-query', 'guides/how-to-query.md'],
|
||||
['docs/guides/common-mistakes', 'guides/common-mistakes.md'],
|
||||
['docs/guides/troubleshooting', 'guides/troubleshooting.md'],
|
||||
['docs/guides/llm-guidelines', 'guides/llm-guidelines.md'],
|
||||
['docs/advanced/testing-env', 'guides/testing-environment.md'],
|
||||
['docs/advanced/understanding-act', 'guides/understanding-act.md'],
|
||||
['cookbook/basics/custom-render', 'cookbook/custom-render.md'],
|
||||
['cookbook/basics/async-events', 'cookbook/async-events.md'],
|
||||
['cookbook/advanced/network-requests', 'cookbook/network-requests.md'],
|
||||
]);
|
||||
|
||||
return markdown.replace(/\]\(\/(?:14\.x\/)?([^)#]+)(#[^)]+)?\)/g, (match, target, hash = '') => {
|
||||
const mapped = linkTargets.get(target);
|
||||
|
||||
if (!mapped) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const fromDirectory = path.posix.dirname(output);
|
||||
const relative = path.posix.relative(fromDirectory, mapped);
|
||||
const normalized = relative.startsWith('.') ? relative : `./${relative}`;
|
||||
|
||||
return `](${normalized}${hash})`;
|
||||
});
|
||||
}
|
||||
|
||||
function buildReadme(pages) {
|
||||
return `# React Native Testing Library package docs
|
||||
|
||||
These markdown files are bundled with the npm package for coding agents. They describe the installed package version.
|
||||
|
||||
Start with [LLM Guidelines](./guides/llm-guidelines.md) for the agent setup snippet and quick rules, or use the included page list below to load specific references.
|
||||
|
||||
## Included pages
|
||||
|
||||
${pages.map((page) => `- [${page.title}](./${page.output}) - ${page.description}`).join('\n')}
|
||||
`;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ React Native Testing Library consists of following APIs:
|
||||
- [Fire Event](/docs/api/events/fire-event) - simulate any component event in a simplified way purposes
|
||||
- Misc APIs:
|
||||
- [`renderHook` function](/docs/api/misc/render-hook) - render hooks for testing
|
||||
- [Async utils](/docs/api/misc/async): `findBy*` queries, `wait`, `waitForElementToBeRemoved`
|
||||
- [Async utils](/docs/api/misc/async): `findBy*` queries, `waitFor`, `waitForElementToBeRemoved`
|
||||
- [Configuration](/docs/api/misc/config): `configure`, `resetToDefaults`
|
||||
- [Accessibility](/docs/api/misc/accessibility): `isHiddenFromAccessibility`
|
||||
- [Other](/docs/api/misc/other): `within`, `act`, `cleanup`
|
||||
|
||||
@@ -16,6 +16,8 @@ The `fireEvent` API triggers event handlers on both host and composite component
|
||||
|
||||
Unlike User Event, this API does not automatically pass event object to event handler, this is responsibility of the user to construct such object.
|
||||
|
||||
The base `fireEvent(instance, eventName, ...data)` API can pass multiple custom arguments to the handler. Convenience helpers such as `fireEvent.press` and `fireEvent.scroll` are different: they create a default event object and accept one optional object to merge into it.
|
||||
|
||||
This function uses async `act` internally to execute all pending React updates during event handling.
|
||||
|
||||
```jsx
|
||||
@@ -67,11 +69,11 @@ Use the User Event [`press()`](/docs/api/events/user-event#press) helper instead
|
||||
```tsx
|
||||
fireEvent.press: (
|
||||
instance: TestInstance,
|
||||
...data: Array<any>,
|
||||
) => Promise<unknown>
|
||||
eventProps?: Record<string, unknown>,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Invokes `press` event handler on the element or parent element in the tree.
|
||||
Builds a press event object, merges `eventProps` into it, and invokes the `press` handler on the element or nearest eligible parent.
|
||||
|
||||
```jsx
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
@@ -94,7 +96,7 @@ await render(
|
||||
);
|
||||
|
||||
await fireEvent.press(screen.getByText('Press me'), eventData);
|
||||
expect(onPressMock).toHaveBeenCalledWith(eventData);
|
||||
expect(onPressMock).toHaveBeenCalledWith(expect.objectContaining(eventData));
|
||||
```
|
||||
|
||||
### `fireEvent.changeText` {#change-text}
|
||||
@@ -106,8 +108,8 @@ Use the User Event [`type()`](/docs/api/events/user-event#type) helper instead.
|
||||
```tsx
|
||||
fireEvent.changeText: (
|
||||
instance: TestInstance,
|
||||
...data: Array<any>,
|
||||
) => Promise<unknown>
|
||||
text: string,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Invokes `changeText` event handler on the element or parent element in the tree.
|
||||
@@ -137,11 +139,11 @@ Prefer [`user.scrollTo`](/docs/api/events/user-event#scrollto) over `fireEvent.s
|
||||
```tsx
|
||||
fireEvent.scroll: (
|
||||
instance: TestInstance,
|
||||
...data: Array<any>,
|
||||
) => Promise<unknown>
|
||||
eventProps?: Record<string, unknown>,
|
||||
) => Promise<void>
|
||||
```
|
||||
|
||||
Invokes `scroll` event handler on the element or parent element in the tree.
|
||||
Builds a scroll event object, merges `eventProps` into it, and invokes the `scroll` handler on the element or nearest eligible parent.
|
||||
|
||||
#### On a `ScrollView`
|
||||
|
||||
|
||||
@@ -405,7 +405,6 @@ expect(
|
||||
).not.toBeOnTheScreen();
|
||||
|
||||
// Include hidden elements
|
||||
expect(screen.getByText('Hidden from accessibility')).toBeOnTheScreen();
|
||||
expect(
|
||||
screen.getByText('Hidden from accessibility', { includeHiddenElements: true })
|
||||
).toBeOnTheScreen();
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
["how-to-query", "common-mistakes", "troubleshooting", "faq", "community-resources"]
|
||||
[
|
||||
"llm-guidelines",
|
||||
"how-to-query",
|
||||
"common-mistakes",
|
||||
"troubleshooting",
|
||||
"faq",
|
||||
"community-resources"
|
||||
]
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14.
|
||||
|
||||
## Agent docs in the package
|
||||
|
||||
RNTL ships package-specific documentation for coding agents in `node_modules/@testing-library/react-native/docs/`.
|
||||
Add this snippet to your project's `AGENTS.md` or `CLAUDE.md` file so agents use the docs for the installed package version:
|
||||
|
||||
```md
|
||||
<!-- BEGIN:react-native-testing-library-agent-rules -->
|
||||
|
||||
# React Native Testing Library in this project
|
||||
|
||||
This project uses `@testing-library/react-native`. Its APIs and testing conventions can differ from your training data.
|
||||
Before writing or changing RNTL tests, read the relevant guide in
|
||||
`node_modules/@testing-library/react-native/docs/`, starting with
|
||||
`node_modules/@testing-library/react-native/docs/guides/llm-guidelines.md`.
|
||||
Prefer those package docs over stale assumptions, and follow deprecation notices.
|
||||
|
||||
<!-- END:react-native-testing-library-agent-rules -->
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
|
||||
### render
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# Introduction
|
||||
|
||||
:::warning Alpha Version
|
||||
|
||||
This version is currently in alpha. APIs and behavior may change before the stable release. Please report any issues you encounter.
|
||||
|
||||
:::
|
||||
|
||||
## The problem
|
||||
|
||||
You want to write maintainable tests for your React Native components. As a part of this goal, you want your tests to avoid including implementation details of your components and focus on making your tests give you the confidence they are intended. As part of this, you want your tests to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down.
|
||||
|
||||
@@ -2,12 +2,6 @@ import { PackageManagerTabs } from '@rspress/core/theme';
|
||||
|
||||
# Migration to 14.x
|
||||
|
||||
:::warning Alpha Version
|
||||
|
||||
This version is currently in alpha. APIs and behavior may change before the stable release. Please report any issues you encounter.
|
||||
|
||||
:::
|
||||
|
||||
This guide describes the migration to React Native Testing Library version 14 from version 13.x.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -2,22 +2,16 @@ import { PackageManagerTabs } from '@rspress/core/theme';
|
||||
|
||||
# Quick Start
|
||||
|
||||
:::warning Alpha Version
|
||||
|
||||
This version is currently in alpha. APIs and behavior may change before the stable release. Please report any issues you encounter.
|
||||
|
||||
:::
|
||||
|
||||
## Installation
|
||||
|
||||
Open a Terminal in your project's folder and run:
|
||||
|
||||
<PackageManagerTabs
|
||||
command={{
|
||||
npm: 'npm install -D @testing-library/react-native@alpha',
|
||||
yarn: 'yarn add -D @testing-library/react-native@alpha',
|
||||
pnpm: 'pnpm add -D @testing-library/react-native@alpha',
|
||||
bun: 'bun add -D @testing-library/react-native@alpha',
|
||||
npm: 'npm install -D @testing-library/react-native',
|
||||
yarn: 'yarn add -D @testing-library/react-native',
|
||||
pnpm: 'pnpm add -D @testing-library/react-native',
|
||||
bun: 'bun add -D @testing-library/react-native',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user