Testing navigation with react-testing-library
Asked Answered
S

3

8

I want to test if a sidebar that I have created for navigation works or not, this is a simple example of what I have made

<ul>
  <li><Link to="/location1">location 1</Link></li>
  <li><Link to="/location2">location 2</Link></li>
</ul>

this is my test

const routerWrapper = ({children}) => <BrowserRouter>{children}</BrowserRouter>

// The first one runs fine
it('navigation to location 1' , () => {
render(<SideBar/> , {wrapper:routerWrapper})
// get the element and click on it
// check if title of page is correct
})

it('navigation to location 2' , () => {
render(<SideBar/> , {wrapper:routerWrapper})
// can't run because location is at /location1
})

When I test the navigation with RTL, first test runs fine, but after that the location remains at that directory for next test, how do I clear location after every test? or any suggestions how should I test navigation?

Scone answered 3/2, 2022 at 15:52 Comment(0)
S
5

Well I did figure it out, we can use <MemoryRouter> for this purpose. https://reactrouter.com/docs/en/v6/api#memoryrouter

Just passed the value ['/'] to initialEntries prop from <MemoryRouter> and tests work fine.

also slideshowp2 answer is very useful if you are interested

Scone answered 7/2, 2022 at 8:31 Comment(2)
but / is the value by default, and it does not navigate yet.Phenol
link updated: reactrouter.com/en/main/router-components/memory-routerPict
G
8

From the Checking location in tests doc:

  1. You can also use BrowserRouter if your test environment has the browser globals window.location and window.history (which is the default in Jest through JSDOM, but you cannot reset the history between tests).
  1. Instead of passing a custom route to MemoryRouter, you can use the base Router with a history prop from the history package

E.g.

index.tsx:

import React from 'react';
import { Link } from 'react-router-dom';

export function SideBar() {
  return (
    <ul>
      <li>
        <Link to="/location1">location 1</Link>
      </li>
      <li>
        <Link to="/location2">location 2</Link>
      </li>
    </ul>
  );
}

index.test.tsx:

import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { createMemoryHistory } from 'history';
import { SideBar } from './';
import { Router } from 'react-router';

const createRouterWrapper = (history): React.ComponentType => ({ children }) => (
  <Router history={history}>{children}</Router>
);

describe('SideBar', () => {
  it('navigation to location 1', () => {
    const history = createMemoryHistory();
    render(<SideBar />, { wrapper: createRouterWrapper(history) });
    fireEvent.click(screen.getByText('location 1'));
    expect(history.location.pathname).toBe('/location1');
  });

  it('navigation to location 2', () => {
    const history = createMemoryHistory();
    render(<SideBar />, { wrapper: createRouterWrapper(history) });
    fireEvent.click(screen.getByText('location 2'));
    expect(history.location.pathname).toBe('/location2');
  });
});

Test result:

 PASS  stackoverflow/70974339/index.test.tsx (8.701 s)
  SideBar
    ✓ navigation to location 1 (40 ms)
    ✓ navigation to location 2 (4 ms)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |     100 |      100 |     100 |     100 |                   
 index.tsx |     100 |      100 |     100 |     100 |                   
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        9.232 s
Gwenette answered 7/2, 2022 at 4:56 Comment(2)
Actually I didn't know about <MemoryRouter> when I asked this, I ended up using that. but this also seems to be a good way to do it. thanksScone
I get Property 'location' does not exist on type 'History'.Pict
S
5

Well I did figure it out, we can use <MemoryRouter> for this purpose. https://reactrouter.com/docs/en/v6/api#memoryrouter

Just passed the value ['/'] to initialEntries prop from <MemoryRouter> and tests work fine.

also slideshowp2 answer is very useful if you are interested

Scone answered 7/2, 2022 at 8:31 Comment(2)
but / is the value by default, and it does not navigate yet.Phenol
link updated: reactrouter.com/en/main/router-components/memory-routerPict
P
2

I had troubles to implemented the accepted answer.

The following solution will not check directly for a path being opened, but for the component that should be rendered at that path.

describe('SideBar', () => {
  it('navigation to location 1', () => {
    // setup
    render(
        <MemoryRouter initialIndex={0} initialEntries={['/testbase']}>
          <Routes>
            <Route path="/testbase" element={<Sidebar/>}/>
            <Route path="/location1" element={<div>location 1</div>} />
          </Routes>
        </MemoryRouter>
    );

    // run 
    fireEvent.click(screen.getByText('location 1'));

    // check
    expect(screen.getByText('location 1')).toBeTruthy();
  });
});

Notes:

  • initialEntries provides a history that can be accessed.
  • initialIndex tells the router which route of initialEntries should be opened first (in this case /testbase.

OT Notes:

  • It's probably a good idea to wrap fireEvent in act and use await. (to prevent test failing once the component becomes more sophisticated)
  • It might be a good idea to wrap render in act and use await (to prevent test failing once the component becomes more sophisticated)

Encapsulate MemoryRouter

If you'd like to encapsulate MemoryRouter to make the tests more concise, here's an idea (in Typescript):

import React from 'react'
import { MemoryRouter, Route, Routes } from 'react-router-dom';

export type Route = {
  path: string;
  element: React.ReactElement;
}

export type TestRouterProps = {
  initialRoute?: string;
  routes: Route[];
};

/**
 *
 * How to use: 
 *       <TestRouter initialRoute="/testbase"
 *          routes={[
 *            { path: '/testbase', element: <MyComponent/> },
 *            { path: '/', element: <div>aaa</div> },
 *          ]}
 *       />
 *
 **/
export const TestRouter: React.FC<TestRouterProps> = (props: TestRouterProps) => {

  const routerProps = props.initialRoute ? {
    initialIndex: 0,
    initialEntries: [props.initialRoute],
  } : {};

  return (
  <MemoryRouter {...routerProps}>
    <Routes>
      {props.routes.map((route) => <Route path={route.path} element={route.element} />)}
    </Routes>
  </MemoryRouter>);
}
Pict answered 14/8, 2023 at 5:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.