diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 81597bb610cc..6d16144b3ac9 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -2496,7 +2496,7 @@ bool Bun__deepMatch( } } - PropertyNameArrayBuilder subsetProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); + PropertyNameArrayBuilder subsetProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); subsetObj->getPropertyNames(globalObject, subsetProps, DontEnumPropertiesMode::Exclude); RETURN_IF_EXCEPTION(throwScope, false); @@ -2508,13 +2508,14 @@ bool Bun__deepMatch( // arrays should match exactly bool objIsArray = isArray(globalObject, objValue); RETURN_IF_EXCEPTION(throwScope, false); - bool subsetIsArray = objIsArray && isArray(globalObject, subsetValue); + bool subsetIsArray = isArray(globalObject, subsetValue); RETURN_IF_EXCEPTION(throwScope, false); if (subsetIsArray) { - if (obj->getArrayLength() != subsetObj->getArrayLength()) { + // An array pattern needs an array. An object pattern can be satisfied by one. + if (!objIsArray || obj->getArrayLength() != subsetObj->getArrayLength()) { return false; } - PropertyNameArrayBuilder objProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); + PropertyNameArrayBuilder objProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); obj->getPropertyNames(globalObject, objProps, DontEnumPropertiesMode::Exclude); RETURN_IF_EXCEPTION(throwScope, false); if (objProps.size() != subsetProps.size()) { diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index db7fe78a3959..e6a156f203d5 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -7,6 +7,7 @@ */ import { stripVTControlCharacters } from "node:util"; +import { runInNewContext } from "node:vm"; // import these functions typed with the bun:test types, // so this test can also be used to detect issues with the "bun:test" type definitions import test_interop from "./test-interop.js"; @@ -4194,6 +4195,81 @@ describe("expect()", () => { expect(a1).not.toMatchObject({ 1: 1 }); expect(a1).toMatchObject(a1); }); + + // Jest, Vitest and toEqual() do not see a #field of a class. + test("the private fields of a class are not compared", () => { + class Counter { + #n; + constructor(/** @type {number} */ n) { + this.#n = n; + this.name = "c"; + } + get n() { + return this.#n; + } + } + class Holder { + #o; + constructor(/** @type {number} */ n) { + this.#o = { n }; + } + get o() { + return this.#o; + } + } + expect(new Counter(1)).toMatchObject(new Counter(2)); + expect({ k: new Counter(1) }).toMatchObject({ k: new Counter(2) }); + expect([new Counter(1)]).toMatchObject([new Counter(2)]); + expect(new Holder(1)).toMatchObject(new Holder(2)); + expect({ name: "c" }).toMatchObject(new Counter(1)); + // the other properties are compared as before + const other = new Counter(1); + other.name = "d"; + expect(new Counter(1)).not.toMatchObject(other); + expect({}).not.toMatchObject(new Counter(1)); + }); + + test("an expected array needs an array", () => { + expect({}).not.toMatchObject([]); + expect({ k: {} }).not.toMatchObject({ k: [] }); + expect({ 0: 1, length: 1 }).not.toMatchObject([1]); + expect(new Uint8Array([1])).not.toMatchObject([1]); + const arrayLike = Object.create(Array.prototype); + arrayLike[0] = 1; + arrayLike.length = 1; + expect(arrayLike).not.toMatchObject([1]); + // an expected object can be satisfied by an array + expect([1]).toMatchObject({ 0: 1 }); + expect([]).toMatchObject({}); + // an array without a prototype and an array of another realm are arrays + expect(Object.setPrototypeOf(["x"], null)).toMatchObject(["x"]); + const otherRealm = () => runInNewContext("[1, { a: 1 }]"); + expect(otherRealm()).toMatchObject([1, { a: 1 }]); + expect([1, { a: 1 }]).toMatchObject(otherRealm()); + expect({ k: otherRealm() }).toMatchObject({ k: [1, { a: 1 }] }); + expect(otherRealm()).not.toMatchObject([1, { a: 2 }]); + }); + + if (isBun) { + test("Bun.deepMatch leaves out private fields, and an array in the subset needs an array", () => { + class Counter { + #n; + constructor(/** @type {number} */ n) { + this.#n = n; + } + get n() { + return this.#n; + } + } + expect(Bun.deepMatch(new Counter(2), new Counter(1))).toBe(true); + expect(Bun.deepMatch({ k: [new Counter(2)] }, { k: [new Counter(1)] })).toBe(true); + expect(Bun.deepMatch([], {})).toBe(false); + expect(Bun.deepMatch([1], { 0: 1, length: 1 })).toBe(false); + expect(Bun.deepMatch({ 0: 1 }, [1])).toBe(true); + expect(Bun.deepMatch(["x"], Object.setPrototypeOf(["x"], null))).toBe(true); + expect(Bun.deepMatch([1, 2], runInNewContext("[1, 2]"))).toBe(true); + }); + } }); describe("toMatch()", () => { diff --git a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts index 41bbca2fcd5a..f016d0ac03bb 100644 --- a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts +++ b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts @@ -33,6 +33,34 @@ test("ArrayBuffer values are serialized like typed arrays", () => { `); }); +test("property matchers leave out the private fields of a class", () => { + class Counter { + #n; + name = "c"; + constructor(n: number) { + this.#n = n; + } + get n() { + return this.#n; + } + } + expect({ k: new Counter(1), id: 5 }).toMatchInlineSnapshot( + { k: new Counter(2), id: expect.any(Number) }, + ` + { + "id": Any, + "k": Counter { + "name": "c", + }, + } + `, + ); + // the other properties of the instance are still compared + expect(() => { + expect({ k: new Counter(1) }).toMatchSnapshot({ k: Object.assign(new Counter(1), { name: "x" }) }); + }).toThrow("to match properties from received object"); +}); + describe("toMatchSnapshot errors", () => { it("should throw if property matchers exist and received is not an object", () => { expect(() => {