Example repo: https://github.com/remy/next-examples/tree/master/with-jest#fails-with
The jest example in the examples directory is too simple, so it quickly falls over when I started testing a slightly larger app (than a single page with some jsx).
How do we get snapshot testing to work with next? Can we have jest tell next (somehow???) that prefetch
shouldn't happen?
Hmm, on the back of this, prefetch
is supposed to be production only - so is jest running next.js in production mode?
Note: I tried NODE_ENV=dev npm test
and it fails with the same result You should only use "next/router" inside the client side of your app
.
+1. Echoed too on https://github.com/zeit/next.js/issues/1204
I think we need to fix this bug. On it.
@arunoda did you get anywhere on this? I don't know how to get around this at all, so my application is currently failing all tests :(
Not sure if this is related, but I'm getting the same error if I'll try to test container that uses Router.push()
function directly. I haven't really found a way to mock it.
Yes. When we use next/router
or next/link
with prefetch, it'll fail inside JEST.
That's because there's no actual router instance.
We need to find a way to mock this.
Something like this would work.
Haven't tested.
import Router from `next/router`
const mockedRouter = { push: () => {} }
Router.router = mockedRouter
@arunoda that seems to work. Thanks!
For anyone else landing here trying to make this work with a <Link prefetch/>
in storybook, I used @arunoda's example but had to add a prefetch
noop on the mocked router as well.
With next 3.x, even without prefetch, <Link />
causes this error
@kgoggin could you please show your code? did not really understand what you mean by noop and when you put it.
@kgoggin it seems that I understood.
for anybody using prefetch:
import Router from 'next/router'
const mockedRouter = { push: () => {}, prefetch: () => {} }
Router.router = mockedRouter
ps when I mock it like this in storybook config for stories to work then I get Uncaught TypeError: Cannot read property 'then' of undefined
at Link.linkClicked (link.js:123)
because onClick
calls linkClicked
which has .then(...)
after calling Router.router.push
. how can we mock this one?
in the end for storybook (not for jest) I mocked it like this in config:
const actionWithPromise = () => {
action('clicked link')();
// we need to return promise because it is needed by Link.linkClicked
return new Promise((resolve, reject) => reject());
};
const mockedRouter = {
push: actionWithPromise,
replace: actionWithPromise,
prefetch: () => {},
};
Router.router = mockedRouter;
for jest I just have router fully ignored:
jest.mock('next/router');
though it may be not enough if Link onClick needs to be simulated
Anybody got advice on mocking withRouter
within Storybook and Jest?
I tried withRouter = Component => props => <Component {...props} router={mockedRouter} />
with no success.
@kylemh I ran into this and was able to get it working by wrapping the story in a HOC that provides a custom router object in it's context. It looks like next/router relies on the legacy context API so this will probably break in later versions. Here's my mock files:
const { Component } = require('react');
const Router = require('next/router').default;
const { action } = require('@storybook/addon-actions');
const PropTypes = require('prop-types');
const actionWithPromise = () => {
action('clicked link')();
return new Promise((resolve, reject) => reject());
};
const mockedRouter = {
push: actionWithPromise,
replace: actionWithPromise,
prefetch: () => {},
route: '/mock-route',
pathname: 'mock-path',
};
Router.router = mockedRouter;
const withMockRouterContext = (mockRouter) => {
class MockRouterContext extends Component {
getChildContext() {
return {
router: Object.assign(mockedRouter, mockRouter),
};
}
render() {
return this.props.children;
}
}
MockRouterContext.childContextTypes = {
router: PropTypes.object,
};
return MockRouterContext;
};
module.exports.mockedRouter = mockedRouter;
module.exports.withMockRouterContext = withMockRouterContext;
Then just wrap your story with a MockRouter:
import { withMockRouterContext } from 'test-utils/react/nextjs/router';
const MockRouter1 = withMockRouterContext([extendDefaultMockRouter]);
stories.add(
'Default',
() => (
<MockRouter1>
<ActiveLink href="test-active">Test</ActiveLink>
</MockRouter1>
)
);
@ssylvia looks like @nickluger got a convo going in #5205
Yours worked. I also added a bit more logic and used it as a global decorator so as to not interfere with the withInfo
decorator.
Will be migrating to storybook@4
by the end of October - interested to see how this tale progresses.
Would be good to see this fixed after almost 2 years. This is a barrier to anyone coming to Next.js as they'll bump into this pretty soon when they try to mount
a component with <Link prefetch>
- which you guys recommend.
thanks @ssylvia, I used a tweaked version of what you suggested:
/* tslint:disable */
import { Component } from 'react';
import Router from 'next/router';
import { action } from '@storybook/addon-actions';
import PropTypes from 'prop-types';
const actionWithPromise = () => {
action('clicked link')();
return new Promise((_, reject) => reject());
};
const mockedRouter = {
push: actionWithPromise,
replace: actionWithPromise,
prefetch: () => {},
route: '/mock-route',
pathname: 'mock-path',
};
// @ts-ignore
Router.router = mockedRouter;
const withMockRouterContext = mockRouter => {
class MockRouterContext extends Component {
public getChildContext() {
return {
router: { ...mockedRouter, ...mockRouter },
};
}
public render() {
return this.props.children;
}
}
// @ts-ignore
MockRouterContext.childContextTypes = {
router: PropTypes.object,
};
return MockRouterContext;
};
export const StorybookRouterFix = withMockRouterContext(mockedRouter);
import { StorybookRouterFix } from '/utils/storybook/StoryRouterFix';
// ....
storiesOf('ComponentThatHasALink', module)
.add('ComponentThatHasALink', () => (
<StorybookRouterFix>
<ComponentThatHasALink
/>
</StorybookRouterFix>
));
If you only want to show the component without the Router
functionalities, try this:
const Router = {}
const mockedRouter = { push: () => {}, prefetch: () => {} }
Router.router = mockedRouter
Leaving my two cents, based on previous replies:
const actionWithPromise = () => new Promise((_, reject) => reject());
jest.mock('next/router', () => ({
push: actionWithPromise,
replace: actionWithPromise,
prefetch: () => {},
route: '/mock-route',
pathname: 'mock-path',
}));
The withMockRouterContext
solution works well when the router in the original component is implemented with withRouter()
. Is there a way to make it work if you're using router as a React Hook with useRouter()
?
Edit: found this: https://github.com/zeit/next.js/issues/7479
Adding to @wilson-alberto-kununu 's example this is what we cooked up in the team when your component's behavior depends on values coming from router values (and why else would you use withRouter
?), for example you need to access a parameter in query string and you want to write multiple unit tests.
// (In your the test file of `<Something />` component:
async function createComponent(queryStringParams = {}) {
jest.doMock('next/router', () => ({
withRouter: component => {
component.defaultProps = {
...component.defaultProps,
router: {
pathname: 'something',
query: queryStringParams
},
};
return component;
},
}));
const { Something } = await import('./something'); // 馃憟 please note the dynamic import
return mount(<Something />);
}
So it looks like this workaround (at least for storybook) is broken in next 9.1.7. I'd been running with 9.1.4 for a while and when updating to 9.1.7 storybook broke for any components that had next/link(s) in them. Rolling back to 9.1.6 fixed the issue.
Most helpful comment
@kgoggin it seems that I understood.
for anybody using prefetch: