Skip to content

stacksjs/very-happy-dom

Social Card of this repo

npm version GitHub Actions Commitizen friendly

very-happy-dom

A blazingly fast, lightweight virtual DOM implementation powered by Bun. Drop-in replacement for happy-dom and jsdom in testing environments.

Features

  • Comprehensive DOM - Full DOM manipulation, CSS selectors, XPath, events with bubbling/capturing
  • Network APIs - Fetch, XMLHttpRequest, WebSocket, and request interception
  • Browser APIs - Storage, Timers, Canvas 2D, Observers, Clipboard, History, Cookies, File API
  • Web Components - Custom Elements and Shadow DOM
  • Framework Agnostic - Works with Bun, Vitest, or any testing framework
  • Easy Migration - API-compatible with happy-dom; one-line switch from jsdom

Installation

bun add -d very-happy-dom

Or with npm/pnpm:

npm install --save-dev very-happy-dom
pnpm add -D very-happy-dom

Quick Start

import { Window } from 'very-happy-dom'

const window = new Window()
const document = window.document

document.body.innerHTML = '<h1>Hello World</h1>'
const heading = document.querySelector('h1')
console.log(heading?.textContent) // "Hello World"

Testing with Bun

The simplest way — create a Window per test:

import { describe, expect, test } from 'bun:test'
import { Window } from 'very-happy-dom'

describe('MyComponent', () => {
  test('renders correctly', () => {
    const window = new Window()
    const document = window.document

    document.body.innerHTML = '<div class="container">Test</div>'
    const element = document.querySelector('.container')

    expect(element?.textContent).toBe('Test')
  })
})

Global DOM Environment

For Testing Library, React, and other frameworks that expect browser globals (document, window, etc.), use GlobalRegistrator:

// happy-dom.ts (preload script)
import { GlobalRegistrator } from 'very-happy-dom'

GlobalRegistrator.register()
# bunfig.toml
[test]
preload = ["./happy-dom.ts"]

That's it. All browser globals are now available in your tests:

import { test, expect } from 'bun:test'
import { screen, render } from '@testing-library/react'
import { MyComponent } from './MyComponent'

test('renders correctly', () => {
  render(<MyComponent />)
  expect(screen.getByTestId('my-component')).toBeInTheDocument()
})

Migrating from happy-dom

One-line change — the GlobalRegistrator API is the same:

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

GlobalRegistrator.register()

Advanced Usage

Browser Context

import { Browser } from 'very-happy-dom'

const browser = new Browser()
const context = browser.createContext()
const page = context.newPage()

page.goto('https://example.com')

Request Interception

import { Window } from 'very-happy-dom'

const window = new Window()

window.interceptor.addInterceptor({
  onRequest: (request) => {
    if (request.url.includes('/api/')) {
      return new Response(JSON.stringify({ mocked: true }))
    }
    return request
  }
})

Custom Window Configuration

import { Window } from 'very-happy-dom'

const window = new Window({
  url: 'https://example.com',
  width: 1920,
  height: 1080,
  settings: {
    navigator: {
      userAgent: 'MyCustomUserAgent/1.0'
    },
    device: {
      prefersColorScheme: 'dark'
    }
  }
})

Event Handling

const window = new Window()
const document = window.document

const button = document.createElement('button')
let clicked = false

button.addEventListener('click', () => {
  clicked = true
})

button.click()
console.log(clicked) // true

Storage APIs

const window = new Window()

// localStorage
window.localStorage.setItem('key', 'value')
console.log(window.localStorage.getItem('key')) // "value"

// sessionStorage
window.sessionStorage.setItem('session', 'data')

Observers

const window = new Window()
const document = window.document

// MutationObserver
const observer = new window.MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log('DOM changed:', mutation.type)
  })
})

observer.observe(document.body, {
  childList: true,
  attributes: true,
  subtree: true
})

// IntersectionObserver
const io = new window.IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    console.log('Visibility changed:', entry.isIntersecting)
  })
})

// ResizeObserver
const ro = new window.ResizeObserver((entries) => {
  entries.forEach((entry) => {
    console.log('Size changed:', entry.contentRect)
  })
})

Canvas API

const window = new Window()
const document = window.document

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 100, 100)
ctx.strokeStyle = 'blue'
ctx.strokeRect(10, 10, 80, 80)

// Export canvas data
const dataUrl = canvas.toDataURL()
const blob = await canvas.toBlob()

Performance

vs happy-dom vs jsdom

Operation very-happy-dom happy-dom jsdom Faster by
Window Creation 4.08 µs 92.83 µs 1.22 ms 22.7x
createElement 463.02 ns 2.62 µs 4.67 µs 5.7x
createElement + setAttribute 748.35 ns 15.41 µs 6.62 µs 8.8x
innerHTML (medium) 41.61 µs 47.48 µs 168.98 µs 1.1x
innerHTML (large, 200 nodes) 1.92 ms 3.72 ms 6.27 ms 1.9x
querySelector by ID 81.03 ns n/a 2.76 µs 34.1x
querySelector by class 242.20 ns n/a 3.52 µs 14.5x
querySelectorAll (200 matches) 66.44 µs n/a 66.55 µs ~1x
querySelectorAll + iteration 76.44 µs n/a 170.37 µs 2.2x
appendChild (single) 1.70 µs 4.58 µs 6.14 µs 2.7x
appendChild (1000 children) 852.90 µs 1.54 ms 4.45 ms 1.8x
setAttribute 124.66 ns 2.64 µs 1.43 µs 11.5x
getAttribute 2.18 ns 28.85 ns 194.98 ns 13.2x
classList.add 3.97 µs 6.88 µs 4.87 µs 1.2x
addEventListener + dispatch 2.67 µs 5.43 µs 3.65 µs 1.4x
textContent set 470.48 ns 1.72 µs 4.67 µs 3.7x
cloneNode (deep) 6.16 µs 21.59 µs 15.55 µs 2.5x
style.setProperty 490.62 ns 4.20 µs 4.64 µs 8.6x
Build data table (50x5) 519.30 µs 754.42 µs 2.89 ms 1.5x
Update list items (100) 454.98 µs n/a 2.41 ms 5.3x

Note: "Faster by" compares very-happy-dom to the next-fastest result. Benchmarks run on Apple M3 Pro with Bun 1.3.11. Run them yourself:

bun run bench

Migration

From happy-dom

One-line change — the API is compatible:

// Before
import { Window } from 'happy-dom'

// After
import { Window } from 'very-happy-dom'

From jsdom

// Before (jsdom)
import { JSDOM } from 'jsdom'
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>')
const { window } = dom
const { document } = window

// After (very-happy-dom)
import { Window } from 'very-happy-dom'
const window = new Window()
const document = window.document

API Reference

Core Classes

  • Window - Main window/global object with all browser APIs
  • Document - DOM document with querySelector, createElement, etc.
  • Element - DOM elements with full manipulation API
  • Browser - Browser instance for advanced scenarios
  • BrowserContext - Isolated browser contexts
  • BrowserPage - Individual pages with navigation

Supported APIs

Click to expand full API list

DOM

  • Document, Element, TextNode, CommentNode, DocumentFragment
  • Attributes, ClassList, Style

Selectors

  • querySelector / querySelectorAll
  • getElementById / getElementsByClassName / getElementsByTagName
  • CSS Selectors (all combinators)
  • XPath

Events

  • addEventListener / removeEventListener
  • Event bubbling and capturing
  • CustomEvent
  • Event.preventDefault / stopPropagation

Network

  • fetch(), Request / Response / Headers, FormData
  • XMLHttpRequest
  • WebSocket
  • Request Interception

Storage

  • localStorage, sessionStorage

Timers

  • setTimeout / clearTimeout
  • setInterval / clearInterval
  • requestAnimationFrame / cancelAnimationFrame

Observers

  • MutationObserver, IntersectionObserver, ResizeObserver

Canvas

  • Canvas element, 2D rendering context
  • Basic drawing operations, toDataURL / toBlob

Web Components

  • Custom Elements, Shadow DOM

Other

  • Performance API, Console API, Clipboard API, Navigator API
  • Geolocation API, Notification API, History API, Location API
  • Cookie API, File API, FileReader API

Testing

bun test                  # Run all tests
bun test --coverage      # Run with coverage

Contributing

We welcome contributions! Please see CONTRIBUTING for details.

Changelog

Please see our releases page for more information on what has changed recently.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

Postcardware

Very Happy DOM is free and open-source, but we'd love to receive a postcard from you! Send one to:

Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎

We showcase postcards from around the world on our website!

Sponsors

We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.

License

The MIT License (MIT). Please see LICENSE for more information.

Made with 💙

About

A very performant virtual DOM implementation.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors