Skip to content

Commit

Permalink
fix(expect): re-align expect.toMatchObject api (#6160)
Browse files Browse the repository at this point in the history
  • Loading branch information
eryue0220 authored Nov 1, 2024
1 parent 689fb69 commit 32b4fb6
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 37 deletions.
17 changes: 9 additions & 8 deletions expect/_equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
const seen = new Map();

return (function compare(a: unknown, b: unknown): boolean {
const asymmetric = asymmetricEqual(a, b);
if (asymmetric !== undefined) {
return asymmetric;
}

if (customTesters?.length) {
for (const customTester of customTesters) {
const testContext = {
Expand All @@ -67,11 +72,6 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
return String(a) === String(b);
}

const asymmetric = asymmetricEqual(a, b);
if (asymmetric !== undefined) {
return asymmetric;
}

if (a instanceof Date && b instanceof Date) {
const aTime = a.getTime();
const bTime = b.getTime();
Expand Down Expand Up @@ -119,6 +119,10 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
let aLen = aKeys.length;
let bLen = bKeys.length;

if (strictCheck && aLen !== bLen) {
return false;
}

if (!strictCheck) {
if (aLen > 0) {
for (let i = 0; i < aKeys.length; i += 1) {
Expand All @@ -145,9 +149,6 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
}
}

if (aLen !== bLen) {
return false;
}
seen.set(a, b);
if (isKeyedCollection(a) && isKeyedCollection(b)) {
if (a.size !== b.size) {
Expand Down
62 changes: 33 additions & 29 deletions expect/_matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { assertInstanceOf } from "@std/assert/instance-of";
import { assertIsError } from "@std/assert/is-error";
import { assertNotInstanceOf } from "@std/assert/not-instance-of";
import { assertMatch } from "@std/assert/match";
import { assertObjectMatch } from "@std/assert/object-match";
import { assertNotMatch } from "@std/assert/not-match";
import { AssertionError } from "@std/assert/assertion-error";

Expand All @@ -17,7 +16,11 @@ import { format } from "@std/internal/format";
import type { AnyConstructor, MatcherContext, MatchResult } from "./_types.ts";
import { getMockCalls } from "./_mock_util.ts";
import { inspectArg, inspectArgs } from "./_inspect_args.ts";
import { buildEqualOptions, iterableEquality } from "./_utils.ts";
import {
buildEqualOptions,
iterableEquality,
subsetEquality,
} from "./_utils.ts";

export function toBe(context: MatcherContext, expect: unknown): MatchResult {
if (context.isNot) {
Expand Down Expand Up @@ -462,34 +465,35 @@ export function toMatchObject(
context: MatcherContext,
expected: Record<PropertyKey, unknown> | Record<PropertyKey, unknown>[],
): MatchResult {
if (context.isNot) {
let objectMatch = false;
try {
assertObjectMatch(
// deno-lint-ignore no-explicit-any
context.value as Record<PropertyKey, any>,
expected as Record<PropertyKey, unknown>,
context.customMessage,
);
objectMatch = true;
const actualString = format(context.value);
const expectedString = format(expected);
throw new AssertionError(
`Expected ${actualString} to NOT match ${expectedString}`,
);
} catch (e) {
if (objectMatch) {
throw e;
}
return;
}
} else {
assertObjectMatch(
// deno-lint-ignore no-explicit-any
context.value as Record<PropertyKey, any>,
expected as Record<PropertyKey, unknown>,
context.customMessage,
const received = context.value;

if (typeof received !== "object" || received === null) {
throw new AssertionError("Received value must be an object");
}

if (typeof expected !== "object" || expected === null) {
throw new AssertionError("Received value must be an object");
}

const pass = equal(context.value, expected, {
strictCheck: false,
customTesters: [
...context.customTesters,
iterableEquality,
subsetEquality,
],
});

const triggerError = () => {
const actualString = format(context.value);
const expectedString = format(expected);
throw new AssertionError(
`Expected ${actualString} to NOT match ${expectedString}`,
);
};

if (context.isNot && pass || !context.isNot && !pass) {
triggerError();
}
}

Expand Down
18 changes: 18 additions & 0 deletions expect/_to_match_object_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,21 @@ Deno.test("expect().toMatchObject()", () => {
expect([house0]).not.toMatchObject([desiredHouse]);
}, AssertionError);
});

Deno.test("expect().toMatchObject() with array", () => {
const fixedPriorityQueue = Array.from({ length: 5 });
fixedPriorityQueue[0] = { data: 1, priority: 0 };

expect(fixedPriorityQueue).toMatchObject([
{ data: 1, priority: 0 },
]);
});

Deno.test("expect(),toMatchObject() with asyAsymmetric matcher", () => {
expect({ position: { x: 0, y: 0 } }).toMatchObject({
position: {
x: expect.any(Number),
y: expect.any(Number),
},
});
});
82 changes: 82 additions & 0 deletions expect/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,40 @@ function isObject(a: unknown) {
return a !== null && typeof a === "object";
}

function isObjectWithKeys(a: unknown) {
return (
isObject(a) &&
!(a instanceof Error) &&
!Array.isArray(a) &&
!(a instanceof Date) &&
!(a instanceof Set) &&
!(a instanceof Map)
);
}

function getObjectKeys(object: object): Array<string | symbol> {
return [
...Object.keys(object),
...Object.getOwnPropertySymbols(object).filter(
(s) => Object.getOwnPropertyDescriptor(object, s)?.enumerable,
),
];
}

function hasPropertyInObject(object: object, key: string | symbol): boolean {
const shouldTerminate = !object || typeof object !== "object" ||
object === Object.prototype;

if (shouldTerminate) {
return false;
}

return (
Object.prototype.hasOwnProperty.call(object, key) ||
hasPropertyInObject(Object.getPrototypeOf(object), key)
);
}

// deno-lint-ignore no-explicit-any
function entries(obj: any) {
if (!isObject(obj)) return [];
Expand Down Expand Up @@ -199,3 +233,51 @@ export function iterableEquality(
bStack.pop();
return true;
}

// Ported from https://github.com/jestjs/jest/blob/442c7f692e3a92f14a2fb56c1737b26fc663a0ef/packages/expect-utils/src/utils.ts#L341
export function subsetEquality(
object: unknown,
subset: unknown,
customTesters: Tester[] = [],
): boolean | undefined {
const filteredCustomTesters = customTesters.filter((t) =>
t !== subsetEquality
);

const subsetEqualityWithContext =
(seenReferences: WeakMap<object, boolean> = new WeakMap()) =>
// deno-lint-ignore no-explicit-any
(object: any, subset: any): boolean | undefined => {
if (!isObjectWithKeys(subset)) {
return undefined;
}

if (seenReferences.has(subset)) return undefined;
seenReferences.set(subset, true);

const matchResult = getObjectKeys(subset).every((key) => {
if (isObjectWithKeys(subset[key])) {
if (seenReferences.has(subset[key])) {
return equal(object[key], subset[key], {
customTesters: filteredCustomTesters,
});
}
}
const result = object != null &&
hasPropertyInObject(object, key) &&
equal(object[key], subset[key], {
customTesters: [
...filteredCustomTesters,
subsetEqualityWithContext(seenReferences),
],
});
seenReferences.delete(subset[key]);
return result;
});
seenReferences.delete(subset);

return matchResult;
};

return subsetEqualityWithContext()(object, subset);
}

0 comments on commit 32b4fb6

Please sign in to comment.