Reputation: 4779
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
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
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
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
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