Published on

Test Driving Header with React Navigation

Authors

Introduction

Photo by freestocks on Unsplash

In this blog post, I’ll show different approaches to test your screen header with using react-navigation library.

Learning Outcomes

  • How to test react-navigation header configuration

  • How to interact third part party library in test scope

Let’s imagine we have subscription screen that displaying a cancel button on the left side and a title on the center of headerbar. So, we want to test that we are actually rendering the title and cancel button. In addition, when cancel button is pressed we want go to previous screen.

    import React, {useEffect} from 'react';
    import {View, Text, Pressable} from 'react-native';
    
    export default function Subscription() {
      return (
        <View>
          <Text>Subscription Screen Content</Text>
          <Pressable>
            <Text>Get Subscription</Text>
          </Pressable>
        </View>
      );
    }

And Let’s configure a sample Navigation as well to demonstrate header configuration.

    import React from 'react';
    import {Pressable, Text} from 'react-native';
    import {NavigationContainer} from '@react-navigation/native';
    import {createNativeStackNavigator} from '@react-navigation/native-stack';
    
    import subscription from '../screens/subscription';
    import {goBack, navigationRef} from './utils';
    import Home from '../screens/Home';
    
    const Stack = createNativeStackNavigator();
    
    function Navigation() {
      return (
        <NavigationContainer ref={navigationRef}>
          <Stack.Navigator>
            <Stack.Screen name="home" component={Home} />
            <Stack.Screen
              name="subscription"
              component={Subscription}
              options={{
              headerTitle: () => <Text>Subscription</Text>,
                presentation: 'modal',
                headerLeft: () => (
                  <Pressable onPress={goBack}>
                    <Text>Cancel</Text>
                  </Pressable>
                ),
              }}
            />
          </Stack.Navigator>
        </NavigationContainer>
      );
    }
    
    export default Navigation;

So, our use case is the test in the subscription page, we actually rendering the cancel button and title. In addition, when cancel button is pressed we should navigate to previous screen that means Home Screen.

So we have two options to render header in test scope.

1- Render Subscription component by wrapping NavigationContainer

2- Render Navigation component directly.

For the first option, we can accomplish to render the component and look for header;

      test('should render correctly', () => {
        render(
          <NavigationContainer>
            <Stack.Navigator>
              <Stack.Screen
                        name="subscription"
                        component={Subscription}
                        options={{
                          title: 'Subscription',
                          presentation: 'modal',
                          headerLeft: () => (
                            <Pressable onPress={goBack}>
                              <Text>Cancel</Text>
                            </Pressable>
                          ),
                        }}
                      />
            </Stack.Navigator>
          </NavigationContainer>,
          );
      });

We can extract Navigator creation to another component and use it in the test and production to prevent duplicate.

By wrapping NavigationContainer we have to put the Stack.Navigator and Stack.Screen as well. So if you to your tests and add screen.debug() you will se the rendered JSX elements includes navigation related components.

Second option is rendering the Navigation component directly. We can accomplish it with;

    test('should render correctly', () => {
        render(<Navigation />);
      });

But How we can test that when a cancel button is pressed and navigate to previous screen. If we follow second option which is rendering Navigation. We can follow below steps.

1- Render Navigation component 2- It will render first screen ( home ) in the navigator. 3- Navigate user to subscribe screen by pressing “Get Subscription” button. 4- Check Cancel button is displayed and press cancel button 5- Query something ( like a text ) from previous screen ( home )

So above steps demonstrate the way we can interact more than one screen in the navigator. We can navigate screens and query elements on the focused screen like the way real user use our application. So let’s write the test for above steps.

    test('should display cancel button and on press navigate previous screen', async () => {
        render(<Navigation />);
        // first, render home because it is placed first in the navigator
        const subButton = screen.getByText('Get Subscription');
        await userEvent.press(subButton); // go to subscription page
    
        const cancelButton = await screen.getByText(/cancel/i); // get the cancel button
        expect(cancelButton).toBeOnTheScreen();
        await userEvent.press(cancelButton);
    
    
        // query something from previous screen to make sure screen navigated to previous screen
        expect(screen.getByText('Home')).toBeOnTheScreen();
      });

What about if we ever want to query function calls on navigation such as navigate, goBack and so on. In order to mock the function, we need to have wrap the related function for example goBack to spy on it. Luckily react-navigation provide us to have a custom functions like above:

    import {createNavigationContainerRef} from '@react-navigation/native';
    
    export const navigationRef = createNavigationContainerRef();
    
    export function goBack() {
      if (navigationRef.isReady()) {
        navigationRef.goBack();
      } else {
      }
    }

Just pass refnavigationRef in NavigationContainer and you are ready to use navigation object globally.

I believe it is important to note that you shouldn’t test implementation details. So, managing navigation object globally is not a perfect solution. But if you really need to test it that’s way!

    test('should display cancel button and on press navigate previous screen', async () => {
        const spyOnGoBack = jest.spyOn(NavigationUtils, 'goBack');
        render(<Navigation />);
        // first, render home because it is placed first in the navigator
        const subButton = screen.getByText('Get Subscription');
        await userEvent.press(subButton); // go to subscription page
    
        const cancelButton = await screen.getByText(/cancel/i); // get the cancel button
        expect(cancelButton).toBeOnTheScreen();
        await userEvent.press(cancelButton);
        expect(spyOnGoBack).toHaveBeenCalledTimes(1); // expect goBack is called only one time!
    
        // query something from previous screen to make sure screen navigated to previous screen
        expect(screen.getByText('Home')).toBeOnTheScreen();
      });

Another way of testing the navigation object is passing navigation as props. So that way you can assert the call, parameters and so on.

Header bar is a good example to demonstrate how you can interact and test third party components. That’s it for this blog post. See you in next one!