mrkeyoor.com_
Sun 20 Sept 17:55 UTC
npmTestingupdated 20 Sept 2026

happy-dom review

happy-dom 20.11.6 supplies a browser-like Window, Document, custom elements, shadow DOM, events, MutationObserver, fetch, and related web APIs inside Node. Its main job is giving unit and component tests a DOM without launching Chrome. It can be selected directly by Vitest, connected to Jest through @happy-dom/jest-environment, or created as isolated Window and Browser objects in scripts. It does not draw pixels or reproduce a browser layout engine. Our browser-targeted esbuild run could not build the package, which confirms that it belongs in Node test tooling. The 20.11.6 release changes documentation for the global registrator package, with no runtime feature listed.

Verdict

happy-dom 20.11.6 installed in 2.9 seconds and used 23 MB across 9 packages with 0 audit findings, while our browser bundle failed. Use it for fast Node DOM tests, then reserve a real browser for layout, compatibility, and security boundaries.

We installed it

Lab card: what happened when we installed happy-domScreenshot of happy-dom documentation
Install✓ · 2.9s9 packages on disk · 23 MB
ImportESM import works · require() works · ESM 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 happy-dom install cleanly?

Yes. In a fresh container with an empty cache, npm install happy-dom finished in 3 seconds, leaving 9 packages and 23 MB on disk. npm audit reported no known vulnerabilities.

Can happy-dom 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 happy-dom work with both ESM and CommonJS?

Yes. Both import 'happy-dom' and require('happy-dom') worked in Node 22 in our run. The package is published as ESM.

Does happy-dom include TypeScript types?

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

happy-dom or jsdom: which should you use?

jsdom: Use it when your framework or test tooling assumes jsdom and its compatibility tradeoffs are already understood. happy-dom 20.11.6 installed in 2.9 seconds and used 23 MB across 9 packages with 0 audit findings, while our browser bundle failed.

When should you not use happy-dom?

Your assertion depends on layout boxes, CSS painting, screenshots, font metrics, or real focus behavior. happy-dom has no rendering engine.

API stability2/5happy-dom has reached major version 20 while implementing a browser surface that keeps changing. Window, Document, events, fetch, and custom-element behavior can shift between major releases even when test code still type-checks. Pinning the version and reading every major migration is sensible, especially for suites that assert exact event order or browser edge cases.
Docs3/5The GitHub wiki explains Vitest, Jest, global registration, Window construction, browser settings, asynchronous completion, and resource loading. Release 20.11.6 consists solely of a correction to global-registrator documentation, which shows that setup guidance can lag package behavior. Individual DOM differences and less common classes often require reading source or issue threads.
Maintenance4/5Version 20.11.6 and the repository's latest push both landed on August 19, 2026, and the project is not archived. GitHub reports 388 open issues and pull requests. That queue is understandable for a browser implementation, but it also means an obscure standards mismatch may wait while releases continue around higher-use test and DOM paths.
Ecosystem4/5npm recorded 15,201,508 downloads in the latest completed week and GitHub reports 4,618 stars. Vitest recognizes happy-dom as an environment, and companion packages connect it to Jest or Node globals. Its ecosystem stops at simulated DOM behavior: Playwright, Cypress, or another browser runner is still required for pixels, engine compatibility, and end-to-end navigation.

Use it if

  • Vitest tests need document, events, custom elements, or shadow roots and do not assert rendered geometry.
  • A Node utility needs selectors and DOM mutation against controlled HTML without opening a real browser.
  • Most component behavior can run in a fast simulated DOM while a smaller Playwright suite covers browser boundaries.
  • Tests benefit from separate Window instances with controlled URLs, viewports, timers, and fetch interception.
Skip it if

Setup reality

We installed happy-dom 20.11.6 in a fresh Node 22 Bookworm container in 2.9 seconds. The result was 9 packages and 23 MB on disk. Its package data has 7 direct dependencies, 0 peers, and a Node >=20 requirement. npm audit returned 0 known vulnerabilities. Both require() and ESM import succeeded even though the package declares ESM. Our package inspection found no TypeScript types. esbuild could not create a browser-targeted bundle.

Vitest accepts environment: 'happy-dom'; Jest needs the separate @happy-dom/jest-environment package. Scripts that want document and window globals can use @happy-dom/global-registrator, also installed separately. Give the environment a real base URL if code resolves relative links, reads location, or sets cookies. An implicit about:blank origin can make an otherwise ordinary test fail for the wrong reason.

JavaScript evaluation and external resource loading need explicit policy. Enabling evaluation lets page code run in the Node process, and the project says its VM context cannot safely contain untrusted scripts. Fetch, stylesheet, and script loads can also leave a hermetic test suite. Disable them or intercept requests unless network access is part of the case.

Tracked fetches and timers can continue after an assertion. waitUntilComplete() waits for work happy-dom knows about; an interval can keep it waiting, while abort() cancels pending tasks and close() releases a Window or Browser. A configured viewport affects matchMedia but does not create real element rectangles. Keep at least one real-browser test for any behavior that depends on layout or event ordering.

Patterns

Use happy-dom for a Vitest project configure-vitest

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom',
    environmentOptions: { happyDOM: { url: 'https://app.test/' } },
  },
});

Version 20 requires Node 20 or newer. Set the URL when code relies on an origin, cookies, or relative links.

Enable a DOM for one test file select-file-environment

// @vitest-environment happy-dom
import { expect, test } from 'vitest';

test('adds the button', () => {
  document.body.innerHTML = '<button>Save</button>';
  expect(document.querySelector('button')?.textContent).toBe('Save');
});

The environment comment belongs before imports. Node-only test files can then avoid creating a DOM.

Work with an isolated Window create-window

import { Window } from 'happy-dom';

const page = new Window({ url: 'https://app.test/', width: 1024, height: 768 });
page.document.body.innerHTML = '<main id=app>Hello</main>';
console.log(page.document.querySelector('#app')?.textContent);
await page.close();

Close the Window in teardown so timers and pending requests do not keep the process alive.

Install DOM globals for a Node script register-globals

import { GlobalRegistrator } from '@happy-dom/global-registrator';

GlobalRegistrator.register({ url: 'https://app.test/' });
await import('./module-that-reads-document.js');
await GlobalRegistrator.unregister();

@happy-dom/global-registrator is a separate package. Register before importing modules that touch document at module load.

Select the Jest companion environment configure-jest

// jest.config.cjs
module.exports = {
  testEnvironment: '@happy-dom/jest-environment',
  testEnvironmentOptions: { url: 'https://app.test/' },
};

Install @happy-dom/jest-environment separately and keep its major compatible with happy-dom.

Wait for tracked asynchronous work wait-for-work

widget.connectedCallback();
await window.happyDOM.waitUntilComplete();
expect(document.querySelector('[data-ready]')).not.toBeNull();
await window.happyDOM.abort();

A repeating timer can prevent completion. abort() cancels tasks that a test intentionally leaves pending.

Mount a custom element with shadow DOM test-custom-element

class StatusDot extends window.HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = '<span>ready</span>';
  }
}
window.customElements.define('status-dot', StatusDot);
document.body.innerHTML = '<status-dot></status-dot>';
expect(document.querySelector('status-dot').shadowRoot.textContent).toContain('ready');

Extend HTMLElement from the same Window. Parsed styles still do not yield browser-calculated geometry.

Answer a request inside the Window intercept-fetch

const window = new Window({
  settings: { fetch: { interceptor: {
    beforeAsyncRequest: async ({ request }) =>
      request.url.endsWith('/api/me')
        ? new Response(JSON.stringify({ id: 7 }), { headers: { 'content-type': 'application/json' } })
        : undefined,
  } } }, },
});

Returning undefined lets the request continue. Block external access separately if a URL mismatch must never reach the network.

Keep external scripts and styles off disable-page-code

const window = new Window({
  settings: {
    enableJavaScriptEvaluation: false,
    disableJavaScriptFileLoading: true,
    disableCSSFileLoading: true,
  },
});

Do not enable script evaluation for hostile markup. The VM runs within the Node process and is not an isolation boundary.

Set a viewport for matchMedia test-media-query

window.happyDOM.setViewport({ width: 390, height: 844 });
expect(window.matchMedia('(max-width: 600px)').matches).toBe(true);

The viewport drives media queries, but getBoundingClientRect still cannot prove rendered layout.

Replace the test URL change-location

window.happyDOM.setURL('https://app.test/orders?state=open');
const state = new URL(window.location.href).searchParams.get('state');

setURL changes location state without performing a real browser navigation or loading a new document.

Navigate with the Browser API open-browser-page

import { Browser } from 'happy-dom';

const browser = new Browser();
const page = browser.newPage();
await page.goto('https://example.com/');
console.log(page.mainFrame.document.title);
await browser.close();

goto performs network I/O. Intercept it for repeatable tests and close the Browser after the final page.

Alternatives

PackageRegistryPick it when
jsdomnpmUse it when your framework or test tooling assumes jsdom and its compatibility tradeoffs are already understood.
linkedomnpmUse it for compact server-side HTML parsing and manipulation with a smaller browser-API target.
dominonpmUse it when an older, narrow server DOM is sufficient and newer web APIs are irrelevant.

More testing guides

pytest · chai · vitest · jsdom · playwright · coverage · 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.