AphrodyBun GitHub

Runtime docs · Test Runner · Specialized Testing

DOM testing

Learn how to test DOM elements and components using Bun with happy-dom and React Testing Library

Bun's test runner plays well with existing component and DOM testing libraries, including React Testing Library and happy-dom.

happy-dom

For headless tests of your frontend code and components, we recommend happy-dom. It implements a complete set of HTML and DOM APIs in plain JavaScript, so it can simulate a browser environment with high fidelity.

Install the @happy-dom/global-registrator package as a dev dependency.

bun add -d @happy-dom/global-registrator

Use Bun's preload feature to register the happy-dom globals before your tests run, which makes browser APIs like document available in the global scope. Create a file called happydom.ts in the root of your project with the following code:

happydom.ts
import { GlobalRegistrator } from "@happy-dom/global-registrator";

GlobalRegistrator.register();

To preload this file before bun test, open or create a bunfig.toml file and add the following lines.

bunfig.toml
[test]
preload = ["./happydom.ts"]

bun test now executes happydom.ts before your tests, so they can use browser APIs like document and window.

dom.test.ts
import { test, expect } from "bun:test";

test("dom test", () => {
  document.body.innerHTML = `<button>My button</button>`;
  const button = document.querySelector("button");
  expect(button?.innerText).toEqual("My button");
});

TypeScript Support

Depending on your tsconfig.json setup, you may see a "Cannot find name 'document'" type error in the earlier code. To load the types for document and other browser APIs, add the following triple-slash directive to the top of any test file.

dom.test.ts
/// <reference lib="dom" />

import { test, expect } from "bun:test";

test("dom test", () => {
  document.body.innerHTML = `<button>My button</button>`;
  const button = document.querySelector("button");
  expect(button?.innerText).toEqual("My button");
});

Run the test with bun test:

bun test
bun test v1.3.3

dom.test.ts:
✓ dom test [0.82ms]

 1 pass
 0 fail
 1 expect() calls
Ran 1 test across 1 file. [125.00ms]

Per-file environment

A test file can ask for a DOM in its leading comment, as in Jest and Vitest. bun test loads the window from the project's dependencies, installs its globals for that file only, and removes them when the file finishes.

CommentPackage loaded
@jest-environment jsdom or @vitest-environment jsdomjsdom, or the one jest-environment-jsdom uses
@jest-environment happy-dom or @vitest-environment happy-domhappy-dom, or the one @happy-dom/jest-environment uses
@jest-environment nodenone
button.test.ts
/**
 * @jest-environment jsdom
 */
import { test, expect } from "bun:test";

test("button", () => {
  document.body.innerHTML = `<button>My button</button>`;
  expect(document.querySelector("button")?.textContent).toBe("My button");
});

The language globals, the timers, console, crypto, performance, fetch and structuredClone stay Bun's, so jest.useFakeTimers() still applies.

React Testing Library

Bun works with React Testing Library for testing React components. After setting up happy-dom as described earlier, install and use React Testing Library normally.

bun add -d @testing-library/react @testing-library/jest-dom
component.test.tsx
/// <reference lib="dom" />

import { test, expect } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

function Button({ children }: { children: React.ReactNode }) {
  return <button>{children}</button>;
}

test('renders button', () => {
  render(<Button>Click me</Button>);
  expect(screen.getByRole('button')).toHaveTextContent('Click me');
});

Advanced DOM Testing

Custom Elements

Test custom elements and web components with the same setup:

custom-element.test.ts
/// <reference lib="dom" />

import { test, expect } from "bun:test";

test("custom element", () => {
  // Define a custom element
  class MyElement extends HTMLElement {
    constructor() {
      super();
      this.innerHTML = "<p>Custom element content</p>";
    }
  }

  customElements.define("my-element", MyElement);

  // Use it in tests
  document.body.innerHTML = "<my-element></my-element>";
  const element = document.querySelector("my-element");
  expect(element?.innerHTML).toBe("<p>Custom element content</p>");
});

Event Testing

Test DOM events and user interactions:

events.test.ts
/// <reference lib="dom" />

import { test, expect } from "bun:test";

test("button click event", () => {
  let clicked = false;

  document.body.innerHTML = '<button id="test-btn">Click me</button>';
  const button = document.getElementById("test-btn");

  button?.addEventListener("click", () => {
    clicked = true;
  });

  button?.click();
  expect(clicked).toBe(true);
});

Configuration Tips

Global Setup

For more involved setups, create a preload file that also registers global mocks:

test-setup.ts
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import "@testing-library/jest-dom";

// Register happy-dom globals
GlobalRegistrator.register();

// Add any global test configuration here
global.ResizeObserver = class ResizeObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
};

// Mock other APIs as needed
Object.defineProperty(window, "matchMedia", {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

Then update your bunfig.toml:

bunfig.toml
[test]
preload = ["./test-setup.ts"]

Troubleshooting

Common Issues

TypeScript errors for DOM APIs: Include the /// <reference lib="dom" /> directive at the top of your test files.

Missing globals: Check that your preload file imports and registers @happy-dom/global-registrator.

React component rendering issues: Check that @testing-library/react is installed and happy-dom is set up.

Performance Considerations

happy-dom is fast, but for very large test suites you may want to:

  • Use beforeEach to reset the DOM state between tests
  • Avoid creating too many DOM elements in a single test
  • Use cleanup functions from testing libraries
test-setup.ts
import { afterEach } from "bun:test";
import { cleanup } from "@testing-library/react";

afterEach(() => {
  cleanup();
  document.body.innerHTML = "";
});