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
13 changes: 13 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,19 @@ bool Bun__deepMatch(
JSObject* obj = objValue.getObject();
JSObject* subsetObj = subsetValue.getObject();

// The property walk below cannot see the entries of Headers/URLSearchParams/URL; compare those first.
uint8_t subsetType = subsetObj->type();
if ((subsetType == JSDOMWrapperType || subsetType == JSAsJSONType) && obj->type() == subsetType) {
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer contentsGCBuffer;
std::optional<bool> contentsEqual = specialObjectsDequal<false, enableAsymmetricMatchers, false>(globalObject, contentsGCBuffer, stack, throwScope, subsetObj, obj);
RETURN_IF_EXCEPTION(throwScope, false);
if (contentsEqual.has_value()) return *contentsEqual;
contentsEqual = specialObjectsDequal<false, enableAsymmetricMatchers, false>(globalObject, contentsGCBuffer, stack, throwScope, obj, subsetObj);
RETURN_IF_EXCEPTION(throwScope, false);
if (contentsEqual.has_value()) return *contentsEqual;
}

PropertyNameArrayBuilder subsetProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include);
subsetObj->getPropertyNames(globalObject, subsetProps, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(throwScope, false);
Expand Down
14 changes: 14 additions & 0 deletions test/js/bun/bun-object/deep-match.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ describe("Bun.deepMatch", () => {
new Set(["a", "b", "c"]),
new Set(["a", "b", "c"]),
],

// Headers / URLSearchParams are compared by their entries
[new Headers({ a: "1", b: "2" }), new Headers({ b: "2", a: "1" })],
[{ headers: new Headers({ a: "1" }) }, { headers: new Headers({ a: "1" }), status: 200 }],
[new URLSearchParams("a=1&b=2"), new URLSearchParams("a=1&b=2")],
[{ query: new URLSearchParams("a=1") }, { query: new URLSearchParams("a=1"), path: "/" }],
])("Bun.deepMatch(%p, %p) === true", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(true);
});
Expand Down Expand Up @@ -133,6 +139,14 @@ describe("Bun.deepMatch", () => {
// new Set(["a", "b", "c"]),
// new Set(["a", "b", "d"]),
// ],

// Headers / URLSearchParams: every entry has to match, the entries themselves are not subset-matched
[new Headers({ a: "1" }), new Headers({ a: "2" })],
[{ headers: new Headers({ a: "1" }) }, { headers: new Headers({ b: "1" }), status: 200 }],
[{ headers: new Headers({ a: "1" }) }, { headers: new Headers({ a: "1", b: "2" }) }],
[new URLSearchParams("a=1"), new URLSearchParams("a=2")],
[{ query: new URLSearchParams("a=1") }, { query: new URLSearchParams("b=1"), path: "/" }],
[{ query: new URLSearchParams("a=1") }, { query: new URLSearchParams("a=1&b=2") }],
])("Bun.deepMatch(%p, %p) === false", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});
Expand Down
95 changes: 95 additions & 0 deletions test/js/bun/test/expect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3340,6 +3340,101 @@ describe("expect()", () => {
expect(Bun.deepMatch({ a: 1 }, { a: 1, b: 2 })).toBe(true);
});
}

// Headers and URLSearchParams have no own properties: their entries live in
// native code, so an expected value of one of these types has to be compared
// by its entries (all of them, like toEqual), not by property walking.
test("Headers on the expected side are compared by their entries", () => {
expect({ h: new Headers({ a: "1" }) }).toMatchObject({ h: new Headers({ a: "1" }) });
expect({ h: new Headers({ a: "1", b: "2" }) }).toMatchObject({ h: new Headers({ b: "2", a: "1" }) });
expect({ h: new Headers({ a: "1" }) }).toMatchObject({ h: expect.any(Headers) });
expect({ h: new Headers({ a: "1" }), other: 1 }).toMatchObject({ h: new Headers({ a: "1" }) });

expect({ h: new Headers({ a: "1" }) }).not.toMatchObject({ h: new Headers({ a: "2" }) });
expect({ h: new Headers({ a: "1" }) }).not.toMatchObject({ h: new Headers({ b: "1" }) });
expect({ h: new Headers({ a: "1" }) }).not.toMatchObject({ h: new Headers({ a: "1", b: "2" }) });
expect({ h: new Headers({ a: "1", b: "2" }) }).not.toMatchObject({ h: new Headers({ a: "1" }) });
expect({ h: new Headers({ a: "1" }) }).not.toMatchObject({ h: new Headers() });
expect({ h: new Headers() }).not.toMatchObject({ h: new Headers({ a: "1" }) });

expect({ res: { headers: new Headers({ a: "1" }) } }).toMatchObject({
res: { headers: new Headers({ a: "1" }) },
});
expect({ res: { headers: new Headers({ a: "1" }) } }).not.toMatchObject({
res: { headers: new Headers({ a: "2" }) },
});

expect([new Headers({ a: "1" })]).toMatchObject([new Headers({ a: "1" })]);
expect([new Headers({ a: "1" })]).not.toMatchObject([new Headers({ a: "2" })]);

expect(new Headers({ a: "1" })).toMatchObject(new Headers({ a: "1" }));
expect(new Headers({ a: "1" })).not.toMatchObject(new Headers({ a: "2" }));
});

test("URLSearchParams on the expected side are compared by their entries", () => {
expect({ p: new URLSearchParams("a=1") }).toMatchObject({ p: new URLSearchParams("a=1") });
expect({ p: new URLSearchParams("a=1&b=2") }).toMatchObject({ p: new URLSearchParams("a=1&b=2") });
expect({ p: new URLSearchParams("a=1") }).toMatchObject({ p: expect.any(URLSearchParams) });

expect({ p: new URLSearchParams("a=1") }).not.toMatchObject({ p: new URLSearchParams("a=2") });
expect({ p: new URLSearchParams("a=1") }).not.toMatchObject({ p: new URLSearchParams("b=1") });
expect({ p: new URLSearchParams("a=1") }).not.toMatchObject({ p: new URLSearchParams("a=1&b=2") });
expect({ p: new URLSearchParams("a=1&b=2") }).not.toMatchObject({ p: new URLSearchParams("a=1") });

expect({ req: { query: new URLSearchParams("a=1") } }).toMatchObject({
req: { query: new URLSearchParams("a=1") },
});
expect({ req: { query: new URLSearchParams("a=1") } }).not.toMatchObject({
req: { query: new URLSearchParams("a=2") },
});

expect([new URLSearchParams("a=1")]).toMatchObject([new URLSearchParams("a=1")]);
expect([new URLSearchParams("a=1")]).not.toMatchObject([new URLSearchParams("a=2")]);

expect(new URLSearchParams("a=1")).toMatchObject(new URLSearchParams("a=1"));
expect(new URLSearchParams("a=1")).not.toMatchObject(new URLSearchParams("a=2"));
});

if (isBun) {
test("URL on the expected side is compared by href", () => {
expect({ u: new URL("https://example.com/a") }).toMatchObject({ u: new URL("https://example.com/a") });
expect({ u: new URL("https://example.com/a") }).not.toMatchObject({ u: new URL("https://example.com/b") });
});

test("Bun.deepMatch compares Headers and URLSearchParams by their entries", () => {
expect(Bun.deepMatch({ h: new Headers({ a: "1" }) }, { h: new Headers({ a: "1" }) })).toBe(true);
expect(Bun.deepMatch({ h: new Headers({ a: "1" }) }, { h: new Headers({ a: "2" }) })).toBe(false);
expect(Bun.deepMatch({ h: new Headers({ a: "1" }) }, { h: new Headers({ a: "1", b: "2" }) })).toBe(false);
expect(Bun.deepMatch(new Headers({ a: "1" }), new Headers({ a: "1" }))).toBe(true);
expect(Bun.deepMatch(new Headers({ a: "1" }), new Headers({ a: "2" }))).toBe(false);

expect(Bun.deepMatch({ p: new URLSearchParams("a=1") }, { p: new URLSearchParams("a=1") })).toBe(true);
expect(Bun.deepMatch({ p: new URLSearchParams("a=1") }, { p: new URLSearchParams("a=2") })).toBe(false);
// same size and every entry of one side is found on the other side, but not the other way around
expect(Bun.deepMatch({ p: new URLSearchParams("a=1&b=1") }, { p: new URLSearchParams("a=1&a=1") })).toBe(false);
expect(Bun.deepMatch({ p: new URLSearchParams("a=1&a=1") }, { p: new URLSearchParams("a=1&b=1") })).toBe(false);
expect(Bun.deepMatch(new URLSearchParams("a=1"), new URLSearchParams("a=1"))).toBe(true);
expect(Bun.deepMatch(new URLSearchParams("a=1"), new URLSearchParams("a=2"))).toBe(false);
});

test("snapshot property matchers compare Headers and URLSearchParams by their entries", () => {
expect(() =>
expect({ h: new Headers({ a: "1" }) }).toMatchInlineSnapshot({ h: new Headers({ a: "2" }) }, `unreachable`),
).toThrow("to match properties from received object");
expect(() =>
expect({ p: new URLSearchParams("a=1") }).toMatchInlineSnapshot(
{ p: new URLSearchParams("a=2") },
`unreachable`,
),
).toThrow("to match properties from received object");
});

test("expect.objectContaining with a Headers sample matches by its entries", () => {
expect(new Headers({ a: "1" })).toEqual(expect.objectContaining(new Headers({ a: "1" })));
expect(new Headers({ a: "1" })).not.toEqual(expect.objectContaining(new Headers({ a: "2" })));
});
}

test("with expect matcher", () => {
const f = Symbol.for("foo");
const b = Symbol.for("bar");
Expand Down
Loading