scottrippey / next-router-mock Goto Github PK
View Code? Open in Web Editor NEWMock implementation of the Next.js Router
License: MIT License
Mock implementation of the Next.js Router
License: MIT License
In our codebase we use the logic of dynamic paths and query parameters to create a more complex abstraction on top of the basic next.js routing logic. In essence, we have a couple of specific pages between we navigate using a next/link like in the example below:
<NextLink
as={isStatic ? as : href} // isStatic resolves to false, so as=href
prefetch={false}
href={
isStatic
? href
: {
pathname: FriendlyPagesDestinations[type],
query: { [SLUG_TYPE_QUERY_PARAMETER_NAME]: FriendlyPagesTypes[type], ...queryParams },
}
}
{...props}
>
{children}
</NextLink>
If I navigate using the following link:
<ExtendedNextLink type="category" href="/test-category">
link text
</ExtendedNextLink>,
the actual implementation of next/link shows me the following properties:
{
asPath: "/test-category".
pathname: "/categories/[categorySlug]", // this is caused by the dynamic pathname shown above
query: {
slugType: "front_product_list" // also caused by the dynamic query above)
},
}
but the mocked one gets overwritten because of the as
prop and by this logic in the mock (specifically in MemoryRouter
):
const newRoute = parseUrlToCompleteUrl(url, this.pathname);
let asPath;
let asRoute;
if (as === undefined || as === null) {
asRoute = undefined;
asPath = getRouteAsPath(newRoute.pathname, newRoute.query, newRoute.hash);
}
else {
asRoute = parseUrlToCompleteUrl(as, this.pathname);
asPath = getRouteAsPath(asRoute.pathname, asRoute.query, asRoute.hash);
}
My question is, am I misunderstanding something? According to my understanding, if I navigate using a next/link where I paste arbitrary URL and query, the as
prop should not overwrite it, as it should only modify the asPath
property of the router. Is this a bug, or a feature?
I might have programmed my self a bit into a corner here, but I have an app where I have the following rewrites:
async rewrites() {
return [
{
source: '/',
destination: '/null',
},
{
source: '/some-route/:someId',
destination: '/:someId',
},
]
}
I do this in order to have a single page, called [someId].tsx
to handle both root (e.g. no ID passed), and /some-route/some-value
. This is of course a bit weird, but that's not important here. ๐ But to handle both rewrites pointing to the same page, I expect, according to the rewrites, the path parameter to be an actual string of type 'null'
or an actual ID, and handle this is the code accordingly.
So naturally when testing this using jest and next-router-mock, when the application code redirects to /
it would normally hit the rewrite, but in the test it doesn't. So the [someId].tsx
page is rendered with an "illegal" state (illegal according to my own rewrite rules).
I could implement support for this test-only case in the application code, but I'm trying to avoid that. I also looked at using mockRouter.events.on('routeChangeStart', (evts) => {...})
as a way to handle my own rewrites, but it seems that was a dead end.
I know this is a bit of a weird case, but if anyone has any smart suggestions I'd be grateful. ๐ It would of course be cool if the dynamic routing could be configured to handle these kind of rewrites as well, but I'm not sure how that would work.
Hi, you mention this library can be used with storybook but I don't see any documentation for it, is it using MemoryRouter export and put it in component tree?
The code here also converts the literal url "/"
into an empty string ""
:
next-router-mock/src/MemoryRouter.tsx
Line 27 in 8a47b4e
This works for a minimum reproduction:
mockRouter.setCurrentUrl('/')
expect(mockRouter.asPath).toEqual('/')
expect(received).toEqual(expected) // deep equality
Expected: "/"
Received: ""
Module not found: Error: Can't resolve 'next/dist/next-server/lib/router-context'
Must be nice to have an implementation to mock useRouter
of next/navigation
Since NextJS doesn't have native active links, I am using the router to detect the current pathname and make sure it matches up to the Next Link href.
Component:
const router = useRouter();
const isActive = router.pathname === href;
In the test, I am mocking the router and setting the current url, but pathname is still undefined when I run the test, causing it to fail. "TypeError: Cannot read properties of null (reading 'pathname')"
Test:
import React from 'react';
import { render, screen } from '@testing-library/react';
import { faSearch } from '@fortawesome/pro-light-svg-icons';
import mockRouter from 'next-router-mock';
import { NavLink } from './NavLink';
jest.mock('next/dist/client/router', () => require('next-router-mock'));
describe('NavLink', () => {
beforeEach(() => {
mockRouter.setCurrentUrl('/initial');
});
it('should render the link label', () => {
render(
<NavLink
icon={faSearch}
label="Jobs"
isActive={false}
collapsed={false}
href="/"
/>
);
const link = screen.getByText('Jobs');
expect(link).toBeInTheDocument();
});
});
If any of the router
properties are accessed in a delayed fashion, if the route changes between the useRouter
call and accessing the properties, then the original route's data is returned when using next-router-mock
.
If the Next.js Router is used, the updated route's data is returned.
Example:
const router = useRouter()
useEffect(() => {
const onRouteChange = (url) => {
console.log('routeChangeComplete', router.query, router.asPath, url)
}
router.events.on('routeChangeComplete', onRouteChange)
return () => {
router.events.off('routeChangeComplete', onRouteChange)
}
}, [router])
const routeChangeA = useCallback(() => {
router.push('/path?x=A')
})
const routeChangeB = useCallback(() => {
router.push('/path?x=B')
})
Start at /path
, then trigger routeChangeA
and then routeChangeB
. When using the Next.js Router, you'll get...
'routeChangeComplete' { x: 'A' } '/path?x=A' '/path?x=A'
'routeChangeComplete' { x: 'B' } '/path?x=B' '/path?x=B'
... but with next-router-mock
you'll get...
'routeChangeComplete' {} '/path' '/path?x=A'
'routeChangeComplete' { x: 'A' } '/path?x=A' '/path?x=B'
Tring to install/upgrade to v0.7.3, npm install
fails due to postinstall
script added on 16dd116.
Does this necessary for users of this package?
$ npm i -D next-router-mock
npm ERR! code 2
npm ERR! path /home/mmyoji/tmp/node_modules/next-router-mock
npm ERR! command failed
npm ERR! command sh -c (cd test/next-10 && npm i); (cd test/next-11 && npm i); (cd test/next-12 && npm i); (cd test/next-canary && npm i)
npm ERR! sh: 1: cd: can't cd to test/next-10
npm ERR! sh: 1: cd: can't cd to test/next-11
npm ERR! sh: 1: cd: can't cd to test/next-12
npm ERR! sh: 1: cd: can't cd to test/next-canary
npm ERR! A complete log of this run can be found in:
npm ERR! /home/mmyoji/.npm/_logs/2022-06-13T02_20_43_937Z-debug-0.log
Any idea how to make the next-router-module work with the [email protected] or later?
I'm getting this error:
Cannot find module 'next/dist/shared/lib/router-context' from 'node_modules/next-router-mock/dist/MemoryRouterProvider/next-11.js'
Require stack:
node_modules/next-router-mock/dist/MemoryRouterProvider/next-11.js
node_modules/next-router-mock/dist/MemoryRouterProvider/next-13.js
node_modules/next-router-mock/dist/MemoryRouterProvider/index.js
src/lib/testHelpers.tsx
I know there's a createDynamicrouterParser
but it seems like this is only intended to be used in Jest. Is there something equivalent for storybook?
Hi! ๐
Firstly, thanks for your work on this project! ๐
Today I used patch-package to patch [email protected]
for the project I'm working on.
The problem is that next does not export getRouteRegex
and getRouteMatcher
from theirs next/dist/shared/lib/router/utils
I am using the next@canary (18 currently)
Here is the diff that solved my problem:
diff --git a/node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js b/node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js
index 71b36de..b05f2b6 100644
--- a/node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js
+++ b/node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js
@@ -1,6 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-const utils_1 = require("next/dist/shared/lib/router/utils");
+const utils = require("next/dist/shared/lib/router/utils");
+const regex = require("next/dist/shared/lib/router/utils/route-regex");
+const matcher = require("next/dist/shared/lib/router/utils/route-matcher");
+
+const utils_1 = { ...utils, ...regex, ...matcher }
//
const normalize_page_path_1 = require("next/dist/server/normalize-page-path");
const MemoryRouter_registerPaths_1 = require("./MemoryRouter.registerPaths");
diff --git a/node_modules/next-router-mock/dist/dynamic-routes/extensions-12.js b/node_modules/next-router-mock/dist/dynamic-routes/extensions-12.js
index f5f78bd..211337c 100644
--- a/node_modules/next-router-mock/dist/dynamic-routes/extensions-12.js
+++ b/node_modules/next-router-mock/dist/dynamic-routes/extensions-12.js
@@ -1,6 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-const utils_1 = require("next/dist/shared/lib/router/utils");
+const utils = require("next/dist/shared/lib/router/utils");
+const regex = require("next/dist/shared/lib/router/utils/route-regex");
+const matcher = require("next/dist/shared/lib/router/utils/route-matcher");
+
+const utils_1 = { ...utils, ...regex, ...matcher }
// @ts-ignore
const normalize_page_path_1 = require("next/dist/shared/lib/page-path/normalize-page-path");
const MemoryRouter_registerPaths_1 = require("./MemoryRouter.registerPaths");
diff --git a/node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts b/node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts
index aae89ce..d2e46e0 100644
--- a/node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts
+++ b/node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts
@@ -1,10 +1,16 @@
import {
- getRouteMatcher,
- getRouteRegex,
getSortedRoutes,
isDynamicRoute,
//
} from "next/dist/shared/lib/router/utils";
+import {
+ getRouteRegex
+ //
+} from "next/dist/shared/lib/router/utils/route-regex";
+import {
+ getRouteMatcher
+ //
+} from "next/dist/shared/lib/router/utils/route-matcher";
//
import { normalizePagePath } from "next/dist/server/normalize-page-path";
diff --git a/node_modules/next-router-mock/src/dynamic-routes/extensions-12.ts b/node_modules/next-router-mock/src/dynamic-routes/extensions-12.ts
index 5460ddc..77f0db0 100644
--- a/node_modules/next-router-mock/src/dynamic-routes/extensions-12.ts
+++ b/node_modules/next-router-mock/src/dynamic-routes/extensions-12.ts
@@ -1,10 +1,16 @@
import {
- getRouteMatcher,
- getRouteRegex,
getSortedRoutes,
isDynamicRoute,
//
} from "next/dist/shared/lib/router/utils";
+import {
+ getRouteRegex
+ //
+} from "next/dist/shared/lib/router/utils/route-regex";
+import {
+ getRouteMatcher
+ //
+} from "next/dist/shared/lib/router/utils/route-matcher";
// @ts-ignore
import { normalizePagePath } from "next/dist/shared/lib/page-path/normalize-page-path";
This issue body was partially generated by patch-package.
I pose this as a question and not a bug.
I have a new nextjs instance (13) and I've setup two brief routes.
/pages/index.tsx
/pages/test/index.tsx
The first index, or root, is currently just a span of text. Test is running great.
The second page (test) has a few layout components, ie head, aside, main. I have a navigation link section in the head. The individual links have a useRouter
hook for active styles.
I mock out the router since the hook will throw without context and run a simple expect test.
It works, the text is found within the layout and I'm happy, mostly.
The test is taking 15s to run. I have 0 external data at the moment, no async code to be found. I'm not currently testing route transitions just span text.
import {render, screen} from '@testing-library/react'
import App from 'pages/index'
describe('App Landing', () => {
test('Renders text', () => {
render(<App />)
expect(screen.getByText('This is some sort of login screen perhaps.', {exact: false})).toBeInTheDocument()
})
})
import {render, screen} from 'test/test-utils'
import App from 'pages/test/index'
describe('Test Landing', () => {
test.only('Renders the Test', () => {
render(<App />)
expect(screen.getByText('Test', {exact: false})).toBeInTheDocument()
})
})
Both tests are very simple in asserting text. The difference between the pages again is the first is only a single span, and the second has presentation containers with a navigation container with link
<NavigationWrapper>
<NavLink href="/test/1" title="One" />
<NavLink href="/test/2" title="Two" disabled />
<NavLink href="/test/3" title="Three" disabled />
<NavLink href="/dash/4" title="Four" disabled />
</NavigationWrapper>
The NavLink component is just a next/link
component with a useRouter
hook to attach an active class for styles.
I'm just trying to see if anyone has experienced something like this and what you did to help solve it. I'm ok with it as is because it's technically working, but I would like to not rack up a lot of wasted cpu time once these tests make it to CI.
Hey hello,
Love the package, use it with Storybook.
However it does not seem to work with the latest version of next
, at the time of this writing.
Are there any plans to add the missing support?
I'm upgrading my app from 12.1.2 to 12.1.6 and it seems some internal next paths have changed, causing the patching to fail.
When I run my tests:
Cannot find module 'next/dist/server/normalize-page-path' from 'node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js'
Require stack:
node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js
jest.setup.js
at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:324:11)
at Object.<anonymous> (node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts:9:1)
Edit: I narrowed it down a bit, with next 12.1.5 still works as expected.
Hello! First of all thanks for awesome package, @scottrippey!
I try to mock next-router with storybook. Is it real to mock default Router as we do it for useRouter?
For instance useRouter works fine:
import { useCallback } from 'react'
import { useRouter } from 'next/router'
const UseRouterPage = () => {
const router = useRouter()
const handleClick = useCallback(() => router.push({
pathname: router?.pathname,
query: {
...router?.query,
test: '1'
},
}, undefined,
{
scroll: false,
shallow: true
}), [])
return (
<main>
<button onClick={handleClick}>button</button>
</main>
)
}
export default UseRouterPage
But with default exported Router, the next error appears:
I try use default exported Router because in this case there is no component re-render.
import { useCallback } from 'react'
import Router from 'next/router'
const RouterPage = () => {
const handleClick = useCallback(() => Router.push({
pathname: Router?.pathname,
query: {
...Router?.query,
test: '1'
},
}, undefined,
{
scroll: false,
shallow: true
}), [])
return (
<main>
<button onClick={handleClick}>button</button>
</main>
)
}
export default RouterPage
Repo to reproduce: https://github.com/sedlukha/next-router-mock-example
Hey there,
Spent a while trying many different things but ultimately I think there's something up with the behavior of next-router-mock
when passHref
is used with next/link
. We use Stitches CSS to wrap the Link
component.
The issue is the mockRouter
just never updating.
Let me know if there's something you find here, I very well could be doing something wrong but I've exhausted all different things I could think to try.
const Breadcrumb = ({ history }: Props) => {
const router = useRouter();
return (
<Navigation>
<Arrow height={16} width={16} onClick={() => router.back()} name="ChevronLeft" />
<List>
{history.map((item) => {
return (
<Crumb key={item.id}>
<Link passHref href={{ pathname: item.pathname, query: item.query }}> // next Link
<CustomLink active={item.active}>{item.name}</CustomLink> // anchor tag
</Link>
</Crumb>
);
})}
</List>
</Navigation>
);
};
jest.mock('next/router', () => jest.requireActual('next-router-mock'));
const history = [
{
pathname: '/admin/plans',
query: null,
name: 'Plans',
id: 'plans',
active: false,
},
{
pathname: '/admin/plans',
query: { planId: 1 },
name: 'Plan 1',
id: 'plan-1',
active: true,
},
];
test('clicking previous link should navigate back', () => {
render(<Breadcrumb history={history} />, { wrapper: MemoryRouterProvider });
const link = screen.getByRole('link', { name: 'Plans' });
userEvent.click(link);
expect(mockRouter).toMatchObject({
asPath: '/admin/plans',
pathname: '/admin/plans',
query: {},
});
});
Next v12
next-router-mocker v0.9.8
Hi, i'am using this packages but i have one issue, maybe this not support router push with undefined pathname,
router.push({
query: {
five:5
}
});
console.log(router.pathname)
// i want return "/PATH/NAME?five=5"
// real "/?five=5"
it working in next-router-mock? ( i run add one test-case in your 'pushing URLs should update the route' test but not it's fail)
await memoryRouter.push({
query:{
five: "5",
four: "4"
}
})
expect(memoryRouter).toMatchObject({
asPath: "/one/two/three?four=4&five=5",
pathname: "/one/two/three",
query: {
five: "5",
four: "4",
},
})
"next": "12.1.0",
"next-router-mock": "^0.6.5",
thx
Hello,
It would be nice if you could add a changelog, either via the Github releases page or via a CHANGELOG.md file.
Here's some guidance about how to format a changelog : https://keepachangelog.com/en/1.0.0/
There must some tools that can automate (part of) the work, I haven't looked into it though to find the one that fits your workflow best.
Thank you!
Hello!
I've happened to runtime error with the testing readme.
import singletonRouter from 'next/router';
import NextLink from 'next/link';
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
import mockRouter from 'next-router-mock';
jest.mock('next/router', () => require('next-router-mock'));
// This is needed for mocking 'next/link':
jest.mock('next/dist/client/router', () => require('next-router-mock'));
describe('next-router-mock', () => {
beforeEach(() => {
mockRouter.setCurrentUrl('/initial');
});
it('works with next/link', async () => {
render(
<NextLink href="/example?foo=bar">
<a>Example Link</a>
</NextLink>
);
fireEvent.click(screen.getByText('Example Link'));
await waitFor(() => {
expect(singletonRouter).toMatchObject({
asPath: '/example?foo=bar',
pathname: '/example',
query: { foo: 'bar' },
});
});
});
});
The test fails with the following error:
Error: Uncaught [TypeError: Cannot use 'in' operator to search for 'softPush' in null]
This error occurs with fireEvent.click
My environment follows:
Hello,
I'm trying to use next-router-mock to test class based components which are using withRouter().
My components looks like this:
import React from 'react'
import Head from 'next/head'
import {connect} from 'react-redux'
import {withRouter} from 'next/router'
import {WithRouterProps} from 'next/dist/client/with-router'
import {IRootState} from '../../store'
interface Props extends WithRouterProps {
}
interface State {
}
class View extends React.Component<Props, State> {
render() {
return (
<div>
<Head>
<title>...</title>
</Head>
</div>
)
}
}
const mapStateToProps = (state: IRootState) => ({})
const mapDispatchToProps = {}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(View))
And my test looks like this:
import mockRouter from 'next-router-mock'
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import renderer from 'react-test-renderer'
import {Provider} from 'react-redux'
import View from './View'
jest.mock('next/router', () => require('next-router-mock'))
jest.mock('next/dist/client/router', () => require('next-router-mock'))
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
describe('View', () => {
test('should match snapshot', () => {
mockRouter.push('/xxx')
const store = mockStore({...})
const component = renderer.create((
<Provider store={store}>
<View/>
</Provider>
))
let tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
})
When I try to run it, it fails:
FAIL View.test.tsx
โ Test suite failed to run
TypeError: (0 , _router.withRouter) is not a function
As I see there is no reference of withRouter() in the next-router-mock source, so I guess it does not support this.
Can you help me with any workaround? I guess I have to write my own mock code of withRouter() ? Or can you please implement this?
Thank you!
Hello, when I bump from 0.9.2 to 0.9.3 I have a test like this that is now failing. Test is stripped down a bit.
const token = 'myToken'
const redirectUrl = 'myRedirection'
window.localStorage.setItem('accessToken', token)
mockRouter.setCurrentUrl({
pathname: Routes.WorkerLogin,
query: { redirectUrl },
})
const { user } = await render()
await editPhoneInput(user, '0625045131')
await editPasswordInput(user, '1234')
await clickSignInButton(user)
await waitFor(() => {
expect(window.location.assign).toHaveBeenCalledTimes(1)
})
Error: expect(window.location.assign).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
It seems like this is an issue with Next 12 as it affects other scripts in dist/client
but apart from anything I feel like an issue in next-router-mock
would be helpful for anyone running into the same problem.
Hi, I have a language selector where I render every <NextLink />
passing the property locale
so the URL uses the correct locale. But this is not being reflected and actually ignored by Link.
I was expecting that
<Link href={{ pathname: "/asdf", query: { test: "1234" }} locale="en" />
to result in something like /en/asdf?test=1234
, but it is omitting the /en
part.
Any ideas what part of the next/link
I may need to mock to be able to test this?
I've been trying a ton of different ways but nothing i try seems to work. Any ideas or suggestions would be greatly appreciated.
// Test file
import React from "react";
import { render, screen } from "@accolade-x/test-utils";
import { jest } from "@jest/globals";
jest.unstable_mockModule("next/router", () => require("next-router-mock"));
describe("BackButton", () => {
it("should render an button tag", async () => {
const { BackButton } = await import("./BackButton");
render(<BackButton />);
expect(await screen.findByRole("button")).toBeInTheDocument();
});
});
// Component file
export const BackButton = ({
onClick,
...rest
}: BackButtonProps) => {
const router = useRouter();
const clickHandler: MouseEventHandler<HTMLButtonElement> = (e) => {
if (onClick) {
onClick(e);
}
router.back();
};
return (
<Button onClick={clickHandler} {...rest}>
Back
</Button>
);
};
Hello ๐
Is it possible to have the defaultLocale
property support?
For example:
const {defaultLocale} = useRouter();
Then in our tests, we can set the default locale like this:
import routerMock from 'next-router-mock';
routerMock.defaultLocale = 'en';
It would be really appreciated!
Hey,
I have just updated your package from v0.6.3 to v0.6.9 and I have a typescript error when I use registerPaths
Here's my code:
import memoryRouter from "next-router-mock";
import "next-router-mock/dynamic-routes/next-12";
memoryRouter.registerPaths([PRODUCTS, PRODUCTS + "/[...params]"]);
I also should mention that the next version I am using is v12.1.7-canary.16
I think something has changed between these versions and typescript is not able to get the right types
Hi there!
I was playing around with mock router and found this peculiar edge case, maybe I missing something.
jest.mock('next/router', () => require('next-router-mock'));
const ExampleComponent = ({ href = '' }) => {
const router = useRouter();
return (
<button onClick={() => router.push({ hash: `#${href}` })}>
The current route is: "{router.asPath}"
</button>
);
};
describe('next-router-mock', () => {
it('mocks the useRouter hook', () => {
// Set the initial url:
mockRouter.push('/initial-path');
// Render the component:
render(<ExampleComponent href="test" />);
expect(screen.getByRole('button')).toHaveTextContent(
'The current route is: "/initial-path"',
);
// Click the button:
fireEvent.click(screen.getByRole('button'));
// Ensure the router was updated:
expect(mockRouter).toMatchObject({
asPath: '/initial-path#test',
pathname: '/initial-path',
});
// THIS WILL THROW
expect(screen.getByRole('button')).toHaveTextContent(
'The current route is: "/initial-path#test"',
);
});
});
The error:
Expected element to have text content:
The current route is: "/initial-path#test"
Received:
The current route is: "/initial-path"
Apparently when we update just the hash the asPath is not updating in the component (although in next it does)
Mac OS Ventura 13.4
[email protected]
[email protected]
[email protected]
Currently, I am using this library to run testing on our app. However, I seems not being able to make it work with userEvent
. I apologise beforehand if this is a known issue.
Render with NextLink and try clicking the link using userEvent
will cause the test to fail.
Mock router asPath
value to be updated with the URL href value.
Mock router asPath
doesn't changed even though click has been triggered.
next-router-mock
doesn't support Next.js 11. When running npm install
:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! Found: [email protected]
npm ERR! node_modules/next
npm ERR! next@"11.0.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer next@"^10.0.0" from [email protected]
Howdy! Great lib! I've been using it these past few days and really like it, thanks for making it. โค๏ธ โค๏ธ โค๏ธ
One issue I was running into was the auto-import suggestion for createDynamicRouteParser
does not recommend what you recommend in the example, next-router-mock/dynamic-routes
, it wants to import from a subfolder of dist/
Have you considered bundling your dist folder? For example, I've used tsup
on a few projects and it works well. You can use package.types
to point to a bundled dist/index.d.ts
as well. That way everything always exports from next-router-mock
. If you are interested, and I have time, I can try to throw up a PR. Let me know. ๐
If bundling is not feasible due to complications I'm not aware of, perhaps another strategy is possible, like an explicit re-exporting from an dist/dynamic-routes/index.ts
, that way the nextjs-versioned exports are still accessible via the dist subfolders, but the default export is in a more "intuitive" location.
Let me know what you think. And, one more time, thanks for the lib! โค๏ธ โค๏ธ โค๏ธ
The reference of router
returned from useRouter
is not preserved.
This differs from the actual behavior of Next.js, where the singleton router is returned from useRouter
(thus preserving the reference).
The actual behavior of Next.js is that they make a subscription to the router's update and triggers the re-render of the page. Since the props object is created every time, the page component effectively re-renders every time.
In contrast, next-router-mock
always creates a new instance of the router. When using useEffect
with router
in the deps, this results in an infinite loop. (In actual next.js, the app functions as normal)
Hi!
I have something to suggest while using it, so I am registering an issue.
Can I change the condition of as parameter to falsy values
other than undefined
?
next-router-mock/src/MemoryRouter.tsx
Lines 145 to 147 in f212a96
I wish change
if (!as) {
asRoute = undefined;
asPath = getRouteAsPath(newRoute.pathname, newRoute.query, newRoute.hash);
What do you think? Have a good day ๐
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
...
<MemoryRouterProvider>
<></>
</MemoryRouterProvider>
...
I take it this is related to React 18's dropping of the "implicit" children
prop from FC:
ATM it looks like something strange is happening when the mock router fires the onPush event for query changes.
a tested component does:
import { useRouter } from 'next/router';
import qs from 'qs';
...
... inside of component's render:
const router = useRouter();
router.push(
{
pathname: '/moo-cow',
query: qs.stringify({ moo: true}, {
encode: false,
}),
},
);
but when the tests run, i get this:
Timed out retrying after 4000ms: expected pushStub to have been called with arguments matching
{ query: { moo: true }}
The following calls were made:
pushStub("/moo-cow?0=m&1=o&2=o&3=%3D&4=t&5=r&6=u&7=e", {shallow: false})
For some reason the query params are parsed erroneously inside of the router. (eg ?0=m&1=o&2=o&3=%3D&4=t&5=r&6=u&7=e
instead of ?moo=true
In my next.config.js I have skipTrailingSlashRedirect: true
, i.e. the URLs should retain their trailing slash. next-router-mock
however seems to trim the trailing slash from all URLs.
This fails:
// Component
router.push('/foo/')
// Test
const spy = jest.fn()
render(<MemoryRouterProvider onPush={spy}><Component /></MemoryRouterProvider)
expect(spy).toHaveBeenCalledWith('/foo/') // Instead, it is called with /foo
I'm able to fix this by patching the package (I changed normalizeTrailingSlash()
), but it would be cool to have first class support.
hello, i have this error :
Test suite failed to run
Cannot find module 'next/dist/server/normalize-page-path' from 'node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js'
Require stack:
node_modules/next-router-mock/dist/dynamic-routes/extensions-11.1.js
components/Breadcrumb/__tests__/BreadcrumbActu.test.js
at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:491:11)
at Object.<anonymous> (node_modules/next-router-mock/src/dynamic-routes/extensions-11.1.ts:9:1)
My version of next.js : "12.1.6"
Getting MaxListenersExceededWarning
for routeChangeComplete
when multiple next/link
components are used, for example in Header
component (if component is using useRouer
from next/router
):
There is a workaround though. We can add this line in the jest setup file:
require('events').prototype.setMaxListeners(200);
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!
Hi,
first of all, thanks for providing this library! I have no idea one should write integration tests for Next.js without it ๐ค
I've written a couple of tests for a Next.js page component, but even though all tests basically render the same, only one test throws this warning:
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Link (C:\Users\{project}\node_modules\next\client\link.tsx:131:19)
at LinkIconButton (C:\Users\{project}\libs\ui\src\lib\button.tsx:61:23)
at div
at td
at TCell (C:\Users\{project}\libs\ui\src\lib\table\table.tsx:59:78)
at tr
at TRow (C:\Users\{project}\libs\ui\src\lib\table\table.tsx:37:87)
at AufgabenListItem (C:\Users\{project}\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
I tried to create a minimal reproduction of the issue, but I really struggle to pinpoint the underlying problem. But I noticed one thing which probably is the cause.
I modified the code of useMemoryRouter
to output some log statements:
// Trigger updates on route changes:
useEffect(() => {
const id = lodash.uniqueId();
console.log(`subscribe: ${id}`);
const handleRouteChange = () => {
console.log(`handleRouteChange: ${id}`);
// Ensure the reference changes each render:
setRouter(MemoryRouter.snapshot(singletonRouter));
};
singletonRouter.events.on("routeChangeComplete", handleRouteChange);
return () => {
console.log(`UNsubscribe: ${id}`);
singletonRouter.events.off("routeChangeComplete", handleRouteChange);
};
}, [singletonRouter]);
Basically, it logs when useMemoryRouter
subscribes and unsubscribes to routeChangeComplete
and also logs every call of handleRouteChange
. And each subscription gets a unique ID.
The interesting thing is this: In the following excerpt of my test log, there is a handleRouteChange
execution for Subscription 7 even though the same subscription has already been unsubscribed. That probably explains why react complains about a state update on an unmounted component.
console.log
UNsubscribe: 7
at ../../node_modules/next-router-mock/src/useMemoryRouter.tsx:28:25
console.log
UNsubscribe: 6
at ../../node_modules/next-router-mock/src/useMemoryRouter.tsx:28:25
console.log
handleRouteChange: 5
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.log
handleRouteChange: 6
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Link (C:\Users\Marvin\Projekte\tbit\oneplatform\node_modules\next\client\link.tsx:131:19)
at LinkIconButton (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\button.tsx:61:23)
at div
at td
at TCell (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\table\table.tsx:59:78)
at tr
at TRow (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\table\table.tsx:37:87)
at AufgabenListItem (C:\Users\Marvin\Projekte\tbit\oneplatform\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
console.log
handleRouteChange: 7
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at AufgabenListItem (C:\Users\Marvin\Projekte\tbit\oneplatform\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
It seems that there's a short window where the MittEmitter
behind singletonRouter.events
still executes some event handlers that shouldn't be there anymore at that point.
I validated this with this dirty hack which actually solves the issue:
useEffect(() => {
let isUnsubscribed = false;
const handleRouteChange = () => {
if (!isUnsubscribed) {
// Ensure the reference changes each render:
setRouter(MemoryRouter.snapshot(singletonRouter));
}
};
singletonRouter.events.on("routeChangeComplete", handleRouteChange);
return () => {
isUnsubscribed = true;
singletonRouter.events.off("routeChangeComplete", handleRouteChange);
}
}, [singletonRouter]);
Im aware that it's unfortunate, that I cannot provide a reproduction for this issue, but the project I'm working on is proprietary and my attempts to extract the issue have failed so far.
Hey there
the memory router helped a lot to create snapshot even for complex components in JEST โค๏ธ
unfortunately the hash routing events are not supported
we will try to prepare a new pr tomorrow
Hi @scottrippey,
thanks for providing this library! We are currently facing the issue that we want to test the following code using the library:
const pathWithParams = '/our/path?with=params';
const pathWithoutParams = '/our/path'
router.push(pathWithParams, pathWithoutParams);
We write our assertion as follows:
expect(SingletonRouter).toMatchObject({
pathname: '/our/path',
asPath: '/our/path',
query: {
with: 'params',
},
});
However, this assertion will fail, as the asPath
property will have the value: /our/path?with=params
. In our opinion, this is wrong as the asPath
property should have the same value that is passed as the second parameter of the push
(or replace
) function.
After looking at the code, we found the following function:
next-router-mock/src/MemoryRouter.tsx
Line 153 in fbe8950
as
parameter (e.g. from the push
) function. However, this parameter is never used in the function. Instead to compute the asPath
property, the following line is used: next-router-mock/src/MemoryRouter.tsx
Line 168 in fbe8950
In our opinion, this should be changed to
const asPath = as ?? getRouteAsPath(newRoute.pathname, newRoute.query, newRoute.hash);
(note the as ??
part). To make sure the functionality matches the actual functionality from the Next Router. By implementing it this way, the as
parameter will be used if it is defined and if it is not defined, it will fall back to the current functionality.
See the linked PR for our approach on how to solve this.
What do you think? Is this reasoning valid? Should we open a PR or would you rather add it yourself?
Cheers ๐
hi :)
would it be possible to drop next 10 in a major update?
Vite is not able to compile next-router-mock
as it can't resolve imports inside src/MemoryRouterProvider/next-10.tsx
Hi,
When I use it after configuring webpack alias in storybook, it doesn't seem to work in storybook test.
spec
next 12.2.5
storybook 6.5
I read the readme and set it up as below.
https://github.com/scottrippey/next-router-mock#storybook-configuration
https://github.com/scottrippey/next-router-mock#storybook-example
I'm using storybook interaction test as my testing tool
My code
// component.tsx
import qs from 'qs';
import Link from 'next/link';
import { useRouter } from 'next/router';
const WithNextRouter = () => {
const router = useRouter();
const { pathname, asPath, query } = router;
const onClickQueryButton = () =>
router.push('/test?query=1', null, { shallow: true });
return (
<div>
<Link href="/test">home</Link>
<br />
<Link href="/test/abc">a</Link>
<br />
<button type="button" onClick={onClickQueryButton}>
query
</button>
<ul>
<li>pathname: {pathname}</li>
<li>asPath: {asPath}</li>
<li>query: {qs.stringify(query)}</li>
</ul>
</div>
);
};
export default WithNextRouter;
// component.stories.tsx
export default {
title: 'With Next Router',
component: WithNextRouterComponent,
parameters: {
url: {
pathname: '/test',
asPath: '/test',
query: {
param1: 1,
param2: 2,
},
},
jsx: {
skip: 1,
displayName: () => 'WithNextRouterComponent',
},
},
} as ComponentMeta<typeof WithNextRouterComponent>;
export const WithNextRouter: ComponentStory<
typeof WithNextRouterComponent
> = () => <WithNextRouterComponent />;
WithNextRouter.play = async ({
canvasElement,
}) => {
const canvas = within(canvasElement);
await step('show pathname', async () => {
// Given
const pathnameElement = canvas.getByText(/pathname:/);
// Then
await expect(pathnameElement).toBeInTheDocument();
await expect(pathnameElement.textContent).toBe('pathname: /test');
});
await step('click on abc link displays the relevant information.', async () => {
// Given
const linkElement = canvas.getByRole('link', { name: /abc/i });
const pathnameElement = canvas.getByText(/pathname:/);
const asPathElement = canvas.getByText(/asPath:/);
// When
await userEvent.click(linkElement);
// Then
await expect(pathnameElement.textContent).toBe(
'pathname: /test/abc'
);
await expect(asPathElement.textContent).toBe('asPath: /test/abc');
});
}
First in โshow pathnameโ, the test is conducted by setting the initial url parameters.
The expected result is โ/testโ, but the actual result is โ/โ.
Second, โclick on abc link displays the relevant information.โ is a test when a click event occurs through next/link.
The expected result is '/test/abc', but the actual result is '/'.
If you use the storybook test tool after configuring webpack, it will not work.
If I don't configure webpack alias, it works normally. Do you know why?
Any help would be appreciated.
First of all thank you so much for this library. Do you know how we can use it along with next/link
? For example, clicking on a link and then checking the pathname
? I somehow managed to make it work. Bit hacky for now:
// jest.setup.js
jest.mock('next/router', () => {
const router = require('next-router-mock');
const i18nConfig = require('./next-i18next.config');
router.default.locales = i18nConfig.i18n.locales;
router.default.locale = i18nConfig.i18n.defaultLocale;
return router;
});
jest.mock('next/link', () => {
const { cloneElement } = require('react');
const router = require('next-router-mock');
return ({ children, href, replace, as, shallow, locale, scroll }) => {
const onClick = () =>
router.default[replace ? 'replace' : 'push'](href, as, {
shallow,
locale,
scroll,
});
return cloneElement(children, {
href,
onClick,
});
};
});
and then doing expect(router.pathname).toBe('/path/name');
in my tests.
Only problem I can see here is around resetting the pathname
, query
, asPath
etc for router
object for every test in that particular test file.
In next/router proper, I am seeing asPath
values that include the URL hash as I expect. However, passing hashes to next-router-mock
is removing hashes. It looks like the pathing logic only deals with pathname
and query
but doesn't preserve hash
so my tests are "passing" even though at runtime the behavior is different (and failing) causing a false-positive test suite.
router.push('/page#hash=true');
router.push(new URL('/page#hash=true', 'https://www.google.com'));
console.log(asPath);
// logs: "/page"
I'm trying to text links with this package and I am getting inconsistent behavior.
I defined a custom render as follows:
const mockPath = "/streaming";
const mockItem = {
label: "Live",
path: mockPath,
//decorator: "streaming-status",
};
const renderWithRouter = (ui: React.ReactElement, { route = "/" } = {}) => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouterProvider url={route}>{children}</MemoryRouterProvider>
);
return {
user: userEvent.setup(),
...render(ui, { wrapper }),
};
};
And I'm using it on my test like this:
const setup = () => {
const view = renderWithRouter(
<Navbar>
<HeaderItemBase {...mockItem} />
</Navbar>,
{ route: mockItem.path }
);
return view;
};
it("should have blue text color when route is same with path", async () => {
const view = setup();
expect(view).toBeTruthy();
const { user } = view;
const link = screen.getByRole("link", { name: mockItem.label });
await user.click(link);
console.log("Current Path", mockRouter.asPath);
expect(mockRouter.asPath).toStrictEqual(mockPath);
/** TODO
* `next/navigation` is not fully suported on the mocking library yet.
* https://github.com/scottrippey/next-router-mock/issues/67
* expect(link).toHaveClass("text-primary-400");
* */
});
Logging the router path we get:
17:32:15 | header item base > should have white text color when route is different to path | stdout
This is the route /streaming
Current Path /
17:32:15 | header item base > should have blue text color when route is same with path | stdout
This is the route /streaming
Current Path /
17:32:16 | header item base > should have blue text color when route is same with path | stdout
Current Path after Click /
Yeah, it should be "Current Path /streaming".
Also, after clicking on the button, it should navigate easily, but it seems the route is always overwritten to be the given one and it does not change.
When using it without url
property it works well, and navigates correctly.
It seems I got the property wrong, it should work as an initial route, right? Could you write an example of writing it as a provider with options using testing library?
I see the error since v0.7.0.
error TS2306: File '/Users/yusukegoto/src/myapp/node_modules/next-router-mock/dist/MemoryRouterProvider/index.d.ts' is not a module.
And as error mesage said, index.d.js
is empty.
ls -l ./node_modules/next-router-mock/dist/MemoryRouterProvider/index.d.ts
-rw-r--r-- 1 yusukegoto staff 0 10 Jun 15:57 ./node_modules/next-router-mock/dist/MemoryRouterProvider/index.d.ts
I import it like below. This is the same one written in README.
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
Am I missing some configuration?
type: feature request
Use case:
I am testing my i18n components. I need the locale support in the useRouter hook.
const { locale, locales } = useRouter()
I can work and submit a PR if you are ok with it
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.