Search code examples
javascriptreactjsjestjsreact-router-domreact-testing-library

Unit Test - Cannot render a <Router> inside another <Router>


I am learning Unit tests with React (new to both), I am stumped at why this isn't working

I want to Test my App.js

import "./App.css";
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./Components/Home";
import PokemonDetail from "./Components/PokemonDetail";

function App() {
  return (
    <BrowserRouter basename={process.env.PUBLIC_URL}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/:offset?" element={<Home />} />
        <Route path="/pokemon-detail/:id" element={<PokemonDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Here is my Unit test

import React from "react";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import App from "../../App";
import "@testing-library/jest-dom";

describe("App", () => {
  it("should render Home component by default", () => {
    render(
      <MemoryRouter initialEntries={["/"]}>
        <App />
      </MemoryRouter>
    );
    const homeElement = screen.getByTestId("home");
    expect(homeElement).toBeInTheDocument();
  });

  it("renders the PokemonDetail component when the URL is /pokemon-detail/:id", () => {
    render(
      <MemoryRouter initialEntries={["/:offset?"]}>
        <App />
      </MemoryRouter>
    );
    const pokemonDetailElement = screen.getByTestId("pokemon-detail-id");
    expect(pokemonDetailElement).toBeInTheDocument();
    screen.debug();
  });
});

The error I am getting is

Error: Uncaught [Error: You cannot render a <Router> inside another <Router>. You should never have more than one in your app.]

But I am only using it in the App and no where else. I get initiating the MemoryRouter is basically wrapping a router around the router inside the App's router, but how can I test offset etc without?


Solution

  • Yes, react-router@6 it is an invariant violation to render any router inside another router.

    Promote the BrowserRouter out of the App component so App renders just the routes and can be wrapped by any other router during unit testing.

    index.js

    import { StrictMode } from "react";
    import { createRoot } from "react-dom/client";
    import { BrowserRouter as Router } from "react-router-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    const root = createRoot(rootElement);
    
    root.render(
      <StrictMode>
        <Router basename={process.env.PUBLIC_URL}>
          <App />
        </Router>
      </StrictMode>
    );
    

    App.jsx

    import "./App.css";
    import React from "react";
    import { Route, Routes } from "react-router-dom";
    import Home from "./Components/Home";
    import PokemonDetail from "./Components/PokemonDetail";
    
    function App() {
      return (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/:offset?" element={<Home />} />
          <Route path="/pokemon-detail/:id" element={<PokemonDetail />} />
        </Routes>
      );
    }
    
    export default App;
    

    The unit test files can now correctly wrap the App component with any router more suitable for unit testing, e.g. the MemoryRouter.

    import { render, screen } from "@testing-library/react";
    import { MemoryRouter } from "react-router-dom";
    import App from "../../App";
    
    describe("App", () => {
      it("should render Home component by default", () => {
        render(
          <MemoryRouter initialEntries={["/"]}>
            <App />
          </MemoryRouter>
        );
        const homeElement = screen.getByTestId("home");
        expect(homeElement).toBeInTheDocument();
      });
    
      it("renders the PokemonDetail component when the URL is /pokemon-detail/:id", () => {
        render(
          <MemoryRouter initialEntries={["/:offset?"]}>
            <App />
          </MemoryRouter>
        );
        const pokemonDetailElement = screen.getByTestId("pokemon-detail-id");
        expect(pokemonDetailElement).toBeInTheDocument();
        screen.debug();
      });
    });