How can I test React Router with Jest
Asked Answered
S

2

11

I'm new to testing with jest and I want to test the following code.

import React from "react";
import "./ButtonLogin.css";
import { Link } from 'react-router-dom';

function ButtonLogin() {
    return (
        <Link to="/login"> <button className="button-login">Iniciar sesión</button></Link>
    )
}

export default ButtonLogin;
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent, Link } from '@testing-library/react';
import { ButtonLogin } from './ButtonLogin';

it('routes to a new route', async () => {

  ButtonLogin = jest.fn();

  const { getByText } = render(
    <MemoryRouter ButtonLogin={ButtonLogin}>
      <Link to="/login">Iniciar sesión</Link>
    </MemoryRouter>
  );

  fireEvent.click(getByText('Iniciar sesión'));

  expect(ButtonLogin).toHaveBeenCalledWith('/login');
});

I have performed the following test but it fails and I get the following error in line 9. routes to a new route

"ButtonLogin" is read-only.
Siddur answered 8/11, 2021 at 2:38 Comment(1)
Best example that I've found for v6 so far: testing-library.com/docs/example-react-routerAndersen
H
21

You can use the createMemoryHistory function and Router component to test it. Create a memory history with initial entries to simulate the current location, this way we don't rely on the real browser environment. After firing the click event, assert the pathname is changed correctly or not.

ButtonLogin.tsx:

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

function ButtonLogin() {
  return (
    <Link to="/login">
      <button className="button-login">Iniciar sesión</button>
    </Link>
  );
}

export default ButtonLogin;

ButtonLogin.test.tsx:

import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { Router } from 'react-router-dom';
import ButtonLogin from './ButtonLogin';
import { createMemoryHistory } from 'history';
describe('ButtonLogin', () => {
  test('should change current location to login when button is clicked', () => {
    const history = createMemoryHistory({ initialEntries: ['/home'] });
    const { getByText } = render(
      <Router history={history}>
        <ButtonLogin />
      </Router>
    );
    expect(history.location.pathname).toBe('/home');
    fireEvent.click(getByText('Iniciar sesión'));
    expect(history.location.pathname).toBe('/login');
  });
});

test result:

 PASS  examples/69878146/ButtonLogin.test.tsx (10.675 s)
  ButtonLogin
    ✓ should pass (41 ms)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |                   
 ButtonLogin.tsx |     100 |      100 |     100 |     100 |                   
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.722 s, estimated 12 s

package version: "react-router-dom": "^5.2.0"

Hemp answered 8/11, 2021 at 2:54 Comment(4)
Note that <Router history={history} /> doesn't work in v6, which is what you currently get when you npm i react-router-dom: https://mcmap.net/q/270803/-quot-cannot-read-properties-of-undefined-reading-39-pathname-39-quot-when-testing-pages-in-the-v6-react-router/3001761Rathenau
@Rathenau Thanks. Good to know. I am still using react-router v5Hemp
so what's the way to do this in v6 , any idea @RathenauBurglarious
@Burglarious did you read the post I linked above?Rathenau
C
3

Late to this, but React Router Dom v6 can handle tests this way.


import {BrowserRouter, MemoryRouter} from "react-router-dom";

const reactRouterContext = (children: any) => {

    if (isTest) {
        return <MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>
    }

    return <BrowserRouter>{children}</BrowserRouter>

}

EDIT - adding implementation per request:

const reactRouterContext = (children: any) => {

    if (isTest) {

        return <MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>

    }

    if (this.props.browserRouter ?? true) {

        return <BrowserRouter>{children}</BrowserRouter>

    }

    return <HashRouter>{children}</HashRouter>

}

return reactRouterContext(<>
    <Routes>
        {'' === user_id
            ? <>
                <Route path="/login" element={<Login/>}/>
                <Route path="/register" element={<Register/>}/>
                <Route path="/forgot-password" element={<ForgetPassword/>}/>
                <Route path="/recover-password" element={<RecoverPassword/>}/>
                <Route path="/*" element={<Navigate to={'/login'}/>}/>
             </>
             : <Route path="/" element={<Main/>}>
                   <Route path="/sub-menu-2" element={<Blank/>}/>
                   <Route path="/sub-menu-1" element={<SubMenu/>}/>
                   <Route path="/blank" element={<Blank/>}/>
                   <Route path="/profile" element={<Profile/>}/>
                   <Route path="/" element={<Dashboard/>}/>
                   <Route path="/*" element={<Navigate to={'/'}/>}/>
              </Route>}
    </Routes>
    <ToastContainer
        autoClose={3000}
        draggable={false}
        position="top-right"
        hideProgressBar={false}
        newestOnTop
        closeOnClick
        rtl={false}
        pauseOnHover
        />
</>);
Conni answered 17/4, 2023 at 17:45 Comment(2)
Interesting. What's the implementation look like ?Andersen
Nothing changes in your tests; this just enables a non-browser implementation.Conni

© 2022 - 2024 — McMap. All rights reserved.