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