mrkeyoor.com_
Sun 20 Sept 02:41 UTC
npmTestingupdated 19 Sept 2026

jsdom review

jsdom 30.0.1 creates a browser-shaped window and document inside Node for HTML parsing, selectors, events, forms, cookies, storage, and controlled script execution. Its target is a useful subset of WHATWG DOM and HTML behavior, not a rendered browser: navigation and layout remain outside the implementation. The 30.0.1 patch fixes getComputedStyle() failures around calc() values and improves Range performance on large trees. Our install took 27 MB and its browser bundle could not be built, both consistent with a Node test environment rather than frontend code.

91.1Mdownloads / wk
Verdict

Our jsdom 30.0.1 install took 5 seconds, consumed 27 MB across 37 packages, had no audit findings, and failed the browser-bundle check. Use it for DOM semantics in supported Node releases; use a real browser for pixels or navigation, and never execute untrusted page scripts with it.

We installed it

Lab card: what happened when we installed jsdomScreenshot of jsdom documentation
Install✓ · 5s37 packages on disk · 27 MB
ImportESM import works · require() works · CommonJS package
Browsern/acould not be bundled for the browser (Node-only code, most likely)
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does jsdom install cleanly?

Yes. In a fresh container with an empty cache, npm install jsdom finished in 5 seconds, leaving 37 packages and 27 MB on disk. npm audit reported no known vulnerabilities.

Can jsdom run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does jsdom work with both ESM and CommonJS?

Yes. Both import 'jsdom' and require('jsdom') worked in Node 22 in our run. The package is published as CommonJS.

Does jsdom include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

jsdom or happy-dom: which should you use?

happy-dom: Choose it when faster test startup matters and its implemented browser APIs cover the component. Our jsdom 30.0.1 install took 5 seconds, consumed 27 MB across 37 packages, had no audit findings, and failed the browser-bundle check.

When should you not use jsdom?

Results depend on computed geometry, paint, screenshots, navigation, or a real networking stack; jsdom documents layout and navigation as missing

API stability4/5JSDOM(html, options), dom.window, fragment(), fromURL(), fromFile(), serialize(), VirtualConsole, and CookieJar remain the recognizable public pieces. Compatibility shifts still arrive through standards fixes and major Node floors: 30.0.1 accepts only specific Node 22 and 24 patch lines or 26+. Tests that accidentally depend on an unimplemented browser edge can change when jsdom becomes more accurate, so pinning and staging upgrades matters.
Docs5/5The README explains parsing options, safe and dangerous script modes, subresource loading, interceptors, visual hints, virtual consoles, cookies, VM access, source locations, encodings, canvas, cleanup, and known gaps. Its security warning appears beside runScripts: 'dangerously', and it states that layout and navigation are missing. The document is long because the runtime has many boundaries, but those boundaries are findable and concrete.
Maintenance5/5GitHub showed an unarchived repository pushed on 2026-08-26 with 403 open issues and pull requests across a broad standards implementation. Version 30.0.1 remains current and fixes calc()-related computed-style crashes plus a large-document Range performance problem. The project follows new Node and web-platform behavior closely, which is active maintenance but also produces strict runtime floors and compatibility changes worth testing.
Ecosystem5/5npm counted 97,132,568 downloads in the latest completed week, and GitHub reported 21,659 stars. Test runners and DOM-dependent packages already recognize jsdom environments. Internally it combines 21 direct dependencies for HTML, selectors, CSS, URLs, cookies, encoding, and HTTP behavior, with canvas available as one optional peer. That coverage explains both its usefulness and our measured 37-package, 27 MB footprint.

Discussed on

  1. hnUsing jQuery and node.js to scrape html pages in 5 lines133 points
  2. hnScraping Web Pages With jQuery, Node.js and Jsdom88 points
  3. hnSimplify templating with node.js, jsdom 0.2.0 and weld47 points
  4. hnFrom Karma to Mocha, with a taste of jsdom31 points
  5. hnJsdom 4.0 – JavaScript standards-compliant DOM for io.js18 points

Use it if

  • Node tests need selectors, events, forms, storage, or cookies without paying for a browser process
  • A scraper benefits from HTML-standard parsing, a configurable base URL, a cookie jar, and controlled resource fetching
  • Tests need source offsets for parsed nodes or a VM context tied to a particular document
  • The assertion concerns DOM state and events while pixels, geometry, and navigation are deliberately excluded
Skip it if

Setup reality

Our jsdom 30.0.1 install completed in 5 seconds inside an unprivileged Node 22 Bookworm sandbox. It left 37 packages occupying 27 MB. The distribution was 8,712 KB unpacked, declared 21 direct dependencies plus 1 peer, and produced 0 npm audit findings. It is CommonJS without an exports map, although both require() and ESM import succeeded. We found no TypeScript declarations.

Version 30.0.1 accepts Node ^22.22.2 || ^24.15.0 || >=26.0.0, so a generic Node 22 or 24 label is insufficient. canvas is an optional peer compatible with 3.2.3; without it, installing jsdom does not provide a functional canvas implementation. Our esbuild browser attempt failed. That is a useful boundary: this package emulates browser APIs for Node and should not be pushed into client code.

A new JSDOM neither executes embedded scripts nor fetches subresources. Trusted inline code requires runScripts: 'dangerously'. External scripts also require resources: 'usable' and a non-blank URL because about:blank cannot resolve relative paths. The README warns that dangerous execution can escape to Node. Custom resource interceptors and Undici dispatchers also cannot control synchronous XMLHttpRequest, since that path crosses a process boundary.

pretendToBeVisual exposes visibility state and requestAnimationFrame, but it still calculates no layout. Pages that start asynchronous work do not offer jsdom a universal completion signal; tests need an event or callback owned by the page. Close every window when finished. A pending window timer keeps the Node process alive and retains its document. includeNodeLocations adds parsing cost and works only with HTML, not XML.

Patterns

Query a parsed heading parse-html

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

const dom = new JSDOM('<main><h1>Hello</h1></main>');
console.log(dom.window.document.querySelector('h1').textContent);
dom.window.close();

HTML parsing creates the omitted html, head, and body nodes around this fragment.

Resolve a relative link from its origin set-base-url

const dom = new JSDOM('<a href="/docs">Docs</a>', {
  url: 'https://example.com/start',
});
console.log(dom.window.document.querySelector('a').href);

Without url, the document uses about:blank and root-relative paths cannot be resolved.

Run test code inside the document VM run-controlled-script

const dom = new JSDOM('<p id="out"></p>', {runScripts: 'outside-only'});
dom.window.eval(`document.querySelector('#out').textContent = 'done'`);

outside-only supplies fresh globals and window.eval while leaving script elements in the HTML inactive.

Fetch and execute a trusted page's assets load-page-resources

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

dangerously permits page JavaScript to reach the host, so arbitrary user or internet content is unsafe here.

Collect jsdom failures in a dedicated logger capture-jsdom-errors

const {JSDOM, VirtualConsole} = require('jsdom');
const vc = new VirtualConsole();
vc.on('jsdomError', error => logger.warn({type: error.type, error}));
const dom = new JSDOM(html, {virtualConsole: vc});

Register the listener before new JSDOM(), since parsing can emit a failure during construction.

Build detached list items parse-fragment

const fragment = JSDOM.fragment('<li>one</li><li>two</li>');
console.log([...fragment.children].map(node => node.textContent));

fragment() provides no window or options and never starts subresource loading.

Recover a node's HTML offsets find-source-location

const dom = new JSDOM(source, {includeNodeLocations: true});
const node = dom.window.document.querySelector('[data-id]');
console.log(dom.nodeLocation(node));

includeNodeLocations costs parsing time and cannot be combined with XML content types.

Dispose of a test window in finally close-window

const dom = new JSDOM(html, {runScripts: 'outside-only'});
try {
  await runChecks(dom.window);
} finally {
  dom.window.close();
}

A live window timer can hold both the DOM and the Node process until close() runs.

Alternatives

PackageRegistryPick it when
happy-domnpmChoose it when faster test startup matters and its implemented browser APIs cover the component.
linkedomnpmChoose it for a smaller DOM parser and tree API without jsdom's wider browser state.
cheerionpmChoose it for server-side selectors and markup edits that need no window or event loop.
playwrightnpmChoose it when Chromium, Firefox, or WebKit must calculate layout and display the result.

More testing guides

pytest · chai · vitest · playwright · coverage · axe-core · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.