Assert window.location properties with Jest and jsdom
I have a React component that is part of a wider Ruby on Rails application. It fetches a record, and if the record does not exist, it redirects to the server-rendered 404 page.
Unfortunately, while the user will see the ‘not found’ page, the HTTP status code will be 200 OK by default, because this page is static. Best practise would be to define a location block in your web server (Nginx, Apache, IIS etc) to respond with a 404 status when this static file is requested.
Having made this component, I’d like to add a Jest assertion that when the record is absent, the component does not render but instead tells the window to change it’s location.
Unfortunately, a naieve assertion does not work the way I expected:
it("navigates to /404 when the record is absent", () => {
render(<MyComponent record={undefined} />);
expect(window.location.pathname).toBe("/404");
});
Jest tells us that we can’t use window.location
in our component, because jsdom can’t navigate:
Error: Not implemented: navigation (except hash changes)
That’s fair enough! Adding navigation would be a big increase in scope for jsdom, and I can’t blame anyone for not supporting this.
The trick to getting this to work is to redefine the location
property on the window
object in our test, so that we are using and asserting against an object that we control:
it("navigates to /404 when the record is absent", () => {
// jsdom doesn't allow us to 'navigate' from a component, because how would that work?
// Since we don't need to navigate, but just assert that navigation _would_ have occurred,
// we can replace window.location with a URL, which has a pathname property we can assert
// against
Object.defineProperty(window, "location", {
value: new URL("http://example.com"),
configurable: true,
});
render(<MyComponent record={undefined} />);
expect(window.location.pathname).toBe("/404");
});
You might notice that we’re using a
URL
object there. Why
is that? Well, it turns out that window.location
shares many properties with
URL
. Not all, and
that’s important to know - especially for things like events and functions like
reload()
, but if you’re trying to assert that a property of the location
changed, this is perfect. We replace the window.location
property with a new
URL object, change a property of that URL within our component, then assert that
the property changed to the value we expect it to.
You might also notice that we’re passing an option to defineProperty
-
configurable: true
. Adding this option means that other tests can change the
value of this property later on the test. It’s also important to note that we’re
replacing window.location
from when this test runs onwards. To me, this is
OK, since window.location
raises errors in jsdom and doesn’t work the way we
expect anyway, but if you want to avoid side effects, you can assign the
original window.location
to a variable, then assign the property back at the
end of your test - or put all this in a before
/after
hook in your test file
or suite.
References
- https://stackoverflow.com/questions/54021037/how-to-mock-window-location-href-with-jest-vuejs
- https://www.benmvp.com/blog/mocking-window-location-methods-jest-jsdom/
- https://remarkablemark.org/blog/2018/11/17/mock-window-location/