Testing React Native Navigation

How to confirm the navigation work in RN apps running through Expo.

Let's take a React Native application that implements the app navigation using React Navigation packages. Our application first shows a home screen. The user can navigate to the Pokemon List screen, and then to an individual Pokemon card.

The user is navigating through the Pokemon RN application

How do we test the navigation when this React Native application is running through Expo? Notice in the above video the URL was not changing at all. Thus we cannot use the typical Cypress commands like cy.location and cy.hash to confirm the screen changes. Instead we need to use the page titles.

The navigation

The Application implements the navigation like this:

App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from 'react'
import 'react-native-gesture-handler'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'

// Import your screens
import PokeList from './components/PokeList'
import Pokemon from './components/Pokemon'
import Home from './components/Home'

const Stack = createStackNavigator()

function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="PokeList" component={PokeList} />
<Stack.Screen name="Pokemon" component={Pokemon} />
</Stack.Navigator>
</NavigationContainer>
)
}

export default App

🎁 You can find the application and its tests in the repo bahmutov/pokemon-api-app. I copied the original code before making it compatible with Expo from the blog post Fetching Data in React Native.

The individual Pokemon card component gets its name from the route parameters.

components/Pokemon/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Pokemon extends PureComponent {
static navigationOptions = ({ route }) => ({
title: `${route.params.name} Info`,
})

render() {
const { route } = this.props
return (
<View style={styles.container}>
<Image
source={{
uri: 'https://res.cloudinary.com/aa1997/image/upload/v1535930682/pokeball-image.jpg',
}}
style={styles.pokemonImage}
/>
<Text testID="pokemon-name" style={styles.nameOfPokemon}>
{route.params.name}
</Text>
</View>
)
}
}

export default Pokemon

Let's confirm the navigation works.

The navigation tests

Let's confirm the application is on the home screen when it loads. We can use the title - which is equal to the name property set by the <Stack.Screen name="Home" component={Home} /> element.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
/// <reference types="cypress" />

describe('Pokemon', () => {
it('shows a card', () => {
cy.visit('/')
cy.title().should('equal', 'Home')
})
})

The home screen test

Let's go to the list screen by clicking the button.

1
2
3
4
5
6
it('shows a card', () => {
cy.visit('/')
cy.title().should('equal', 'Home')
cy.contains('Go to Pokes').click()
cy.title().should('equal', 'PokeList')
})

The above test goes through very quickly, making it hard to see what's happening.

The test goes to the next screen very very quickly

Blink - and you will miss it! Thus I found it useful to add a custom command to slow down the tests where needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// using small delays can make the video a lot more useful
Cypress.Commands.add('delay', (ms = 500) => {
cy.wait(ms, { log: false })
})

describe('Pokemon', () => {
it('shows a card', () => {
cy.visit('/')
cy.title().should('equal', 'Home').delay()
cy.contains('Go to Pokes').click()

cy.title().should('equal', 'PokeList')
cy.delay()
})
})

The test with delay command

A small half-second delay makes the video of the test with screen transitions a lot more useful.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('shows a card', () => {
cy.visit('/')
cy.title().should('equal', 'Home').delay()
cy.contains('Go to Pokes').click()

cy.title().should('equal', 'PokeList')
cy.delay()
// the list is fetched from the API
cy.get('[data-testid="poke-card"]')
.should('have.length', 20)
.first()
.click()

cy.title().should('equal', 'Pokemon')
cy.get('[data-testid=pokemon-name]').should('be.visible').delay()
})

The test finishes at the individual Pokemon card screen.

The finished test

Navigating back

To go back to the Home screen the user would click the "<-" button at the top of the screen. The navigation library inserts the default accessible HTML markup for the button, even adding a label that includes the target screen's name.

The Back button markup

Let's extend our test to navigate back to the Home screen. I like adding a log statement to the Command Log for major parts of the test.

1
2
3
4
5
cy.log('**go back to Home screen**')
cy.get('[aria-label="PokeList, back"]').click().delay()
cy.title().should('equal', 'PokeList').delay()
cy.get('[aria-label="Home, back"]').click()
cy.title().should('equal', 'Home').delay()

As always, you can use the Command Log to travel back in time, observing the button clicked. Notice the "Before / After" DOM snapshots the Test Runner shows when hovering over the "Click" command.

Time-traveling debugger and the Back button

Happy React Native testing!