expect: show URL fields in toEqual/toStrictEqual diffs instead of 'URL {}'
Fixes #7253.
Repro
import { test, expect } from "bun:test";
test("url", () => {
expect(new URL("https://www.google.com")).toStrictEqual(new URL("https://github.com"));
});
Before:
error: expect(received).toStrictEqual(expected)
Expected: URL {}
Received: URL {}
After:
error: expect(received).toStrictEqual(expected)
URL {
"hash": "",
- "host": "github.com",
- "hostname": "github.com",
- "href": "https://github.com/",
- "origin": "https://github.com",
+ "host": "www.google.com",
+ "hostname": "www.google.com",
+ "href": "https://www.google.com/",
+ "origin": "https://www.google.com",
"password": "",
"pathname": "/",
...
}
Cause
JSC__JSValue__forEachPropertyOrdered (used by the jest/snapshot formatter and Bun.inspect({ sorted: true })) only collected own property names. DOM wrappers like URL and Event subclasses expose all their state through accessors on the prototype and have no own data properties, so the formatter found nothing and rendered URL {}. forEachProperty (used by console.log) already walks the prototype chain, which is why console.log(new URL(...)) was never affected.
Fix
In forEachPropertyOrdered, after collecting own property names, also walk the prototype chain for JSDOMWrapperType / JSEventType cells (mirroring forEachProperty's traversal and stop conditions). PropertyNameArrayBuilder::add already deduplicates. Other object types keep own-properties-only behaviour, so boxed primitives (new Number(n) stays Number {}) and user class instances are unchanged, preserving existing snapshot output.
Headers is also JSDOMWrapperType but its prototype is almost entirely methods, so the walk alone would have produced a wall of [Function: ...] lines with no signal. Added a FetchHeaders branch in the jest formatter that prints toJSON() output, matching the existing FormData branch and the console formatter's Headers handling in jsc_hooks.rs, so a toStrictEqual failure on two Headers now diffs the entries:
Headers
{
- "b": "2",
+ "a": "1",
}
Also fixed handle_first_property in the jest formatter, which emitted the object's display name twice when the object had a Symbol.toStringTag (the toStringTag lookup and the class-name lookup both printed). It now uses the toStringTag when present and falls back to the class name, matching the empty-object branch. This path was previously unreachable for URL because no properties were ever visited.
Verification
test/regression/issue/07253.test.ts fails on main (4 of 5 tests, with Expected: URL {} / Headers {} in stderr) and passes with the fix. Ran inspect.test.js, expect.test.js, the snapshot suites and the console tests locally with no new failures.
[review] gate passed · iteration 0 · 3 files touched
fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/07253.test.ts"
bun test v1.4.0 (90ea0d86d)
test/regression/issue/07253.test.ts:
38 | test("url", () => {
39 | expect(new URL("https://a.example/?x=1")).toEqual(new URL("https://b.example/?y=2"));
40 | });
41 | `);
42 |
43 | expect(stderr).not.toContain("URL {}");
^
error: expect(received).not.toContain(expected)
Expected to not contain: "URL {}"
Received: "\nurl.test.ts:\n1 | \n2 | import { expect, test } from \"bun:test\";\n3 | test(\"url\", () => {\n4 | expect(new URL(\"https://a.example/?x=1\")).toEqual(new URL(\"https://b.example/?y=2\"));\n ^\nerror: expect(received).toEqual(expected)\n\nExpected: URL {}\nReceived: URL {}\n\n at <anonymous> (/tmp/issue-07253_MFpDs2/url.test.ts:4:49)\n(fail) url [9.54ms]\n\n 0 pass\n 1 fail\n 1 expect() calls\nRan 1 test across 1 file. [436.00ms]\n"
at <anonymous> (/workspace/bun/test/regression/issue/07253.test.ts:43:22)
(fail) toEqual failure diff for
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (e83e9da47)
test/regression/issue/07253.test.ts:
(pass) toStrictEqual failure diff for Headers shows entries, not prototype methods [63.88ms]
(pass) toEqual failure diff for URL shows the differing fields [94.47ms]
(pass) toStrictEqual failure diff for URL shows the differing fields [98.95ms]
(pass) Bun.inspect with sorted: true enumerates URL prototype accessors [0.33ms]
(pass) Bun.inspect with sorted: true does not walk the prototype for non-DOM objects [0.11ms]
5 pass
0 fail
22 expect() calls
Ran 5 tests across 1 file. [339.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/07253.test.ts"
bun test v1.4.0 (90ea0d86d)
test/regression/issue/07253.test.ts:
(pass) toStrictEqual failure diff for URL shows the differing fields [819.97ms]
(pass) toStrictEqual failure diff for Headers shows entries, not prototype methods [697.79ms]
(pass) toEqual failure diff for URL shows the differing fields [1267.45ms]
(pass) Bun.inspect with sorted: true enumerates URL prototype accessors [19.64ms]
(pass) Bun.inspect with sorted: true does not walk the prototype for non-DOM objects [10.04ms]
5 pass
0 fail
22 expect() calls
Ran 5 tests across 1 file. [8.34s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 2712ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/9] gen cpp.rs (cppbind)
[2/9] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 239 extern-C blocks audited
[2/9] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)
[1m[92m Compiling[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
[1m[92m Compiling[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
[1m[92m Compiling[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
[1m[92m Compiling[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
[1m[92m Compiling[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
[1m[92m Compiling[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
[1m[92m Compiling[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
[1m[92m Compiling[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
[1m[92m Compiling[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
[1m[92m
... (truncated)
diff hotspot
src/jsc/bindings/bindings.cpp | 26 +++++++++++
src/runtime/test_runner/pretty_format.rs | 37 +++++++++------
test/regression/issue/07253.test.ts | 79 ++++++++++++++++++++++++++++++++
3 files changed, 129 insertions(+), 13 deletions(-)
gate history · 3 passed · 0 rejected · iteration 0
evidence per changed file
file reads edits tests
src/jsc/bindings/bindings.cpp 10 8 0
src/runtime/test_runner/pretty_format.rs 10 6 0
test/regression/issue/07253.test.ts 1 6 0