jsdom is a pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js. In general, the goal of the project is to emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications.
The latest versions of jsdom require newer Node.js versions; see the package.json "engines" field for details.
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
To use jsdom, you will primarily use the JSDOM constructor, which is a named export of the jsdom main module. Pass the constructor a string. You will get back a JSDOM object, which has a number of useful properties, notably window:
const dom = new JSDOM(`<!DOCTYPE html>
Hello world
`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"
(Note that jsdom will parse the HTML you pass it just like a browser does, including implied <html>, <head>, and <body> tags.)
The resulting object is an instance of the JSDOM class, which contains a number of useful properties and methods besides window. In general, it can be used to act on the jsdom from the "outside," doing things that are not possible with the normal DOM APIs. For simple cases, where you don't need any of this functionality, we recommend a coding pattern like
const { window } = new JSDOM(`...`);
// or even
const { document } = (new JSDOM(`...`)).window;
Full documentation on everything you can do with the JSDOM class is below, in the section "JSDOM Object API".
The JSDOM constructor accepts a second parameter which can be used to customize your jsdom in the following ways.
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000
});
url sets the value returned by window.location, document.URL, and document.documentURI, and affects things like resolution of relative URLs within the document and the same-origin restrictions and referrer used while fetching subresources. It defaults to "about:blank".referrer just affects the value read from document.referrer. It defaults to no referrer (which reflects as the empty string).contentType affects the value read from document.contentType, as well as how the document is parsed: as HTML or as XML. Values that are not a HTML MIME type or an XML MIME type will throw. It defaults to "text/html". If a charset parameter is present, it can affect binary data processing.includeNodeLocations preserves the location info produced by the HTML parser, allowing you to retrieve it with the nodeLocation() method (described below). It also ensures that line numbers reported in exception stack traces for code running inside <script> elements are correct. It defaults to false to give the best performance, and cannot be used with an XML content type since our XML parser does not support location info.storageQuota is the maximum size in code units for the separate storage areas used by localStorage and sessionStorage. Attempts to store data larger than this limit will cause a DOMException to be thrown. By default, it is set to 5,000,000 code units per origin, as inspired by the HTML specification.Note that both url and referrer are canonicalized before they're used, so e.g. if you pass in "https:example.com", jsdom will interpret that as if you had given "https://example.com/". If you pass an unparseable URL, the call will throw. (URLs are parsed and serialized according to the URL Standard.)
jsdom's most powerful ability is that it can execute scripts inside the jsdom. These scripts can modify the content of the page and access all the web platform APIs jsdom implements.
However, this is also highly dangerous when dealing with untrusted content. The jsdom sandbox is not foolproof, and code running inside the DOM's <script>s can, if it tries hard enough, get access to the Node.js environment, and thus to your machine. As such, the ability to execute scripts embedded in the HTML is disabled by default:
const dom = new JSDOM(`<body>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`);
// The script will not be executed, by default:
console.log(dom.window.document.getElementById("content").children.length); // 0
To enable executing scripts inside the page, you can use the runScripts: "dangerously" option:
const dom = new JSDOM(`<body>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`, { runScripts: "dangerously" });
// The script will be executed and modify the DOM:
console.log(dom.window.document.getElementById("content").children.length); // 1
Again we emphasize to only use this when feeding jsdom code you know is safe. If you use it on arbitrary user-supplied code, or code from the Internet, you are effectively running untrusted Node.js code, and your machine could be compromised.
If you want to execute external scripts, included via <script src="">, you'll also need to ensure that they load them. To do this, add the option resources: "usable" as described below. (You'll likely also want to set the url option, for the reasons discussed there.)
Event handler attributes, like `
, are also governed by this setting; they will not function unlessrunScriptsis set to"dangerously". (However, event handler _properties_, likediv.onclick = ..., will function regardless ofrunScripts`.) Note that this guarantee covers the web content being processed by jsdom. It does not cover scenarios where the host Node.js environment itself has been compromised (e.g. through prototype pollution). See the security policy for more details.
If you are simply trying to execute script "from the outside", instead of letting <script> elements and event handlers attributes run "from the inside", you can use the runScripts: "outside-only" option, which enables fresh copies of all the JavaScript spec-provided globals to be installed on window. This includes things like window.Array, window.Promise, etc. It also, notably, includes window.eval, which allows running scripts, but with the jsdom window as the global:
const dom = new JSDOM(`<body>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`, { runScripts: "outside-only" });
// run a script outside of JSDOM:
dom.window.eval('document.getElementById("content").append(document.createElement("p"));');
console.log(dom.window.document.getElementById("content").children.length); // 1
console.log(dom.window.document.getElementsByTagName("hr").length); // 0
console.log(dom.window.document.getElementsByTagName("p").length); // 1
This is turned off by default for performance reasons, but is safe to enable.
Note that in the default configuration, without setting runScripts, the values of window.Array, window.eval, etc. will be the same as those provided by the outer Node.js environment. That is, window.eval === eval will hold, so window.eval will not run scripts in a useful way.
We strongly advise against trying to "execute scripts" by mashing together the jsdom and Node global environments (e.g. by doing global.window = dom.window), and then executing scripts or test code inside the Node global environment. Instead, you should treat jsdom like you would a browser, and run all scripts and tests that need access to a DOM inside the jsdom environment, using window.eval or runScripts: "dangerously". This might require, for example, creating a browserify bundle to execute as a <script> element—just like you would in a browser.
Finally, for advanced use cases you can use the dom.getInternalVMContext() method, documented below.
jsdom does not have the capability to render visual content, and will act like a headless browser by default. It provides hints to web pages through APIs such as document.hidden that their content is not visible.
When the pretendToBeVisual option is set to true, jsdom will pretend that it is rendering and displaying content. It does this by:
document.hidden to return false instead of truedocument.visibilityState to return "visible" instead of "prerender"window.requestAnimationFrame() and window.cancelAnimationFrame() methods, which otherwise do not existconst window = (new JSDOM(``, { pretendToBeVisual: true })).window;
window.requestAnimationFrame(timestamp => {
console.log(timestamp > 0);
});
Note that jsdom still does not do any layout or rendering, so this is really just about pretending to be visual, not about implementing the parts of the platform a real, visual web browser would implement.
By default, jsdom will not load any subresources such as scripts, stylesheets, images, or iframes. If you'd like jsdom to load such resources, you can pass the resources: "usable" option, which will load all usable resources. Those are:
<frame> and <iframe><link rel="stylesheet"><script>, but only if runScripts: "dangerously" is also set<img>, but only if the canvas npm package is also installed (see "Canvas Support" below)When attempting to load resources, recall that the default value for the url option is "about:blank", which means that any resources included via relative URLs will fail to load. (The result of trying to parse the URL /something against the URL about:blank is an error.) So, you'll likely want to set a non-default value for the url option in those cases, or use one of the convenience APIs that do so automatically.
To more fully customize jsdom's resource-loading behavior, including the initial load made by JSDOM.fromURL() or any loads made with dom.window.XMLHttpRequest or dom.window.WebSocket, you can pass an options object as the resources option value. Doing so will opt you in to the above-described resources: "usable" behavior as the baseline, on top of which your customizations can be layered.
The available options are:
userAgent affects the User-Agent header sent, and thus the resulting value for navigator.userAgent. It defaults to `Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${jsdomVersion}`.
dispatcher can be set to a custom undici Dispatcher for advanced use cases such as configuring a proxy or custom TLS settings. For example, to use a proxy, you can use undici's ProxyAgent.
interceptors can be set to an array of undici interceptor functions. Interceptors can be used to modify requests or responses without writing an entirely new Dispatcher.
For the simple case of inspecting an incoming request or returning a synthetic response, you can use jsdom's requestInterceptor() helper, which receives a Request object and context, and can return a Response:
const { JSDOM, requestInterceptor } = require("jsdom");
const dom = new JSDOM(`<script src="https://example.com/some-specific-script.js"></script>`, {
url: "https://example.com/",
runScripts: "dangerously",
resources: {
userAgent: "Mellblomenator/9000",
dispatcher: new ProxyAgent("http://127.0.0.1:9001"),
interceptors: [
requestInterceptor((request, context) => {
// Override the contents of this script to do something unusual.
if (request.url === "https://example.com/some-specific-script.js") {
return new Response("window.someGlobal = 5;", {
headers: { "Content-Type": "application/javascript" }
});
}
// Return undefined to let the request proceed normally
})
]
}
});
The context object passed to the interceptor includes element (the DOM element that initiated the request, or null for requests that are not from DOM elements). For example:
requestInterceptor((request, { element }) => {
if (element) {
console.log(`Element ${element.localName} is requesting ${request.url}`);
}
// Return undefined to let the request proceed normally
})
To be clear on the flow: when something in your jsdom fetches resources, first the request is set up by jsdom, then it is passed through any interceptors in the order provided, then it reaches any provided dispatcher (defaulting to undici's global dispatcher). If you use jsdom's requestInterceptor(), returning promise fulfilled with a Response will prevent any further interceptors from running, or the base dispatcher from being reached.
[!WARNING] All resource loading customization is ignored when scripts inside the jsdom use sync
$ claude mcp add jsdom \
-- python -m otcore.mcp_server <graph>