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
4 changes: 3 additions & 1 deletion expect/_matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}`
Expand Down
71 changes: 71 additions & 0 deletions expect/_to_match_object_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,75 @@ 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:/);
});

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<string, unknown> = { a: 1 };
received.self = received;
const e = assertThrows(
() => expect(received).toMatchObject({ a: 999, self: {} }),
AssertionError,
);
assertMatch(e.message, /999/);
});
});
50 changes: 50 additions & 0 deletions expect/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, boolean> = 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<string, unknown>;
const sub = subset as Record<string, unknown>;
const trimmed: Record<string, unknown> = {};
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;
}
Loading