Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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()) {
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
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()) {
Expand Down
76 changes: 76 additions & 0 deletions test/js/bun/test/expect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()", () => {
Expand Down
28 changes: 28 additions & 0 deletions test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Number>,
"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(() => {
Expand Down
Loading