Simulating NextJS router triggers using Jest

I've been attempting to simulate NextJS router events using Jest. I came across a useful resource at NextJS router & Jest. The approach outlined there closely resembles mine.

Unfortunately, the solution provided in that post is not yielding the desired results for me.

Here's my test code:

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, much like the example I referred to earlier, I have the following:

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

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

However, unlike the referenced example, I am still encountering the error below when running my code:

TypeError: Cannot read property 'on' of undefined

Can anyone point out what might be missing?

Answer №1

After extensive research, I came across a wealth of relevant examples online. The thread on vercel/next.js proved to be particularly valuable when working with NextJS 11. Drawing inspiration from that discussion, I successfully crafted the following functional solution:

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

No other methods produced satisfactory results for me. Additionally, the test for

expect(useRouter).toHaveBeenCalledTimes(1)
remains unresolved. :)

Answer №2

Before starting my test, I added the following code snippet at the beginning of my test file, just above the describe function.

jest.mock('next/router', () => ({
  useRouter() {
    return {
      pathname: '',
      // ... any other functions you need to use on `router`
    };
  },
}));

Answer №3

Initially, you are mocking the useRouter function.

jest.mock('next/router', () => ({
  userRouter: jest.fn()
})

Next, you assign a return value to your mock. In this case, we require events.on and events.off so we include them in the return.

test('some test', () => {
  useRouter.mockReturnValue({
    events: {
      on: () => {},
      off: () => {},
    }
  })
  render(<SomeComponent />)
  expect(useRouter).toHaveBeenCalled()
})

That's it. There is no need to add a mocking function for the on method like on: jest.fn() since you can't access them anyway. Therefore, using dummy functions () => {} suffices. If you wish to verify if events.on is called with the correct values, follow these steps:

jest.mock('next/router', () => ({
  useRouter: jest.fn()
}))

const mockOn = jest.fn()

test('some test', () => {
  useRouter.mockReturnValue({
    events: {
      on: mockOn,
      off: () => {},
    }
  })
  render(<SomeComponent />)
  expect(useRouter).toHaveBeenCalled()
  expect(mockOn).toHaveBeenCalled()
  expect(mockOn).toHaveBeenCalledWith('some event', expect.any(Function))
})

To learn more about mocking next/router, check out my article on dev.to.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the secret behind Redactor JS's ability to display properly indented code snippets?

If you take a look at the Redactor JS demo and click on the button to show the HTML source code of the displayed content... ...you will notice that the code is properly indented: Most rich text editors or wysiwyg editors typically display code in a singl ...

"Enhancement in Chrome: Inclusion of Origin header in same-origin requests

When we POST an AJAX request to a server running locally, the code looks like this: xhr.open("POST", "http://localhost:9000/context/request"); xhr.addHeader(someCustomHeaders); xhr.send(someData); The webpage where this javascript is executed is also on ...

Here's a method of sorting through an array of objects and displaying all objects that include a specific keyword in one of their values

I am currently working on a project involving an array of book objects. My task is to display a list of all books that have either "companies" or "people" in their title using console.log. const books = [{ title: 'How to win friends and influence ...

Within the .module.scss file, there exist multiple classes that are identical; however, only one classname is reflected

How can I differentiate elements with the same class name? :S In Next.js, only one column1 is generated for each end cell1, cell2 class, etc.. Is there a way to solve this??: className={styles.left.column1}, className={styles.left.column1.cell1}, className ...

Identify the index of a list item using a custom list created from buttons

When dealing with a dynamically built list like this: <ul id="shortcuts"> <li><input type="checkbox" value="false"/><button>foo</button><button>-</button></li> <li><input type="checkbox" value ...

Can you explain the significance of npm WARN excluding symbolic link?

Could you please explain the meaning of npm WARN excluding symbolic link? Also, any advice on how to resolve this issue? ...

Arranging an Array of Arrays Containing Strings

Looking for a solution to sort an array containing arrays of strings? A similar issue was discussed here. Here is the array in question: var myArray = [ ['blala', 'alfred', '...'], ['jfkdj', ...

Encountering issues with CSS selectors when using Selenium WebDriver

I am encountering an error with the following code: elem = new Array() elem = driver.findElements(By.CssSelector('input')); What could be causing the issue in the code above? If I have an HTML form like this: <form role="form" method="post ...

Display information from a mysql database table within a selection menu

Currently, I am working on a dropdown menu that should display data from a MySQL table. However, I am facing an issue which is outlined below: In my PHP script, I have approached it in the following manner: <label class="col-form-label" for="formGrou ...

A guide on adding an onClick listener to a span element dynamically and then saving it to MongoDB

I have been attempting to add an onClick event within a span tag as shown below, with the goal of storing it in MongoDb. However, my event does not seem to be saving and is automatically removed. When I retrieve data from the database, it is not present. H ...

Encountering a TypeError when attempting to pass the onChange function as props to Grandchildren due to 'this' being undefined

Struggling to pass an onChange function from parent to grandchild component and encountering an error. TypeError: this is undefined The code snippet causing the issue: const TaskTable = React.createClass({ getInitialState: function() { return {dat ...

Rearrange div elements following an ajax request based on a data attribute and applying the .animate method

I am dealing with a collection of div elements, each assigned a unique numeric id and data-position in sequential order (1 for the first in the list, 2 for the second, and so on). Upon making an ajax call using jQuery, the response is a JSON object that r ...

Typescript is throwing an error stating that the type 'Promise<void>' cannot be assigned to the type 'void | Destructor'

The text editor is displaying the following message: Error: Type 'Promise' is not compatible with type 'void | Destructor'. This error occurs when calling checkUserLoggedIn() within the useEffect hook. To resolve this, I tried defin ...

Integrating a Vue application with an OpenId provider using the OpenId Connect library

Currently, I am in the process of developing a Single Page Application with Vue on the client-side and Java Spring REST APIs on the backend. My goal is to add security measures using OpenId Connect, specifically with RapidIdentity as the provider. Unlike ...

Is it possible for an animation to complete even if it is stopped midway?

I am currently working on a media player project where I have implemented a scrolling marquee effect for the song meta information using JavaScript and CSS. The scrolling effect only occurs when the track is playing, and I achieve this by adding/removing ...

Is it possible to alter CSS based on the width of the webpage using javascript

We are currently developing a web application that will be deployed on various devices such as mobile phones, iPads, iPhones, and Android. Instead of using user agents to show different views, I prefer having CSS that adjusts based on the screen width. We ...

What is the method of including a null option in a dropdown menu?

I have a basic dropdown menu with the placeholder text "none." I want users to be able to clear their selection without adding another option to the dropdown. Can anyone help me achieve this? Thank you! Below is my code snippet: Check out the live demo h ...

Updating or deleting query strings using JavaScript

My URL is structured as follows: http://127.0.0.1:8000/dashboard/post?page=2&order=title I am seeking a way to eliminate the query string ?page={number} or &page={number} Due to my limited knowledge of regular expressions, I am wondering if there ...

What is the best method to showcase an array representing a key-value pair enclosed in {} as a list item within VueJS?

I have a specific object structure with a key that contains an array as its value. How can I present this information to the user in a list format? allComponents: [ {name: 'Standard field', uses: ['Inconsistent inputs', 'Formul ...

The chart appears oversized in the vue js. How can I make it smaller in size?

I recently integrated a line chart from Chart JS into my Vue.js project, but the chart is taking up too much space on my webpage. I'm looking for ways to make it appear smaller and more compact. This is my first time working with charts in Vue.js, so ...