Jest 30 uses JSDOM 26, which breaks my tests. I want to test if location.search was modified.
Since JSDOM 21, it is not possible to mock location.search as easily as it used to be:
// does not work anymore
delete window.location;
Object.defineProperty(window, 'location', { // TypeError: Cannot redefine property: location
writable: true,
value: { search: '' },
});
Then I learned about @jest/environment-jsdom-abstract. It is designed to inject custom behavior into JSDOM.
So I created a wrapper and tried it without jest.
const { JSDOM } = require("jsdom");
/** @implements {Partial<Location>} */
class DummyLocation {
hash = "";
host = "";
hostname = "";
href = "";
toString() { return this.href; }
origin = "";
pathname = "";
port = "";
protocol = "";
search = "";
/** @param {string | URL} url */
assign(url) {
this.href = String(url);
}
reload() {}
/** @param {string | URL} url */
replace(url) {
this.href = String(url);
}
}
class JSDOMWithDummyLocation extends JSDOM {
constructor(...args) {
super(...args);
}
#mockLocation = new DummyLocation();
#documentProxy = new Proxy(super.window.document, {
get: (target, prop, receiver) => {
if(prop !== "location") return Reflect.get(target, prop, receiver);
return this.#mockLocation;
}
});
#windowProxy = new Proxy(super.window, {
get: (target, prop, receiver) => {
switch (prop) {
case "document": return this.#documentProxy;
case "location": return this.#mockLocation;
default: return Reflect.get(target, prop, receiver);
}
}
});
get window() {
return this.#windowProxy;
}
}
const dom = new JSDOMWithDummyLocation(``, {});
console.assert(
dom.window.location.search === "",
"document.location.search should be\n",
"",
"\nbut is\n",
dom.window.location.search
);
const searchValue = "foo=x";
dom.window.location.search = searchValue;
console.assert(
dom.window.location.search === searchValue,
"document.location.search should be\n",
searchValue,
"\nbut is\n",
dom.window.location.search
);
It works as expected.
Now I wrap it in a file to use it for jest-environment
// jest-environment-jsdom-with-mock-location.js
import BaseEnv from "@jest/environment-jsdom-abstract";
import JSDOM from "jsdom";
/** @implements {Partial<Location>} */
class DummyLocation {
//...
}
class JSDOMWithDummyLocation extends JSDOM {
//...
}
export default class JestJSDOMEnvironment extends BaseEnv {
/**
* @param {*} config
* @param {*} context
*/
constructor(config, context) {
super(config, context, {...JSDOM, JSDOM: JSDOMWithDummyLocation});
}
}
And here is the test file:
/**
* @jest-environment ./jest-environment-jsdom-with-mock-location.js
*/
describe("location", () => {
test("window.document.location", () => {
expect(window.document.location.search).toBe("");
window.document.location.search = "foo=bar";
expect(window.document.location.search).toBe("foo=bar"); // fails
});
});
It fails.
It also logs: Error: Not implemented: navigation to another Document.
This means that it doesn't use my implementation, but the original JSDOM implementation.
What concept of @jest/environment-jsdom-abstract did I not understand?
How do I have to change my jest-environment-jsdom-with-mock-location.js to properly redirect to my implementation of location?
@jest/environment-jsdom-abstractis correct and expected. Besides, I was thinking instead of setting value directly forwindow.location, how about we use browser API? Something likewindow.history.pushState({}, '', '/?foo=bar');expect(window.document.location.search).toBe('?foo=bar');I don't know how to fix that Proxy problem. It seems to be difficult to mockwindow.locationthese days@jest/environment-jsdom-abstractbut I couldn't find any place where "my" window is replaced with the original.this.globalreturns your Window proxy object andthis.global.locationreturnsDummyLocation. I only see the errorconsole.error Error: Not implemented: navigation (except hash changes)