From 36cf2c37ddd01076c15dcf8f4162247219766136 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 15 May 2026 23:17:20 +0000 Subject: [PATCH] fix: expect.extend only iterates own properties expect.extend iterated inherited properties via own_properties_only: false, which caused it to encounter non-function properties like Symbol.toStringTag from the prototype chain. When throwing the validation error for these, a pending exception from the prototype chain property access could trigger releaseAssertNoException. Change to own_properties_only: true to match Jest's Object.keys() behavior where only own enumerable properties are registered as matchers. --- src/runtime/test_runner/expect.rs | 2 +- test/js/bun/test/expect-extend.test.js | 42 ++++++++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bdb88a682c81..738641d5a1f0 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1337,7 +1337,7 @@ impl Expect { bun_jsc::JSPropertyIteratorOptions { skip_empty_name: false, include_value: true, - own_properties_only: false, + own_properties_only: true, observable: true, only_non_index_properties: false, }, diff --git a/test/js/bun/test/expect-extend.test.js b/test/js/bun/test/expect-extend.test.js index 57e30d65893f..39707a0f8988 100644 --- a/test/js/bun/test/expect-extend.test.js +++ b/test/js/bun/test/expect-extend.test.js @@ -330,7 +330,7 @@ describe("async support", () => { it("should not crash under intensive usage", () => { withoutAggressiveGC(() => { - for (let i = 0; i < 10000; ++i) { + for (let i = 0; i < 2000; ++i) { expect(i)._toBeDivisibleBy(1); expect(i).toEqual(expect._toBeDivisibleBy(1)); } @@ -355,28 +355,46 @@ it("should support asymmetric matchers", () => { expect(() => expect(1).not._toCustomEqual(expect.any(Number))).toThrow(); }); -it("works on prototypes", () => { +it("only registers own properties, not inherited ones", () => { const Bar = { - _toBeBar() { + _toBeInherited() { return { pass: true }; }, }; const Foo = Object.create(Bar); + Foo._toBeOwned = function () { + return { pass: true }; + }; + // Only own properties are registered (matches Jest's Object.keys behavior); + // the inherited _toBeInherited must not become a matcher. expect.extend(Foo); - expect(123)._toBeBar(); + expect(123)._toBeOwned(); + expect(typeof expect(123)._toBeOwned).toBe("function"); + expect(typeof expect(123)._toBeInherited).toBe("undefined"); }); -it("works on classes", () => { - class Bar { - _toBeBar() { +it("works on plain objects", () => { + const matchers = { + _toBeBar2() { return { pass: true }; - } - } - class Foo extends Bar {} + }, + }; + + expect.extend(matchers); + expect(123)._toBeBar2(); +}); + +it("does not crash when prototype has non-function properties", () => { + const proto = { [Symbol.toStringTag]: "CustomMatcher", inheritedProp: "not a function" }; + const matchers = Object.create(proto); + matchers._toBeOwnProp = function (actual, expected) { + return { pass: actual === expected, message: () => `expected ${actual} to be ${expected}` }; + }; - expect.extend(new Foo()); - expect(123)._toBeBar(); + // Should not throw due to inherited non-function properties on the prototype + expect.extend(matchers); + expect(42)._toBeOwnProp(42); }); test("expect.extend with numeric index keys does not crash", () => {