From 7788a21bf043ed93ffe240dee60f7ee65625a1ad Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Wed, 1 Apr 2026 21:52:25 -0700 Subject: [PATCH 1/2] fix(expect): scope toMatchObject diff to only keys present in expected --- expect/_matchers.ts | 4 ++- expect/_to_match_object_test.ts | 17 +++++++++++ expect/_utils.ts | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/expect/_matchers.ts b/expect/_matchers.ts index 3c5d455e2804..3c74bf0b7047 100644 --- a/expect/_matchers.ts +++ b/expect/_matchers.ts @@ -21,6 +21,7 @@ import { getMockCalls } from "./_mock_util.ts"; import { inspectArg, inspectArgs } from "./_inspect_args.ts"; import { buildEqualOptions, + getObjectSubset, iterableEquality, subsetEquality, } from "./_utils.ts"; @@ -601,7 +602,8 @@ export function toMatchObject( : defaultMessage, ); } else { - const defaultMessage = buildEqualErrorMessage(received, expected); + const subset = getObjectSubset(received, expected, context.customTesters); + const defaultMessage = buildEqualErrorMessage(subset, expected); throw new AssertionError( context.customMessage ? `${context.customMessage}: ${defaultMessage}` diff --git a/expect/_to_match_object_test.ts b/expect/_to_match_object_test.ts index 4063c1ef8f24..e20607705bdd 100644 --- a/expect/_to_match_object_test.ts +++ b/expect/_to_match_object_test.ts @@ -227,4 +227,21 @@ Deno.test("expect().toMatchObject() displays a diff", async (t) => { ); }, ); + + await t.step("omits keys not in expected", () => { + const e = assertThrows( + () => + expect({ a: 1, b: 2, c: 3, d: { e: 4, f: 5 } }).toMatchObject({ + a: 1, + d: { e: 999 }, + }), + AssertionError, + ); + // The diff should mention the mismatched value + assertMatch(e.message, /999/); + // The diff should NOT mention keys absent from expected + assertNotMatch(e.message, /b:/); + assertNotMatch(e.message, /c:/); + assertNotMatch(e.message, /f:/); + }); }); diff --git a/expect/_utils.ts b/expect/_utils.ts index e7e0bc59d2ee..b43a35cbb284 100644 --- a/expect/_utils.ts +++ b/expect/_utils.ts @@ -281,3 +281,53 @@ export function subsetEquality( return subsetEqualityWithContext()(object, subset); } + +// Ported from https://github.com/jestjs/jest/blob/442c7f692e3a92f14a2fb56c1737b26fc663a0ef/packages/expect-utils/src/utils.ts#L82 +export function getObjectSubset( + object: unknown, + subset: unknown, + customTesters: Tester[] = [], + seenReferences: WeakMap = new WeakMap(), +): unknown { + if (Array.isArray(object)) { + if (Array.isArray(subset) && subset.length === object.length) { + return subset.map((_: unknown, i: number) => + getObjectSubset(object[i], subset[i], customTesters, seenReferences) + ); + } + } else if (object instanceof Date) { + return object; + } else if (isObject(object) && isObject(subset)) { + if ( + equal(object, subset, { + customTesters: [...customTesters, iterableEquality, subsetEquality], + }) + ) { + return subset; + } + + const obj = object as Record; + const sub = subset as Record; + const trimmed: Record = {}; + seenReferences.set(object as object, true); + + for (const key of Object.keys(obj)) { + if (!Object.prototype.hasOwnProperty.call(sub, key)) continue; + + const val = obj[key]; + if (typeof val === "object" && val !== null && seenReferences.has(val)) { + trimmed[key] = val; + } else { + trimmed[key] = getObjectSubset( + val, + sub[key], + customTesters, + seenReferences, + ); + } + } + + if (Object.keys(trimmed).length > 0) return trimmed; + } + return object; +} From 266771d9590837853be1166784d6b38f4aadbe6f Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Thu, 2 Apr 2026 16:22:59 +0000 Subject: [PATCH 2/2] test(expect): add coverage for getObjectSubset branches --- expect/_to_match_object_test.ts | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/expect/_to_match_object_test.ts b/expect/_to_match_object_test.ts index e20607705bdd..8b58b360a21f 100644 --- a/expect/_to_match_object_test.ts +++ b/expect/_to_match_object_test.ts @@ -244,4 +244,58 @@ Deno.test("expect().toMatchObject() displays a diff", async (t) => { assertNotMatch(e.message, /c:/); assertNotMatch(e.message, /f:/); }); + + await t.step("omits keys not in expected with arrays", () => { + const e = assertThrows( + () => + expect([{ a: 1, extra: true }, { b: 2 }]).toMatchObject([ + { a: 999 }, + { b: 2 }, + ]), + AssertionError, + ); + assertMatch(e.message, /999/); + assertNotMatch(e.message, /extra/); + }); + + await t.step("omits keys not in expected with Date values", () => { + const e = assertThrows( + () => + expect({ + d: new Date("2020-01-01"), + extra: "noise", + }).toMatchObject({ d: new Date("2025-01-01") }), + AssertionError, + ); + assertNotMatch(e.message, /noise/); + }); + + await t.step("omits keys not in expected with equal nested subset", () => { + const e = assertThrows( + () => + expect({ + a: { x: 1, y: 2 }, + b: 999, + extra: true, + }).toMatchObject({ a: { x: 1 }, b: 42 }), + AssertionError, + ); + // b is the mismatch + assertMatch(e.message, /999/); + assertMatch(e.message, /42/); + // extra top-level key should be omitted + assertNotMatch(e.message, /extra/); + // y should be omitted (not in expected.a) + assertNotMatch(e.message, /y:/); + }); + + await t.step("handles circular references without throwing", () => { + const received: Record = { a: 1 }; + received.self = received; + const e = assertThrows( + () => expect(received).toMatchObject({ a: 999, self: {} }), + AssertionError, + ); + assertMatch(e.message, /999/); + }); });