lzl124631x
lzl124631x

Reputation: 4779

How to test routing logic with React Router v6 and testing-library?

I migrated from React Router v5 to v6 following this tutorial. I want to test it with react-testing-library, but my old unit tests (using the pattern in this doc) stopped working.

My app with React Router v6 is like this

const router = createBrowserRouter([
    {
        path: "/",
        element: (
            <>
                <SiteHeader />
                <Outlet />
            </>
        ),
        errorElement: <NotFound />,
        children: [
            { path: "/", element: <Home /> },
            { path: "/posts", element: <Posts /> },
            { path: "/post/:postId", element: <PostPage /> },
        ],
    },
]);

function App() {
    return (
        <div className="app">
            <RouterProvider router={router} />
        </div>
    );
}

As you can see, it's using RouterProvider instead of Switch/Route (so I'm confused that this SO question says it's using React Router v6 but it looks so different.).

The code in official doc of testing-library is not using RouterProvider either.

I want to test some routing logic like this pseudo code:

renderWithRouter(<App />, "/posts"); // loads /posts page initially
await user.click(screen.getByText("some post title")); // trigger click
expect(getUrl(location)).toEqual("/post/123"); // checks the URL changed correctly

How can I create a renderWithRouter function like this with RouterProvider? Note that this renderWithRouter worked for me when I used React Router v5, but after migrating to v6, it stopped working.

My current dependency versions:


I tried this

test("click post goes to /post/:postId", async () => {
    render(
        <MemoryRouter initialEntries={["/posts"]}>
            <App />
        </MemoryRouter>,
    );
    // ...
});

but I got error

You cannot render a <Router> inside another <Router>. You should never have more than one in your app.

      31 | test("click post goes to /post/:postId", async () => {
    > 32 |     render(
         |     ^
      34 |         <MemoryRouter initialEntries={["/posts"]}>
      36 |             <App />

Upvotes: 17

Views: 31998

Answers (4)

Airah
Airah

Reputation: 710

I think it's also important to clearly point out what was causing the error in this question.

If you are encountering a similar error but your routing config does not look like this or you are not using the [email protected] Data Routers:

You cannot render a <Router> inside another <Router>. You should never have more than one in your app.

      31 | test("click post goes to /post/:postId", async () => {
    > 32 |     render(
         |     ^
      34 |         <MemoryRouter initialEntries={["/posts"]}>
      36 |             <App />

It is still just as the error indicated:

You have embedded the logic for BrowserRouter into your App component somewhere, and you are now wrapping it again with a MemoryRouter in your tests.

For those still using the older Routers i.e <BrowserRouter>, the simplest fix would be to ensure you are not wrapping App component with it. A good place for the BrowserRouter would be in your index.js file.

Upvotes: 0

Sanka Sanjeeva
Sanka Sanjeeva

Reputation: 3520

(Copied from @Drew's answer)

My idea is to test a piece of UI component, so I don't want to pass a full route config(I thought about performance). I just used a straightforward route config for each test file.

const router = createMemoryRouter([{ path: '/', element: <MyComponent /> }]);

test('should render MyComponent', () => {
  render(<RouterProvider router={router} />);

  //   assertions
});

I ended up creating the renderWithRouter method like this

export const renderWithRouter = (
  ui: ReactElement,
  path = '/',
  options?: Omit<RenderOptions, 'wrapper'>
) => {
  const { pathname } = new URL(`http://www.test.com${path}`);

  const router = createMemoryRouter(
    [{ path: pathname, element: <Providers>{ui}</Providers> }],
    { initialEntries: [path] }
  );

  return render(<RouterProvider router={router} />, { ...options });
};

Usage

renderWithRouter(<MyCompoent />);

// to assert search params
renderWithRouter(<MyCompoent />, '/user?user=1234');

Upvotes: 1

lzl124631x
lzl124631x

Reputation: 4779

FWIW, I created my own renderWithRouter for React Router V6.

export const renderWithRouter = (route = "/") => {
    window.history.pushState({}, "Test page", route);
    return {
        user: userEvent.setup(),
        ...render(<RouterProvider router={createBrowserRouter(routes)} />),
    };
};

And this is an example test.

test("click Posts => shows Posts page", async () => {
    const { user } = renderWithRouter();
    const postsLink = screen.getByText("Posts").closest("a");
    expect(postsLink).not.toHaveClass("active");
    await user.click(postsLink as HTMLAnchorElement);
    expect(postsLink).toHaveClass("active");
    expect(getUrl(location)).toEqual("/posts");
});

Upvotes: 3

Drew Reese
Drew Reese

Reputation: 202836

If you want to test your routes configuration as a whole, using the new [email protected] Data Routers, then I'd suggest a bit of a refactor of the code to allow being able to stub in a MemoryRouter for any unit testing.

Declare the routes configuration on its own and export.

const routesConfig = [
  {
    path: "/",
    element: (
      <>
        <SiteHeader />
        <Outlet />
      </>
    ),
    errorElement: <NotFound />,
    children: [
      { path: "/", element: <Home /> },
      { path: "/posts", element: <Posts /> },
      { path: "/post/:postId", element: <PostPage /> },
    ],
  },
];

export default routesConfig;

In the app code import routesConfig and instantiate the BrowserRouter the app uses.

import {
  RouterProvider,
  createBrowserRouter,
} from "react-router-dom";
import routesConfig from '../routes';

const router = createBrowserRouter(routesConfig);

function App() {
  return (
    <div className="app">
      <RouterProvider router={router} />
    </div>
  );
}

For unit tests import the routesConfig and instantiate a MemoryRouter.

import {
  RouterProvider,
  createMemoryRouter,
} from "react-router-dom";
import { render, waitFor } from "@testing-library/react";
import routesConfig from '../routes';

...

test("click post goes to /post/:postId", async () => {
  const router = createMemoryRouter(routesConfig, {
    initialEntries: ["/posts"],
  });

  render(<RouterProvider router={router} />);

  // make assertions, await changes, etc...
});

Upvotes: 31

Related Questions