diff --git a/assert/object_match.ts b/assert/object_match.ts index c7a38378e..67990dfa2 100644 --- a/assert/object_match.ts +++ b/assert/object_match.ts @@ -49,7 +49,7 @@ function isObject(val: unknown): boolean { return typeof val === "object" && val !== null; } -function filter(a: loose, b: loose) { +function filter(a: loose, b: loose): loose { const seen = new WeakMap(); return filterObject(a, b); @@ -97,7 +97,7 @@ function filter(a: loose, b: loose) { // On array references, build a filtered array and filter nested objects inside if (Array.isArray(value) && Array.isArray(subset)) { - filtered[key] = filterObject({ ...value }, { ...subset }); + filtered[key] = filterArray(value, subset); continue; } @@ -135,4 +135,66 @@ function filter(a: loose, b: loose) { return filtered; } + + function filterArray(a: unknown[], b: unknown[]): unknown[] { + // Prevent infinite loop with circular references with same filter + if (seen.has(a) && (seen.get(a) === b)) { + return a; + } + + seen.set(a, b); + + const filtered: unknown[] = []; + const count = Math.min(a.length, b.length); + + for (let i = 0; i < count; ++i) { + const value = a[i]; + const subset = b[i]; + + // On regexp references, keep value as it to avoid loosing pattern and flags + if (value instanceof RegExp) { + filtered.push(value); + continue; + } + + // On array references, build a filtered array and filter nested objects inside + if (Array.isArray(value) && Array.isArray(subset)) { + filtered.push(filterArray(value, subset)); + continue; + } + + // On nested objects references, build a filtered object recursively + if (isObject(value) && isObject(subset)) { + // When both operands are maps, build a filtered map with common keys and filter nested objects inside + if ((value instanceof Map) && (subset instanceof Map)) { + const map = new Map( + [...value].filter(([k]) => subset.has(k)) + .map(([k, v]) => { + const v2 = subset.get(k); + if (isObject(v) && isObject(v2)) { + return [k, filterObject(v as loose, v2 as loose)]; + } + + return [k, v]; + }), + ); + filtered.push(map); + continue; + } + + // When both operands are set, build a filtered set with common values + if ((value instanceof Set) && (subset instanceof Set)) { + filtered.push(value.intersection(subset)); + continue; + } + + filtered.push(filterObject(value as loose, subset as loose)); + continue; + } + + filtered.push(value); + } + + return filtered; + } } diff --git a/assert/object_match_test.ts b/assert/object_match_test.ts index de2d8d4db..98a311a68 100644 --- a/assert/object_match_test.ts +++ b/assert/object_match_test.ts @@ -31,6 +31,11 @@ const n = { ["b", b], ]), }; +const o = { foo: [new Map([["bar", n.bar], ["baz", null]])] }; +const p = { bar: [new Set([1, 2, 3])] }; +const q = { foo: [1, 2] as unknown[] }; +q.foo[2] = q.foo; +const r = { bar: [[1, [2, [q]]]] }; Deno.test("assertObjectMatch() matches simple subset", () => { assertObjectMatch(a, { @@ -79,6 +84,7 @@ Deno.test("assertObjectMatch() matches subset with circular reference", () => { }, }, }); + assertObjectMatch(q, { foo: [1, 2, [1, 2, [1, 2, [1, 2]]]] }); }); Deno.test("assertObjectMatch() matches subset with interface", () => { @@ -105,6 +111,7 @@ Deno.test("assertObjectMatch() matches subset with nested array inside", () => { assertObjectMatch(j, { foo: [[1, 2, 3]] }); assertObjectMatch(k, { foo: [[1, [2, [3]]]] }); assertObjectMatch(l, { foo: [[1, [2, [a, e, j, k]]]] }); + assertObjectMatch(r, { bar: [[1, [2, [q]]]] }); }); Deno.test("assertObjectMatch() matches subset with regexp", () => { @@ -117,6 +124,9 @@ Deno.test("assertObjectMatch() matches subset with built-in data structures", () assertObjectMatch(n, { bar: new Map([["bar", 2]]) }); assertObjectMatch(n, { baz: new Map([["b", b]]) }); assertObjectMatch(n, { baz: new Map([["b", { foo: true }]]) }); + assertObjectMatch(o, { foo: [new Map([["baz", null]])] }); + assertObjectMatch(o, { foo: [new Map([["bar", n.bar]])] }); + assertObjectMatch(p, { bar: [new Set([2, 3])] }); }); Deno.test("assertObjectMatch() throws when a key is missing from subset", () => { @@ -341,6 +351,10 @@ Deno.test("assertObjectMatch() prints inputs correctly", () => { description: "foo", }, name: "somegroup", + nodes: [ + "somenode", + "someothernode", + ], }, }; @@ -356,9 +370,46 @@ Deno.test("assertObjectMatch() prints inputs correctly", () => { + description: "foo", + }, + name: "somegroup", ++ nodes: [ ++ "somenode", ++ "someothernode", ++ ], - message: "NodeNotFound", }, protocol: "graph", }`, ); + + assertThrows( + () => assertObjectMatch({ foo: [] }, { foo: ["bar"] }), + AssertionError, + ` { ++ foo: [ ++ "bar", ++ ], +- foo: [], + }`, + ); + + const a = {}; + const b = {}; + + Object.defineProperty(a, "hello", { + value: "world", + enumerable: false, + }); + + Object.defineProperty(b, "foo", { + value: "bar", + enumerable: false, + }); + + assertThrows( + () => assertObjectMatch(a, b), + AssertionError, + ` { +- hello: "world", ++ foo: "bar", + }`, + ); });