3

An app that uses Reach Router. Has two pages, apage and bpage rendering APage and BPage
Apage has a heading and button. When the button is clicked, app navigates to bpage.

App.js

import { Router, navigate } from "@reach/router";

export default function AppWithRouter() {


  const APage = () => {

    const handle = () => {
       navigate('bpage')
    }

    return(
      <div>
        <h1>A Page</h1>
      <button onClick={handle}>Sign In</button>
      </div>
    )   

  }

  const BPage = () => (<div><h1>B Page</h1>BPage</div>)


  return (
      <Router>
        <APage path="/apage" />
        <BPage path="/bpage" />
      </Router>
  );
}

Using @testing-library/react for testing this navigation from apage to bpage using the text content of the heading.

Based on https://testing-library.com/docs/example-reach-router

renderwithRouter.js

export default function renderWithRouter(ui, route = "/") {
  let source = createMemorySource(route);
  let history = createHistory(source);

  return {
    ...render(<LocationProvider history={history}>{ui}</LocationProvider>),
    history
  };
}

My test for the navigation

App.test.js

test('click navigates to b page', async () => {


  const { getByRole  } = renderWithRouter(<App />,  '/apage')

  fireEvent.click(getByRole("button"));

  await wait(() => {
    expect(getByRole('heading')).toHaveTextContent(/b page/i)
  });

});

Fails giving

 Expected element to have text content:
      /b page/i
    Received:
      A Page

How so I test for this navigation?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Shorn Jacob
  • 1,183
  • 5
  • 22
  • 38
  • 1
    yes there seems some issues with navigate being invoked from the testing environment. It works if you directly visit that page. Here is the working sandbox. https://codesandbox.io/s/dark-hooks-yyfgy will keep you posted if I am able to figure out and let me know if you find a way :) – Karthik Venkateswaran Dec 12 '19 at 07:27

4 Answers4

1

I had this same problem. A work around I used was to mock @reach-router and assert that its navigate function is called. History won't change during the tests, but it will work on the application. Not sure what caused the issue to begin with. Sorry I can't be more help. Here's an example.

import {navigate} from '@reach/router'
import {waitFor, act} from '@testing-library/react'
import MagicUser from '@testing-library/user-event'
import {ValidUser} from '@test-utils/fixtures/user'
import {Routes} from '@utils/routes'
import {renderAuthForm} from './_utils'

const {username, password} = ValidUser
jest.mock('../../api', () => ({
  AuthFormApi: {
    login: jest.fn(() => Promise.resolve({user: {username}})),
    register: jest.fn(() => Promise.resolve({user: {username}})),
  },
}))

jest.mock('@reach/router', () => ({
  navigate: jest.fn(),
}))

describe('Successful user login', () => {
  describe('submitting the form', () => {
    it('navigates to the home page and adds user to local storage', async () => {
      const {getByRole} = renderAuthForm({
        username,
        password,
      })
      const submitButton = getByRole('button')
      await act(async () => {
        MagicUser.click(submitButton)
        await waitFor(() => {
          expect(navigate).toHaveBeenCalledWith(Routes.Home)
          expect(navigate).toHaveBeenCalledTimes(1)
        })
      })
    })
  })
})



1

Simply just mock @reach/router navigate function

import { navigate } from '@reach/router'
import * as React from 'react'
import { render, fireEvent } from 'react-testing-library'
import PageA from './PageA'

jest.mock('@reach/router', () => ({
  navigate: jest.fn(),
}))

describe('Page A', () => {
  it('Should navigate from Page A to Page B', () => {
    const { getByText } = render(<Page A />)
    fireEvent.click(getByText(/Sign In/i))
    expect(navigate).toHaveBeenCalledTimes(1)
    expect(navigate).toHaveBeenCalledWith('/bpage')
  })
})

As we believe if navigate function called with correct path will navigate to Page B, example here

Nelson Frank
  • 169
  • 1
  • 5
  • It doesn't feel right to have to mock a function of an external package in order to test this. It is making too many assumptions about how an external dependency works. – chesscov77 Mar 06 '23 at 23:01
  • @chesscov77 Good concern, mocking external packages helps isolate the code being tested from external dependencies, ensuring reliable and consistent test results. I made a gist explain more [Link](https://gist.github.com/nelsonfrank/c6b08c67843dc21be913f7a20a4ea895) – Nelson Frank Mar 29 '23 at 10:41
  • 1
    In general I agree. However, in your example you are assuming a lot about the internals of React Router. The component that handles the navigation in React Route (presumably a Link) may or may not call `navigate` when its clicked. That's internal to the package. You could mock Link instead, or even spy on it, and assert that it was clicked or something like that. – chesscov77 Mar 30 '23 at 14:21
  • @chesscov77 Yes, but in this question navigation is handle by calling navigate function. – Nelson Frank Apr 01 '23 at 14:29
0

I am late to the party, still, I'd like to share an implementation I've found on react-testing-library docs.

https://testing-library.com/docs/example-reach-router/

I won't get into the implementation details, which are well explained in the aforementioned link.

The whole point is that we don't need to simulate/fire any click event. As a matter of fact, we can simply navigate to the desired page by invoking the navigate function returned in the history object passed via <LocationProvider />

In addition to the code provided in the official docs, here's an example of how I adjusted it per my needs. (Note: I copy-pasted renderWithRouter function and used it as is)

it('Should navigate to <Launches /> page when button is displayed in <Dashboard /> page', async () => {
    const {
      history: { navigate: reachRouterNavigate },
    } = renderWithRouter(<App />);

    expect(screen.getByText(/upcoming launches/i)).toBeInTheDocument();
    await reachRouterNavigate(APP_ROUTES.LAUNCHES);
    expect(screen.getByText(/launches page/i)).toBeInTheDocument();
  });
HereBeAndre
  • 220
  • 3
  • 6
  • I want to understand why it requires `reachRouterNavigate` to navigate, and why can't we use the click functionality to test if navigation is successful. – A dev Dec 14 '22 at 06:59
0

Using navigate to change the location of your app is not the correct approach in React Router. With React Router, or in general with everything in React, you should have your UI react to changes in state. This is what is called declarative programming. What you are trying to do is imperative programming. In declarative programming, you would render the current page based off the state of the program. For example, it could be as simple as currentPage = 'A'. When the button is clicked, your App changes currentPage = 'B', causing a different page to be rendered:

<Router>
    currentPage === 'A' && <APage path="/apage" />
    currentPage === 'B' && <BPage path="/bpage" />
</Router>
chesscov77
  • 770
  • 8
  • 14