diff --git a/README.md b/README.md index 5230831..e4db4d1 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Throws an `AssertionError` unless `actual` and `expected` are the same value, re Throws an `AssertionError` unless `actual` and `expected` are not the same value, recursively. +#### `assert.partialDeepStrictEqual(actual, expected[, message])` + +Throws an `AssertionError` unless `actual` and `expected` are the same value, partially and recursively. + #### `assert.match(actual, regexp[, message])` Throws an `AssertionError` unless `actual` is a string that matches `regexp`. diff --git a/index.d.ts b/index.d.ts index e677b88..36c9002 100644 --- a/index.d.ts +++ b/index.d.ts @@ -23,6 +23,8 @@ declare namespace assert { export function notDeepStrictEqual(actual: any, expected: any, message?: string | Error): void + export function partialDeepStrictEqual(actual: any, expected: any, message?: string | Error): void + export function match(actual: string, expected: RegExp, message?: string | Error): void export function doesNotMatch(actual: string, expected: RegExp, message?: string | Error): void diff --git a/index.js b/index.js index 36d117e..11956b8 100644 --- a/index.js +++ b/index.js @@ -122,7 +122,16 @@ exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, messa assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, memo = new Memoization()) { +exports.partialDeepStrictEqual = function partialDeepStrictEqual(actual, expected, message) { + if (deepStrictEqualValue(actual, expected, true)) return + + assertFail( + { message, actual, expected, operator: 'partialDeepStrictEqual' }, + partialDeepStrictEqual + ) +} + +function deepStrictEqualValue(a, b, partial = false, memo = new Memoization()) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) @@ -133,32 +142,41 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) { if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b - if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) - if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) + // Anything that can be settled without descending into the values is settled + // first. A pair that already differs in its own right is unequal whatever the + // surrounding structures do. + if (!deepStrictEqualShallow(a, b, type, prototype, partial)) return false + + if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b, partial) + + if (type.isArrayBuffer() || type.isSharedArrayBuffer()) { + return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b), partial) + } + if (type.isDataView()) { return deepStrictEqualBuffer( new Uint8Array(a.buffer, a.byteOffset, a.byteLength), - new Uint8Array(b.buffer, b.byteOffset, b.byteLength) + new Uint8Array(b.buffer, b.byteOffset, b.byteLength), + partial ) } - // Anything that can be settled without descending into the values is settled - // first. A pair that already differs in its own right is unequal whatever the - // surrounding structures do. - if (!deepStrictEqualShallow(a, b, type, prototype)) return false - if (memo.has(a, b)) { return memo.compare(a, b) } else { memo.add(a, b) } + if (partial === true && type.isArray()) { + if (!partialDeepStrictEqualArray(a, b, partial, memo)) return false + } + let result - if (type.isError()) result = deepStrictEqualError(a, b, memo) - else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) - else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) - else result = deepStrictEqualObject(a, b, memo) + if (type.isError()) result = deepStrictEqualError(a, b, type, partial, memo) + else if (type.isMap()) result = deepStrictEqualMap(a, b, type, partial, memo) + else if (type.isSet()) result = deepStrictEqualSet(a, b, type, partial, memo) + else result = deepStrictEqualObject(a, b, type, partial, memo) memo.remove(a, b) @@ -167,7 +185,7 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) { // Compares everything about a pair that can be decided on the spot, leaving // only the values reachable from it for the caller to walk. -function deepStrictEqualShallow(a, b, type, prototype) { +function deepStrictEqualShallow(a, b, type, prototype, partial) { if ( prototype === BigInt.prototype || prototype === Boolean.prototype || @@ -179,45 +197,68 @@ function deepStrictEqualShallow(a, b, type, prototype) { } else if (type.isRegExp()) { if (a.lastIndex !== b.lastIndex || a.flags !== b.flags || a.source !== b.source) return false } else if (type.isTypedArray()) { - if (!deepStrictEqualBuffer(a, b)) return false + if (!deepStrictEqualBuffer(a, b, partial)) return false } else if (type.isDate()) { if (!Object.is(a.getTime(), b.getTime())) return false - } else if (type.isArguments() || type.isArray()) { - if (a.length !== b.length) return false + } + + if (type.isArguments() || type.isArray()) { + if (partial === true) { + if (b.length > a.length) return false + } else { + if (a.length !== b.length) return false + } } else if (type.isMap() || type.isSet()) { - if (a.size !== b.size) return false + if (partial === true) { + if (b.size > a.size) return false + } else { + if (a.size !== b.size) return false + } + } else if (type.isSharedArrayBuffer()) { + if (partial === true) { + if (b.byteLength > a.byteLength) return false + } else { + if (a.byteLength !== b.byteLength) return false + } } - return getEnumerableKeys(a).length === getEnumerableKeys(b).length + if (partial === true) { + return getEnumerableKeys(a).length >= getEnumerableKeys(b).length + } else { + return getEnumerableKeys(a).length === getEnumerableKeys(b).length + } } -function deepStrictEqualBuffer(a, b) { +function deepStrictEqualBuffer(a, b, partial) { + if (partial === true) return partialDeepStrictEqualBuffer(a, b) + return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 } -function deepStrictEqualError(a, b, memo) { +function deepStrictEqualError(a, b, type, partial, memo) { return ( - deepStrictEqualValue(a.name, b.name, memo) && - deepStrictEqualValue(a.message, b.message, memo) && - deepStrictEqualObjectKeys(a, b, ['cause', 'errors'], memo) && - deepStrictEqualObject(a, b, memo) + deepStrictEqualValue(a.name, b.name, partial, memo) && + ((partial === true && b.message === '') || + deepStrictEqualValue(a.message, b.message, partial, memo)) && + deepStrictEqualObjectKeys(a, b, ['cause', 'errors'], partial, memo) && + deepStrictEqualObject(a, b, type, partial, memo) ) } -function deepStrictEqualArrayUnordered(a, b, memo) { - if (a.length !== b.length) return false +function deepStrictEqualArrayUnordered(a, b, partial, memo) { + if (partial === false && a.length !== b.length) return false - for (let i = 0; i < a.length; i++) { + for (let i = 0; i < b.length; i++) { let found = false - const itemA = a[i] + const itemB = b[i] - for (let j = 0; j < b.length; j++) { - const itemB = b[j] + for (let j = 0; j < a.length; j++) { + const itemA = a[j] - if (deepStrictEqualValue(itemA, itemB, memo)) { + if (deepStrictEqualValue(itemA, itemB, partial, memo)) { found = true - b.splice(j, 1) + a.splice(j, 1) break } @@ -242,65 +283,121 @@ function requiresDeepKeyMatch(key) { return (type === 'object' && key !== null) || type === 'function' } -function deepStrictEqualMap(a, b, memo) { - if (!deepStrictEqualObject(a, b, memo)) return false +function deepStrictEqualMap(a, b, type, partial, memo) { + if (!deepStrictEqualObject(a, b, type, partial, memo)) return false // Match entries with primitive keys directly through `b` in linear time and // leave only the object-keyed entries for the quadratic fallback. - const restA = [] const restB = [] + const restA = [] - for (const [key, value] of a) { + for (const [key, value] of b) { if (requiresDeepKeyMatch(key)) { - restA.push([key, value]) - } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) { + restB.push({ key, value }) + } else if (!a.has(key) || !deepStrictEqualValue(a.get(key), value, partial, memo)) { return false } } - for (const entry of b) { - if (requiresDeepKeyMatch(entry[0])) restB.push(entry) + for (const [key, value] of a) { + if (requiresDeepKeyMatch(key)) restA.push({ key, value }) } - return deepStrictEqualArrayUnordered(restA, restB, memo) + return deepStrictEqualArrayUnordered(restA, restB, partial, memo) } -function deepStrictEqualSet(a, b, memo) { - if (!deepStrictEqualObject(a, b, memo)) return false +function deepStrictEqualSet(a, b, type, partial, memo) { + if (!deepStrictEqualObject(a, b, type, partial, memo)) return false // Match primitive members directly through `b` in linear time and leave only // the object members for the quadratic fallback. - const restA = [] const restB = [] - - for (const value of a) { - if (requiresDeepKeyMatch(value)) restA.push(value) - else if (!b.has(value)) return false - } + const restA = [] for (const value of b) { if (requiresDeepKeyMatch(value)) restB.push(value) + else if (!a.has(value)) return false + } + + for (const value of a) { + if (requiresDeepKeyMatch(value)) restA.push(value) } - return deepStrictEqualArrayUnordered(restA, restB, memo) + return deepStrictEqualArrayUnordered(restA, restB, partial, memo) } -function deepStrictEqualObjectKeys(a, b, keys, memo) { +function deepStrictEqualObjectKeys(a, b, keys, partial, memo) { for (const key of keys) { const hasA = key in a const hasB = key in b + if (partial === true && !hasB) continue if ((hasA ^ hasB) === 1) return false - if (hasA && hasB && !deepStrictEqualValue(a[key], b[key], memo)) return false + if (hasA && !deepStrictEqualValue(a[key], b[key], partial, memo)) return false } return true } // The key counts have already been compared, so only the values are left. -function deepStrictEqualObject(a, b, memo) { - for (const key of getEnumerableKeys(a)) { - if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false +function deepStrictEqualObject(a, b, type, partial, memo) { + const aKeys = getEnumerableKeys(a) + const bKeys = getEnumerableKeys(b) + + const indexedPartial = partial === true && (type.isArray() || type.isTypedArray()) + + for (const key of bKeys) { + // the indexes of partial objects are tested at + // partialDeepStrictEqualArray and partialDeepStrictEqualBuffer + if (indexedPartial && typeof key === 'string' && !isNaN(key)) continue + + if (!aKeys.includes(key) || !deepStrictEqualValue(a[key], b[key], partial, memo)) { + return false + } + } + + return true +} + +function partialDeepStrictEqualArray(a, b, partial, memo) { + let j = -1 + + for (let i = 0; i < b.length; i++) { + let found = false + + if (!(i in b)) continue // Ignore hole in array + + while (++j < a.length) { + if (!(j in a)) continue // Ignore hole in array + + if (deepStrictEqualValue(a[j], b[i], partial, memo)) { + found = true + + break + } + } + + if (found === false) return false + } + + return true +} + +function partialDeepStrictEqualBuffer(a, b) { + let j = -1 + + for (let i = 0; i < b.length; i++) { + let found = false + + while (++j < a.length) { + if (Object.is(a[j], b[i])) { + found = true + + break + } + } + + if (found === false) return false } return true diff --git a/test.js b/test.js index 6b58d17..379ab79 100644 --- a/test.js +++ b/test.js @@ -1193,3 +1193,1318 @@ test('notDeepStrictEqual, shared reference', (t) => { /should fail/ ) }) + +test('partialDeepStrictEqual, basic', (t) => { + t.execution(() => assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })) + t.execution(() => assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { b: 2 })) + t.execution(() => assert.partialDeepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], [4, 5, 9])) + t.execution(() => + assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }])) + ) + assert.partialDeepStrictEqual( + new Map([ + ['foo', 'foo'], + ['bar', 'bar'] + ]), + new Map([['bar', 'bar']]) + ) + + t.exception( + () => assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }, 'should fail'), + /should fail/ + ) + t.exception( + () => assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }, 'should fail'), + /should fail/ + ) + t.exception( + () => assert.partialDeepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], [5, 4, 8], 'should fail'), + /should fail/ + ) +}) + +test('partialDeepStrictEqual, map', (t) => { + const foo = new Map([ + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value2'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value4'], + [{ a: 1 }, 'value2'] + ]) + + const bar = new Map([ + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value2'] + ]) + + t.execution(() => assert.partialDeepStrictEqual(foo, bar)) +}) + +test('partialDeepStrictEqual, set', (t) => { + t.execution(() => + assert.partialDeepStrictEqual(new Set([{ foo: 1, bar: 2 }]), new Set([{ foo: 1 }])) + ) +}) + +test('partialDeepStrictEqual, sparse array', (t) => { + t.execution(() => assert.partialDeepStrictEqual([1, , , undefined, , 3], [1, , undefined, 3])) + + t.execution(() => { + const foo = new Array(15) + foo[0] = 1 + foo[1] = 2 + foo[5] = 100n + foo[10] = 3 + + const bar = new Array(12) + bar[0] = 1 + bar[1] = 2 + bar[5] = 3 + + assert.partialDeepStrictEqual(foo, bar) + }) + + t.exception( + () => assert.partialDeepStrictEqual([1, , , , 3], [1, , undefined, 3], 'should fail'), + /should fail/ + ) +}) + +test('partialDeepStrictEqual, error', (t) => { + t.execution(() => assert.partialDeepStrictEqual(new Error('message'), new Error())) + t.execution(() => + assert.partialDeepStrictEqual(new Error('message', { cause: 42 }), new Error('message')) + ) + t.execution(() => + assert.partialDeepStrictEqual(new Error('message', { cause: undefined }), new Error('message')) + ) + + t.exception( + () => + assert.partialDeepStrictEqual( + new Error('message'), + new Error('message', { cause: undefined }), + 'should fail' + ), + /should fail/ + ) +}) + +test('partialDeepStrictEqual, error, aggregate error', (t) => { + t.execution(() => + assert.partialDeepStrictEqual(new AggregateError([new Error()]), new AggregateError([])) + ) + + t.exception( + () => + assert.partialDeepStrictEqual( + new AggregateError([]), + new AggregateError([new Error()]), + 'should fail' + ), + /should fail/ + ) +}) + +test('partialDeepStrictEqual, typed array', (t) => { + t.execution(() => + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3, 4, 5]), new Uint8Array([1, 2, 3, 5])) + ) +}) + +test('partialDeepStrictEqual, typed array, float', (t) => { + t.exception( + () => + assert.partialDeepStrictEqual( + new Float16Array([+0.0]), // lunte-disable-line no-undef + new Float16Array([-0.0]), // lunte-disable-line no-undef + 'should fail' + ), + /should fail/ + ) +}) + +test('partialDeepStrictEqual, array, non-enumerable symbol', (t) => { + const foo = [1, 2, 3] + + Object.defineProperty(foo, Symbol.for('test'), { + value: 'test', + enumerable: false + }) + + const bar = [1, 2, 3] + + bar[Symbol.for('test')] = 'test' + + t.exception(() => assert.partialDeepStrictEqual(foo, bar, 'should fail'), /should fail/) +}) + +test('partialDeepStrictEqual, object, numeric keys', (t) => { + t.exception(() => assert.partialDeepStrictEqual({ 0: 'a', 1: 'b' }, { 1: 'c' })) +}) + +test('partialDeepStrictEqual, SharedArrayBuffer', (t) => { + t.execution(() => + assert.partialDeepStrictEqual(new SharedArrayBuffer(10), new SharedArrayBuffer(5)) + ) + + t.exception( + () => + assert.partialDeepStrictEqual( + new SharedArrayBuffer(5), + new SharedArrayBuffer(10), + 'should fail' + ), + /should fail/ + ) +}) + +// Adapted from https://github.com/nodejs/node/blob/main/test/parallel/test-assert-partial-deep-equal.js +test('partialDeepStrictEqual, node.js test suite', function (t) { + const x = ['x'] + + function createCircularObject() { + const obj = {} + obj.self = obj + obj.set = new Set([x, ['y']]) + return obj + } + + function createDeepNestedObject() { + return { level1: { level2: { level3: 'deepValue' } } } + } + + /* + async function generateCryptoKey() { + const { KeyObject } = require('node:crypto') + const { subtle } = globalThis.crypto + + const cryptoKey = await subtle.generateKey( + { + name: 'HMAC', + hash: 'SHA-256', + length: 256 + }, + true, + ['sign', 'verify'] + ) + + const keyObject = KeyObject.from(cryptoKey) + + return { cryptoKey, keyObject } + } + */ + + t.test('throws an error', (t) => { + const tests = [ + { + description: 'throws when only actual is provided', + actual: { a: 1 }, + expected: undefined + }, + { + description: 'throws when unequal zeros are compared', + actual: 0, + expected: -0 + }, + { + description: 'throws when only expected is provided', + actual: undefined, + expected: { a: 1 } + }, + { + description: 'throws when expected has more properties than actual', + actual: [1, 'two'], + expected: [1, 'two', true] + }, + { + description: 'throws because expected has seven 2 while actual has six one', + actual: [1, 2, 2, 2, 2, 2, 2, 3], + expected: [1, 2, 2, 2, 2, 2, 2, 2] + }, + { + description: 'throws when comparing two different sets with objects', + actual: new Set([{ a: 1 }]), + expected: new Set([{ a: 1 }, { b: 1 }]) + }, + + { + description: 'throws when comparing two WeakSet objects', + actual: new WeakSet(), + expected: new WeakSet() + }, + { + description: 'throws when comparing two WeakMap objects', + actual: new WeakMap(), + expected: new WeakMap() + }, + { + description: 'throws when comparing two different objects', + actual: { a: 1, b: 'string' }, + expected: { a: 2, b: 'string' } + }, + { + description: 'throws when comparing two objects with different nested objects', + actual: createDeepNestedObject(), + expected: { level1: { level2: { level3: 'differentValue' } } } + }, + { + description: 'throws when comparing two objects with different RegExp properties', + actual: { pattern: /abc/ }, + expected: { pattern: /def/ } + }, + { + description: 'throws when comparing two arrays with different elements', + actual: [1, 'two', true], + expected: [1, 'two', false] + }, + { + description: 'throws when comparing [0] with [-0]', + actual: [0], + expected: [-0] + }, + { + description: 'throws when comparing [0, 0, 0] with [0, -0]', + actual: [0, 0, 0], + expected: [0, -0] + }, + { + description: 'throws when comparing ["-0"] with [-0]', + actual: ['-0'], + expected: [-0] + }, + { + description: 'throws when comparing [-0] with [0]', + actual: [-0], + expected: [0] + }, + { + description: 'throws when comparing [-0] with ["-0"]', + actual: [-0], + expected: ['-0'] + }, + { + description: 'throws when comparing ["0"] with [0]', + actual: ['0'], + expected: [0] + }, + { + description: 'throws when comparing [0] with ["0"]', + actual: [0], + expected: ['0'] + }, + { + description: 'throws when comparing two Date objects with different times', + actual: new Date(0), + expected: new Date(1) + }, + { + description: 'throws when comparing two objects with different large number of properties', + actual: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`key${i}`, i])), + expected: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`key${i}`, i + 1])) + }, + { + description: 'throws when comparing two objects with different Symbols', + actual: { [Symbol('test')]: 'symbol' }, + expected: { [Symbol('test')]: 'symbol' } + }, + { + description: 'throws when comparing two objects with different array properties', + actual: { a: [1, 2, 3] }, + expected: { a: [1, 2, 4] } + }, + { + description: 'throws when comparing two objects with different function properties', + actual: { fn: () => {} }, + expected: { fn: () => {} } + }, + { + description: 'throws when comparing two objects with different Error message', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 2') } + }, + { + description: 'throws when comparing two objects with missing cause on the actual Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 1', { cause: 42 }) } + }, + { + description: 'throws when comparing two objects with missing message on the actual Error', + actual: { error: new Error() }, + expected: { error: new Error('Test error 1') } + }, + { + description: 'throws when comparing two Errors with missing cause on the actual Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 1', { cause: undefined }) } + }, + { + description: + 'throws when comparing two AggregateErrors with missing message on the actual Error', + actual: { error: new AggregateError([], 'Test error 1') }, + expected: { error: new AggregateError([new Error()], 'Test error 1') } + }, + { + description: + 'throws when comparing two objects with different TypedArray instances and content', + actual: { typedArray: new Uint8Array([1, 2, 3]) }, + expected: { typedArray: new Uint8Array([4, 5, 6]) } + }, + { + description: 'throws when comparing two Map objects with different entries', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]), + expected: new Map([ + ['key1', 'value1'], + ['key3', 'value3'] + ]) + }, + { + description: 'throws when comparing two Map objects with different keys', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]), + expected: new Map([ + ['key1', 'value1'], + ['key3', 'value2'] + ]) + }, + { + description: 'throws when the expected Map has more entries than the actual Map', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]), + expected: new Map([ + ['key1', 'value1'], + ['key2', 'value2'], + ['key3', 'value3'] + ]) + }, + { + description: + 'throws when the nested array in the Map is not a subset of the other nested array', + actual: new Map([ + ['key1', ['value1', 'value2']], + ['key2', 'value2'] + ]), + expected: new Map([['key1', ['value3']]]) + }, + { + description: 'throws for maps with object keys and different values', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 2 }, 'value2'], + [{ b: 2 }, 'value4'] + ]), + expected: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 2 }, 'value3'] + ]) + }, + { + description: 'throws for maps with multiple identical object keys, just not enough', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 1 }, 'value2'], + [{ a: 1 }, 'value1'] + ]), + expected: new Map([ + [{ a: 1 }, 'value1'], + [{ a: 1 }, 'value1'], + [{ a: 1 }, 'value1'] + ]) + }, + { + description: 'throws for Maps with mixed unequal entries', + actual: new Map([ + [{ a: 2 }, 1], + [1, 1], + [{ b: 1 }, 1], + [[], 1], + [2, 1], + [{ a: 1 }, 1] + ]), + expected: new Map([ + [{ a: 1 }, 1], + [[], 1], + [2, 1], + [{ a: 1 }, 1] + ]) + }, + { + description: 'throws for sets with different object values', + actual: new Set([{ a: 1 }, { a: 2 }, { a: 1 }, { a: 2 }]), + expected: new Set([{ a: 1 }, { a: 2 }, { a: 1 }, { a: 1 }]) + }, + { + description: 'throws when comparing two TypedArray instances with different content', + actual: new Uint8Array(10), + expected: () => { + const typedArray2 = new Int8Array(10) + Object.defineProperty(typedArray2, Symbol.toStringTag, { + value: 'Uint8Array' + }) + Object.setPrototypeOf(typedArray2, Uint8Array.prototype) + + return typedArray2 + } + }, + /* + { + description: + 'throws when comparing two Set objects from different realms with different values', + actual: new vm.runInNewContext('new Set(["value1", "value2"])'), + expected: new Set(['value1', 'value3']) + }, + */ + { + description: 'throws when comparing two Set objects with different values', + actual: new Set(['value1', 'value2']), + expected: new Set(['value1', 'value3']) + }, + { + description: 'throws when comparing one subset object with another', + actual: { a: 1, b: 2, c: 3 }, + expected: { b: '2' } + }, + { + description: 'throws when comparing one subset array with another', + actual: [1, 2, 3], + expected: ['2'] + }, + { + description: 'throws when comparing an array with symbol properties not matching', + actual: (() => { + const array = [1, 2, 3] + array[Symbol.for('test')] = 'test' + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array[Symbol.for('test')] = 'different' + return array + })() + }, + { + description: 'throws when comparing an array with extra properties not matching', + actual: (() => { + const array = [1, 2, 3] + array.extra = 'test' + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array.extra = 'different' + return array + })() + }, + { + description: 'throws when comparing a non matching sparse array', + actual: (() => { + const array = new Array(1000) + array[90] = 1 + array[92] = 2 + array[95] = 1 + array[96] = 2 + array.foo = 'bar' + array.extra = 'test' + return array + })(), + expected: (() => { + const array = new Array(1000) + array[90] = 1 + array[92] = 1 + array[95] = 1 + array.extra = 'test' + array.foo = 'bar' + return array + })() + }, + { + description: 'throws when comparing a same length sparse array with actual less keys', + actual: (() => { + const array = new Array(1000) + array[90] = 1 + array[92] = 1 + return array + })(), + expected: (() => { + const array = new Array(1000) + array[90] = 1 + array[92] = 1 + array[95] = 1 + return array + })() + }, + { + description: + 'throws when comparing an array with symbol properties matching but other enumerability', + actual: (() => { + const array = [1, 2, 3] + array[Symbol.for('abc')] = 'test' + Object.defineProperty(array, Symbol.for('test'), { + value: 'test', + enumerable: false + }) + array[Symbol.for('other')] = 'test' + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array[Symbol.for('test')] = 'test' + return array + })() + }, + { + description: + 'throws comparing an array with extra properties matching but other enumerability', + actual: (() => { + const array = [1, 2, 3] + array.alsoIgnored = [{ nested: { property: true } }] + Object.defineProperty(array, 'extra', { + value: 'test', + enumerable: false + }) + array.ignored = 'test' + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array.extra = 'test' + return array + })() + }, + { + description: 'throws when comparing an ArrayBuffer with a Uint8Array', + actual: new ArrayBuffer(3), + expected: new Uint8Array(3) + }, + { + description: 'throws when comparing an TypedArrays with symbol properties not matching', + actual: (() => { + const typed = new Uint8Array(3) + typed[Symbol.for('test')] = 'test' + return typed + })(), + expected: (() => { + const typed = new Uint8Array(3) + typed[Symbol.for('test')] = 'different' + return typed + })() + }, + { + description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer', + actual: new ArrayBuffer(3), + expected: new SharedArrayBuffer(3) + }, + { + description: 'throws when comparing a SharedArrayBuffer with an ArrayBuffer', + actual: new SharedArrayBuffer(3), + expected: new ArrayBuffer(3) + }, + { + description: 'throws when comparing an Int16Array with a Uint16Array', + actual: new Int16Array(3), + expected: new Uint16Array(3) + }, + { + description: 'throws when comparing two dataviews with different buffers', + actual: { dataView: new DataView(new ArrayBuffer(3)) }, + expected: { dataView: new DataView(new ArrayBuffer(4)) } + }, + { + description: + 'throws because expected Uint8Array(SharedArrayBuffer) is not a subset of actual', + actual: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) }, + expected: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) } + }, + { + description: 'throws because expected SharedArrayBuffer is not a subset of actual', + actual: { typedArray: new SharedArrayBuffer(3) }, + expected: { typedArray: new SharedArrayBuffer(5) } + }, + { + description: 'throws when comparing a DataView with a TypedArray', + actual: { dataView: new DataView(new ArrayBuffer(3)) }, + expected: { dataView: new Uint8Array(3) } + }, + { + description: 'throws when comparing a TypedArray with a DataView', + actual: { dataView: new Uint8Array(3) }, + expected: { dataView: new DataView(new ArrayBuffer(3)) } + }, + { + description: 'throws when comparing Float16Array([+0.0]) with Float16Array([-0.0])', + actual: new Float16Array([+0.0]), // lunte-disable-line no-undef + expected: new Float16Array([-0.0]) // lunte-disable-line no-undef + }, + { + description: 'throws when comparing Float32Array([+0.0]) with Float32Array([-0.0])', + actual: new Float32Array([+0.0]), + expected: new Float32Array([-0.0]) + }, + { + description: 'throws when comparing two Uint8Array objects with non-matching entries', + actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) }, + expected: { typedArray: new Uint8Array([1, 333, 2, 4]) } + }, + { + description: 'throws when comparing two different urls', + actual: new URL('http://foo'), + expected: new URL('http://bar') + }, + { + description: + 'throws when comparing SharedArrayBuffers when expected has different elements actual', + actual: (() => { + const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT) + const sharedArray = new Int32Array(sharedBuffer) + + sharedArray[0] = 1 + sharedArray[1] = 2 + sharedArray[2] = 3 + + return sharedBuffer + })(), + expected: (() => { + const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT) + const sharedArray = new Int32Array(sharedBuffer) + + sharedArray[0] = 1 + sharedArray[1] = 2 + sharedArray[2] = 6 + + return sharedBuffer + })() + } + ] + + /* + if (common.hasCrypto) { + tests.push({ + description: 'throws when comparing two objects with different CryptoKey instances objects', + actual: async () => { + return generateCryptoKey() + }, + expected: async () => { + return generateCryptoKey() + } + }) + + const { createSecretKey } = require('node:crypto') + + tests.push({ + description: 'throws when comparing two objects with different KeyObject instances objects', + actual: createSecretKey(Buffer.alloc(1, 0)), + expected: createSecretKey(Buffer.alloc(1, 1)) + }) + } + */ + + for (const { description, actual, expected } of tests) { + t.exception(() => assert.partialDeepStrictEqual(actual, expected), description) + } + }) + + t.test('does not throw an error', (t) => { + const sym = Symbol('test') + const func = () => {} + + const tests = [ + { + description: 'compares two identical simple objects', + actual: { a: 1, b: 'string' }, + expected: { a: 1, b: 'string' } + }, + { + description: 'compares two objects with different property order', + actual: { a: 1, b: 'string' }, + expected: { b: 'string', a: 1 } + }, + { + description: 'compares two deeply nested objects with partial equality', + actual: { a: { nested: { property: true, some: 'other' } } }, + expected: { a: { nested: { property: true } } } + }, + /* + { + description: 'compares plain objects from different realms', + actual: vm.runInNewContext(`({ + a: 1, + b: 2n, + c: "3", + d: /4/, + e: new Set([5]), + f: [6], + g: new Uint8Array() + })`), + expected: { b: 2n, e: new Set([5]), f: [6], g: new Uint8Array() } + }, + */ + { + description: 'compares two integers', + actual: 1, + expected: 1 + }, + { + description: 'compares two strings', + actual: '1', + expected: '1' + }, + { + description: 'compares two objects with nested objects', + actual: createDeepNestedObject(), + expected: createDeepNestedObject() + }, + { + description: 'compares two objects with circular references', + actual: createCircularObject(), + expected: createCircularObject() + }, + { + description: 'compares two arrays with identical elements', + actual: [1, 'two', true], + expected: [1, 'two', true] + }, + { + description: 'compares [0] with [0]', + actual: [0], + expected: [0] + }, + { + description: 'compares [-0] with [-0]', + actual: [-0], + expected: [-0] + }, + { + description: 'compares [0, -0, 0] with [0, 0]', + actual: [0, -0, 0], + expected: [0, 0] + }, + { + description: 'comparing an array with symbol properties matching', + actual: (() => { + const array = [1, 2, 3] + array[Symbol.for('abc')] = 'test' + array[Symbol.for('test')] = 'test' + Object.defineProperty(array, Symbol.for('hidden'), { + value: 'hidden', + enumerable: false + }) + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array[Symbol.for('test')] = 'test' + return array + })() + }, + { + description: 'comparing an array with extra properties matching', + actual: (() => { + const array = [1, 2, 3] + array.alsoIgnored = [{ nested: { property: true } }] + array.extra = 'test' + array.ignored = 'test' + return array + })(), + expected: (() => { + const array = [1, 2, 3] + array.extra = 'test' + Object.defineProperty(array, 'ignored', { enumerable: false }) + Object.defineProperty(array, Symbol.for('hidden'), { + value: 'hidden', + enumerable: false + }) + return array + })() + }, + { + description: 'compares two Date objects with the same time', + actual: new Date(0), + expected: new Date(0) + }, + { + description: 'compares two objects with large number of properties', + actual: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`key${i}`, i])), + expected: Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`key${i}`, i])) + }, + { + description: 'compares two objects with Symbol properties', + actual: { [sym]: 'symbol' }, + expected: { [sym]: 'symbol' } + }, + { + description: 'compares two objects with RegExp properties', + actual: { pattern: /abc/ }, + expected: { pattern: /abc/ } + }, + { + description: 'compares two objects with identical function properties', + actual: { fn: func }, + expected: { fn: func } + }, + { + description: 'compares two objects with mixed types of properties', + actual: { num: 1, str: 'test', bool: true, sym }, + expected: { num: 1, str: 'test', bool: true, sym } + }, + { + description: 'compares two objects with Buffers', + actual: { buf: Buffer.from('Node.js') }, + expected: { buf: Buffer.from('Node.js') } + }, + { + description: 'compares two objects with identical Error properties', + actual: { error: new Error('Test error') }, + expected: { error: new Error('Test error') } + }, + { + description: 'compares two Uint8Array objects', + actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) }, + expected: { typedArray: new Uint8Array([1, 2, 3, 5]) } + }, + { + description: 'compares two Int16Array objects', + actual: { typedArray: new Int16Array([1, 2, 3, 4, 5]) }, + expected: { typedArray: new Int16Array([1, 2, 3]) } + }, + { + description: 'compares two DataView objects with the same buffer and different views', + actual: { dataView: new DataView(new ArrayBuffer(8), 0, 4) }, + expected: { dataView: new DataView(new ArrayBuffer(8), 4, 4) } + }, + { + description: 'compares two DataView objects with different buffers', + actual: { dataView: new DataView(new ArrayBuffer(8)) }, + expected: { dataView: new DataView(new ArrayBuffer(8)) } + }, + { + description: 'compares two DataView objects with the same buffer and same views', + actual: { dataView: new DataView(new ArrayBuffer(8), 0, 8) }, + expected: { dataView: new DataView(new ArrayBuffer(8), 0, 8) } + }, + { + description: 'compares two SharedArrayBuffers with the same length', + actual: new SharedArrayBuffer(3), + expected: new SharedArrayBuffer(3) + }, + { + description: 'compares two Uint8Array objects from SharedArrayBuffer', + actual: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) }, + expected: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) } + }, + { + description: 'compares two Int16Array objects from SharedArrayBuffer', + actual: { typedArray: new Int16Array(new SharedArrayBuffer(10)) }, + expected: { typedArray: new Int16Array(new SharedArrayBuffer(6)) } + }, + { + description: + 'compares two DataView objects with the same SharedArrayBuffer and different views', + actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 4) }, + expected: { dataView: new DataView(new SharedArrayBuffer(8), 4, 4) } + }, + { + description: 'compares two DataView objects with different SharedArrayBuffers', + actual: { dataView: new DataView(new SharedArrayBuffer(8)) }, + expected: { dataView: new DataView(new SharedArrayBuffer(8)) } + }, + { + description: 'compares two DataView objects with the same SharedArrayBuffer and same views', + actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) }, + expected: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) } + }, + { + description: 'compares two SharedArrayBuffers', + actual: { typedArray: new SharedArrayBuffer(5) }, + expected: { typedArray: new SharedArrayBuffer(3) } + }, + { + description: 'compares two SharedArrayBuffers with data inside', + actual: (() => { + const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT) + const sharedArray = new Int32Array(sharedBuffer) + + sharedArray[0] = 1 + sharedArray[1] = 2 + sharedArray[2] = 3 + sharedArray[3] = 4 + + return sharedBuffer + })(), + expected: (() => { + const sharedBuffer = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT) + const sharedArray = new Int32Array(sharedBuffer) + + sharedArray[0] = 1 + sharedArray[1] = 2 + sharedArray[2] = 3 + + return sharedBuffer + })() + }, + { + description: 'compares two Map objects with identical entries', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]), + expected: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]) + }, + { + description: 'compares two Map where one is a subset of the other', + actual: new Map([ + ['key1', { nested: { property: true } }], + ['key2', new Set([1, 2, 3])], + ['key3', new Uint8Array([1, 2, 3])] + ]), + expected: new Map([ + ['key1', { nested: { property: true } }], + ['key2', new Set([1, 2, 3])], + ['key3', new Uint8Array([1, 2, 3])] + ]) + }, + { + description: 'compares maps with object keys', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value2'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value4'], + [{ a: 1 }, 'value2'] + ]), + expected: new Map([ + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value2'] + ]) + }, + { + describe: 'compares two simple sparse arrays', + actual: new Array(1_000), + expected: new Array(100) + }, + { + describe: 'compares two identical sparse arrays', + actual: (() => { + const array = new Array(100) + array[1] = 2 + return array + })(), + expected: (() => { + const array = new Array(100) + array[1] = 2 + return array + })() + }, + { + describe: 'compares two big sparse arrays', + actual: (() => { + const array = new Array(150_000_000) + array[0] = 1 + array[1] = 2 + array[100] = 100n + array[200_000] = 3 + array[1_200_000] = 4 + array[120_200_000] = [] + return array + })(), + expected: (() => { + const array = new Array(100_000_000) + array[0] = 1 + array[1] = 2 + array[200_000] = 3 + array[1_200_000] = 4 + return array + })() + }, + { + describe: 'compares two array of objects', + actual: [{ a: 5 }], + expected: [{ a: 5 }] + }, + { + describe: 'compares two array of objects where expected is a subset of actual', + actual: [{ a: 5 }, { b: 5 }], + expected: [{ a: 5 }] + }, + { + description: 'compares two Set objects with identical objects', + actual: new Set([{ a: 1 }]), + expected: new Set([{ a: 1 }]) + }, + { + description: 'compares two Set objects where expected is a subset of actual', + actual: new Set([{ a: 1 }, { b: 1 }]), + expected: new Set([{ a: 1 }]) + }, + { + description: 'compares two Sets with mixed entries', + actual: new Set([{ b: 1 }, [], 1, { a: 1 }, 2, []]), + expected: new Set([{ a: 1 }, 2, []]) + }, + { + description: 'compares two Sets with mixed entries different order', + actual: new Set([{ a: 1 }, 1, { b: 1 }, [], 2, { a: 1 }]), + expected: new Set([{ a: 1 }, [], 2, { a: 1 }]) + }, + { + description: 'compares two Sets with mixed entries different order 2', + actual: new Set([{ a: 1 }, { a: 1 }, 1, { b: 1 }, [], 2, { a: 1 }]), + expected: new Set([{ a: 1 }, [], 2, { a: 1 }]) + }, + { + description: 'compares two Set objects with identical arrays', + actual: new Set(['value1', 'value2']), + expected: new Set(['value1', 'value2']) + }, + { + description: 'compares two Set objects', + actual: new Set(['value1', 'value2', 'value3']), + expected: new Set(['value1', 'value2']) + }, + /* + { + description: 'compares two Map objects from different realms with identical entries', + actual: new vm.runInNewContext('new Map([["key1", "value1"], ["key2", "value2"]])'), + expected: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]) + }, + */ + { + description: 'compares two Map objects where expected is a subset of actual', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]), + expected: new Map([['key1', 'value1']]) + }, + { + description: 'compares two deeply nested Maps', + actual: { + a: { + b: { + c: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]) + }, + z: [1, 2, 3] + } + }, + expected: { + a: { + z: [1, 2, 3], + b: { + c: new Map([['key1', 'value1']]) + } + } + } + }, + { + description: 'compares Maps nested into Maps', + actual: new Map([ + [ + 'key1', + new Map([ + ['nestedKey1', 'nestedValue1'], + ['nestedKey2', 'nestedValue2'] + ]) + ], + ['key2', 'value2'] + ]), + expected: new Map([['key1', new Map([['nestedKey1', 'nestedValue1']])]]) + }, + { + description: 'compares Maps with nested arrays inside', + actual: new Map([ + ['key1', ['value1', 'value2']], + ['key2', 'value2'] + ]), + expected: new Map([['key1', ['value1', 'value2']]]) + }, + { + description: 'compares two objects with identical getter/setter properties', + actual: (() => { + let value = 'test' + return Object.defineProperty({}, 'prop', { + get: () => value, + set: (newValue) => { + value = newValue + }, + enumerable: true, + configurable: true + }) + })(), + expected: (() => { + let value = 'test' + return Object.defineProperty({}, 'prop', { + get: () => value, + set: (newValue) => { + value = newValue + }, + enumerable: true, + configurable: true + }) + })() + }, + { + description: 'compares two objects with no prototype', + actual: { __proto__: null, prop: 'value' }, + expected: { __proto__: null, prop: 'value' } + }, + { + description: 'compares two objects with identical non-enumerable properties', + actual: (() => { + const obj = {} + Object.defineProperty(obj, 'hidden', { + value: 'secret', + enumerable: false + }) + return obj + })(), + expected: (() => { + const obj = {} + Object.defineProperty(obj, 'hidden', { + value: 'secret', + enumerable: false + }) + return obj + })() + }, + { + description: 'compares two identical primitives, string', + actual: 'foo', + expected: 'foo' + }, + { + description: 'compares two identical primitives, number', + actual: 1, + expected: 1 + }, + { + description: 'compares two identical primitives, boolean', + actual: false, + expected: false + }, + { + description: 'compares two identical primitives, null', + actual: null, + expected: null + }, + { + description: 'compares two identical primitives, undefined', + actual: undefined, + expected: undefined + }, + { + description: 'compares two identical primitives, Symbol', + actual: sym, + expected: sym + }, + { + description: 'compares one subset object with another', + actual: { a: 1, b: 2, c: 3 }, + expected: { b: 2 } + }, + { + description: 'compares one subset array with another', + actual: [1, 2, 3, 4, 5, 6, 7, 8, 9], + expected: [2, 5, 6, 7, 8] + }, + /* + { + description: 'ensures that File extends Blob', + actual: Object.getPrototypeOf(File.prototype), + expected: Blob.prototype + }, + */ + { + description: 'compares NaN with NaN', + actual: NaN, + expected: NaN + }, + { + description: 'compares two identical urls', + actual: new URL('http://foo'), + expected: new URL('http://foo') + }, + { + description: 'compares a more complex object with additional parts on the actual', + actual: [ + { + foo: 'yarp', + nope: { + bar: '123', + a: [1, 2, 0], + c: {}, + b: [ + { + foo: 'yarp', + nope: { bar: '123', a: [1, 2, 0], c: {}, b: [] } + }, + { + foo: 'yarp', + nope: { bar: '123', a: [1, 2, 1], c: {}, b: [] } + } + ] + } + } + ], + expected: [ + { + foo: 'yarp', + nope: { + bar: '123', + c: {}, + b: [ + { foo: 'yarp', nope: { bar: '123', c: {}, b: [] } }, + { foo: 'yarp', nope: { bar: '123', c: {}, b: [] } } + ] + } + } + ] + }, + { + description: 'comparing two Errors with missing cause on the expected Error', + actual: { error: new Error('Test error 1', { cause: 42 }) }, + expected: { error: new Error('Test error 1') } + }, + { + description: 'comparing two Errors with cause set to undefined on the actual Error', + actual: { error: new Error('Test error 1', { cause: undefined }) }, + expected: { error: new Error('Test error 1') } + }, + { + description: 'comparing two Errors with missing message on the expected Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error() } + }, + { + description: + 'comparing two AggregateErrors with no message or errors on the expected Error', + actual: { error: new AggregateError([new Error(), 123]) }, + expected: { error: new AggregateError([]) } + } + ] + + for (const { description, actual, expected } of tests) { + t.execution(() => assert.partialDeepStrictEqual(actual, expected), description) + } + }) +})