Advanced configurations, edge cases, and important limitations of jsdom.
jsdom has some notable limitations compared to real browsers:
Cannot change the global object by clicking links or setting location.href. Use reconfigure() to change URL or window.top, or create new JSDOM instances.
const { JSDOM } = require("jsdom");
const dom = new JSDOM();
console.log(dom.window.location.href); // "about:blank"
// Change the URL (doesn't navigate, just updates)
dom.reconfigure({ url: "https://example.com/" });
console.log(dom.window.location.href); // "https://example.com/"
// The DOM content remains unchanged
// No navigation occurs, just URL updateCannot calculate visual positioning. Methods like getBoundingClientRect() and properties like offsetTop return zeros. Use Object.defineProperty() to mock these if needed.
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body><div id="box"></div></body>`);
const { document } = dom.window;
const box = document.getElementById("box");
// These return zeros
console.log(box.getBoundingClientRect()); // { x: 0, y: 0, width: 0, height: 0 }
console.log(box.offsetTop); // 0
console.log(box.offsetLeft); // 0
// Mock if needed
Object.defineProperty(box, "offsetTop", { value: 100 });
console.log(box.offsetTop); // 100Not a visual browser. Use pretendToBeVisual: true option to make jsdom pretend it's rendering (affects document.hidden, enables requestAnimationFrame).
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body></body>`, {
pretendToBeVisual: true
});
console.log(dom.window.document.hidden); // false
console.log(dom.window.document.visibilityState); // "visible"
// Enables requestAnimationFrame
dom.window.requestAnimationFrame(() => {
console.log("Animation frame");
});jsdom supports the HTML5 Canvas API when the optional canvas npm package (v3.x) is installed as a peer dependency. Without it, <canvas> elements behave like <div> elements.
// With canvas package installed
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body><canvas id="myCanvas"></canvas></body>`);
const { document } = dom.window;
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, 100, 100); // Works if canvas package is installedWhen providing binary data (Buffer, ArrayBuffer, TypedArray) instead of strings, jsdom automatically sniffs the encoding following browser behavior:
<meta charset> tagscontentType option's charset parameterconst { JSDOM } = require("jsdom");
// With binary data
const buffer = Buffer.from('<!DOCTYPE html><p>Hello</p>');
const dom = new JSDOM(buffer); // Auto-detects encodingTerminate all timers and remove event listeners:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
// Clean up when done
dom.window.close(); // Terminates timers, removes event listenersThe beforeParse hook is called before HTML parsing begins, useful for adding shims or modifying the environment:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body></body>`, {
beforeParse(window) {
// Add custom APIs before parsing
window.myCustomAPI = () => console.log("Custom API called");
// Inject libraries
window.jQuery = require("jquery");
}
});
// Custom API is available
dom.window.myCustomAPI(); // "Custom API called"Enable location tracking for debugging and source mapping:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(
`<p>Hello
<img src="foo.jpg">
</p>`,
{ includeNodeLocations: true }
);
const pEl = dom.window.document.querySelector("p");
const location = dom.nodeLocation(pEl);
console.log(location);
// {
// startOffset: 0,
// endOffset: 39,
// startLine: 1,
// startCol: 1,
// endLine: 3,
// endCol: 4,
// startTag: {...},
// endTag: {...}
// }Note: Cannot be used with XML content type.
Configure maximum size for localStorage and sessionStorage:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
url: "https://example.org/",
storageQuota: 10000000 // 10 million code units (default: 5 million)
});
const { window } = dom;
window.localStorage.setItem("key", "value");Access the internal VM context for advanced script execution:
const { Script } = require("vm");
const { JSDOM } = require("jsdom");
const dom = new JSDOM(``, { runScripts: "outside-only" });
// Pre-compile a script
const script = new Script(`
if (!this.ran) {
this.ran = 0;
}
++this.ran;
`);
const vmContext = dom.getInternalVMContext();
// Run the pre-compiled script multiple times
script.runInContext(vmContext);
script.runInContext(vmContext);
script.runInContext(vmContext);
console.assert(dom.window.ran === 3);const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
// GOOD: Access window directly
const { window } = dom;
const div = window.document.createElement("div");
// AVOID: Don't merge with Node.js global
// global.window = dom.window; // Don't do this!
// GOOD: Run code inside jsdom context
dom.window.eval('console.log(document.title);');
// GOOD: Use window constructors for type checks
console.log(div instanceof window.HTMLDivElement); // true
console.log(div instanceof HTMLDivElement); // false (different global)DocumentFragments created with JSDOM.fragment() have limitations:
const { JSDOM } = require("jsdom");
const frag = JSDOM.fragment(`<p>Hello</p>`);
// Note: The fragment's ownerDocument has no browsing context
// - ownerDocument.defaultView is null
// - Resources won't load
// - No associated URLImportant notes about fromURL():
const { JSDOM } = require("jsdom");
JSDOM.fromURL("https://example.com/", {
referrer: "https://google.com/"
}).then(dom => {
// Follows redirects automatically
// Cannot specify `url` or `contentType` options (determined from response)
// `referrer` option becomes the HTTP Referer request header
// Cookies from Set-Cookie headers are stored in the cookie jar
// Cookies in the jar are sent as Cookie request headers
});fromFile() has special default behaviors:
const { JSDOM } = require("jsdom");
// `url` defaults to a file URL (e.g., `file:///path/to/test.html`)
// `contentType` defaults to `"application/xhtml+xml"` for `.xht`, `.xhtml`, or `.xml` extensions
// `contentType` defaults to `"text/html"` for all other extensions
// Binary encoding is sniffed from file contents (BOMs, meta charset tags)
await JSDOM.fromFile("./page.html"); // Parsed as text/html
await JSDOM.fromFile("./page.xhtml"); // Parsed as application/xhtml+xmlResource loading is asynchronous. Scripts and stylesheets may not be loaded immediately:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`
<html>
<head>
<script src="https://example.com/slow-loading.js"></script>
</head>
<body>
<div id="content"></div>
</body>
</html>
`, {
url: "https://example.org/",
resources: "usable",
runScripts: "dangerously"
});
// The page structure is available immediately
console.log(dom.window.document.getElementById("content")); // <div>
// But external scripts may still be loading
// There's no built-in way to know when all resources finish loadingRemember to close jsdom instances when done:
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
// Use the dom...
// Clean up
dom.window.close(); // Important for long-running processes