'Intercept navigation change with jest.js (or how to override and restore location.href)

Application code is calling location.href = "some-url". I want to write a test that verify the navigation redirect has happened.

Using jest on jsdom, I tried to do it with overriding location.href setter using jest mock function and it is working.

But now I can't seems to restore the location.href property at the test cleanup, and it failing the rest of the tests which relay on 'location.href'.

it('test navigation happened', () => {
  const originalLocationHref = Object.getOwnPropertyDescriptor(window.location, 'href'); // returns undefined

  spyFn = jest.fn();
  Object.defineProperty(window.location, 'href', {
    set: spyFn,
    enumerable: true,
    configurable: true
  });

  someAppCodeThatShouldRedirectToSomeUrl();

  expect(spyFn).toBeCalledWith('http://some-url'); // this is working

  // Cleanup code, not working, because originalLocationHref is undefined
  Object.defineProperty(window.location, 'href', originalLocationHref);  
});

What am I missing? Why Object.getOwnPropertyDescriptor(window.location, 'href'); is undefined?

Is there a better way to intercept navigation events in order to test it?

Thanks



Solution 1:[1]

Use location.assign() method instead instead of assigning new location string to location.href. Then you can mock and test it with no problems:

it('test navigation happened', () => {
  window.location.assign = jest.fn();

  // here you call location.assign('http://some-url');
  redirectToSomeUrl();

  expect(window.location.assign).toBeCalledWith('http://some-url');

  // location.href hasn't changed because location.assign was mocked
});

Solution 2:[2]

Newer jest/jsdom versions do not allow to set window.location.assign anymore. It can be fixed like this:

delete window.location;
window.location = { assign: jest.fn() };

Note that this removes all other objects from window.location, you might need to mock more of its objects depending on your test and application code.

Source: https://remarkablemark.org/blog/2018/11/17/mock-window-location/

Solution 3:[3]

As quotesBro already explained in his answer, you should rather use location.assign().

But since Jest v25 (which uses a newer version of JSDOM) you will get the following error:

TypeError: Cannot assign to read only property 'assign' of object '[object Location]'

This is not a Jest/JSDOM bug by the way. This is normal browser behaviour and JSDOM tries to act like a real browser.

A workaround is to remove the location object, create your own one and after running your tests you should reset it to the original location object:

describe('My awesome unit test', () => {
  // we need to save the original object for later to not affect tests from other files
  const realLocation = window.location

  beforeAll(() => {
    delete window.location
    window.location = { assign: jest.fn() }
    // or even like this if you are also using other location properties (or if Typescript complains):
    // window.location = { ...realLocation, assign: jest.fn() }
  })

  afterAll(() => {
    window.location = realLocation
  })

  it('should call location.assign', () => {    
    // ...your test code

    expect(window.location.assign).toHaveBeenCalled()

    // or even better:
    // expect(window.location.assign).toHaveBeenCalledWith('/my_link')
  })
})

Solution 4:[4]

Another pretty simple solution can be to mock the location window.location object at the beginning of the test file

const assignSpy = jest.fn();

Object.defineProperty(window, 'location', {
  value: { assign: assignSpy }
});

describe('My awesome unit test', () => {
it('should call location.assign', () => {    
    // ...your test code

    expect(window.location.assign).toHaveBeenCalled()

    // or even better:
    // expect(window.location.assign).toHaveBeenCalledWith('/my_link')
 })
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 fabb
Solution 3
Solution 4 samuele piazzesi