mrkeyoor.com_
Thu 06 Aug 01:00 UTC
npmWeb Frontendupdated 05 Aug 2026

jsdom

jsdom is a pure-JavaScript implementation of the WHATWG DOM and HTML standards that runs inside Node.js. It parses HTML the way a browser does, builds a real document tree with querySelector, events, localStorage, cookies, and most of the DOM API, and can even execute scripts inside the page if you let it. It is the engine behind the jsdom test environment in Jest and Vitest, which is where most people meet it without knowing. It deliberately does not render anything: there is no layout, no navigation, and getBoundingClientRect returns zeros. Think of it as a headless document, not a headless browser.

Verdict

The default DOM for Node test environments and the most spec-faithful way to handle HTML outside a browser, at a real cost in weight and speed. Use it when correctness matters, drop to cheerio for plain scraping, and go to Playwright when layout or navigation enters the picture.

API stability4/5The JSDOM constructor API has been stable since v10 in 2017, but majors arrive fast (v30 in under two years from v23) and each one tightens Node engine ranges; v30 also reworked resource loading around undici.
Docs4/5The README is a single long, honest document covering every option including the caveats and unimplemented areas. There is no separate docs site or API reference though, so discovering what part of the DOM is implemented takes trial and error.
Maintenance4/5Active volunteer team under the jsdom org with a push in August 2026 and steady releases, but no corporate backing and 405 open issues and PRs against a spec surface that never stops growing.
Ecosystem5/5About 91M weekly downloads and it is the default DOM environment for Jest (jest-environment-jsdom) and a standard option in Vitest, so nearly every frontend test suite in the ecosystem sits on top of it.

Use it if

  • You run component tests in Jest or Vitest and need document, window, and events without booting a real browser for every test
  • You scrape or transform server-rendered HTML and need spec-correct parsing plus the full DOM API, not just selectors
  • You need to run third-party scripts against a fake page in Node, for example to test an analytics snippet or an embed widget
  • You build tooling that must serialize a document back to HTML exactly the way a browser would, doctype and implied tags included
Skip it if

Setup reality

npm install jsdom pulls in roughly two dozen dependencies (parse5, undici, tough-cookie, css-tree and friends), so it is a heavy install for what looks like one package. The engines field is strict: v30 wants Node 22.22+, 24.15+, or 26+, and CI images on older Node fail at install or runtime. canvas is an optional peer dependency; without it every <canvas> behaves like a div, and installing it means native builds with Cairo system packages. In test runners you rarely configure jsdom directly, but upgrading the runner can jump you across jsdom majors and change parsing or resource behavior. Also plan for cleanup: outstanding window timers keep the process alive until you call window.close().

Patterns

Parse HTML and query the documentparse-html

const { JSDOM } = require("jsdom");

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"

jsdom parses like a browser, so implied html, head, and body tags are created even if your string omits them.

Execute scripts embedded in the pagerun-page-scripts

const dom = new JSDOM(`<body>
  <div id="content"></div>
  <script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`, { runScripts: "dangerously" });

console.log(dom.window.document.getElementById("content").children.length); // 1

Only use "dangerously" with HTML you trust; in-page code can escape the sandbox and reach your Node process.

Run your own code against the pageeval-from-outside

const dom = new JSDOM(`<div id="content"></div>`, { runScripts: "outside-only" });

dom.window.eval('document.getElementById("content").append(document.createElement("p"));');
console.log(dom.window.document.querySelector("p") !== null); // true

"outside-only" installs fresh JS globals on window and enables window.eval without letting page scripts run; it is safe to enable.

Build a jsdom from a live URLload-from-url

const dom = await JSDOM.fromURL("https://example.com/", {
  referrer: "https://google.com/",
});
console.log(dom.serialize().slice(0, 60));

Redirects are followed and Set-Cookie headers land in the cookie jar; you cannot pass url or contentType options here.

Build a jsdom from an HTML fileload-from-file

const dom = await JSDOM.fromFile("stuff.html");
console.log(dom.window.document.title);

The url option defaults to the matching file:// URL, so relative links resolve against the file's directory.

Parse a fragment without a full windowparse-fragment

const frag = JSDOM.fragment(`<p>Hello</p><p><strong>Hi!</strong>`);

frag.childNodes.length === 2;
frag.querySelector("strong").textContent === "Hi!";

All fragments share one template document, so this is cheap, but resources never load and options cannot be passed.

Capture or silence page console outputsilence-console

const jsdom = require("jsdom");

const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.on("error", (msg) => myLogger.error(msg));
virtualConsole.forwardTo(console, { jsdomErrors: "none" });

const dom = new JSDOM(``, { virtualConsole });

Attach listeners before constructing the JSDOM; parse-time CSS errors fire immediately and are easy to miss.

Control location, referrer, and originset-page-url

const dom = new JSDOM(``, {
  url: "https://example.org/",
  referrer: "https://example.com/",
  contentType: "text/html",
});
console.log(dom.window.location.href); // "https://example.org/"

url defaults to about:blank, which makes every relative URL in the page unresolvable, so set it whenever resources or links matter.

Load external scripts and stylesheetsload-subresources

const dom = new JSDOM(html, {
  url: "https://example.com/",
  resources: "usable",
  runScripts: "dangerously",
});

resources: "usable" loads frames, stylesheets, and scripts (scripts only together with runScripts); images additionally need the canvas package installed.

Mock a network response for the pageintercept-requests

const { JSDOM, requestInterceptor } = require("jsdom");

const dom = new JSDOM(`<script src="https://example.com/app.js"></script>`, {
  url: "https://example.com/",
  runScripts: "dangerously",
  resources: {
    interceptors: [
      requestInterceptor((request) => {
        if (request.url === "https://example.com/app.js") {
          return new Response("window.someGlobal = 5;", {
            headers: { "Content-Type": "application/javascript" },
          });
        }
      }),
    ],
  },
});

This undici-interceptor style replaced the old ResourceLoader subclassing; returning undefined lets the request continue normally.

Enable requestAnimationFrame in testsfake-visibility

const { window } = new JSDOM(``, { pretendToBeVisual: true });

window.requestAnimationFrame((timestamp) => {
  console.log(timestamp > 0); // true
});

Without pretendToBeVisual, requestAnimationFrame does not exist and document.hidden is true, which trips libraries that pause when hidden.

Shut a jsdom down cleanlycleanup-window

const dom = new JSDOM(html, { runScripts: "dangerously" });

// ... use the dom ...

dom.window.close(); // kills timers and listeners

Pending setTimeout and setInterval calls inside the page keep the Node process alive and block garbage collection until close() runs.

Alternatives

PackageRegistryPick it when
happy-domnpmYou want a faster, lighter DOM for component tests and can live with less spec coverage
cheerionpmYou only parse and query HTML for scraping and never need window, events, or scripts
linkedomnpmYou need DOM-shaped parsing and serialization in SSR or workers with minimal memory overhead
playwrightnpmYou need real rendering, layout, navigation, or must trust the results against actual browsers