console/test: serialize DOM nodes as markup in Bun.inspect and matcher utils

Closed
RO
1mo ago
+691-1

What

Bun.inspect, console.log, this.utils.printReceived/printExpected/stringify, and toMatchSnapshot now serialize DOM nodes (from jsdom / happy-dom) as markup instead of dumping the full object graph.

Received element is not disabled:
  <button />

instead of

Received element is not disabled:
  HTMLButtonElement {
  [Symbol(listeners)]: {},
  [Symbol(listenerOptions)]: {},
  [Symbol(isConnected)]: false,
  [Symbol(parentNode)]: null,
  [Symbol(rootNode)]: null,
  [Symbol(ownerDocument)]: HTMLDocument {
    ...hundreds more lines...

Fixes #10886. Fixes #5540. Fixes #39768.

Not related to #29709 beyond the issue link: that PR fixes a native stack overflow when printing circular JSX / deep Proxy chains, while the #10886 report is a happy-dom element (a plain object to the formatter) producing a bounded but enormous dump, which is what this PR changes. Both still reproduce on current main; this branch has been merged with main.

Reproduction

import * as matchers from "@testing-library/jest-dom/matchers";
import { render, screen } from "@testing-library/react";
expect.extend(matchers);

it("test", () => {
  render(<button>test</button>);
  expect(screen.getByRole("button")).toBeDisabled();
});

Before this change the failure message from toBeDisabled() printed the whole happy-dom HTMLButtonElement tree (the ownerDocument → window graph), which users describe as "infinite". Jest and Vitest print <button /> because this.utils.printReceived is backed by pretty-format with the DOMElement plugin.

Cause

ExpectMatcherUtils.printReceived / stringify route through ConsoleObject::Formatter, which had no handling for DOM nodes (there is no native DOM in bun; these objects come from jsdom / happy-dom as plain JS class instances). The formatter already has a JSX tag for React elements but nothing for Element / Text / Comment / DocumentFragment, so they fell through to Tag::Object and were walked as ordinary objects. The snapshot serializer in pretty_format.rs had the same gap.

Fix

Add a DOMNode tag to both ConsoleObject::Formatter and the test-runner pretty_format that mirrors the pretty-format DOMElement plugin:

  • Detection: an Object/FinalObject is a DOM node when its class name matches /^((HTML|SVG)\w*)?Element$/, Text, Comment, or DocumentFragment and its nodeType agrees with that class. The name check runs first so nodeType (a prototype getter) is never invoked on unrelated objects, and a two-pointer prototype probe bails out immediately for plain {} / Object.create(null) so the common path does not pay for a calculatedClassName walk.
  • Printing: elements render as <tag attr="v">...children...</tag> (attributes sorted, self-closing when empty, … leaf at max_depth); Text as its data, Comment as <!--data-->, DocumentFragment as <DocumentFragment>…</DocumentFragment>.

This matches what browsers do for console.log(element) and what Jest does for printReceived / snapshots. util.inspect is unchanged (still Node-compatible).

Verification

bun bd test test/js/bun/util/inspect.test.js -t "DOM nodes": 12 new tests covering HTML/SVG elements, Text/Comment/DocumentFragment, nested depth, printReceived/stringify in a custom matcher, and the negative cases (wrong nodeType, non-DOM class name, throwing nodeType getter). All fail on current release and pass with this change.

Checked against happy-dom 15/20 and jsdom.


no test proof · iteration 6 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/inspect.test.js