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 />
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...
});
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");
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With