2024-01-01 21:11:32 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2024-04-10 02:43:44 +00:00
|
|
|
// This module is browser compatible.
|
2023-07-13 07:04:30 +00:00
|
|
|
import { assertEquals } from "./assert_equals.ts";
|
|
|
|
|
|
|
|
/**
|
2023-12-06 17:13:38 +00:00
|
|
|
* Make an assertion that `actual` object is a subset of `expected` object,
|
|
|
|
* deeply. If not, then throw.
|
|
|
|
*
|
2024-05-30 02:38:16 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts no-eval
|
2024-04-29 02:57:30 +00:00
|
|
|
* import { assertObjectMatch } from "@std/assert/assert-object-match";
|
2023-12-06 17:13:38 +00:00
|
|
|
*
|
|
|
|
* assertObjectMatch({ foo: "bar" }, { foo: "bar" }); // Doesn't throw
|
|
|
|
* assertObjectMatch({ foo: "bar" }, { foo: "baz" }); // Throws
|
|
|
|
* ```
|
2024-05-30 02:38:16 +00:00
|
|
|
*
|
|
|
|
* @param actual The actual value to be matched.
|
|
|
|
* @param expected The expected value to match.
|
|
|
|
* @param msg The optional message to display if the assertion fails.
|
2023-07-13 07:04:30 +00:00
|
|
|
*/
|
|
|
|
export function assertObjectMatch(
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
actual: Record<PropertyKey, any>,
|
|
|
|
expected: Record<PropertyKey, unknown>,
|
|
|
|
msg?: string,
|
2023-12-06 17:13:38 +00:00
|
|
|
): void {
|
2023-07-13 07:04:30 +00:00
|
|
|
type loose = Record<PropertyKey, unknown>;
|
|
|
|
|
|
|
|
function filter(a: loose, b: loose) {
|
|
|
|
const seen = new WeakMap();
|
|
|
|
return fn(a, b);
|
|
|
|
|
|
|
|
function fn(a: loose, b: loose): loose {
|
|
|
|
// Prevent infinite loop with circular references with same filter
|
|
|
|
if ((seen.has(a)) && (seen.get(a) === b)) {
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
seen.set(a, b);
|
|
|
|
} catch (err) {
|
|
|
|
if (err instanceof TypeError) {
|
|
|
|
throw new TypeError(
|
|
|
|
`Cannot assertObjectMatch ${
|
|
|
|
a === null ? null : `type ${typeof a}`
|
|
|
|
}`,
|
|
|
|
);
|
2024-05-07 00:08:16 +00:00
|
|
|
}
|
2023-07-13 07:04:30 +00:00
|
|
|
}
|
|
|
|
// Filter keys and symbols which are present in both actual and expected
|
|
|
|
const filtered = {} as loose;
|
|
|
|
const entries = [
|
|
|
|
...Object.getOwnPropertyNames(a),
|
|
|
|
...Object.getOwnPropertySymbols(a),
|
|
|
|
]
|
|
|
|
.filter((key) => key in b)
|
|
|
|
.map((key) => [key, a[key as string]]) as Array<[string, unknown]>;
|
|
|
|
for (const [key, value] of entries) {
|
|
|
|
// On array references, build a filtered array and filter nested objects inside
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
const subset = (b as loose)[key];
|
|
|
|
if (Array.isArray(subset)) {
|
|
|
|
filtered[key] = fn({ ...value }, { ...subset });
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} // On regexp references, keep value as it to avoid loosing pattern and flags
|
|
|
|
else if (value instanceof RegExp) {
|
|
|
|
filtered[key] = value;
|
|
|
|
continue;
|
|
|
|
} // On nested objects references, build a filtered object recursively
|
|
|
|
else if (typeof value === "object" && value !== null) {
|
|
|
|
const subset = (b as loose)[key];
|
2023-08-10 09:04:05 +00:00
|
|
|
if ((typeof subset === "object") && subset) {
|
2023-07-13 07:04:30 +00:00
|
|
|
// When both operands are maps, build a filtered map with common keys and filter nested objects inside
|
|
|
|
if ((value instanceof Map) && (subset instanceof Map)) {
|
|
|
|
filtered[key] = new Map(
|
|
|
|
[...value].filter(([k]) => subset.has(k)).map((
|
|
|
|
[k, v],
|
|
|
|
) => [k, typeof v === "object" ? fn(v, subset.get(k)) : v]),
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// When both operands are set, build a filtered set with common values
|
|
|
|
if ((value instanceof Set) && (subset instanceof Set)) {
|
|
|
|
filtered[key] = new Set([...value].filter((v) => subset.has(v)));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
filtered[key] = fn(value as loose, subset as loose);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filtered[key] = value;
|
|
|
|
}
|
|
|
|
return filtered;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return assertEquals(
|
|
|
|
// get the intersection of "actual" and "expected"
|
|
|
|
// side effect: all the instances' constructor field is "Object" now.
|
|
|
|
filter(actual, expected),
|
|
|
|
// set (nested) instances' constructor field to be "Object" without changing expected value.
|
|
|
|
// see https://github.com/denoland/deno_std/pull/1419
|
|
|
|
filter(expected, expected),
|
|
|
|
msg,
|
|
|
|
);
|
|
|
|
}
|