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

Application code is calling location.href = "https://stackoverflow.com/questions/46169824/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

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

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/

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.

Read More:   Remove everything after last backslash

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')
  })
})

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')
 })
}



The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

Similar Posts