Skip to content

bun:dom and the Vitest work in one tree (not for merging) - #45415

Draft
Jarred-Sumner wants to merge 1 commit into
mainfrom
claude/dom-integration
Draft

Jarred-Sumner wants to merge 1 commit into
mainfrom
claude/dom-integration

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Not for merging. It puts two branches in one tree so that CI makes a build of both to try:

  • bun test: Vitest compatibility #44833, the Vitest compatibility work for bun test. Its change to setTimeout(callback, 0) is off here unless BUN_TEST_ZERO_DELAY_TIMERS=1 is set, so that one build can be timed both ways.
  • bun:dom, a DOM built into Bun (WebKit's DOM and HTML parser, without loading, layout and rendering), which will get a pull request of its own

On top of the two there is a test environment for the built-in DOM:

bun test --environment=bun:dom
  • It does what --environment=jsdom does, with no package to install: a new window per test file at http://localhost:3000, the window's members on globalThis, and an error in a listener fails the test.
  • It wins over a @vitest-environment jsdom or happy-dom comment in a test file. The comment says that the file needs a DOM, and the flag says which one. A node comment still wins.
  • Snapshots, failure messages and toEqual treat its nodes like those of jsdom and happy-dom.
  • useFakeTimers() replaces the global requestAnimationFrame. The window's own function stays on the real clock, as in jsdom, so a library that took it when it loaded keeps getting frames.
  • URL is a subclass of Bun's there, as under --environment=jsdom. A test that defines URL.createObjectURL and deletes it afterwards gets Bun's back.
  • --isolate=modules undoes what a file does to the prototypes of the DOM classes, which every file's window shares.

bun:dom's copy of WebCore is from the same WebKit revision as the JavaScriptCore that main links now (upstream 23b0720b40).

New in bun:dom since the last build:

  • With the focus inside a frame, the frame is the activeElement of the documents above it, which give up their own focus (blur, focusout). When a document above takes the focus back, those below give up theirs. As in Chrome.
  • requestAnimationFrame() under a faked performance. It kept its pace by performance.now(), which starts again at 0 when a test fakes it, so a frame could wait for as long as the process had run. It goes by a monotonic clock now.
  • spyOn(window, "setTimeout") on a window that isn't the global object (an iframe's) left the property undefined.
  • An element's name is a property of its document only if the document has a window, as in Chrome. On what DOMParser or createHTMLDocument() made, <img name=body> shadowed document.body, and Object.defineProperty() for such a name threw.
  • window.reportError(). It fires an error event at the window, and goes to console.error() if nobody cancels that. Under the test environment the global reportError is the window's and no longer Bun's, which knows nothing of the window. React 19 reports what a component throws through it.
  • Frames. <iframe> and <frame> have a contentWindow and a contentDocument (about:blank and srcdoc; nothing is loaded). window.open() returns a window. parent, top, opener, frames, length, frameElement, closed and postMessage() between windows work. Under the test environment native code hands out globalThis where it would hand out the window.
  • XML. DOMParser parses the XML types and XMLHttpRequest has responseXML, on Bun's XML parser.
  • Classes. TouchEvent, SVGSVGElement and 15 more SVG classes, Navigator with PluginArray and MimeTypeArray, CSSLayerBlockRule, CSSLayerStatementRule. Navigator is also a global of the runtime, as in Node. The runtime's navigator no longer has onLine, which the last build gave it and Node's doesn't have. A window's keeps it.
  • MessageEvent's source can be any object where the global object stands for a window.
  • elementFromPoint() and elementsFromPoint() are gone. They cannot answer without layout, jsdom has neither, and libraries test for them.
  • The style attribute stands where element.style first wrote to it.
  • A submit or reset event that script dispatches at a control reaches the form's listeners, as in jsdom.
  • A var() that refers to nothing stays as it was declared in computed style. background: transparent reads back as written. An SVG <a href> takes focus.
  • A link with target="_self", _parent or _top navigates its window.
  • requestAnimationFrame() calls back at most 60 times a second, and the first frame after a pause does not wait. With a DOM this fast, a frame 16 ms later came after await user.click() had resolved, where under jsdom it falls inside the click.

Changed outside the DOM:

  • new Blob(), new File() and slice(): type is the type the script gave, in lower case. It used to come back as text/plain;charset=utf-8 for text/plain, and likewise for HTML, CSS, JSON, JavaScript and form data. A Content-Type header made from such a blob still names the charset.
  • CSS: calc(0% + 0px) and calc(50% - 0px) keep both terms, in the bundler too. A sum with a percentage in it is not the same as a length. The sign of a zero was read the wrong way round.

The binary is about 4.1 MB bigger, so the commit message has [skip size check].

How did you verify your code works?

On a release build for Linux x64:

  • The tests of both branches.
  • msw 2.15 with axios on its XHR adapter and with fetch: 9 of 9 checks.
  • 2,560 React 19 + Testing Library tests in 160 files pass with --environment=bun:dom, under --isolate, --isolate=modules, and both with --parallel=8.
  • A project of 320 test files with React 18 and user-event 14 gives the same results as with jsdom.

@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 3:14 PM PT - Oct 20th, 2026

❌ @Jarred-Sumner, your commit b1530fe has 1 failures in Build #129594 (All Failures):

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #129339
    sizeΔ
    ❌ bun-darwin-aarch6488.80 MB84.97 MB+3.83 MB
    ❌ bun-darwin-x6496.42 MB92.85 MB+3.58 MB
    ❌ bun-linux-aarch64106.12 MB102.30 MB+3.81 MB
    ❌ bun-linux-x64105.66 MB102.30 MB+3.36 MB
    ❌ bun-linux-aarch64-musl98.89 MB95.07 MB+3.81 MB
    ❌ bun-linux-x64-musl99.56 MB96.18 MB+3.38 MB
    ❌ bun-linux-aarch64-android110.98 MB107.60 MB+3.38 MB
    ❌ bun-linux-x64-android115.99 MB112.86 MB+3.13 MB
    ❌ bun-freebsd-x64113.32 MB110.20 MB+3.11 MB
    ❌ bun-freebsd-aarch64115.29 MB111.70 MB+3.59 MB
    ❌ bun-windows-x64113.99 MB109.90 MB+4.09 MB
    ❌ bun-windows-aarch6499.32 MB95.62 MB+3.70 MB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 45415

That installs a local version of the PR into your bun-45415 executable, so you can run:

bun-45415 --bun

…e check]

Not for merging. The two parts have their own branches.

jakeleventhal commented Oct 22, 2026 •

Copy link
Copy Markdown

@Jarred-Sumner

Tested b1530fe6d on macOS ARM64 against our unchanged React 19 + DTL 10.4.2 suite, using the same PR binary for both arms. Native used public bun:dom Window with a registrar adapter preserving our preload, not the CLI environment installer.

Results: 2,196/2,439 pass with native vs 2,434/2,439 with Happy DOM 20.14.5. Compatibility gaps observed:

  • Missing IntersectionObserver, cookieStore, and document.elementFromPoint (the last is intentionally omitted without layout).
  • Cross-origin test URL setup: history.replaceState rejects it, while non-fragment Location navigation does not provide Happy DOM's setURL behavior.
  • Remote iframes do not emit load; our disabled-navigation fixtures expect no fetching but still a load event.

Encouraging subset result: an entire 145-test workspace passed both, with median wall 2.804 → 2.224 s and 29.1% less CPU across six alternating pairs. Busy shared host; no full-suite speedup claim.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants