Next.js: jest example fails with `next/link`

Created on 28 Apr 2017  路  23Comments  路  Source: vercel/next.js

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?

bug

Most helpful comment

@kgoggin it seems that I understood.
for anybody using prefetch:

import Router from 'next/router'
const mockedRouter = { push: () => {}, prefetch: () => {} }
Router.router = mockedRouter

All 23 comments

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.

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);

Usage


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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DvirSh picture DvirSh  路  3Comments

timneutkens picture timneutkens  路  3Comments

renatorib picture renatorib  路  3Comments

olifante picture olifante  路  3Comments

knipferrc picture knipferrc  路  3Comments