7

I am using next-routes and in my React component I am using below useEffect code block to detect Router change event:

useEffect(() => {
  // THIS BLOCK WILL BE EXECUTED WHEN THE COMPONENT IS ALREADY MOUNTED AND SCREEN WOULD BE REDNDERED IN WHEN ROUTED CLIENT SIDE
  const handleRouterChangeComplete = url => {
    // console.log('INNNN handleRouterChangeComplete url = ', url);
    // MY REST OF THE LOGIC HERE
  };

  // THIS BLOCK WILL BE EXECUTED WHEN THE SCREEN WOULD BE REDNDERED IN WHEN ROUTED CLIENT SIDE
  Router.events.on('routeChangeComplete', handleRouterChangeComplete);
  return () => {
    Router.events.off('routeChangeComplete', handleRouterChangeComplete);
  };
}, []);

I am using jest to write unit test case for this component and facing below error:

TypeError: Cannot read property 'on' of undefined

      183 |
      184 |     // THIS BLOCK WILL BE EXECUTED WHEN THE SCREEN WOULD BE REDNDERED IN WHEN ROUTED CLIENT SIDE
    > 185 |     Router.events.on('routeChangeComplete', handleRouterChangeComplete);

Can anybody please help me with mocking the Router Change event? Thanks

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Rahul Gupta
  • 9,775
  • 7
  • 56
  • 69

5 Answers5

3

For anyone who is stuck with testing the Router change events in nextjs here is an example.

Short Answer

You can use Router.events.emit to emit the event from your test then assert the callback you are expecting to get fired.

Long Answer

Following is the scenario I am using in the example, however you can modify it to your needs.

Scenario: In my main app, I am using NProgress to display a progress bar on each route change. The custom app looks like this

import Router from 'next/router';
import NProgress from 'nprogress';

Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

export default CustomApp() {
   ...
}

Now, in my test, I want to test that the callbacks are called appropriately. The jest test looks like this:

import Router from 'next/router;
import NProgress from 'nprogress;
import { render } from '@testing-library/react';
import CustomApp from 'pages/_app';

const mockStart = jest.spyOn(NProgress, 'start');
const mockDone = jest.spyOn(NProgress, 'done');

it('starts nprogress on router change start', () => {
  render(<CustomApp />)
  Router.events.emit('routeChangeStart');
  expect(mockStart).toHaveBeenCalled();
});

it('ends nprogress on router change complete', () => {
  render(<CustomApp />)
  Router.events.emit('routeChangeComplete');
  expect(mockDone).toHaveBeenCalled();
});

Subash
  • 7,098
  • 7
  • 44
  • 70
1

You can trigger the relevant change event by using Router.events.emit('event-name');

The code was taken from the router implementation.

felixmosh
  • 32,615
  • 9
  • 69
  • 88
1

I am using useRouter hook from next/router in my implementation.

import { FunctionComponent, useEffect } from 'react';
import { useRouter } from 'next/router';

const handleRouteChange = (url) => {
  // handle route change
  return `handling url - ${url}`
};

const Component: FunctionComponent = () => {
  const router = useRouter();

  useEffect(() => {
    router.events.on('routeChangeComplete', handleRouteChange);
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  // do other things
};

and doing the following in my test to mock useRouter:

jest.mock('next/router');

let eventName;
let routeChangeHandler;

useRouter.mockImplementation(() => {
  return {
    events: {
      on: jest.fn((event, callback) => {
        eventName = event;
        routeChangeHandler = callback;
      }),
      off: jest.fn((event, callback) => {
        eventName = event;
        routeChangeHandler = callback;
      }),
    },
  };
});

and my test looks like

it('should call the required functions', () => {
    render(<Component />);

    expect(useRouter).toHaveBeenCalledTimes(1);
    expect(eventName).toBe('routeChangeComplete'); 
    expect(routeChangeHandler).toBeDefined();

    expect(routeChangeHandler('/')).toBe('handling url - /');

    useRouter().events.on('onEvent', routeChangeHandler);
    expect(useRouter).toHaveBeenCalledTimes(2);
    expect(eventName).toBe('onEvent');

    useRouter().events.off('offEvent', routeChangeHandler);
    expect(useRouter).toHaveBeenCalledTimes(3);
    expect(eventName).toBe('offEvent');
});
sm7
  • 599
  • 5
  • 9
1

I based my answer off of a lengthy thread here: https://github.com/vercel/next.js/issues/7479.

Hopefully someone can swoop in and make a better answer by saying how it works or improving for best practices.

// _app.test.js
/**
 * @jest-environment jsdom
 */

import React from 'react';
import { render } from '@testing-library/react';
import MyComponentThatUsesRouter from '../components/MyComponentThatUsesRouter';

//Router mock
const useRouter = jest.spyOn(require("next/router"), "useRouter");

describe('MyComponentThatUsesRouter', () => {
  it('renders without crashing', () => {
    // hypothetical prop possibly not needed
    const myprop1={red, blue, orange}
    // Specific mock default values - change as needed
    useRouter.mockImplementationOnce(() => ({
        basePath: '/',
        pathname: '/',
        route: '/',
        query: {},
        asPath: '/',
        push: jest.fn(() => Promise.resolve(true)),
        replace: jest.fn(() => Promise.resolve(true)),
        reload: jest.fn(() => Promise.resolve(true)),
        prefetch: jest.fn(() => Promise.resolve()),
        back: jest.fn(() => Promise.resolve(true)),
        beforePopState: jest.fn(() => Promise.resolve(true)),
        isFallback: false,
        events: {
          on: jest.fn(),
          off: jest.fn(),
          emit: jest.fn(),
        },
    }));
    // Your component that is using Router
    render(<MyComponentThatUsesRouter prop1={myprop1} />)

  });
})
Duncan Brain
  • 305
  • 4
  • 11
0

Found a way on this github issue

Just store the callback into a variable so you can "trigger" the event whenever you wish.

let routeChangeComplete;
Router.default.events.on = jest.fn((event, callback) => {
    routeChangeComplete = callback;
  });

https://github.com/vercel/next.js/issues/7586