New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

window.location.href can't be changed in tests. #890

Closed
sterpe opened this Issue Apr 13, 2016 · 43 comments

Comments

Projects
None yet
@sterpe

sterpe commented Apr 13, 2016

Hi @cpojer,

This is actually more of a jsdom@8 issue...see jsdom/jsdom#1388, but I want to pin here as well so Jest picks up whatever solution jsdom comes up with.

Previously with jest-cli@0.8/jsdom@7.x you could write a test like this:

jest.autoMockOff()
jest.setMock('../lib/window', window)

jest.mock('cookies-js')
jest.mock('query-string')
jest.mock('superagent')

describe(['@utils/auth - When an AJAX response returns:'
].join('')
, function () {

  beforeEach(function () {
    window.location.href = 'http://quux.default.com'
    var queryString = require('query-string')
    queryString.__setMockParseReturns([{
      'access_token': '1234foo',
      'expires_in': '9999'
    }])
  })

  it(['should set a redirect token and goto platform ',
    'when the AJAX request returns 401.'
  ].join('')
  , function () {
    var superagent = require('superagent')
    superagent.__setMockAjaxResponses([
      [null, { 'status': 401 }]
    ])

    var href = window.location.href
    var auth = require('../index.js')
    auth.login(function (res) {})
    var Cookies = require('cookies-js')
    var config = require.requireActual('../config')
    expect(decodeURIComponent(window.location.href)).toBe([
      config.loginUrl,
      config.loginServiceLogin,
      '?client_id=',
      config.clientId,
      '&response_type=token',
      '&redirect_uri=',
      config.clientRedirectUri
    ].join(''))
    expect(Cookies.__getMockCookieData()[config.clientId + '_state_locationAfterLogin']).toBe(escape(href))
  })

And that test would pass. Since jsdom@8 this is no longer possible and these tests fail.

Seems like jsdom is looking at some type of capability, just wanted to make sure that Jest will pick up that capability when it is available.

@cpojer

This comment has been minimized.

Contributor

cpojer commented Apr 14, 2016

You are right, this is indeed a jsdom issue. At Facebook, what we have done to work around this, is use this:

Object.defineProperty(window.location, 'href', {
  writable: true,
  value: 'some url'
});

this works for us, however we are still on jsdom 7 internally.

I'll close this, as I believe the Object.defineProperty way of doing things is fine. If that doesn't work for you in jsdom 8, I'm happy to reopen it.

@cpojer cpojer closed this Apr 14, 2016

@sterpe

This comment has been minimized.

sterpe commented Apr 14, 2016

Cool, thanks I will try this out.

@sterpe

This comment has been minimized.

sterpe commented Sep 30, 2016

@cpojer, I can't seem to figure out what I need to click on to reopen this issue...

Is there anyway in jest environment for one to call jsdom.changeUrl(window, url) as described here https://github.com/tmpvar/jsdom#changing-the-url-of-an-existing-jsdom-window-instance in jest-cli@15.1.1?

@th3fallen

This comment has been minimized.

th3fallen commented Apr 20, 2017

old ticket but for those still having this issue we've started using window.location.assign() instead so in our tests we can mock the assign function like so..

it('will redirect with a bad route', () => {
    window.location.assign = jest.fn();
    const redirection = shallow(<Redirection />, {
      context: {
        router: {
          location: {
            pathname: '/wubbalubbadubdub',
          },
        },
      },
    });
    expect(window.location.assign).toBeCalledWith(`${CONFIG.APP_LEGACY_ROOT}`);
  });
@sterpe

This comment has been minimized.

sterpe commented Apr 20, 2017

Thanks @th3fallen . That's cool!

@sterpe

This comment has been minimized.

sterpe commented Apr 20, 2017

Btw @cpojer I start at FB on 1May.... ;P

@cpojer

This comment has been minimized.

Contributor

cpojer commented Apr 21, 2017

Nice!

@okovpashko

This comment has been minimized.

okovpashko commented May 2, 2017

I'm trying to migrate our tests from Mocha+Chai+Sinon.js to Jest and can't figure out how to change location for a particular test.
Jest 19.x uses JSDom 9.12 that does not allow to change location using Object.defineProperty trick. Also, I can't use jsdom.changeURL() because of the reasons described in jsdom/jsdom#1700.
@cpojer what about implementing some proxy method to jsdom.changeURL() in Jest?

@thymikee

This comment has been minimized.

Collaborator

thymikee commented May 2, 2017

@okovpashko we're planning to expose jsdom to the environment: #2460

@cpojer

This comment has been minimized.

Contributor

cpojer commented May 2, 2017

Object.defineProperty works for us at FB.

@okovpashko

This comment has been minimized.

okovpashko commented May 2, 2017

@thymikee I saw that issue but thought that the proposition was rejected.
@cpojer I misread your example and mixed it up with other related to this problem, where people suggested to use Object.defineProperty(window, 'location', {value: 'url'});. Thank you!

I need to change not only the href, so I wrote simple method, that may be useful for someone who will be read this thread:

const setURL = (url) => {
  const parser = document.createElement('a');
  parser.href = url;
  ['href', 'protocol', 'host', 'hostname', 'origin', 'port', 'pathname', 'search', 'hash'].forEach(prop => {
    Object.defineProperty(window.location, prop, {
      value: parser[prop],
      writable: true,
    });
  });
};
@matt-dalton

This comment has been minimized.

matt-dalton commented May 13, 2017

Apologies for dragging out this thread further, but I have tried mocking out push function as suggested...

reactRouterReduxMock.push = (url) => {
   Object.defineProperty(window.location, 'href', {
	writable: true,
	value: url
       })
})

but I'm still getting a jsdom error that I can't seem to get round:

       TypeError: Cannot read property '_location' of null
       at Window.location (/Users/user/projects/app/client/node_modules/jsdom/lib/jsdom/browser/Window.js:148:79)
       at value (/Users/user/projects/app/client/test/integration-tests/initialSetup.js:122:32) //this is the defineProperty line above

I realise this is a jsdom error, but for those who have solved this is there any more setup context you could share that might let me get round this?

Thanks

@th3fallen

This comment has been minimized.

th3fallen commented May 13, 2017

@matt-dalton try my suggestion in #890 (comment) works well for me

@ianlyons

This comment has been minimized.

Contributor

ianlyons commented May 13, 2017

@matt-dalton what's your URL? do you have testURL set in your jest-config.json or does it initialize as about:blank?

@matt-dalton

This comment has been minimized.

matt-dalton commented May 16, 2017

@ianlyons Yeah I set value of "https://test.com/" for this in the package.json, and none of the paths are showing up as blank.

@th3fallen If I understand you correctly, I don't think this works for my use case. Are you passing the url as a context value that causes assign to be triggered? I am trying to put together a rudimentary integration test, so I want to check how the router responds to the initial data load. I have mocked the API response, and then need the URL change to take place using the app logic (i.e. I do not want to trigger it externally myself).

@msholty-fd

This comment has been minimized.

msholty-fd commented Nov 10, 2017

Object.defineProperty seems to do the trick for testing functionality that relies on window.location.search, for instance. That being said it mutates window.location.search so other tests may be impacted. Is there a way to "undo" the changes you've made on window.location.search via Object.defineProperty, kinda like jest mock functions have the mockReset function?

@dmgawel

This comment has been minimized.

dmgawel commented Nov 28, 2017

@msholty-fd you could try this approach:

const origLocation = document.location.href;
let location = origLocation;

beforeAll(() => {
  const parser = document.createElement('a');
  ['href', 'protocol', 'host', 'hostname', 'origin', 'port', 'pathname', 'search', 'hash'].forEach(prop => {
    Object.defineProperty(window.location, prop, {
      get: function() {
        parser.href = location;
        return parser[prop];
      }
    });
  });
});

afterEach(() => {
  location = origLocation;
});

test('location 1', () => {
  location = "https://www.google.com/";
  console.log(document.location.href); // https://www.google.com/
});

test('location 2', () => {
  console.log(document.location.href); // about:blank
});
@modestfake

This comment has been minimized.

modestfake commented Dec 18, 2017

It stopped working in Jest 22.0.1

Object.defineProperty(window.location, 'href', {
  writable: true,
  value: 'some url'
});

Error message:

TypeError: Cannot redefine property: href
        at Function.defineProperty (<anonymous>)
@SimenB

This comment has been minimized.

Collaborator

SimenB commented Dec 18, 2017

@simon360

This comment has been minimized.

simon360 commented Dec 18, 2017

Opened a new issue related to this, since this one was closed: #5124

@probablyup

This comment has been minimized.

Contributor

probablyup commented Dec 19, 2017

@SimenB I'm not convinced that Jest should fix this. JSDOM should allow window.location.assign() to work as intended and reconfigure the output of window.location.href etc.

@bochen2014

This comment has been minimized.

Contributor

bochen2014 commented Jan 15, 2018

I got TypeError: Could not parse "/upgrades/userlogin?hardwareSku=sku1351000490stgvha" as a URL because jsdom has base url default to about:blank

I tried to assign a base url to jsdom, spent 4 hours on it without sucess (I know how to do it, just insert <base href='your_base_url' /> to the dom; but, the dom is created by jest , not by me, so i gave up.

the Object.defineProperty solution only works with old ver of jsdom (you get an 'cannot redefine property error with later version of jsdom);
if you are using jsdom ver > 10, as @th3fallen mentioned is the right solution.
use window.location.assign is the right way to go

@SimenB

This comment has been minimized.

Collaborator

SimenB commented Jan 15, 2018

If you just want some other url than about:blank, you can use testURL config.

@bochen2014

This comment has been minimized.

Contributor

bochen2014 commented Jan 15, 2018

thanks @SimenB for your reply.

No I was talking about base url not url. I have code that will do window.location.href="/login" and when running jest, jsdom throw exception complaining /login is not a valid url

TypeError: Could not parse "/login" as a URL

I checked the source code of jsdom and realised this is because I don't have a base url setup ( this is equivalent of typing "/login" in browser URL bar without a base address).

with jsdom, normally we can set up base url via

global.jsdom = new JSDOM('<html><head> <base href="base_url" /></head></html>')

but because jest set up jsdom , it is beyond our control.
--- update: I suppose I can explicitly add jsdom as dependency and configure jsdom manually. but I'm not sure if it's the recommended way to do it

I then found a solution which is to substitute window.location.href= with window.location.assign and mock assign function and it worked for me

@simon360

This comment has been minimized.

simon360 commented Jan 15, 2018

@bochen2014 this issue has more information on how to use the newer version of jsdom: #5124

tl;dr: you can mock window.location.assign(), or you can use the jest-environment-jsdom-global, which will allow you to reconfigure jsdom in flight.

@bochen2014

This comment has been minimized.

Contributor

bochen2014 commented Jan 15, 2018

thanks @simon360

that's what I did ;-)
I used jsdom.reconfigure to setup different initial urls in my tests, and whenever I need to change url in code (not test), I use window.location.assign and mocked it. which worked for me.

just for people who may/will run into the same issue, to set the url for your jsdom

// jest.config.js
 module.exorts={ 
  testURL: 'http://localhost:3000',
  // or : 
  testEnvironmentOptions: {
     url: "http://localhost:3000/",
    referrer: "https://example.com/",
  }
}

note that this will set url for all your tests;
if you want a different url in some particular tests, use jsdom.reconfigure api;
if you need to change url on the fly outside of unit test code (i.e. production code), you need to use window.location.assign and mock it.

@petar-prog91

This comment has been minimized.

petar-prog91 commented Feb 16, 2018

Posted it on other ticket, but I'll post it here:

Found nice solution for Jest 21.2.1

Ok, so far the easiest solution around this is:
Go into your Jest settings (for example I'll use package.json):

"jest": { "testURL": "http://localhost" }

Now you will have access to window object and then you can set URL to whatever you like during tests.

it('Should set href url to testURL', () => {
    // Here I set href to my needs, opinionated stuff bellow
    const newUrl = 'http://localhost/editor.html/content/raiweb/it/news/2018/02/altered-carbon-best-cyberpunk-tv-series-ever.html';
    Object.defineProperty(window.location, 'href', {
        writable: true,
        value: newUrl
    });

    console.log(window.location.href);
});

it('Should set pathname url to testURL', () => {
    // Here I set href to my needs, opinionated stuff bellow
    const newUrl = '/editor.html/content/raiweb/it/news/2018/02/altered-carbon-best-cyberpunk-tv-series-ever.html';
    Object.defineProperty(window.location, 'pathname', {
        writable: true,
        value: newUrl
    });

    console.log(window.location.pathname);
});

Hopefully this helps someone.

@BarthesSimpson

This comment has been minimized.

BarthesSimpson commented Feb 17, 2018

@petar-prog91 that was helpful. You have a typo though - it should be testURL not TestURL

@petar-prog91

This comment has been minimized.

petar-prog91 commented Feb 17, 2018

@BarthesSimpson thanks for notice, updated comment.

@DanielMSchmidt DanielMSchmidt referenced this issue Feb 27, 2018

Merged

DCOS_OSS-2185: Upgrade jest #2783

11 of 14 tasks complete
@UserNT

This comment has been minimized.

UserNT commented Mar 2, 2018

Stop posting this, it does not work on jest": "^22.4.2"

@annemarie35

This comment has been minimized.

annemarie35 commented Mar 12, 2018

Hi,
I have used this in the test, i delete the global state and create a new one with jsdom... :

   describe('componentDidMount', () => {
    delete global.window
    const window = (new JSDOM(``, {url: 'https://example.org/'})).window
    global.window = window
    describe('When window is defined', () => {
      const spy = jest.spyOn(Utils, 'extractTokenFromUrl')
      it('should call extract token function with window location', () => {
        mount(<Header />)
        expect(spy).toHaveBeenCalledWith('https://example.org/')
      })
    })
  })
@alexbaumgertner

This comment has been minimized.

alexbaumgertner commented Apr 1, 2018

@UserNT confirm — it gives TypeError: Cannot redefine property: href

@annemarie35 not works — ReferenceError: JSDOM is not defined

@hung-phan

This comment has been minimized.

hung-phan commented Apr 3, 2018

I don't know if this would help someone, but this is what I am currently doing.

const redirectTo = (url: string): void => {
  if (process.env.NODE_ENV === "test") {
    global.jsdom.reconfigure({ url: `${getBaseUrl()}${url}` });
  } else {
    window.location.replace(url);
  }
};

Write a redirect function and use that instead. So in testing env, it will rely on jsdom.reconfigure url to change url part.

I use it like this

export const clientFetchData = (
  history: Object,
  routes: Object,
  store: Object
) => {
  const callback = location =>
    match({ routes, location }, (error, redirectLocation, renderProps) => {
      if (error) {
        redirectTo("/500.html");
      } else if (redirectLocation) {
        redirectTo(redirectLocation.pathname + redirectLocation.search);
      } else if (renderProps) {
        if (!isEmpty(window.prerenderData)) {
          // Delete initial data so that subsequent data fetches can occur
          window.prerenderData = undefined;
        } else {
          // Fetch mandatory data dependencies for 2nd route change onwards
          trigger(
            FETCH_DATA_HOOK,
            renderProps.components,
            getDefaultParams(store, renderProps)
          );
        }

        trigger(
          UPDATE_HEADER_HOOK,
          renderProps.components,
          getDefaultParams(store, renderProps)
        );
      } else {
        redirectTo("/404.html");
      }
    });

  history.listen(callback);
  callback(history.getCurrentLocation());
};

After that, in your test, it can be sth like this

    describe("# match route", () => {
      it("should navigate to error page", () => {
        fetchData.clientFetchData(history, components, store);
        reactRouter.match.mock.calls[0][1](true);
        expect(window.location.href).toEqual(`${SERVER_URL}/500.html`);
      });

      it("should redirect to /hello-world.html page", () => {
        fetchData.clientFetchData(history, components, store);
        reactRouter.match.mock.calls[0][1](undefined, {
          pathname: "/hello-world.html",
          search: ""
        });
        expect(window.location.href).toEqual(`${SERVER_URL}/hello-world.html`);
      });
...
@Volcanic-Penguin

This comment has been minimized.

Volcanic-Penguin commented Apr 18, 2018

I ended up doing this which worked:

global.window = new jsdom.JSDOM('', {
  url: 'http://www.test.com/test?foo=1&bar=2&fizz=3'
}).window;
@ljones87

This comment has been minimized.

ljones87 commented Apr 27, 2018

I have this at the top of my JSDOM setup file:

const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
  url: "http://test.com"
});
const { window } = jsdom;

function copyProps(src, target) {
  const props = Object.getOwnPropertyNames(src)
    .filter(prop => typeof target[prop] === 'undefined')
    .map(prop => Object.getOwnPropertyDescriptor(src, prop));
  Object.defineProperties(target, props);
}

global.document = window.document;
global.window = window;
global.navigator = {
  userAgent: 'node.js',
};

global.HTMLElement = window.HTMLElement;
@ulshv

This comment has been minimized.

ulshv commented Jun 29, 2018

Fixed it by setting "testURL": "http://localhost/" in Jest config (I'm using latest version). By default it's "about:blank" and it was causing JSDOM error (you cannot change "about:blank" url to something else).

Resources:
http://jestjs.io/docs/en/configuration#testurl-string
jsdom/jsdom#1372

@Mike-Tran

This comment has been minimized.

Mike-Tran commented Aug 22, 2018

I found this post to be very helpful: https://www.ryandoll.com/post/2018/3/29/jest-and-url-mocking

"In your Jest configuration, make sure to set the following:

"testURL": "https://www.somthing.com/test.html"

Then in your beforeEach() section for your test, change the path as needed by using
history.pushState().

window.history.pushState({}, 'Test Title', '/test.html?query=true');

Voila! Now you change out your path for any test, without having to override any jsdom configurations as others suggest in the thread mentioned above. Not sure on which thread I found this solution on, but kuddos to the dev that posted it!"

@fugroup

This comment has been minimized.

fugroup commented Aug 24, 2018

@Mike-Tran You rock! That totally worked, so simple. I didn't even have to use the testURL setting.

@jcmcneal

This comment has been minimized.

jcmcneal commented Aug 29, 2018

@Mike-Tran That works! Thanks you! However, I didn't need the testURL or beforeEach. I just did:

window.history.pushState({}, 'Test Title', '/test.html?query=true');

And now I don't have to use Object.defineProperty anymore 😅

@khadirbaaoua

This comment has been minimized.

khadirbaaoua commented Oct 9, 2018

@jcmcneal thanks that did it for me! (jest 23.0.0)

@laxa88

This comment has been minimized.

laxa88 commented Oct 9, 2018

If your goal is to mock the window object, here is my (not so elegant, but it works) solution:

Create an interface (not sure if interface is the right word, but I hope you get the point) class:

// window.js
export const { location } = window;

In your actual code, swap out window with the interface method's, e.g. win

// myFile.js
import * as win from './window';

export function doSomethingThatRedirectsPage() {
  win.location.href = 'google.com';
}

Then, in your jest tests, you just mock them out so jsdom doesn't complain. You can even assert them:

// myFile.test.js
import * as myFile from './myFile';
import * as win from './window';

it('should redirect', () => {
  win.location = { href: 'original-url' };

  expect(win.location.href).toBe('original-url');

  myFile.doSomethingThatRedirectsPage();

  expect(win.location.href).toBe('google.com');
});
@Fi1osof

This comment has been minimized.

Fi1osof commented Oct 11, 2018

@Mike-Tran , @jcmcneal thank! All works as aspected!

@naveenthallapani

This comment has been minimized.

naveenthallapani commented Oct 14, 2018

class SSOtestComponent extends React.Component {

componentDidMount() {
	let isSuccess = this.props.location.pathname === '/sso/test/success' ? true : false

	window.opener.postMessage({ type: "sso_test", isSuccess,...this.props.location.query}, window.location.origin)
}

onSsoAuthenticate() {
	
}

componentWillUnmount() {
}

render() {
	return (<Loader />);
}

}

module.exports = SSOtestComponent;

i am write the unit test case using enjyme and jest how will write the condition window.location ...pls give the answer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment