Search code examples
javascriptunit-testingjestjsnext.jsnext-router

Mocking NextJS router events with Jest


I am trying to mock NextJS router events with Jest. Found a relevant resource right here at NextJS router & Jest. The implementation there is very similar to mine.

However, the solution mentioned there is not working for me.

My test is below:

import { mount, ReactWrapper } from 'enzyme';
import FavoritesPage from 'pages/user/favorites';
import configureStore, { MockStore } from 'redux-mock-store';
import storeInitialState from '__tests__/unit/support/storeInitialState';
import { Provider } from 'react-redux';
import { waitFor } from '@testing-library/react';
import { RequestStates } from 'types/State';
import useAdSetup from 'lib/hooks/useAdSetup';

const mockRequestState = RequestStates.Finished;

jest.mock('lib/Auth');
jest.mock('lib/EventLogger');
jest.mock('lib/hooks/useAdSetup');

jest.mock('lib/hooks/useFetchFavorites', () => {
  return (): { requestState: RequestStates } => {
    return {
      requestState: mockRequestState,
    };
  };
});

jest.mock('next/router');

const mockStore = configureStore();

let store: MockStore;

describe('when clicking the price drop subnav button', () => {
  let component: ReactWrapper;

  beforeEach(async () => {
    store = mockStore(storeInitialState);
    await waitFor(() => {
      component = mount(
        <Provider store={store}>
          <FavoritesPage />
        </Provider>
      );
    });
    component.find('.price-drop-nav-item').simulate('click');
  });

  it('shows price drops', () => {
    let eventName;
    let routeChangeHandler;
    let useRouter = jest.fn();

    useRouter.mockImplementation(() => {
      return {
        events: {
          on: jest.fn((event, callback) => {
            eventName = event;
            routeChangeHandler = callback;
          }),
          off: jest.fn((event, callback) => {
            eventName = event;
            routeChangeHandler = callback;
          }),
        },
      };
    });
  
    expect(useRouter).toHaveBeenCalledTimes(1);
    expect(component.find('.price-drop-nav-item').hasClass('active')).toBeTruthy();
  });
});

Inside, my component just like the referenced example has the following:

  useEffect(() => {
    const handleComplete: any = async (url: string) => {
      if (window) {
        await trackReturnToSeachClick(url);
      }
    };
    router.events.on('routeChangeComplete', handleComplete);
    router.events.on('routeChangeError', handleComplete);

    // Cleanup event listeners
    return (): void => {
      router.events.off('routeChangeComplete', handleComplete);
      router.events.off('routeChangeError', handleComplete);
    };
  }, [router]);

Unlike the referenced example, when I run my code, I am still getting the below error:

TypeError: Cannot read property 'on' of undefined

What am I missing?


Solution

  • I found a bunch of relevant samples online. The most useful, as far as NextJS 11 goes was the thread at vercel/next.js. Based on that, I was able to put together the working solution below:

    jest.mock('next/router', () => ({
      useRouter() {
        return ({
          route: '/',
          pathname: '',
          query: '',
          asPath: '',
          push: jest.fn(),
          events: {
            on: jest.fn(),
            off: jest.fn()
          },
          beforePopState: jest.fn(() => null),
          prefetch: jest.fn(() => null)
        });
      },
    }));
    
    const mockStore = configureStore();
    
    let store: MockStore;
    
    describe('when clicking the price drop subnav button', () => {
      let component: ReactWrapper;
      
      beforeEach(async () => {
        store = mockStore(storeInitialState);
    
        const useRouter = jest.spyOn(require("next/router"), "useRouter");
    
        useRouter.mockImplementation(() => ({
          route: '/',
          pathname: '',
          query: '',
          asPath: '',
          push: jest.fn(),
          events: {
            on: jest.fn(),
            off: jest.fn()
          },
          beforePopState: jest.fn(() => null),
          prefetch: jest.fn(() => null)
        }));
    
        component = mount(
            <Provider store={store}>
              <FavoritesPage />
            </Provider>
        );
        component.find('.price-drop-nav-item').simulate('click');
      });
    
      it('shows price drops', () => {
      
        // expect(useRouter).toHaveBeenCalledTimes(1);
        expect(component.find('.price-drop-nav-item').hasClass('active')).toBeTruthy();
      });
    });
    

    Nothing else worked for me. The test for expect(useRouter).toHaveBeenCalledTimes(1) is not working either. :)