next-router-mock
next-router-mock copied to clipboard
Issue with next/link in Next 12.2.0
Hello!
I've noticed that after upgrade to newest version of Next 12.2.0 (released 3 days ago) there is an issue with tests utilising Next Link.
import Link from "next/link"
...
jest.mock("next/router", () => require("next-router-mock"));
jest.mock("next/dist/client/router", () => require("next-router-mock"));
...
it("", () => {
const { getByText } = render(<div><Link href={"/foo"}>Foo</Link></div>);
const foo = getByText("Foo");
fireEvent.click(foo);
});
...
test fails with following error:
TypeError: Cannot read properties of null (reading 'push') 57 | fireEvent.click(foo);
After Next downgrade to previous stable version (12.1.6) everything works fine.
I am on the latest version of next-router-mock
I suspected that there may be some changes in the paths or refactor of Link component in the 12.2.0 release but didn't found anything like that in the PR. Do you have any clue what's going on?
Best!
Looking into the source, I do see some significant changes to next/link in the 12.2.0 release...
In 12.1.6, it was importing the "singleton" router from next/dist/client/router like so:
https://github.com/vercel/next.js/blob/v12.1.6/packages/next/client/link.tsx#L12
import { useRouter } from './router';
But now, it looks like it obtains the router from a Context, like so: https://github.com/vercel/next.js/blob/canary/packages/next/client/link.tsx#L293
let router = React.useContext(RouterContext)
This is not great ... it makes it very difficult to use jest.mock. I'll brainstorm a bit on how this could be solved ...
One potential solution/workaround:
jest.mock('next/dist/shared/lib/router-context', () => {
const React = require("react");
const router = require("next-router-mock");
const RouterContext = createContext(router);
return { RouterContext };
})
This will replace the RouterContext with a new context, defaulted to the singleton mock router.
If this works, it might be worth adding this into the library, like:
jest.mock('next/dist/shared/lib/router-context', () => require('next-router-mock/router-context'));
For reference, I started this https://github.com/vercel/next.js/discussions/38248 to discuss ways to prevent these kinds of breaking changes.
Your solution/workaround doesn't seem to resolve the situation entirely.
I've added the following code (we're on React 18):
jest.mock("next/dist/shared/lib/router-context", () => {
const { createContext } = require("react");
const router = require("next-router-mock");
const RouterContext = createContext(router);
return { RouterContext };
});
This results in an error of:
TypeError: Cannot read property 'split' of undefined
41 | const router = useRouter();
42 | console.log(router);
> 43 | const trail = router.asPath.split("/");
| ^
44 |
45 | const crumbs: Breadcrumb[] = trail.map((crumb, index) => {
46 | return {
If I'm right it's because the Routing object during the test is:
{
useMemoryRouter: [Getter],
MemoryRouter: [Getter],
BaseRouter: [Getter],
memoryRouter: MemoryRouter {
isReady: true,
route: '',
pathname: '<PathDetails>',
hash: '',
query: [Object: null prototype] {},
asPath: '<PathDetails>',
basePath: '',
isFallback: false,
isPreview: false,
isLocaleDomain: false,
locale: undefined,
locales: [],
events: {
on: [Function: on],
off: [Function: off],
emit: [Function: emit]
},
async: false,
registerPaths: [Function (anonymous)],
push: [Function (anonymous)],
replace: [Function (anonymous)],
setCurrentUrl: [Function (anonymous)]
},
useRouter: [Function: useRouter],
withRouter: [Function: withRouter],
default: MemoryRouter {
isReady: true,
route: '',
pathname: '<PathDetails>',
hash: '',
query: [Object: null prototype] {},
asPath: '<PathDetails>',
basePath: '',
isFallback: false,
isPreview: false,
isLocaleDomain: false,
locale: undefined,
locales: [],
events: {
on: [Function: on],
off: [Function: off],
emit: [Function: emit]
},
async: false,
registerPaths: [Function (anonymous)],
push: [Function (anonymous)],
replace: [Function (anonymous)],
setCurrentUrl: [Function (anonymous)]
}
}
Any thoughts on how to resolve?
Oh I see ... this is simply a problem with require, versus import ... let's try this (adding in .default):
jest.mock("next/dist/shared/lib/router-context", () => {
const React = require("react");
const router = require("next-router-mock").default;
const RouterContext = React.createContext(router);
return { RouterContext };
});
Thanks for the input.
Just to note, the above code resolves most of the issue but it still seems like you need both of the following:
jest.mock("next/dist/client/router", () => require("next-router-mock"));
jest.mock("next/dist/shared/lib/router-context", () => {
const { createContext } = require("react");
const router = require("next-router-mock").default;
const RouterContext = createContext(router);
return { RouterContext };
});
Not having line one continues to break the tests but keeping line one passes them all.
Not having line one continues to break the tests but keeping line one passes them all.
Having both lines worked for me, but I had an ESLint warning to disable:
/* eslint-disable global-require */ // Disable global-require for mocks
So I swapped the require statements for jest.requireActual:
jest.mock('next/dist/client/router', () =>
jest.requireActual('next-router-mock')
);
jest.mock('next/dist/shared/lib/router-context', () => {
const { createContext } = jest.requireActual('react');
const router = jest.requireActual('next-router-mock').default;
const RouterContext = createContext(router);
return { RouterContext };
});
Thanks for the feedback! As a long-term solution, I think it might be good to encapsulate some of this logic inside this package, so you could do:
// To support next/router (singleton router and useRouter)
jest.mock("next/router", () => require("next-router-mock"));
// To support next/link with next@<12.2.0
jest.mock("next/dist/client/router", () => require("next-router-mock"));
// To support next/link with next@>=12.2.0
jest.mock("next/dist/shared/lib/router-context", () => require("next-router-mock/router-context"));
// ------------- this would be new ---------------------------------------------^^^^^^^^^^^^^^
But as you can imagine, this is all just excessively brittle -- these internal paths just keep changing. Perhaps it would be better to mock next/link entirely, I'm not sure how hard that would be.
I've started work on this router-context concept, but it's not quite complete, and I'm not sure if it's a long-term solution. I'll post here as I update.
I'm attempting to get in contact with Vercel directly, so that I can make some headway on a long-term solution.
I thank you very much for all your efforts, but I wish Next.JS would be shipped with such a thing under a next/test-support module. I know that I am not helping, but I am moving to Svelte Kit precisely because of this issue.
I've found a fundamental flaw with this approach. Supplying a "default value" for the context is not a good solution, because the value will never update, so the next/link will always be looking at a stale url.
I'm looking for an alternative.
The ONLY way I can think of handling this, currently, is for all tests to add a <MemoryRouterProvider> wrapper. I can probably add this feature, but would need help testing it.
Would anyone here be able to test things out, once I get a prerelease published?
The ONLY way I can think of handling this, currently, is for all tests to add a
<MemoryRouterProvider>wrapper. I can probably add this feature, but would need help testing it.Would anyone here be able to test things out, once I get a prerelease published?
I am using your library and would be willing to try a prerelease, let me know if I can help!
@simonedavico @mateuszlazar I've been working hard to fix this, and I have a prerelease ready to test!
There aren't huge changes in this version, but the documentation was difficult to get right ...
Could you please read this updated README.md, and give me any feedback if it's not clear?
Instructions:
- Install this prerelease:
npm install [email protected] - Read the updated README, especially the section on Compatibility with next/link to update your tests
- Tell me how it goes!
@simonedavico @mateuszlazar I've been working hard to fix this, and I have a prerelease ready to test!
There aren't huge changes in this version, but the documentation was difficult to get right ... Could you please read this updated
README.md, and give me any feedback if it's not clear?Instructions:
- Install this prerelease:
npm install [email protected]- Read the updated README, especially the section on Compatibility with next/link to update your tests
- Tell me how it goes!
Will do and get back to you ASAP! Thanks for you hard work! 🙏
I've merged and published a new version, 0.9.1. This includes primarily a new README, which includes new instructions for compatibility with next/link in Next 12.2+ & 13+.
TL;DR: to mock with next/link, you must add a <MemoryRouterProvider> wrapper.