From 6af05a294dcce4c873a300e040c718101c776502 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 25 Oct 2024 10:08:10 +0200 Subject: [PATCH 1/2] chore: duplicate docs --- website/docs/13.x/_meta.json | 16 + website/docs/13.x/cookbook/_meta.json | 18 + .../docs/13.x/cookbook/advanced/_meta.json | 1 + .../cookbook/advanced/network-requests.md | 380 +++++++++++ website/docs/13.x/cookbook/basics/_meta.json | 1 + .../docs/13.x/cookbook/basics/async-tests.md | 142 ++++ .../13.x/cookbook/basics/custom-render.md | 78 +++ website/docs/13.x/cookbook/index.md | 29 + .../13.x/cookbook/state-management/_meta.json | 1 + .../13.x/cookbook/state-management/jotai.md | 226 ++++++ website/docs/13.x/docs/_meta.json | 24 + website/docs/13.x/docs/advanced/_meta.json | 1 + .../docs/13.x/docs/advanced/testing-env.mdx | 146 ++++ .../13.x/docs/advanced/understanding-act.mdx | 226 ++++++ website/docs/13.x/docs/api.md | 22 + website/docs/13.x/docs/api/_meta.json | 8 + website/docs/13.x/docs/api/events/_meta.json | 4 + .../docs/13.x/docs/api/events/fire-event.mdx | 158 +++++ .../docs/13.x/docs/api/events/user-event.mdx | 300 ++++++++ website/docs/13.x/docs/api/jest-matchers.mdx | 224 ++++++ website/docs/13.x/docs/api/misc/_meta.json | 7 + .../docs/13.x/docs/api/misc/accessibility.mdx | 27 + website/docs/13.x/docs/api/misc/async.mdx | 138 ++++ website/docs/13.x/docs/api/misc/config.mdx | 56 ++ website/docs/13.x/docs/api/misc/other.mdx | 75 ++ .../docs/13.x/docs/api/misc/render-hook.mdx | 126 ++++ website/docs/13.x/docs/api/queries.mdx | 646 ++++++++++++++++++ website/docs/13.x/docs/api/render.mdx | 65 ++ website/docs/13.x/docs/api/screen.mdx | 150 ++++ website/docs/13.x/docs/guides/_meta.json | 1 + .../13.x/docs/guides/community-resources.mdx | 13 + website/docs/13.x/docs/guides/faq.mdx | 39 ++ .../docs/13.x/docs/guides/how-to-query.mdx | 125 ++++ .../docs/13.x/docs/guides/troubleshooting.mdx | 80 +++ website/docs/13.x/docs/migration/_meta.json | 5 + .../13.x/docs/migration/jest-matchers.mdx | 75 ++ .../13.x/docs/migration/previous/_meta.json | 1 + .../docs/13.x/docs/migration/previous/v11.mdx | 53 ++ .../docs/13.x/docs/migration/previous/v2.mdx | 123 ++++ .../docs/13.x/docs/migration/previous/v7.mdx | 115 ++++ .../docs/13.x/docs/migration/previous/v9.mdx | 64 ++ website/docs/13.x/docs/migration/v12.mdx | 70 ++ website/docs/13.x/docs/start/_meta.json | 1 + website/docs/13.x/docs/start/intro.md | 40 ++ website/docs/13.x/docs/start/quick-start.mdx | 51 ++ website/docs/13.x/index.md | 27 + 46 files changed, 4178 insertions(+) create mode 100644 website/docs/13.x/_meta.json create mode 100644 website/docs/13.x/cookbook/_meta.json create mode 100644 website/docs/13.x/cookbook/advanced/_meta.json create mode 100644 website/docs/13.x/cookbook/advanced/network-requests.md create mode 100644 website/docs/13.x/cookbook/basics/_meta.json create mode 100644 website/docs/13.x/cookbook/basics/async-tests.md create mode 100644 website/docs/13.x/cookbook/basics/custom-render.md create mode 100644 website/docs/13.x/cookbook/index.md create mode 100644 website/docs/13.x/cookbook/state-management/_meta.json create mode 100644 website/docs/13.x/cookbook/state-management/jotai.md create mode 100644 website/docs/13.x/docs/_meta.json create mode 100644 website/docs/13.x/docs/advanced/_meta.json create mode 100644 website/docs/13.x/docs/advanced/testing-env.mdx create mode 100644 website/docs/13.x/docs/advanced/understanding-act.mdx create mode 100644 website/docs/13.x/docs/api.md create mode 100644 website/docs/13.x/docs/api/_meta.json create mode 100644 website/docs/13.x/docs/api/events/_meta.json create mode 100644 website/docs/13.x/docs/api/events/fire-event.mdx create mode 100644 website/docs/13.x/docs/api/events/user-event.mdx create mode 100644 website/docs/13.x/docs/api/jest-matchers.mdx create mode 100644 website/docs/13.x/docs/api/misc/_meta.json create mode 100644 website/docs/13.x/docs/api/misc/accessibility.mdx create mode 100644 website/docs/13.x/docs/api/misc/async.mdx create mode 100644 website/docs/13.x/docs/api/misc/config.mdx create mode 100644 website/docs/13.x/docs/api/misc/other.mdx create mode 100644 website/docs/13.x/docs/api/misc/render-hook.mdx create mode 100644 website/docs/13.x/docs/api/queries.mdx create mode 100644 website/docs/13.x/docs/api/render.mdx create mode 100644 website/docs/13.x/docs/api/screen.mdx create mode 100644 website/docs/13.x/docs/guides/_meta.json create mode 100644 website/docs/13.x/docs/guides/community-resources.mdx create mode 100644 website/docs/13.x/docs/guides/faq.mdx create mode 100644 website/docs/13.x/docs/guides/how-to-query.mdx create mode 100644 website/docs/13.x/docs/guides/troubleshooting.mdx create mode 100644 website/docs/13.x/docs/migration/_meta.json create mode 100644 website/docs/13.x/docs/migration/jest-matchers.mdx create mode 100644 website/docs/13.x/docs/migration/previous/_meta.json create mode 100644 website/docs/13.x/docs/migration/previous/v11.mdx create mode 100644 website/docs/13.x/docs/migration/previous/v2.mdx create mode 100644 website/docs/13.x/docs/migration/previous/v7.mdx create mode 100644 website/docs/13.x/docs/migration/previous/v9.mdx create mode 100644 website/docs/13.x/docs/migration/v12.mdx create mode 100644 website/docs/13.x/docs/start/_meta.json create mode 100644 website/docs/13.x/docs/start/intro.md create mode 100644 website/docs/13.x/docs/start/quick-start.mdx create mode 100644 website/docs/13.x/index.md diff --git a/website/docs/13.x/_meta.json b/website/docs/13.x/_meta.json new file mode 100644 index 000000000..4ffae6a41 --- /dev/null +++ b/website/docs/13.x/_meta.json @@ -0,0 +1,16 @@ +[ + { + "text": "Docs", + "link": "/docs/start/intro", + "activeMatch": "^/docs/" + }, + { + "text": "Cookbook", + "link": "/cookbook/", + "activeMatch": "^/cookbook/" + }, + { + "text": "Examples", + "link": "https://github.com/callstack/react-native-testing-library/tree/main/examples" + } +] diff --git a/website/docs/13.x/cookbook/_meta.json b/website/docs/13.x/cookbook/_meta.json new file mode 100644 index 000000000..deb5689d7 --- /dev/null +++ b/website/docs/13.x/cookbook/_meta.json @@ -0,0 +1,18 @@ +[ + "index", + { + "type": "dir", + "name": "basics", + "label": "Basic Recipes" + }, + { + "type": "dir", + "name": "advanced", + "label": "Advanced Recipes" + }, + { + "type": "dir", + "name": "state-management", + "label": "State Management Recipes" + } +] diff --git a/website/docs/13.x/cookbook/advanced/_meta.json b/website/docs/13.x/cookbook/advanced/_meta.json new file mode 100644 index 000000000..9d0399cc6 --- /dev/null +++ b/website/docs/13.x/cookbook/advanced/_meta.json @@ -0,0 +1 @@ +["network-requests"] diff --git a/website/docs/13.x/cookbook/advanced/network-requests.md b/website/docs/13.x/cookbook/advanced/network-requests.md new file mode 100644 index 000000000..08002ef92 --- /dev/null +++ b/website/docs/13.x/cookbook/advanced/network-requests.md @@ -0,0 +1,380 @@ +# 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([]); + const [favoritesData, setFavoritesData] = useState([]); + const [error, setError] = useState(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 An error occurred: {error}; + } + + return ( + <> + + + + ); +}; +``` + +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 => { + 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 => { + 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 = useCallback(({item: {picture}}) => { + return ( + + + + ); + }, []); + + if (users.length === 0) return ( + + Figuring out your favorites... + + ); + + return ( + + ⭐My Favorites + + horizontal + showsHorizontalScrollIndicator={false} + data={users} + renderItem={renderItem} + keyExtractor={(item, index) => `${index}-${item.id.value}`} + /> + + ); +}; + +// 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 = useCallback( + ({ item: { name, email, picture, cell }, index }) => { + const { title, first, last } = name; + const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff'; + return ( + + + + + Name: {title} {first} {last} + + Email: {email} + Mobile: {cell} + + + ); + }, + [], + ); + + if (users.length === 0) return ; + + return ( + + + data={users} + renderItem={renderItem} + keyExtractor={(item, index) => `${index}-${item.id.value}`} + /> + + ); +}; + +// 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 () => { + render(); + + 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(); + render(); + + 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 diff --git a/website/docs/13.x/cookbook/basics/_meta.json b/website/docs/13.x/cookbook/basics/_meta.json new file mode 100644 index 000000000..591daedc8 --- /dev/null +++ b/website/docs/13.x/cookbook/basics/_meta.json @@ -0,0 +1 @@ +["async-tests", "custom-render"] diff --git a/website/docs/13.x/cookbook/basics/async-tests.md b/website/docs/13.x/cookbook/basics/async-tests.md new file mode 100644 index 000000000..c3900d519 --- /dev/null +++ b/website/docs/13.x/cookbook/basics/async-tests.md @@ -0,0 +1,142 @@ +# Async tests + +## Summary + +Typically, you would write synchronous tests, as they are simple and get the work done. However, there are cases when using asynchronous (async) tests might be necessary or beneficial. The two most common cases are: + +1. **Testing Code with asynchronous operations**: When your code relies on asynchronous operations, such as network calls or database queries, async tests are essential. Even though you should mock these network calls, the mock should act similarly to the actual behavior and hence by async. +2. **UserEvent API:** Using the [User Event API](docs/api/events/user-event) in your tests creates more realistic event handling. These interactions introduce delays (even though these are typically event-loop ticks with 0 ms delays), requiring async tests to handle the timing correctly. + +Using async tests when needed ensures your tests are reliable and simulate real-world conditions accurately. + +### Example + +Consider a basic asynchronous 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(); + render(); + + // 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](docs/api/queries#find-by). These are useful when waiting for a matching element to appear. They can be understood as a [`getBy*` queries](docs/api/queries#get-by) used in conjunction with a [`waitFor` function](docs/api/misc/async#waitfor). + +They accept the same predicates as `getBy*` queries like `findByRole`, `findByTest`, etc. They also have a multiple elements variant called [`findAllBy*`](docs/api/queries#find-all-by). + +```typescript +function findByRole: ( + role: TextMatch, + queryOptions?: { + // Query specific options + } + waitForOptions?: { + timeout?: number; + interval?: number; + // .. + } +): Promise; +``` + +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](docs/api/misc/config#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( + expectation: () => T, + options?: { + timeout: number; + interval: number; + } +): Promise; +``` + +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`](docs/api/misc/async#waitforelementtoberemoved), is used to verify that a matching element was present but has since been removed. + +```typescript +function waitForElementToBeRemoved( + expectation: () => T, + options?: { + timeout: number; + interval: number; + } +): Promise {} +``` + +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. diff --git a/website/docs/13.x/cookbook/basics/custom-render.md b/website/docs/13.x/cookbook/basics/custom-render.md new file mode 100644 index 000000000..6d1d88ffd --- /dev/null +++ b/website/docs/13.x/cookbook/basics/custom-render.md @@ -0,0 +1,78 @@ +# 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 function renderWithProviders( + ui: React.ReactElement, + options?: RenderWithProvidersProps +) { + return render( + + {ui} + + ); +} +``` + +```tsx title=custom-render/index.test.tsx +import { screen } from '@testing-library/react-native'; +import { renderWithProviders } from '../test-utils'; +// ... + +test('renders WelcomeScreen with user', () => { + renderWithProviders(, { user: { name: 'Jar-Jar' } }); + expect(screen.getByText(/hello Jar-Jar/i)).toBeOnTheScreen(); +}); + +test('renders WelcomeScreen without user', () => { + renderWithProviders(, { 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', () => { + renderScreen(, { 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 function + +Make it async if you want to put some async setup in your custom render function. + +```tsx title=SomeScreen.test.tsx +test('renders SomeScreen', async () => { + await renderWithAsync(); + // ... +}); +``` diff --git a/website/docs/13.x/cookbook/index.md b/website/docs/13.x/cookbook/index.md new file mode 100644 index 000000000..da99e2155 --- /dev/null +++ b/website/docs/13.x/cookbook/index.md @@ -0,0 +1,29 @@ +# Introduction + +Welcome to the **React Native Testing Library (RNTL) Cookbook**! +This app is your go-to resource for learning how to effectively test React Native applications. +It provides a collection of **best practices**, **ready-made recipes**, and **tips & tricks** to +simplify and improve your testing workflow. Whether you’re a beginner just getting started or a +seasoned developer looking to sharpen your +skills, the Cookbook has something for everyone. + +## What's Inside the Cookbook? + +The Cookbook is currently organized into **three main chapters**: + +- **Basic Recipes**: A great starting point, covering essential testing scenarios such as async + operations and custom render functions. +- **Advanced Recipes**: More complex scenarios like network requests and in the future, navigation + testing and more. +- **State Management Recipes**: Best practices for testing state management libraries + +Each recipe includes a clear explanation along with a corresponding code example to help you get +hands-on with testing. Checkout +the [Cookbook App](https://github.com/callstack/react-native-testing-library/tree/main/examples/cookbook#rntl-cookbook) to see the +recipes in action. + +## What's Next? + +Join the conversation +on [GitHub](https://github.com/callstack/react-native-testing-library/issues/1624) here to discuss +ideas, ask questions, or provide feedback. diff --git a/website/docs/13.x/cookbook/state-management/_meta.json b/website/docs/13.x/cookbook/state-management/_meta.json new file mode 100644 index 000000000..beac50b85 --- /dev/null +++ b/website/docs/13.x/cookbook/state-management/_meta.json @@ -0,0 +1 @@ +["jotai"] diff --git a/website/docs/13.x/cookbook/state-management/jotai.md b/website/docs/13.x/cookbook/state-management/jotai.md new file mode 100644 index 000000000..8471367c0 --- /dev/null +++ b/website/docs/13.x/cookbook/state-management/jotai.md @@ -0,0 +1,226 @@ +# Jotai + +## Introduction + +Jotai is a global state management library for React that uses an atomic approach to optimize +renders and solve issues like extra re-renders and the need for memoization. It scales from simple +state management to complex enterprise applications, offering utilities and extensions to enhance +the developer experience. + +## Task List Example + +Let's assume we have a simple task list component that uses Jotai for state management. The +component has a list of tasks, a text input for typing new task name and a button to add a new task to the list. + +```tsx title=state-management/jotai/TaskList.tsx +import * as React from 'react'; +import { Pressable, Text, TextInput, View } from 'react-native'; +import { useAtom } from 'jotai'; +import { nanoid } from 'nanoid'; +import { newTaskTitleAtom, tasksAtom } from './state'; + +export function TaskList() { + const [tasks, setTasks] = useAtom(tasksAtom); + const [newTaskTitle, setNewTaskTitle] = useAtom(newTaskTitleAtom); + + const handleAddTask = () => { + setTasks((tasks) => [ + ...tasks, + { + id: nanoid(), + title: newTaskTitle, + }, + ]); + setNewTaskTitle(''); + }; + + return ( + + {tasks.map((task) => ( + + {task.title} + + ))} + + {!tasks.length ? No tasks, start by adding one... : null} + + setNewTaskTitle(text)} + /> + + + Add Task + + + ); +} +``` + +## Starting with a Simple Test + +We can test our `TaskList` component using React Native Testing Library's (RNTL) regular `render` +function. Although it is sufficient to test the empty state of the `TaskList` component, it is not +enough to test the component with initial tasks present in the list. + +```tsx title=status-management/jotai/__tests__/TaskList.test.tsx +import * as React from 'react'; +import { render, screen, userEvent } from '@testing-library/react-native'; +import { renderWithAtoms } from './test-utils'; +import { TaskList } from './TaskList'; +import { newTaskTitleAtom, tasksAtom } from './state'; +import { Task } from './types'; + +jest.useFakeTimers(); + +test('renders an empty task list', () => { + render(); + expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen(); +}); +``` + +## Custom Render Function to populate Jotai Atoms with Initial Values + +To test the `TaskList` component with initial tasks, we need to be able to populate the `tasksAtom` with +initial values. We can create a custom render function that uses Jotai's `useHydrateAtoms` hook to +hydrate the atoms with initial values. This function will accept the initial atoms and their +corresponding values as an argument. + +```tsx title=status-management/jotai/test-utils.tsx +import * as React from 'react'; +import { render } from '@testing-library/react-native'; +import { useHydrateAtoms } from 'jotai/utils'; +import { PrimitiveAtom } from 'jotai/vanilla/atom'; + +// Jotai types are not well exported, so we will make our life easier by using `any`. +export type AtomInitialValueTuple = [PrimitiveAtom, T]; + +export interface RenderWithAtomsOptions { + initialValues: AtomInitialValueTuple[]; +} + +/** + * Renders a React component with Jotai atoms for testing purposes. + * + * @param component - The React component to render. + * @param options - The render options including the initial atom values. + * @returns The render result from `@testing-library/react-native`. + */ +export const renderWithAtoms = ( + component: React.ReactElement, + options: RenderWithAtomsOptions, +) => { + return render( + {component}, + ); +}; + +export type HydrateAtomsWrapperProps = React.PropsWithChildren<{ + initialValues: AtomInitialValueTuple[]; +}>; + +/** + * A wrapper component that hydrates Jotai atoms with initial values. + * + * @param initialValues - The initial values for the Jotai atoms. + * @param children - The child components to render. + * @returns The rendered children. + + */ +function HydrateAtomsWrapper({ initialValues, children }: HydrateAtomsWrapperProps) { + useHydrateAtoms(initialValues); + return children; +} +``` + +## Testing the `TaskList` Component with initial tasks + +We can now use the `renderWithAtoms` function to render the `TaskList` component with initial tasks. The +`initialValues` property will contain the `tasksAtom`, `newTaskTitleAtom` and their initial values. We can then test the component to ensure that the initial tasks are rendered correctly. + +:::info +In our test, we populated only one atom and its initial value, but you can add other Jotai atoms and their corresponding values to the initialValues array as needed. +::: + +```tsx title=status-management/jotai/__tests__/TaskList.test.tsx +======= +const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }]; + +test('renders a to do list with 1 items initially, and adds a new item', async () => { + renderWithAtoms(, { + initialValues: [ + [tasksAtom, INITIAL_TASKS], + [newTaskTitleAtom, ''], + ], + }); + + expect(screen.getByText(/buy bread/i)).toBeOnTheScreen(); + expect(screen.getAllByTestId('task-item')).toHaveLength(1); + + const user = userEvent.setup(); + await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk'); + await user.press(screen.getByRole('button', { name: /add task/i })); + + expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen(); + expect(screen.getAllByTestId('task-item')).toHaveLength(2); +}); +``` + +## Modifying atom outside of React components + +In several cases, you might need to change an atom's state outside a React component. In our case, +we have a set of functions to get tasks and set tasks, which change the state of the task list atom. + +```tsx title=state-management/jotai/state.ts +import { atom, createStore } from 'jotai'; +import { Task } from './types'; + +export const tasksAtom = atom([]); +export const newTaskTitleAtom = atom(''); + +// Available for use outside React components +export const store = createStore(); + +// Selectors +export function getAllTasks(): Task[] { + return store.get(tasksAtom); +} + +// Actions +export function addTask(task: Task) { + store.set(tasksAtom, [...getAllTasks(), task]); +} +``` + +## Testing atom outside of React components + +You can test the `getAllTasks` and `addTask` functions outside the React component's scope by setting +the initial to-do items in the store and then checking if the functions work as expected. +No special setup is required to test these functions, as `store.set` is available by default by +Jotai. + +```tsx title=state-management/jotai/__tests__/TaskList.test.tsx +import { addTask, getAllTasks, store, tasksAtom } from './state'; + +//... + +test('modify store outside of React component', () => { + // Set the initial to do items in the store + store.set(tasksAtom, INITIAL_TASKS); + expect(getAllTasks()).toEqual(INITIAL_TASKS); + + const NEW_TASK = { id: '2', title: 'Buy almond milk' }; + addTask(NEW_TASK); + expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]); +}); +``` + +## Conclusion + +Testing a component or a function that depends on Jotai atoms is straightforward with the help of +the `useHydrateAtoms` hook. We've seen how to create a custom render function `renderWithAtoms` that +sets up atoms and their initial values for testing purposes. We've also seen how to test functions +that change the state of atoms outside React components. This approach allows us to test components +in different states and scenarios, ensuring they behave as expected. diff --git a/website/docs/13.x/docs/_meta.json b/website/docs/13.x/docs/_meta.json new file mode 100644 index 000000000..4387b5b9a --- /dev/null +++ b/website/docs/13.x/docs/_meta.json @@ -0,0 +1,24 @@ +[ + { + "type": "dir", + "name": "start", + "label": "Getting started" + }, + { "type": "dir", "name": "api", "label": "API reference" }, + { + "type": "dir", + "name": "guides", + "label": "Guides" + }, + { + "type": "dir", + "name": "advanced", + "label": "Advanced Guides" + }, + { + "type": "dir", + "name": "migration", + "label": "Migration Guides", + "collapsed": true + } +] diff --git a/website/docs/13.x/docs/advanced/_meta.json b/website/docs/13.x/docs/advanced/_meta.json new file mode 100644 index 000000000..32b909fa0 --- /dev/null +++ b/website/docs/13.x/docs/advanced/_meta.json @@ -0,0 +1 @@ +["testing-env", "understanding-act"] diff --git a/website/docs/13.x/docs/advanced/testing-env.mdx b/website/docs/13.x/docs/advanced/testing-env.mdx new file mode 100644 index 000000000..41292a3ff --- /dev/null +++ b/website/docs/13.x/docs/advanced/testing-env.mdx @@ -0,0 +1,146 @@ +# 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 allows you to write integration and component tests for your React Native app or library. While the JSX code used in tests closely resembles your React Native app, things are not as simple as they might appear. This document will describe the key elements of our testing environment and highlight things to be aware of when writing more 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. + +## React Test Renderer + +Instead, RNTL uses React Test Renderer, a specialized renderer that allows rendering to pure JavaScript objects without access to mobile OS and can run in a Node.js environment using Jest (or any other JavaScript test runner). + +Using React Test Renderer has pros and cons. + +Benefits: + +- tests can run on most CIs (Linux, etc) and do not require a mobile device or emulator +- faster test execution +- light runtime environment + +Disadvantages: + +- Tests do not execute native code +- Tests are unaware of the view state that would be managed by native components, e.g., focus, unmanaged text boxes, etc. +- Assertions do not 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 `TestRenderer.create()` method. The output tree represents your React Native component tree, and each node of that tree is an "instance" of some React component (to be more precise, each node represents a React fiber, and only class components have instances, while function components store the hook state using fibers). + +These tree elements are represented by `ReactTestInstance` type: + +```tsx +interface ReactTestInstance { + type: ElementType; + props: { [propName: string]: any }; + parent: ReactTestInstance | null; + children: Array; + + // Other props and methods +} +``` + +Based on: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer/index.d.ts + +## Host and composite components + +One of the most important aspects of the element tree is that it is composed of both host and composite components: + +- [Host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components) will have direct counterparts in the native view tree. Typical examples are ``, `` , ``, and `` from React Native. You can think of these as an analog of `
`, `` 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 in the element tree. +- 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`. + +The part of the tree looks as follows: + +```jsx +* (composite) + * (host) + * children prop passed in JSX +``` + +A similar relation exists between other composite and host pairs: e.g. `Text` , `TextInput`, and `Image` components: + +```jsx +* (composite) + * (host) + * string (or mixed) content +``` + +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 +* (composite) + * (host) + * children prop passed in JSX +``` + +### Differentiating between host and composite elements + +Any easy way to differentiate between host and composite elements is the `type` prop of `ReactTestInstance`: + +- for host components, it's always a string value representing a component name, e.g., `"View"` +- for composite components, it's a function or class corresponding to the component + +You can use the following code to check if a given element is a host one: + +```jsx +function isHostElement(element: ReactTestInstance) { + return typeof element.type === 'string'; +} +``` + +## Tree nodes + +We encourage you to only assert values on host views in your tests because they represent the user interface view and controls which the user can see and interact with. Users cannot see or interact with composite views as they exist purely in the JavaScript domain and do not generate any visible UI. + +### Asserting props + +For example, suppose you assert a `style` prop of a composite element. In that case, there is no guarantee that the style will be visible to the user, as the component author can forget to pass this prop to some underlying `View` or other host component. Similarly `onPress` event handler on a composite prop can be unreachable by the user. + +```jsx +function ForgotToPassPropsButton({ title, onPress, style }) { + return ( + + {title} + + ); +} +``` + +In the above example, user-defined components accept both `onPress` and `style` props but do not pass them (through `Pressable`) to host views, so they will not affect the user interface. Additionally, React Native and other libraries might pass some of the props under different names or transform their values between composite and host components. + +## 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 will encounter host and composite elements when navigating a tree of react elements using `parent` or `children` props of a `ReactTestInstance` element. You should be careful when navigating the element tree, as the tree structure for third-party components can change independently from your code and cause unexpected test failures. + +Inside RNTL, we have various tree navigation helpers: `getHostParent`, `getHostChildren`, etc. These are intentionally not exported, as using them is not recommended. + +## Queries + +All recommended Testing Library queries return host components to encourage the best practices described above. + +Only `UNSAFE_*ByType` and `UNSAFE_*ByProps` queries can return both host and composite components depending on used predicates. They are marked as unsafe precisely because testing composite components makes your test more fragile. diff --git a/website/docs/13.x/docs/advanced/understanding-act.mdx b/website/docs/13.x/docs/advanced/understanding-act.mdx new file mode 100644 index 000000000..a6dfd519d --- /dev/null +++ b/website/docs/13.x/docs/advanced/understanding-act.mdx @@ -0,0 +1,226 @@ +# Understanding `act` function + +When writing RNTL tests one of the things that confuses developers the most are cryptic [`act()`](https://reactjs.org/docs/testing-recipes.html#act) function errors logged into console. In this article I will try to build an understanding of the purpose and behaviour of `act()` so you can build your tests with more confidence. + +## `act` warnings + +Let’s start with typical `act()` warnings logged to console. There are two kinds of these issues, let’s call the first one the "sync `act()`" warning: + +``` +Warning: An update to Component 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 */ +``` + +The second one relates to async usage of `act` so let’s call it the "async `act`" error: + +``` +Warning: You called act(async () => ...) without await. This could lead to unexpected +testing behaviour, interleaving multiple act calls and mixing their scopes. You should +- await act(async () => ...); +``` + +## Synchronous `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. + +To showcase that behaviour let make a small experiment. First we define a function component that uses `useEffect` hook in a trivial way. + +```jsx +function TestComponent() { + const [count, setCount] = React.useState(0); + React.useEffect(() => { + setCount((c) => c + 1); + }, []); + + return Count {count}; +} +``` + +In the following tests we will directly use `ReactTestRenderer` 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 +test('render without act', () => { + const renderer = TestRenderer.create(); + + // Bind RNTL queries for root element. + const view = within(renderer.root); + expect(view.getByText('Count 0')).toBeOnTheScreen(); +}); +``` + +When testing without `act` call wrapping rendering call, we see that the assertion runs just after the rendering but before `useEffect`hooks effects are applied. Which is not what we expected in our tests. + +```jsx +test('render with act', () => { + let renderer: ReactTestRenderer; + act(() => { + renderer = TestRenderer.create(); + }); + + // Bind RNTL queries for root element. + const view = within(renderer!.root); + expect(view.getByText('Count 1')).toBeOnTheScreen(); +}); +``` + +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 - `ReactTestRenderer.create` call +- re-rendering of component -`renderer.update` call +- triggering any event handlers that cause component tree render + +Thankfully, for these basic cases RNTL has got you covered as our `render`, `update` and `fireEvent` methods already wrap their calls in sync `act` so that you do not have to do it explicitly. + +Note that `act` calls can be safely nested and internally form a stack of calls. However, overlapping `act` calls, which can be achieved using async version of `act`, [are not supported](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js#L161). + +### Implementation + +As of React version of 18.1.0, 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. This implementation has been fairly stable since React 17.0. + +RNTL exports `act` for convenience of the users as defined in the [act.ts source file](https://github.com/callstack/react-native-testing-library/blob/main/src/act.ts). That file refers to [ReactTestRenderer.js source](https://github.com/facebook/react/blob/ce13860281f833de8a3296b7a3dad9caced102e9/packages/react-test-renderer/src/ReactTestRenderer.js#L52) file from React Test Renderer package, which finally leads to React act implementation in ReactAct.js (already mentioned above). + +## Asynchronous `act` + +So far we have seen synchronous version of `act` which runs its callback immediately. This can deal with things like synchronous effects or mocks using already resolved promises. However, not all component code is synchronous. Frequently our components or mocks contain some asynchronous behaviours like `setTimeout` calls or network calls. Starting from React 16.9, `act` can also be called in asynchronous mode. In such case `act` implementation checks that the passed callback returns [object resembling promise](https://github.com/facebook/react/blob/ce13860281f833de8a3296b7a3dad9caced102e9/packages/react/src/ReactAct.js#L60). + +### Asynchronous code + +Asynchronous version of `act` also is executed immediately, but the callback is not yet completed because of some asynchronous operations inside. + +Lets look at a simple example with component using `setTimeout` call to simulate asynchronous behaviour: + +```jsx +function TestAsyncComponent() { + const [count, setCount] = React.useState(0); + React.useEffect(() => { + setTimeout(() => { + setCount((c) => c + 1); + }, 50); + }, []); + + return Count {count}; +} +``` + +```jsx +import { render, screen } from '@testing-library/react-native'; + +test('render async natively', () => { + render(); + 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 sync act warning: + +``` +Warning: An update to TestAsyncComponent 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 */ +``` + +Note that this is not yet the infamous async act warning. It only asks us to wrap our event code with `act` calls. However, this time our immediate state change does not originate from externally triggered events but rather forms an internal part of the component. So how can we apply `act` in such scenario? + +### Solution with fake timers + +First solution is to use Jest's fake timers inside out tests: + +```jsx +test('render with fake timers', () => { + jest.useFakeTimers(); + render(); + + act(() => { + jest.runAllTimers(); + }); + expect(screen.getByText('Count 1')).toBeOnTheScreen(); +}); +``` + +That way we can wrap `jest.runAllTimers()` call which triggers the `setTimeout` updates inside an `act` call, hence resolving the act warning. Note that this whole code is synchronous thanks to usage of Jest fake timers. + +### Solution with real timers + +If we wanted to stick with real timers then things get a bit more complex. Let’s start by applying a crude solution of opening async `act()` call for the expected duration of components updates: + +```jsx +test('render with real timers - sleep', async () => { + render(); + await act(async () => { + 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. + +Let’s try more elegant solution using `waitFor` that will wait for our desired state: + +```jsx +test('render with real timers - waitFor', async () => { + render(); + + 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 () => { + render(); + + 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. + +### Async act warning + +If we modify any of the above async tests and remove `await` keyword, then we will trigger the notorious async `act()`warning: + +```jsx +Warning: You called act(async () => ...) without await. This could lead to unexpected +testing behaviour, interleaving multiple act calls and mixing their scopes. You should +- await act(async () => ...); +``` + +React decides to show this error whenever it detects that async `act()`call [has not been awaited](https://github.com/facebook/react/blob/ce13860281f833de8a3296b7a3dad9caced102e9/packages/react/src/ReactAct.js#L93). + +The exact reasons why you might see async `act()` warnings vary, but finally it means that `act()` has been called with callback that returns `Promise`-like object, but it has not been waited on. + +## References + +- [React `act` implementation source](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js) +- [React testing recipes: `act()`](https://reactjs.org/docs/testing-recipes.html#act) diff --git a/website/docs/13.x/docs/api.md b/website/docs/13.x/docs/api.md new file mode 100644 index 000000000..ef91c3ae8 --- /dev/null +++ b/website/docs/13.x/docs/api.md @@ -0,0 +1,22 @@ +--- +uri: /api +--- +# API Overview + +React Native Testing Library consists of following APIs: + +- [`render` function](docs/api/render) - render your UI components for testing purposes +- [`screen` object](docs/api/screen) - access rendered UI: + - [Queries](docs/api/queries) - find relevant components by various predicates: role, text, test ids, etc + - Lifecycle methods: [`rerender`](docs/api/screen#rerender), [`unmount`](docs/api/screen#unmount) + - Helpers: [`debug`](docs/api/screen#debug), [`toJSON`](docs/api/screen#tojson), [`root`](docs/api/screen#root) +- [Jest matchers](docs/api/jest-matchers) - validate assumptions about your UI +- [User Event](docs/api/events/user-event) - simulate common user interactions like [`press`](docs/api/events/user-event#press) or [`type`](docs/api/events/user-event#type) in a realistic way +- [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` + - [Configuration](docs/api/misc/config): `configure`, `resetToDefaults` + - [Accessibility](docs/api/misc/accessibility): `isHiddenFromAccessibility` + - [Other](docs/api/misc/other): `within`, `act`, `cleanup` diff --git a/website/docs/13.x/docs/api/_meta.json b/website/docs/13.x/docs/api/_meta.json new file mode 100644 index 000000000..033be926d --- /dev/null +++ b/website/docs/13.x/docs/api/_meta.json @@ -0,0 +1,8 @@ +[ + { "type": "file", "name": "render", "label": "Render function" }, + { "type": "file", "name": "screen", "label": "Screen object" }, + "queries", + "jest-matchers", + { "type": "dir", "name": "events", "label": "Triggering events" }, + { "type": "dir", "name": "misc", "label": "Miscellaneous" } +] diff --git a/website/docs/13.x/docs/api/events/_meta.json b/website/docs/13.x/docs/api/events/_meta.json new file mode 100644 index 000000000..363342aa2 --- /dev/null +++ b/website/docs/13.x/docs/api/events/_meta.json @@ -0,0 +1,4 @@ +[ + { "type": "file", "name": "user-event", "label": "User Event" }, + { "type": "file", "name": "fire-event", "label": "Fire Event" } +] diff --git a/website/docs/13.x/docs/api/events/fire-event.mdx b/website/docs/13.x/docs/api/events/fire-event.mdx new file mode 100644 index 000000000..7f072d48b --- /dev/null +++ b/website/docs/13.x/docs/api/events/fire-event.mdx @@ -0,0 +1,158 @@ +# Fire Event API + +```ts +function fireEvent(element: ReactTestInstance, eventName: string, ...data: unknown[]): void; +``` + +:::note +For common events like `press` or `type` it's recommended to use [User Event API](docs/api/events/user-event) as it offers +more realistic event simulation 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. +::: + +The `fireEvent` API allows you to trigger all kinds of event handlers on both host and composite components. It will try to invoke a single event handler traversing the component tree bottom-up from passed element and trying to find enabled event handler named `onXxx` when `xxx` is the name of the event passed. + +Unlike User Event, this API does not automatically pass event object to event handler, this is responsibility of the user to construct such object. + +```jsx +import { render, screen, fireEvent } from '@testing-library/react-native'; + +test('fire changeText event', () => { + const onEventMock = jest.fn(); + render( + // MyComponent renders TextInput which has a placeholder 'Enter details' + // and with `onChangeText` bound to handleChangeText + + ); + + fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab'); + expect(onEventMock).toHaveBeenCalledWith('ab'); +}); +``` + +:::note +Please note that from version `7.0` `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 } from '@testing-library/react-native'; + +const onBlurMock = jest.fn(); + +render( + + + +); + +// you can omit the `on` prefix +fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur'); +``` + +FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`. + +### `fireEvent.press` {#press} + +``` +fireEvent.press: (element: ReactTestInstance, ...data: Array) => void +``` + +:::note +It is recommended to use the User Event [`press()`](docs/api/events/user-event#press) helper instead as it offers more realistic simulation of press interaction, including pressable support. +::: + +Invokes `press` event handler on the element or parent element in the tree. + +```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, + }, +}; + +render( + + + Press me + + +); + +fireEvent.press(screen.getByText('Press me'), eventData); +expect(onPressMock).toHaveBeenCalledWith(eventData); +``` + +### `fireEvent.changeText` {#change-text} + +``` +fireEvent.changeText: (element: ReactTestInstance, ...data: Array) => void +``` + +:::note +It is recommended to use the User Event [`type()`](docs/api/events/user-event#type) helper instead as it offers more realistic simulation of text change interaction, including key-by-key typing, element focus, and other editing events. +::: + +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'; + +render( + + + +); + +fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT); +``` + +### `fireEvent.scroll` {#scroll} + +``` +fireEvent.scroll: (element: ReactTestInstance, ...data: Array) => void +``` + +Invokes `scroll` event handler on the element or parent element in the tree. + +#### 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, + }, + }, +}; + +render( + + XD + +); + +fireEvent.scroll(screen.getByText('scroll-view'), eventData); +``` + +:::note + +Prefer using [`user.scrollTo`](docs/api/events/user-event#scrollto) over `fireEvent.scroll` for `ScrollView`, `FlatList`, and `SectionList` components. User Event provides a more realistic event simulation based on React Native runtime behavior. + +::: diff --git a/website/docs/13.x/docs/api/events/user-event.mdx b/website/docs/13.x/docs/api/events/user-event.mdx new file mode 100644 index 000000000..5a8379fe8 --- /dev/null +++ b/website/docs/13.x/docs/api/events/user-event.mdx @@ -0,0 +1,300 @@ +# User Event interactions + +:::info RNTL minimal version + +User Event interactions require RNTL v12.2.0 or later. + +::: + +## 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, you should always prefer it over the Fire Event counterpart, as it will make your tests much more realistic and, hence, reliable. In other cases, e.g., when User Event does not support the given event or when invoking event handlers on composite elements, you have to use Fire Event as the only available option. + +## `setup()` + +```ts +userEvent.setup(options?: { + delay: number; + advanceTimers: (delay: number) => Promise | void; +}) +``` + +Example + +```ts +const user = userEvent.setup(); +``` + +Creates a User Event object instance, which can be used to trigger events. + +### Options {#setup-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( + element: ReactTestInstance, +): Promise +``` + +Example + +```ts +const user = userEvent.setup(); +await user.press(element); +``` + +This helper simulates a press on any pressable element, e.g. `Pressable`, `TouchableOpacity`, `Text`, `TextInput`, etc. Unlike `fireEvent.press()`, a more straightforward API that will only call the `onPress` prop, this function simulates the entire press interaction in a more realistic way by reproducing the event sequence emitted by React Native runtime. This helper will trigger 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( + element: ReactTestInstance, + options: { duration: number } = { duration: 500 } +): Promise +``` + +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. This should be useful if you use 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 {#longpress-options} + +- `duration` - duration of the press in milliseconds. The default value is 500 ms. + +## `type()` + +```ts +type( + element: ReactTestInstance, + text: string, + options?: { + skipPress?: boolean + submitEditing?: boolean + } +``` + +Example + +```ts +const user = userEvent.setup(); +await user.type(textInput, 'Hello world!'); +``` + +This helper simulates the user 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 {#type-options} + +- `skipPress` - if true, `pressIn` and `pressOut` events will not be triggered. +- `submitEditing` - if true, `submitEditing` event will be triggered after typing the text. + +### Sequence of events {#type-sequence} + +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. + +## `clear()` + +```ts +clear( + element: ReactTestInstance, +) +``` + +Example + +```ts +const user = userEvent.setup(); +await user.clear(textInput); +``` + +This helper simulates the user 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 {#clear-sequence} + +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( + element: ReactTestInstance, + text: string, +) +``` + +Example + +```ts +const user = userEvent.setup(); +await user.paste(textInput, 'Text to paste'); +``` + +This helper simulates the user pasting given text to a `TextInput` element. + +This function supports only host `TextInput` elements. Passing other element types will result in throwing an error. + +### Sequence of events {#paste-sequence} + +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` + +**Leaving the element**: + +- `endEditing` +- `blur` + +## `scrollTo()` \{#scroll-to} + +:::note +`scrollTo` interaction has been introduced in RNTL v12.4.0. +::: + +```ts +scrollTo( + element: ReactTestInstance, + 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 }, + } +``` + +Example + +```ts +const user = userEvent.setup(); +await user.scrollTo(scrollView, { y: 100, momentumY: 200 }); +``` + +This helper simulates the user 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 {#scroll-to-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 {#scroll-sequence} + +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` diff --git a/website/docs/13.x/docs/api/jest-matchers.mdx b/website/docs/13.x/docs/api/jest-matchers.mdx new file mode 100644 index 000000000..26255ada9 --- /dev/null +++ b/website/docs/13.x/docs/api/jest-matchers.mdx @@ -0,0 +1,224 @@ +# Jest matchers + +:::info RNTL minimal version + +Built-in Jest matchers require RNTL v12.4.0 or later. + +::: + +This guide describes built-in Jest matchers, we recommend using these matchers as they provide readable tests, accessibility support, and a better developer experience. + +## Setup + +You can use the built-in matchers by adding the following line to your `jest-setup.ts` file (configured using [`setupFilesAfterEnv`](https://jestjs.io/docs/configuration#setupfilesafterenv-array)): + +```ts title=jest-setup.ts +import '@testing-library/react-native/extend-expect'; +``` + +Alternatively, you can add above script to your Jest configuration (usually located either in the `jest.config.js` file or in the `package.json` file under the `"jest"` key): + +```json title=jest.config.js +{ + "preset": "react-native", + "setupFilesAfterEnv": ["@testing-library/react-native/extend-expect"] +} +``` + +## Migration from legacy Jest Native matchers. + +If you are already using legacy Jest Native matchers we have a [migration guide](docs/migration/jest-matchers) for moving to the built-in matchers. + +## Checking element existence + +### `toBeOnTheScreen()` + +```ts +expect(element).toBeOnTheScreen(); +``` + +This allows you to assert whether an element is attached to the element tree or not. If you hold a reference to an element and it gets unmounted during the test it will no longer pass this assertion. + +## Element Content + +### `toHaveTextContent()` + +```ts +expect(element).toHaveTextContent( + text: string | RegExp, + options?: { + exact?: boolean; + normalizer?: (text: string) => string; + }, +) +``` + +This allows you to assert whether the given element has the given text content or not. It accepts either `string` or `RegExp` matchers, as well as [text match options](docs/api/queries#text-match-options) of `exact` and `normalizer`. + +### `toContainElement()` + +```ts +expect(container).toContainElement( + element: ReactTestInstance | null, +) +``` + +This allows you to assert whether the given container element does contain another host element. + +### `toBeEmptyElement()` + +```ts +expect(element).toBeEmptyElement(); +``` + +This allows you to assert whether the given element does not have any host child elements or text content. + +## Checking element state + +### `toHaveDisplayValue()` + +```ts +expect(element).toHaveDisplayValue( + value: string | RegExp, + options?: { + exact?: boolean; + normalizer?: (text: string) => string; + }, +) +``` + +This allows you to assert whether the given `TextInput` element has a specified display value. It accepts either `string` or `RegExp` matchers, as well as [text match options](docs/api/queries#text-match-options) of `exact` and `normalizer`. + +### `toHaveAccessibilityValue()` + +```ts +expect(element).toHaveAccessibilityValue( + value: { + min?: number; + max?: number; + now?: number; + text?: string | RegExp; + }, +) +``` + +This allows you to assert whether the given element has a specified accessible value. + +This matcher will assert accessibility value based on `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` and `accessibilityValue` props. Only defined value entries will be used in the assertion, the element might have additional accessibility value entries and still be matched. + +When querying by `text` entry a string or `RegExp` might be used. + +### `toBeEnabled()` / `toBeDisabled` {#tobeenabled} + +```ts +expect(element).toBeEnabled(); +expect(element).toBeDisabled(); +``` + +These allow you to assert whether the given element is enabled or disabled from the user's perspective. It relies on the accessibility disabled state as set by `aria-disabled` or `accessibilityState.disabled` props. It will consider a given element disabled when it or any of its ancestors is disabled. + +:::note +These matchers are the negation of each other, and both are provided to avoid double negations in your assertions. +::: + +### `toBeSelected()` + +```ts +expect(element).toBeSelected(); +``` + +This allows you to assert whether the given element is selected from the user's perspective. It relies on the accessibility selected state as set by `aria-selected` or `accessibilityState.selected` props. + +### `toBeChecked()` / `toBePartiallyChecked()` {#tobechecked} + +```ts +expect(element).toBeChecked(); +expect(element).toBePartiallyChecked(); +``` + +These allow you to assert whether the given element is checked or partially checked from the user's perspective. It relies on the accessibility checked state as set by `aria-checked` or `accessibilityState.checked` props. + +:::note + +- `toBeChecked()` matcher works only on `Switch` host elements and accessibility elements with `checkbox`, `radio` or `switch` role. +- `toBePartiallyChecked()` matcher works only on elements with `checkbox` role. + +::: + +### `toBeExpanded()` / `toBeCollapsed()` {#tobeexpanded} + +```ts +expect(element).toBeExpanded(); +expect(element).toBeCollapsed(); +``` + +These allows you to assert whether the given element is expanded or collapsed from the user's perspective. It relies on the accessibility disabled state as set by `aria-expanded` or `accessibilityState.expanded` props. + +:::note +These matchers are the negation of each other for expandable elements (elements with explicit `aria-expanded` or `accessibilityState.expanded` props). However, both won't pass for non-expandable elements (ones without explicit `aria-expanded` or `accessibilityState.expanded` props). +::: + +### `toBeBusy()` + +```ts +expect(element).toBeBusy(); +``` + +This allows you to assert whether the given element is busy from the user's perspective. It relies on the accessibility selected state as set by `aria-busy` or `accessibilityState.busy` props. + +## Checking element style + +### `toBeVisible()` + +```ts +expect(element).toBeVisible(); +``` + +This allows you to assert whether the given element is visible from the user's perspective. + +The element is considered invisible when itself or any of its ancestors has `display: none` or `opacity: 0` styles, as well as when it's hidden from accessibility. + +### `toHaveStyle()` + +```ts +expect(element).toHaveStyle( + style: StyleProp