React router not navigating to new page inside test
dmaksimov opened this issue · 9 comments
@testing-library/reactversion: 13.2.0- Testing Framework and version: jest@28.1.0
- DOM Environment: jsdom@19.0.0
Relevant code or config:
test('full app rendering/navigating', async () => {
const history = createMemoryHistory()
render(
<Router location={history.location} navigator={history}>
<App />
</Router>,
)
const user = userEvent.setup()
// verify page content for expected route
// often you'd use a data-testid or role query, but this is also possible
expect(screen.getByText(/you are home/i)).toBeInTheDocument()
await user.click(screen.getByText(/about/i))
// check that the content changed to the new page
expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument()
})What you did:
I'm writing a test for React router. I've been following the example from the React Router testing docs.
What happened:
When I run the test, it fails with the following output
FAIL src/App.test.js
✕ full app rendering/navigating (89 ms)
✓ landing on a bad page (4 ms)
✓ rendering a component that uses useLocation (3 ms)
● full app rendering/navigating
TestingLibraryElementError: Unable to find an element with the text: /you are on the about page/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, <script />, <style />
<body>
<div>
<div>
<a
href="/"
>
Home
</a>
<a
href="/about"
>
About
</a>
<div>
You are home
</div>
<div
data-testid="location-display"
>
/
</div>
</div>
</div>
</body>
24 |
25 | // check that the content changed to the new page
> 26 | expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument()
| ^
27 | })
28 |
29 | test('landing on a bad page', () => {
at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:38:19)
at node_modules/@testing-library/dom/dist/query-helpers.js:90:38
at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
at getByText (node_modules/@testing-library/dom/dist/query-helpers.js:111:19)
at Object.<anonymous> (src/App.test.js:26:17)
Reproduction:
- Create a new React app
- Install router
- Update App.js to match react router testing docs
- Update App.test.js to match react router testing docs
- Run tests
I created a repo here with the example code: https://github.com/dmaksimov/react-router-test
Problem description:
It doesn't seem to be changing the page when clicking on a router link inside the test. We would expect to see the content of the new page after the click event but it still shows the old page.
Think there are a few issues here and once fixed, the tests pass (see fixed example: https://stackblitz.com/edit/node-dpfmkn?file=src/App.test.js).
-
You had a syntax error as a result of a typo in your
Appimport in the test (you were importingappwhen it should have beenApp; see https://github.com/dmaksimov/react-router-test/blob/8ad48a5d232dd6744b392ae6f52e370a19172fcf/src/App.test.js#L9). -
Take a look at waitFor which should probably be included in the documentation example you were following.
Long story short, when you have to wait for events such as routes changing/pages rendering, it's easy to encounter race conditions. waitFor is usually more than enough, but sometimes you can do something as simple as setTimeout(() => {}) and that will be enough of a delay/wait until the next tick in the event loop.
I tried using waitFor and had no luck.
If I were to use setTimeout how would I "tell" the test to wait until that timeout is complete?
I tried using
waitForand had no luck.If I were to use
setTimeouthow would I "tell" the test to wait until that timeout is complete?
Did you look at the example I provided in StackBlitz? I cloned your repository and it works with waitFor.
However, to answer your other question, regarding setTimeout(), you can use a promise and resolve the promise inside the setTimeout like this: await new Promise(resolve => setTimeout(resolve, 100));
waitFor is really the preferred way to accomplish this in RTL, but hopefully the above explanation helps some.
@dmaksimov I had a very similar problem. The setup was very close to yours, I was using Router and history as well.
I was not able to make it work this way.
I decided to try a different approach, instead of using Router and history, i directly used MemoryRouter and used the initialEntries prop to setup the tests. Basically everything worked just fine so far, links, navigate and <Navigate to="..." />
I'm able to assert things about the new rendered component tree after the redirects.
Did you look at the example I provided in StackBlitz? I cloned your repository and it works with
waitFor.
waitForis really the preferred way to accomplish this in RTL, but hopefully the above explanation helps some.
I face similar problem with my tests and though your example was a solution. Unfortunately it isn't, you've forgotten await in your waitFor example, so it never truly resolves and it passes regardless of the wrapped expect.
Looks like a support question that is more suited to discuss on our Discord.
Did you look at the example I provided in StackBlitz? I cloned your repository and it works with
waitFor.
waitForis really the preferred way to accomplish this in RTL, but hopefully the above explanation helps some.I face similar problem with my tests and though your example was a solution. Unfortunately it isn't, you've forgotten
awaitin your waitFor example, so it never truly resolves and it passes regardless of the wrapped expect.
You are right, I did not and from looking at it, it does not pass. One big difference between the example in the docs and this one, is the docs are using React Router v5 (<Switch> and exact do not exist in v6) in theirs (vs. this using v6). The only reason the other two tests are working is due to history.push being called before the component is rendered in the test, which means the initial render of <App /> is with the history.location.pathname already being set to /about (as an example).
However, by spying on history.push, it did capture the result of a click event and outputting history.location.pathname correctly showing as /about. In other words, you could test for different effects than the page actually changing, such as:
test('full app rendering/navigating', async () => {
const history = createMemoryHistory();
const spy = jest.spyOn(history, 'push');
render(
<Router location={history.location} navigator={history}>
<App />
</Router>
);
const user = userEvent.setup()
expect(screen.getByText(/you are home/i)).toBeInTheDocument();
await user.click(screen.getByText(/about/i));
expect(history.location.pathname).toBe('/about');
expect(spy).toBeCalled();
});Those both work, although not really sure what the point of a test like that would be unless you were trying to unit test third-party libraries and their ability to mock the History API.
@eps1lon I would say the example may need to be updated in the docs, as it currently states the example works with React Router v6 and the example itself doesn't appear to be using v6 (and may not at all, if using v6 setup).
I don't mind submitting a PR updating the docs, but want to make sure that's what needs to be done and you're cool with a submission first. Or maybe this is better as an upstream issue, too.
edit
I was finally able to get the test working with the below. Biggest problem was the use of <Router> instead of <BrowserRouter>. Docs may still benefit from being more clear and/or providing a v6 example.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createMemoryHistory } from "history";
import React from "react";
import { BrowserRouter } from "react-router-dom";
import "@testing-library/jest-dom";
import { App, LocationDisplay } from "./App";
test("full app rendering/navigating", async () => {
const history = createMemoryHistory();
render(
<BrowserRouter location={history.location} navigator={history}>
<App />
</BrowserRouter>
);
const user = userEvent.setup();
// verify page content for expected route
// often you'd use a data-testid or role query, but this is also possible
expect(screen.getByText(/you are home/i)).toBeInTheDocument();
await user.click(screen.getByText(/about/i));
expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument();
});@dmaksimov I just encountered this problem when following react router v6 example test . The problem is, the example itself is not even using React Router v6. <Switch /> only exists on v5.
The temporary solution I have found was by using the unstable <HistoryRouter />.
With this, you can still do everything on the v6 example test , the only difference is the way you're passing the history instance to the Router's props.
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
test('full app rendering/navigating', async () => {
const history = createMemoryHistory() // or createBrowserHistory
render(
<HistoryRouter history={history}>
<App />
</HistoryRouter>,
);
const user = userEvent.setup()
...
});Just be aware of this warning
This API is currently prefixed as unstable_ because you may unintentionally add two versions of the history library to your app, the one you have added to your package.json and whatever version React Router uses internally. If it is allowed by your tooling, it's recommended to not add history as a direct dependency and instead rely on the nested dependency from the react-router package. Once we have a mechanism to detect mis-matched versions, this API will remove its unstable_ prefix.