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(); const seen = new Map();
return (function compare(a: unknown, b: unknown): boolean { return (function compare(a: unknown, b: unknown): boolean {
const asymmetric = asymmetricEqual(a, b);
if (asymmetric !== undefined) {
return asymmetric;
}
if (customTesters?.length) { if (customTesters?.length) {
for (const customTester of customTesters) { for (const customTester of customTesters) {
const testContext = { const testContext = {
@ -67,11 +72,6 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
return String(a) === String(b); return String(a) === String(b);
} }
const asymmetric = asymmetricEqual(a, b);
if (asymmetric !== undefined) {
return asymmetric;
}
if (a instanceof Date && b instanceof Date) { if (a instanceof Date && b instanceof Date) {
const aTime = a.getTime(); const aTime = a.getTime();
const bTime = b.getTime(); const bTime = b.getTime();
@ -119,6 +119,10 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
let aLen = aKeys.length; let aLen = aKeys.length;
let bLen = bKeys.length; let bLen = bKeys.length;
if (strictCheck && aLen !== bLen) {
return false;
}
if (!strictCheck) { if (!strictCheck) {
if (aLen > 0) { if (aLen > 0) {
for (let i = 0; i < aKeys.length; i += 1) { 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); seen.set(a, b);
if (isKeyedCollection(a) && isKeyedCollection(b)) { if (isKeyedCollection(a) && isKeyedCollection(b)) {
if (a.size !== b.size) { 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 { assertIsError } from "@std/assert/is-error";
import { assertNotInstanceOf } from "@std/assert/not-instance-of"; import { assertNotInstanceOf } from "@std/assert/not-instance-of";
import { assertMatch } from "@std/assert/match"; import { assertMatch } from "@std/assert/match";
import { assertObjectMatch } from "@std/assert/object-match";
import { assertNotMatch } from "@std/assert/not-match"; import { assertNotMatch } from "@std/assert/not-match";
import { AssertionError } from "@std/assert/assertion-error"; 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 type { AnyConstructor, MatcherContext, MatchResult } from "./_types.ts";
import { getMockCalls } from "./_mock_util.ts"; import { getMockCalls } from "./_mock_util.ts";
import { inspectArg, inspectArgs } from "./_inspect_args.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 { export function toBe(context: MatcherContext, expect: unknown): MatchResult {
if (context.isNot) { if (context.isNot) {
@ -462,34 +465,35 @@ export function toMatchObject(
context: MatcherContext, context: MatcherContext,
expected: Record<PropertyKey, unknown> | Record<PropertyKey, unknown>[], expected: Record<PropertyKey, unknown> | Record<PropertyKey, unknown>[],
): MatchResult { ): MatchResult {
if (context.isNot) { const received = context.value;
let objectMatch = false;
try { if (typeof received !== "object" || received === null) {
assertObjectMatch( throw new AssertionError("Received value must be an object");
// deno-lint-ignore no-explicit-any }
context.value as Record<PropertyKey, any>,
expected as Record<PropertyKey, unknown>, if (typeof expected !== "object" || expected === null) {
context.customMessage, throw new AssertionError("Received value must be an object");
); }
objectMatch = true;
const actualString = format(context.value); const pass = equal(context.value, expected, {
const expectedString = format(expected); strictCheck: false,
throw new AssertionError( customTesters: [
`Expected ${actualString} to NOT match ${expectedString}`, ...context.customTesters,
); iterableEquality,
} catch (e) { subsetEquality,
if (objectMatch) { ],
throw e; });
}
return; const triggerError = () => {
} const actualString = format(context.value);
} else { const expectedString = format(expected);
assertObjectMatch( throw new AssertionError(
// deno-lint-ignore no-explicit-any `Expected ${actualString} to NOT match ${expectedString}`,
context.value as Record<PropertyKey, any>,
expected as Record<PropertyKey, unknown>,
context.customMessage,
); );
};
if (context.isNot && pass || !context.isNot && !pass) {
triggerError();
} }
} }

View File

@ -50,3 +50,21 @@ Deno.test("expect().toMatchObject()", () => {
expect([house0]).not.toMatchObject([desiredHouse]); expect([house0]).not.toMatchObject([desiredHouse]);
}, AssertionError); }, 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"; 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 // deno-lint-ignore no-explicit-any
function entries(obj: any) { function entries(obj: any) {
if (!isObject(obj)) return []; if (!isObject(obj)) return [];
@ -199,3 +233,51 @@ export function iterableEquality(
bStack.pop(); bStack.pop();
return true; 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);
}