Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 1, 2024
Merged
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
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 { 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 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 @@
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");
}

Check warning on line 472 in expect/_matchers.ts

View check run for this annotation

Codecov / codecov/patch

expect/_matchers.ts#L471-L472

Added lines #L471 - L472 were not covered by tests

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

Check warning on line 476 in expect/_matchers.ts

View check run for this annotation

Codecov / codecov/patch

expect/_matchers.ts#L475-L476

Added lines #L475 - L476 were not covered by tests

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 @@
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,

Check warning on line 58 in expect/_utils.ts

View check run for this annotation

Codecov / codecov/patch

expect/_utils.ts#L57-L58

Added lines #L57 - L58 were not covered by tests
),
];
}

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

if (shouldTerminate) {
return false;
}

Check warning on line 69 in expect/_utils.ts

View check run for this annotation

Codecov / codecov/patch

expect/_utils.ts#L68-L69

Added lines #L68 - L69 were not covered by tests

return (

Check warning on line 71 in expect/_utils.ts

View check run for this annotation

Codecov / codecov/patch

expect/_utils.ts#L71

Added line #L71 was not covered by tests
Object.prototype.hasOwnProperty.call(object, key) ||
hasPropertyInObject(Object.getPrototypeOf(object), key)

Check warning on line 73 in expect/_utils.ts

View check run for this annotation

Codecov / codecov/patch

expect/_utils.ts#L73

Added line #L73 was not covered by tests
);
}

// deno-lint-ignore no-explicit-any
function entries(obj: any) {
if (!isObject(obj)) return [];
Expand Down Expand Up @@ -199,3 +233,51 @@
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,
});
}

Check warning on line 264 in expect/_utils.ts

View check run for this annotation

Codecov / codecov/patch

expect/_utils.ts#L261-L264

Added lines #L261 - L264 were not covered by tests
}
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);
}