Jsdom: Error: Not implemented: navigation

Created on 12 Jan 2018  ·  49Comments  ·  Source: jsdom/jsdom

After recent upgrade jest (which uses jsdom in background) from version 21.2.0 to 22.0.6 I have started getting error: "Error: Not implemented:" navigation

My code relies on window.location and I use in tests:

beforeEach(() => {
                window.location.href = `/ms/submission/?mybib`;
                window.location.search = '?mybib';
});

Is there a way to define a value of window.location.search using new version of jsdom?

Most helpful comment

Allow me to post the answer to my own question 😁
I simply replace the usages of window.location = url; and window.location.href = url; with

window.location.assign(url);

and then in my tests I did:

sinon.stub(window.location, 'assign');
expect(window.location.assign).to.have.been.calledWith(url);

Works like a charm - hope it can be of help to someone else 👍

All 49 comments

I'm getting this error too

Error: Not implemented: navigation (except hash changes)
    at module.exports (...\node_modules\jsdom\lib\jsdom\browser\not-implemented.js:9:17)
    at navigateFetch (...\node_modules\jsdom\lib\jsdom\living\window\navigation.js:74:3)

jsdom does not support navigation, so setting window.location.href or similar will give this message. I'm not sure if Jest was just suppressing these messages before, or what.

This is probably something you should fix in your tests, because it means that if you were running those tests in the browser, the test runner would get completely blown away as you navigated the page to a new URL, and you would never see any tests results. In jsdom instead we just output a message to the console, which you can ignore if you want, or you can fix your tests to make them work better in more environments.

Anyway, I'd like to add more documentation on this for people, so I'll leave this issue open to track doing so.

Totally get what you're saying. The recent Jest 22 update went from JSDOM 9 to 11 IIRC, so the behavior back in 9.x might have been quite different.

All that aside, I would love to see navigation implemented in JSDOM with some sort of flag to make it a no-op in terms of loading a different page (in similar spirit to HTML5 pushstate.) The library is very commonly used for testing purposes so, while perhaps a quirky request, it would be used often.

I don't think we should add a flag that makes your tests run different in jsdom than in browsers. Then your stuff could be broken in browsers (e.g. it could be redirecting users to some other page, instead of doing the action that your tests see happening) and you wouldn't even notice!

Well in this case it wouldn't doing anything different, other than not unloading the current page context. I'd still expect window.location.href to be updated, etc.

@domenic I'm having the same issue and I was wondering if there is some sort of best practice to setup JSDOM with an app that sets window.location. From what I can tell JSDOM throws an error when trying to set window.location and logs an error when trying to set window.location.href - however I'm reading on mdn that the two should be synonyms. Should I be updating the location in another way thats easier to stub?
Thankful for help 😅

Allow me to post the answer to my own question 😁
I simply replace the usages of window.location = url; and window.location.href = url; with

window.location.assign(url);

and then in my tests I did:

sinon.stub(window.location, 'assign');
expect(window.location.assign).to.have.been.calledWith(url);

Works like a charm - hope it can be of help to someone else 👍

Agree this should work out of the box. We mock window.location at FB, but that conflicts with jsdom's History implementation.

As a small team, we would certainly appreciate help from the larger projects that depend on us to properly implement navigation in jsdom.

If anyone is interested, https://github.com/jsdom/jsdom/pull/1913 could be a good place to start.

Possible solution is to rely on dependency injection/mock for the window object in unit tests.

Something like:

it('can test', () => {
  const mockWindow = {location: {href: null}};
  fn({window: mockWindow});
  expect(mockWindow.href).toEqual('something');
});

This is not ideal but as said by @domenic:

This is probably something you should fix in your tests, because it means that if you were running those tests in the browser, the test runner would get completely blown away as you navigated the page to a new URL

For now we live with this and yes we change our implementation code for tests which is considered bad practice but we also sleep well at night!

Happy testing

@hontas's solution helped:

I did use window.location.assign(Config.BASE_URL); in my code.

And here's the test:

jest.spyOn(window.location, 'assign').mockImplementation( l => {
   expect(l).toEqual(Config.BASE_URL);
})

window.location.assign.mockClear();

Same problem, I'm using window.location.search = foo; in my code and I would like to test it using jsdom (and jest) 🤔

PS: Related to https://github.com/facebook/jest/issues/5266

After updating to jsdom 12.2.0 got error:
TypeError: Cannot redefine property: assign
onconst assign = sinon.stub(document.location, 'assign')
how to fix it?

@yuri-sakharov

After updating to jsdom 12.2.0 got error:
TypeError: Cannot redefine property: assign
onconst assign = sinon.stub(document.location, 'assign')
how to fix it?

sinon.stub(document.location, 'assign')

needs to be:

sinon.stub(window.location, 'assign')

you need to replacedocument with window

I have the following function

export const isLocalHost = () => Boolean(
  window.location.hostname === 'localhost' ||
  // [::1] is the IPv6 localhost address.
  window.location.hostname === '[::1]' ||
  // 127.0.0.1/8 is considered localhost for IPv4.
  window.location.hostname.match(
    /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
  )
);

for me to be able to test that it works I inject directly the hostname

it('#isLocalHost should return true for all the cases of localhost', () => {
    window.location.hostname = 'localhost';
    expect(isLocalHost()).toBeTruthy();

    window.location.hostname = '[::1]';
    expect(isLocalHost()).toBeTruthy();

    window.location.hostname = '127.0.0.1';
    expect(isLocalHost()).toBeTruthy();

    // Reset back the hostname to avoid issues with it
    window.location.hostname = '';
  });

But I am getting this error.

I don't expect jsdom to fully implement the navigation but at least add the keys and mock the functions.

I am confused on why I keep getting this error, I just want to be able to setup the value.

@nickhallph
I replaced it to window.location as you wrote but result the same
TypeError: Cannot redefine property: assign
Any ideas?

Same problem as @yuri-sakharov running mocha.

By no means I seem able to replace/update/mock or do anything with window.location.*. The only way I see around this is creating my own custom window.location mock and change the entire codebase to depend on that.

@hontas's solution helped:

I did use window.location.assign(Config.BASE_URL); in my code.

And here's the test:

jest.spyOn(window.location, 'assign').mockImplementation( l => {
   expect(l).toEqual(Config.BASE_URL);
})

window.location.assign.mockClear();

@zxiest's Jest version of @hontas' solution didn't work for me, but this did:

window.location.assign = jest.fn();
expect(window.location.assign).toHaveBeenCalledWith('https://correct-uri.com');
window.location.assign.mockRestore();

If you don't want to change your code to use location.assign - this seems to work with JSDom 11 and 13 (though there's a chance JSDom might break it in the future...)

delete window.location;
window.location = {}; // or stub/spy etc.

The last answer worked for me, but I had to define replace:

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

Hope it helps.

I'm getting TypeError: Cannot redefine property: assign with sinon 7.2.3 and jsdom 13.2.0. No idea why this works for some people and not others?

This is what worked for me:

    global.window = Object.create(window);
    const url = 'http://localhost';
    Object.defineProperty(window, 'location', {
      value: {
        href: url,
      },
      writable: true,
    });

We used pushState to make this to work

 window.history.pushState(
        {},
        '',
        'http://localhost/something/123?order=asc'
      );

This is a difficult situation. JSDOM does not fully supports navigation (other than breadcrumbs) and JSDOM does not allow us to mock-out navigation. The end result is that I can't write tests which ultimately attempt to trigger navigation.

If JSDOM did learn to navigate (I'm not even sure what that means), maybe I could assert about being on the appropriate page. For my testing use cases, though, asserting the navigation was triggered, rather than actually performed, is much cleaner/faster. It's what I've historically done when testing using jsdom and now it's broken.

Allow me to post the answer to my own question
I simply replace the usages of window.location = url; and window.location.href = url; with

window.location.assign(url);

and then in my tests I did:

sinon.stub(window.location, 'assign');
expect(window.location.assign).to.have.been.calledWith(url);

Works like a charm - hope it can be of help to someone else

That if you have control to the target url, but what if you are loading google or Instagram website. or any website? how can you solve this problem ?

Based on answer from @chrisbateman I managed to get this working in a Jest environment. For anyone who is using Jest, here is my workaround:

describe('', () => {
    const originalLocation = window.location;

    beforeEach(() => {
        delete window.location;

        window.location = {
            href: '',
        };
    });

    afterEach(() => {
        window.location = originalLocation;
    });

    it('', () => {
        // test here
    });
});

I solved this issue using this config. I wanted to test redirection for hash urls:

    beforeEach(() => {
      delete global.window;
      global.window = {
        location: { replace: jest.fn(url => ({ href: url })) },
      };
    });
    it('should redirect hash url', () => {
      window.location.hash = '#/contrat?id=8171675304';
      global.window.location.href =
        'http://localhost:3000/#/contrat?id=8171675304';
      redirectHashUrl();

      expect(window.location.replace).toHaveBeenCalled();
    });

@chrisbateman @hamzahamidi thanks, that solutions worked well.

Maybe it's not a good practice, but we have some tests, those rely on location/host/hostname and other location properties. So mocking location as we want and restoring afterwards worked for me.

const realLocation = window.location;

describe('bla bla', () => {
  afterEach(() => {
    window.location = realLocation;
  });

  it('test where I want to use hostname', () => {
    delete window.location;
    window.location = { 
      hostname: 'my-url-i-expect.com'
    };
    // check my function that uses hostname
  });
});

This is what worked for me:

    global.window = Object.create(window);
    const url = 'http://localhost';
    Object.defineProperty(window, 'location', {
      value: {
        href: url,
      },
      writable: true,
    });

it doesnot work on a CI like circleci

@hontas's solution helped:

I did use window.location.assign(Config.BASE_URL); in my code.

And here's the test:

jest.spyOn(window.location, 'assign').mockImplementation( l => {
   expect(l).toEqual(Config.BASE_URL);
})

window.location.assign.mockClear();

Thanks mate, you've saved me a day! :)

In my case I'm testing for the query string, and since the subject of my spec IS the query string itself, I don't want to bypass it, but I'm happy to stub it out. in my case this worked well:

    let name = "utm_content"
    window.history.pushState({}, 'Test Title', '/test.html?utm_content=abc');
    expect(ParseUrlUtils.getParam(name)).toBe("abc")

Note that here it doesn't matter what the title of the window is and it also doesn't matter that it is /test.html (which is not real), all that matters is that the query string is fetched correctly (this passes)

The issue with examples is that methods and getters are not use. What I do is basically replace Location object by URL object. URL has all the properties of Location (search, host, hash, etc).

const realLocation = window.location;

describe('My test', () => {

    afterEach(() => {
        window.location = realLocation;
    });

    test('My test func', () => {

        // @ts-ignore
        delete window.location;

        // @ts-ignore
        window.location = new URL('http://google.com');

        // ...
    });
});

I'm getting this error too

Error: Not implemented: navigation (except hash changes)
    at module.exports (...\node_modules\jsdom\lib\jsdom\browser\not-implemented.js:9:17)
    at navigateFetch (...\node_modules\jsdom\lib\jsdom\living\window\navigation.js:74:3)

@hontas's solution helped:

I did use window.location.assign(Config.BASE_URL); in my code.

And here's the test:

jest.spyOn(window.location, 'assign').mockImplementation( l => {
   expect(l).toEqual(Config.BASE_URL);
})

window.location.assign.mockClear();

This worked for me, thanks! However I had to use the done argument from jest test(), otherwise I wouldn't be sure that the expect was evaluated and the test might had ended successfully anyway:

it('should', (done) => {
jest.spyOn(window.location, 'assign').mockImplementation( l => {
   expect(l).toEqual(Config.BASE_URL);
   done();
})

window.location.assign.mockClear();
}

@hontas Does not work for me :( Unable to rewrite assign/replace property

Is there any way to find out which test is triggering the "Not implemented: navigation" message? I have a suite of 43 tests - the error is being displayed only once and it keeps bouncing around. I can't tell which test to fix!!! The stack trace gives me no indication of the culprit:

console.error
  Error: Not implemented: navigation (except hash changes)
      at module.exports (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17)
      at navigateFetch (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/window/navigation.js:76:3)
      at exports.navigate (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/window/navigation.js:54:3)
      at Timeout._onTimeout (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/nodes/HTMLHyperlinkElementUtils-impl.js:81:7)
      at listOnTimeout (internal/timers.js:531:17)
      at processTimers (internal/timers.js:475:7) undefined

    at VirtualConsole.<anonymous> (node_modules/jsdom/lib/jsdom/virtual-console.js:29:45)
    at module.exports (node_modules/jsdom/lib/jsdom/browser/not-implemented.js:12:26)
    at navigateFetch (node_modules/jsdom/lib/jsdom/living/window/navigation.js:76:3)
    at exports.navigate (node_modules/jsdom/lib/jsdom/living/window/navigation.js:54:3)
    at Timeout._onTimeout (node_modules/jsdom/lib/jsdom/living/nodes/HTMLHyperlinkElementUtils-impl.js:81:7)

@hontas Does not work for me :( Unable to rewrite assign/replace property

As I understand jest changed something in new version ("jest": "^26.0.1") so this works right now:

// Mock
  Object.defineProperty(window, 'location', {
    value: {
      pathname: '/terminals',
      assign: jest.fn(),
    },
  });

// Then test
expect(window.location.assign).toBeCalledWith('/auth');

Is there any way to find out which test is triggering the "Not implemented: navigation" message? I have a suite of 43 tests - the error is being displayed only once and it keeps bouncing around. I can't tell which test to fix!!! The stack trace gives me no indication of the culprit:

console.error
  Error: Not implemented: navigation (except hash changes)
      at module.exports (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17)
      at navigateFetch (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/window/navigation.js:76:3)
      at exports.navigate (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/window/navigation.js:54:3)
      at Timeout._onTimeout (/Users/naresh/projects/mobx-state-router/node_modules/jsdom/lib/jsdom/living/nodes/HTMLHyperlinkElementUtils-impl.js:81:7)
      at listOnTimeout (internal/timers.js:531:17)
      at processTimers (internal/timers.js:475:7) undefined

    at VirtualConsole.<anonymous> (node_modules/jsdom/lib/jsdom/virtual-console.js:29:45)
    at module.exports (node_modules/jsdom/lib/jsdom/browser/not-implemented.js:12:26)
    at navigateFetch (node_modules/jsdom/lib/jsdom/living/window/navigation.js:76:3)
    at exports.navigate (node_modules/jsdom/lib/jsdom/living/window/navigation.js:54:3)
    at Timeout._onTimeout (node_modules/jsdom/lib/jsdom/living/nodes/HTMLHyperlinkElementUtils-impl.js:81:7)

It's tough to tell from which test not implemented error comes.

I add a breakpoint at node_modules/jsdom/lib/jsdom/browser/not-implemented.js:12, run debug session and wait till breakpoint is reached. Only then I see which test I should improve to get rid of this message.
image

Sometimes it takes 2-3 minutes till runner reaches test with a problem.

P.S: In my current project there are 176 jsdom related tests

I am currently using Jest 26.0.1 and the below works for me:

  1. Use window.location.assign(url) to change the window location.

  2. Mock the location object as follows (note that not deleting and rebuilding the object first still results in the not implemented error in the noted version of Jest and earlier I think, also Object.defineProperty doesn't work and still results in the error):

delete window.location;
window.location = {
    href: '',
    hostname: '',
    pathname: '',
    protocol: '',
    assign: jest.fn()
};
  1. Assert with:
expect(window.location.assign).toBeCalledWith(url);

It would be really nice if jest allowed us to easily mock location without all of these constant issues and changes. I was previously using just window.location.assign = jest.fn() without any problems for a long time, then I upgraded from v24 and now this.

Is there a good reason that the Window APIs were locked/frozen?
It seems like that's the barrier for people trying to work-around navigation not being implemented.

If not, can we just not freeze them? I don't think even browsers are that strict at preventing you from altering window objects.

If you have an example of jsdom behaving differently from browsers, please file an issue following the issue template (including the jsbin or similar showing browser behavior).

Is anyone else running into the same Error: Not implemented: navigation (except hash changes) during Jest tests that fire a click event on an anchor element with an href? For my tests, I'm spying on a function that gets called onClick and making assertions about that so I need to actually fire the click event on the anchor element.

The solutions above related to mocking window.location work for me where I am explicitly calling window.location.replace or window.location.assign, but don't help for this case where the navigation originates from an anchor element being clicked.

Any ideas on solutions? Thanks!

How would you test that code in a browser? Keep in mind that in the browser clicking on the link would blow away your whole page and throw away any test results. So whatever you would do to prevent that, will also prevent the much-less-dramatic warning message jsdom outputs to the console.

In my situation project upgraded Jest from 23 to 26 version.
I had a problem with location.search. Had the same error Error: Not implemented: navigation.
Module I tested gets values of search query params.
Next implementation worked for me:

beforeAll(() => {
  delete window.location;
  window.location = new URL('your URL');
})

afterAll(() => {
  window.location.search = '';
})

@mattcphillips have you figured out how to fix this? i'm also having problem when clicking

@Sabrinovsky No, I ended up just adding a script to the setupFiles in my jest config that would swallow the console errors coming from jsdom navigation so that they weren't cluttering up our tests. More of a bandaid than anything else, but it sounded like these errors are to be expected in our test setup.

Here's the script that I run before my tests:

// There should be a single listener which simply prints to the
// console. We will wrap that listener in our own listener.
const listeners = window._virtualConsole.listeners('jsdomError');
const originalListener = listeners && listeners[0];

window._virtualConsole.removeAllListeners('jsdomError');

// Add a new listener to swallow JSDOM errors that orginate from clicks on anchor tags.
window._virtualConsole.addListener('jsdomError', error => {
  if (
    error.type !== 'not implemented' &&
    error.message !== 'Not implemented: navigation (except hash changes)' &&
    originalListener
  ) {
    originalListener(error);
  }

  // swallow error
});

This worked for me.

delete global.window.location
global.window.location = { href: 'https://test.com' }

This worked for me

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

If you get TypeError: Cannot assign to read only property 'assign' of object '[object Location]', then using something like this in your jest.setup.ts:

```
global.window = Object.create(window);
Object.defineProperty(window, 'location', {
value: {
...window.location,
},
writable: true,
});
````

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tolmasky picture tolmasky  ·  4Comments

cg433n picture cg433n  ·  3Comments

jacekpl picture jacekpl  ·  4Comments

potapovDim picture potapovDim  ·  4Comments

JacksonGariety picture JacksonGariety  ·  4Comments