fix(expect): re-align expect.toMatchObject api (#6160)

This commit is contained in:
eryue0220 2024-11-01 18:19:57 +08:00 committed by GitHub
parent 689fb69c14
commit 32b4fb62d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 142 additions and 37 deletions

View File

@ -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 = {
@ -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();
@ -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) {
@ -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) {

View File

@ -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";
@ -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) {
@ -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();
}
}

View File

@ -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),
},
});
});

View File

@ -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 [];
@ -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);
}