diff --git a/versioned_docs/version-7.x/testing.md b/versioned_docs/version-7.x/testing.md
index 07b86a0e89..4f7fcdb9ae 100644
--- a/versioned_docs/version-7.x/testing.md
+++ b/versioned_docs/version-7.x/testing.md
@@ -25,19 +25,13 @@ If you're using `@react-navigation/stack`, you will only need to mock:
To add the mocks, create a file `jest/setup.js` (or any other file name of your choice) and paste the following code in it:
```js
-// include this line for mocking react-native-gesture-handler
+// Include this line for mocking react-native-gesture-handler
import 'react-native-gesture-handler/jestSetup';
-// include this section and the NativeAnimatedHelper section for mocking react-native-reanimated
-jest.mock('react-native-reanimated', () => {
- const Reanimated = require('react-native-reanimated/mock');
-
- // The mock for `call` immediately calls the callback which is incorrect
- // So we override it with a no-op
- Reanimated.default.call = () => {};
-
- return Reanimated;
-});
+// Include this section for mocking react-native-reanimated
+jest.mock('react-native-reanimated', () =>
+ require('react-native-reanimated/mock')
+);
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
@@ -54,59 +48,855 @@ Then we need to use this setup file in our jest config. You can add it under `se
Make sure that the path to the file in `setupFiles` is correct. Jest will run these files before running your tests, so it's the best place to put your global mocks.
+If your configuration works correctly, you can skip this section, but in some unusual cases you will need to mock `react-native-screens` as well. To add mock of the particular component, e.g. `Screen`, add the following code in `jest/setup.js` file:
+
+```js
+// Include this section form mocking react-native-screens
+jest.mock('react-native-screens', () => {
+ // Require actual module instead of a mock
+ let screens = jest.requireActual('react-native-screens');
+
+ // All exports in react-native-screens are getters
+ // We cannot use spread for cloning as it will call the getters
+ // So we need to clone it with Object.create
+ screens = Object.create(
+ Object.getPrototypeOf(screens),
+ Object.getOwnPropertyDescriptors(screens)
+ );
+
+ // Add mock of the Screen component
+ Object.defineProperty(screens, 'Screen', {
+ value: require('react-native').View,
+ });
+
+ return screens;
+});
+```
+
If you're not using Jest, then you'll need to mock these modules according to the test framework you are using.
## Writing tests
We recommend using [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) along with [`jest-native`](https://github.com/testing-library/jest-native) to write your tests.
-Example:
+We will go through some real-world case test code examples. Each code example consists of tested navigator and test code file.
+
+### Example 1
+
+Navigate to settings screen by "Go to Settings" button press.
+
+
+
+
+```js
+import { useNavigation } from '@react-navigation/native';
+import { createStackNavigator } from '@react-navigation/stack';
+import { Button, Text, View } from 'react-native';
+
+const HomeScreen = () => {
+ const navigation = useNavigation();
+
+ return (
+
+ Home screen
+
+ );
+};
+
+const SettingsScreen = () => {
+ return (
+
+ Settings screen
+
+ );
+};
+
+export const StackNavigator = createStackNavigator({
+ screens: {
+ Home: HomeScreen,
+ Settings: SettingsScreen,
+ },
+});
+```
+
+
+
+
+```js
+import { createStackNavigator } from '@react-navigation/stack';
+import { Button, Text, View } from 'react-native';
+
+const HomeScreen = ({ navigation }) => {
+ return (
+
+ Home screen
+
+ );
+};
+
+const SettingsScreen = () => {
+ return (
+
+ Settings screen
+
+ );
+};
+
+export const StackNavigator = () => {
+ const Stack = createStackNavigator();
+ return (
+
+
+
+
+ );
+};
+```
+
+
+
+
+
+
+
+```js
+import { expect, test } from '@jest/globals';
+import { createStaticNavigation } from '@react-navigation/native';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { StackNavigator } from './StackNavigator';
+
+test('navigates to settings by "Go to Settings" button press', () => {
+ const StackNavigation = createStaticNavigation(StackNavigator);
+ render();
+
+ fireEvent.press(screen.queryByText('Go to Settings'));
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
+});
+```
+
+
+
+
+```js
+import { expect, test } from '@jest/globals';
+import { NavigationContainer } from '@react-navigation/native';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { StackNavigator } from './StackNavigator';
+
+test('navigates to settings by "Go to Settings" button press', () => {
+ render(
+
+
+
+ );
+
+ fireEvent.press(screen.queryByText('Go to Settings'));
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
+});
+```
+
+
+
+
+We use `FireEvent` to press button and `expect` to check if rendered screen's content matches settings screen.
+
+### Example 2
+
+Navigate to settings screen by tab bar button press.
+
+
+
+
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Text, View } from 'react-native';
+
+const HomeScreen = () => {
+ return (
+
+ Home screen
+
+ );
+};
+
+const SettingsScreen = () => {
+ return (
+
+ Settings screen
+
+ );
+};
+
+export const TabNavigator = createBottomTabNavigator({
+ screens: {
+ Home: HomeScreen,
+ Settings: SettingsScreen,
+ },
+ screenOptions: {
+ headerShown: false,
+ },
+});
+```
+
+
+
+
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Text, View } from 'react-native';
+
+const HomeScreen = () => {
+ return (
+
+ Home screen
+
+ );
+};
+
+const SettingsScreen = () => {
+ return (
+
+ Settings screen
+
+ );
+};
+
+const Tab = createBottomTabNavigator();
+
+export const TabNavigator = () => {
+ return (
+
+
+
+
+ );
+};
+```
+
+
+
+
+
+
+
+```js
+import { expect, jest, test } from '@jest/globals';
+import { createStaticNavigation } from '@react-navigation/native';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+test('navigates to settings by tab bar button press', () => {
+ jest.useFakeTimers();
+
+ const TabNavigation = createStaticNavigation(TabNavigator);
+ render();
+
+ const button = screen.getByRole('button', { name: 'Settings, tab, 2 of 2' });
+
+ const event = {};
+ fireEvent.press(button, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
+});
+```
+
+
+
+
+```js
+import { expect, jest, test } from '@jest/globals';
+import { NavigationContainer } from '@react-navigation/native';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+test('navigates to settings by tab bar button press', () => {
+ jest.useFakeTimers();
+
+ render(
+
+
+
+ );
+
+ const button = screen.getByRole('button', { name: 'Settings, tab, 2 of 2' });
+
+ const event = {};
+ fireEvent.press(button, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
+});
+```
+
+
+
+
+We get settings tab bar button, press it and check if rendered content is correct.
+
+To find settings tab bar button you cannot use `queryByText`, because there is no text that can be queried. You can use `getByRole` instead and pass object with `name` as the second argument.
+
+```js
+// Pass object with settings tab name
+const button = screen.getByRole('button', { name: 'Settings, tab, 2 of 2' });
+```
+
+Tab bar buttons `handlePress` function expects to receive `GestureResponderEvent`. To avoid error you should pass `event` object as the second argument of `fireEvent`.
+
+```js
+// Pass event object to avoid error
+const event = {};
+fireEvent.press(button, event);
+```
+
+While writing tests containing navigation with animations you need to wait until animations finish before querying components. To do so, you have to use `fake timers`. [`Fake Timers`](https://jestjs.io/docs/timer-mocks) replace real implementation of times function to use fake clock. They allow you to instantly skip animation time. To avoid getting state change error, wrap `runAllTimers` in `act`.
+
+```js
+// Enable fake timers
+jest.useFakeTimers();
+
+// ...
+
+// Wrap jest.runAllTimers in act to prevent state change error
+// Skip all timers including animations
+act(() => jest.runAllTimers());
+```
+
+### Example 3
+
+Always displays settings screen after settings tab bar button press.
+
+
+
+
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { useNavigation } from '@react-navigation/native';
+import { createStackNavigator } from '@react-navigation/stack';
+import { useEffect } from 'react';
+import { Button, Text, View } from 'react-native';
+
+function HomeScreen() {
+ return (
+
+ Home screen
+
+ );
+}
+
+function SettingsScreen() {
+ const navigation = useNavigation();
+
+ return (
+
+ Settings screen
+
+ );
+}
+
+function DetailsScreen() {
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ const unsubscribe = navigation.getParent().addListener('tabPress', (e) => {
+ navigation.popToTop();
+ });
+ return unsubscribe;
+ }, [navigation]);
+
+ return (
+
+ Details screen
+
+ );
+}
+
+const SettingsNavigator = createStackNavigator({
+ screens: {
+ Settings: SettingsScreen,
+ Details: DetailsScreen,
+ },
+});
+
+export const TabNavigator = createBottomTabNavigator({
+ screens: {
+ Home: HomeScreen,
+ SettingsStack: SettingsNavigator,
+ },
+ screenOptions: {
+ headerShown: false,
+ },
+});
+```
+
+
+
-
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { createStackNavigator } from '@react-navigation/stack';
+import { useEffect } from 'react';
+import { Button, Text, View } from 'react-native';
+
+function HomeScreen() {
+ return (
+
+ Home screen
+
+ );
+}
+
+function SettingsScreen({ navigation }) {
+ return (
+
+ Settings screen
+
+ );
+}
+
+function DetailsScreen({ navigation }) {
+ useEffect(() => {
+ const unsubscribe = navigation.getParent().addListener('tabPress', (e) => {
+ navigation.popToTop();
+ });
+ return unsubscribe;
+ }, [navigation]);
+
+ return (
+
+ Details screen
+
+ );
+}
+
+const SettingsStack = createStackNavigator();
+
+function SettingsStackScreen() {
+ return (
+
+
+
+
+ );
+}
+
+const Tab = createBottomTabNavigator();
+
+export function TabNavigator() {
+ return (
+
+
+
+
+ );
+}
+```
+
+
+
+
+
-```js name='Testing with jest'
-import * as React from 'react';
-import { screen, render, fireEvent } from '@testing-library/react-native';
+```js
+import { expect, jest, test } from '@jest/globals';
import { createStaticNavigation } from '@react-navigation/native';
-import { RootNavigator } from './RootNavigator';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+test('always displays settings screen after settings tab bar button press', () => {
+ jest.useFakeTimers();
+
+ const TabNavigation = createStaticNavigation(TabNavigator);
+ render();
+
+ const homeTabButton = screen.getByRole('button', {
+ name: 'Home, tab, 1 of 2',
+ });
-const Navigation = createStaticNavigation(RootNavigator);
+ const settingsTabButton = screen.getByRole('button', {
+ name: 'SettingsStack, tab, 2 of 2',
+ });
-test('shows profile screen when View Profile is pressed', () => {
- render();
+ const event = {};
- fireEvent.press(screen.getByText('View Profile'));
+ fireEvent.press(settingsTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
- expect(screen.getByText('My Profile')).toBeOnTheScreen();
+ fireEvent.press(screen.queryByText('Go to Details'), event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Details screen')).toBeOnTheScreen();
+
+ fireEvent.press(homeTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Home screen')).toBeOnTheScreen();
+
+ fireEvent.press(settingsTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
});
```
-```js name='Testing with jest'
-import * as React from 'react';
-import { screen, render, fireEvent } from '@testing-library/react-native';
+```js
+import { expect, jest, test } from '@jest/globals';
import { NavigationContainer } from '@react-navigation/native';
-import { RootNavigator } from './RootNavigator';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+test('always displays settings screen after settings tab bar button press', () => {
+ jest.useFakeTimers();
-test('shows profile screen when View Profile is pressed', () => {
render(
-
+
);
- fireEvent.press(screen.getByText('View Profile'));
+ const homeTabButton = screen.getByRole('button', {
+ name: 'Home, tab, 1 of 2',
+ });
+ const settingsTabButton = screen.getByRole('button', {
+ name: 'SettingsStack, tab, 2 of 2',
+ });
+
+ const event = {};
+
+ fireEvent.press(settingsTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
+
+ fireEvent.press(screen.queryByText('Go to Details'), event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Details screen')).toBeOnTheScreen();
+
+ fireEvent.press(homeTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Home screen')).toBeOnTheScreen();
- expect(screen.getByText('My Profile')).toBeOnTheScreen();
+ fireEvent.press(settingsTabButton, event);
+ act(() => jest.runAllTimers());
+ expect(screen.queryByText('Settings screen')).toBeOnTheScreen();
});
```
+We query tab bar buttons, press buttons and check if rendered screens are correct.
+
+### Example 4
+
+Display loading state while waiting for data and then fetched profile nick on every profile screen focus.
+
+
+
+
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { useFocusEffect } from '@react-navigation/native';
+import { useCallback, useState } from 'react';
+import { Text, View } from 'react-native';
+
+function HomeScreen() {
+ return (
+
+ Home screen
+
+ );
+}
+
+const url = 'placeholder_url';
+
+function ProfileScreen() {
+ const [loading, setLoading] = useState(true);
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+
+ useFocusEffect(
+ useCallback(() => {
+ fetch(url)
+ .then((res) => res.json())
+ .then((data) => setData(data))
+ .catch((error) => setError(error))
+ .finally(() => setLoading(false));
+
+ return () => {
+ setData(undefined);
+ setError(undefined);
+ setLoading(true);
+ };
+ }, [])
+ );
+
+ return (
+
+ {loading && Loading}
+ {!loading && error && {error.message}}
+ {!loading && !error && {data.profile.nick}}
+
+ );
+}
+
+export const TabNavigator = createBottomTabNavigator({
+ screens: {
+ Home: HomeScreen,
+ Profile: ProfileScreen,
+ },
+ screenOptions: {
+ headerShown: false,
+ },
+});
+```
+
+
+
+
+```js
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { useFocusEffect } from '@react-navigation/native';
+import { useCallback, useState } from 'react';
+import { Text, View } from 'react-native';
+
+function HomeScreen() {
+ return (
+
+ Home screen
+
+ );
+}
+
+const url = 'placeholder_url';
+
+function ProfileScreen() {
+ const [loading, setLoading] = useState(true);
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+
+ useFocusEffect(
+ useCallback(() => {
+ fetch(url)
+ .then((res) => res.json())
+ .then((data) => setData(data))
+ .catch((error) => setError(error))
+ .finally(() => setLoading(false));
+
+ return () => {
+ setData(undefined);
+ setError(undefined);
+ setLoading(true);
+ };
+ }, [])
+ );
+
+ return (
+
+ {loading && Loading}
+ {!loading && error && {error.message}}
+ {!loading && !error && {data.profile.nick}}
+
+ );
+}
+
+const Tab = createBottomTabNavigator();
+
+export function TabNavigator() {
+ return (
+
+
+
+
+ );
+}
+```
+
+
+
+
+
+
+
+```js
+import { expect, jest, test } from '@jest/globals';
+import { createStaticNavigation } from '@react-navigation/native';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+async function mockedFetch() {
+ const mockResponse = {
+ profile: {
+ nick: 'CookieDough',
+ },
+ };
+ return {
+ ok: true,
+ status: 200,
+ json: async () => {
+ return mockResponse;
+ },
+ };
+}
+
+test('Display loading state while waiting for data and then fetched profile nick on every profile screen focus', async () => {
+ jest.useFakeTimers();
+
+ const TabNavigation = createStaticNavigation(TabNavigator);
+ render();
+
+ const spy = jest.spyOn(window, 'fetch').mockImplementation(mockedFetch);
+
+ const homeTabButton = screen.getByRole('button', {
+ name: 'Home, tab, 1 of 2',
+ });
+
+ const profileTabButton = screen.getByRole('button', {
+ name: 'Profile, tab, 2 of 2',
+ });
+
+ const event = {};
+ fireEvent.press(profileTabButton, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Loading')).toBeOnTheScreen();
+ expect(spy).toHaveBeenCalled();
+ expect(await screen.findByText('CookieDough')).toBeOnTheScreen();
+
+ fireEvent.press(homeTabButton, event);
+ fireEvent.press(profileTabButton, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Loading')).toBeOnTheScreen();
+ expect(spy).toHaveBeenCalled();
+ expect(await screen.findByText('CookieDough')).toBeOnTheScreen();
+});
+```
+
+
+
+
+```js
+import { expect, jest, test } from '@jest/globals';
+import { NavigationContainer } from '@react-navigation/native';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TabNavigator } from './TabNavigator';
+
+async function mockedFetch() {
+ const mockResponse = {
+ profile: {
+ nick: 'CookieDough',
+ },
+ };
+ return {
+ ok: true,
+ status: 200,
+ json: async () => {
+ return mockResponse;
+ },
+ };
+}
+
+test('Display loading state while waiting for data and then fetched profile nick on every profile screen focus', async () => {
+ jest.useFakeTimers();
+
+ render(
+
+
+
+ );
+
+ const spy = jest.spyOn(window, 'fetch').mockImplementation(mockedFetch);
+
+ const homeTabButton = screen.getByRole('button', {
+ name: 'Home, tab, 1 of 2',
+ });
+ const profileTabButton = screen.getByRole('button', {
+ name: 'Profile, tab, 2 of 2',
+ });
+
+ const event = {};
+ fireEvent.press(profileTabButton, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Loading')).toBeOnTheScreen();
+ expect(spy).toHaveBeenCalled();
+ expect(await screen.findByText('CookieDough')).toBeOnTheScreen();
+
+ fireEvent.press(homeTabButton, event);
+ fireEvent.press(profileTabButton, event);
+ act(() => jest.runAllTimers());
+
+ expect(screen.queryByText('Loading')).toBeOnTheScreen();
+ expect(spy).toHaveBeenCalled();
+ expect(await screen.findByText('CookieDough')).toBeOnTheScreen();
+});
+```
+
+
+
+
+We query tab buttons and mock fetch function using `spyOn` and `mockImplementation`. We navigate to profile screen and check if loading state is rendered correctly. Then, to check if fetched data is displayed, we use `findByText` - we need to wait for the fetch to finish before checking it's result. To ensure that operation will succeed not only on the first focus, we navigate back to home, then to settings and check loading state and fetched data again.
+
+To make test deterministic and isolate it from the real backend you can mock fetch function. You can use `spyOn` to override real implementation of fetch with `mockedFetch`.
+
+```js
+// Mock implementation of fetch function
+async function mockedFetch() {
+ const mockResponse = {
+ profile: {
+ nick: 'CookieDough',
+ },
+ };
+ return {
+ ok: true,
+ status: 200,
+ json: async () => {
+ return mockResponse;
+ },
+ };
+}
+
+test('display loading state while waiting for data and then fetched profile nick on every profile screen focus', async () => {
+ // ...
+
+ // Replace fetch implementation with mock
+ const spy = jest.spyOn(window, 'fetch').mockImplementation(mockedFetch);
+
+ // ...
+
+ // Check if mock fetch was called
+ expect(spy).toHaveBeenCalled();
+
+ // ...
+});
+```
+
## Best practices
There are a couple of things to keep in mind when writing tests for components using React Navigation: