UNPKG

@testing-library/react-native

Version:

Simple and complete React Native testing utilities that encourage good testing practices.

554 lines (381 loc) 21.4 kB
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. If your project uses coding agents, add the RNTL package-docs instruction snippet from the [Quick Start](./quick-start.md#agent-docs-in-the-package) so agents read the docs that match your installed package version. ## 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