assert: improve partialDeepStrictEqual

This significantly improves the assert.partialDeepStrictEqual
implementation by reusing the already existing algorithm.
It is significantly faster and handles edge cases like symbols
identical as the deepStrictEqual algorithm. This is crucial to
remove the experimental status from the implementation.

PR-URL: https://github.com/nodejs/node/pull/57370
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
This commit is contained in:
Ruben Bridgewater authored and Node.js GitHub Bot committed 2025-03-12 18:26:13 +00:00
1 parent d615a3cfcb
commit f1b0812754
5 files changed
+688 -472

No files matched your search

+100 -37
View File
@@ -2596,87 +2596,151 @@ added:
- v22.13.0
-->
> Stability: 1.0 - Early development
> Stability: 1.2 - Release candidate
* `actual` {any}
* `expected` {any}
* `message` {string|Error}
[`assert.partialDeepStrictEqual()`][] Asserts the equivalence between the `actual` and `expected` parameters through a
deep comparison, ensuring that all properties in the `expected` parameter are
present in the `actual` parameter with equivalent values, not allowing type coercion.
The main difference with [`assert.deepStrictEqual()`][] is that [`assert.partialDeepStrictEqual()`][] does not require
all properties in the `actual` parameter to be present in the `expected` parameter.
This method should always pass the same test cases as [`assert.deepStrictEqual()`][], behaving as a super set of it.
Tests for partial deep equality between the `actual` and `expected` parameters.
"Deep" equality means that the enumerable "own" properties of child objects
are recursively evaluated also by the following rules. "Partial" equality means
that only properties that exist on the `expected` parameter are going to be
compared.
This method always passes the same test cases as [`assert.deepStrictEqual()`][],
behaving as a super set of it.
### Comparison details
* Primitive values are compared using [`Object.is()`][].
* [Type tags][Object.prototype.toString()] of objects should be the same.
* [`[[Prototype]]`][prototype-spec] of objects are not compared.
* Only [enumerable "own" properties][] are considered.
* {Error} names, messages, causes, and errors are always compared,
even if these are not enumerable properties.
`errors` is also compared.
* Enumerable own {Symbol} properties are compared as well.
* [Object wrappers][] are compared both as objects and unwrapped values.
* `Object` properties are compared unordered.
* {Map} keys and {Set} items are compared unordered.
* Recursion stops when both sides differ or both sides encounter a circular
reference.
* {WeakMap} and {WeakSet} instances are **not** compared structurally.
They are only equal if they reference the same object. Any comparison between
different `WeakMap` or `WeakSet` instances will result in inequality,
even if they contain the same entries.
* {RegExp} lastIndex, flags, and source are always compared, even if these
are not enumerable properties.
* Holes in sparse arrays are ignored.
```mjs
import assert from 'node:assert';
assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
{ a: { b: { c: 1 } } },
{ a: { b: { c: 1 } } },
);
// OK
assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } });
assert.partialDeepStrictEqual(
{ a: 1, b: 2, c: 3 },
{ b: 2 },
);
// OK
assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[4, 5, 8],
);
// OK
assert.partialDeepStrictEqual(new Set(['value1', 'value2']), new Set(['value1', 'value2']));
assert.partialDeepStrictEqual(
new Set([{ a: 1 }, { b: 1 }]),
new Set([{ a: 1 }]),
);
// OK
assert.partialDeepStrictEqual(new Map([['key1', 'value1']]), new Map([['key1', 'value1']]));
assert.partialDeepStrictEqual(
new Map([['key1', 'value1'], ['key2', 'value2']]),
new Map([['key2', 'value2']]),
);
// OK
assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]));
assert.partialDeepStrictEqual(123n, 123n);
// OK
assert.partialDeepStrictEqual(/abc/, /abc/);
// OK
assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]);
// OK
assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }]));
// OK
assert.partialDeepStrictEqual(new Date(0), new Date(0));
// OK
assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[5, 4, 8],
);
// AssertionError
assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
{ a: 1 },
{ a: 1, b: 2 },
);
// AssertionError
assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } });
assert.partialDeepStrictEqual(
{ a: { b: 2 } },
{ a: { b: '2' } },
);
// AssertionError
```
```cjs
const assert = require('node:assert');
assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
{ a: { b: { c: 1 } } },
{ a: { b: { c: 1 } } },
);
// OK
assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } });
assert.partialDeepStrictEqual(
{ a: 1, b: 2, c: 3 },
{ b: 2 },
);
// OK
assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[4, 5, 8],
);
// OK
assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]);
assert.partialDeepStrictEqual(
new Set([{ a: 1 }, { b: 1 }]),
new Set([{ a: 1 }]),
);
// OK
assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }]));
assert.partialDeepStrictEqual(
new Map([['key1', 'value1'], ['key2', 'value2']]),
new Map([['key2', 'value2']]),
);
// OK
assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(123n, 123n);
// OK
assert.partialDeepStrictEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[5, 4, 8],
);
// AssertionError
assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 });
assert.partialDeepStrictEqual(
{ a: 1 },
{ a: 1, b: 2 },
);
// AssertionError
assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } });
assert.partialDeepStrictEqual(
{ a: { b: 2 } },
{ a: { b: '2' } },
);
// AssertionError
```
@@ -2700,7 +2764,6 @@ assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } });
[`assert.notEqual()`]: #assertnotequalactual-expected-message
[`assert.notStrictEqual()`]: #assertnotstrictequalactual-expected-message
[`assert.ok()`]: #assertokvalue-message
[`assert.partialDeepStrictEqual()`]: #assertpartialdeepstrictequalactual-expected-message
[`assert.strictEqual()`]: #assertstrictequalactual-expected-message
[`assert.throws()`]: #assertthrowsfn-error-message
[`getColorDepth()`]: tty.md#writestreamgetcolordepthenv
+4 -313
View File
@@ -21,45 +21,22 @@
'use strict';
const {
ArrayBufferIsView,
ArrayBufferPrototypeGetByteLength,
ArrayFrom,
ArrayIsArray,
ArrayPrototypeIndexOf,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypeSlice,
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
Error,
FunctionPrototypeCall,
MapPrototypeGet,
MapPrototypeGetSize,
MapPrototypeHas,
NumberIsNaN,
ObjectAssign,
ObjectIs,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
ObjectPrototypeToString,
ReflectApply,
ReflectHas,
ReflectOwnKeys,
RegExpPrototypeExec,
SafeArrayIterator,
SafeMap,
SafeSet,
SafeWeakSet,
SetPrototypeGetSize,
String,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeSplit,
Symbol,
SymbolIterator,
TypedArrayPrototypeGetLength,
Uint8Array,
} = primordials;
const {
@@ -73,20 +50,9 @@ const {
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const { inspect } = require('internal/util/inspect');
const { Buffer } = require('buffer');
const {
isArrayBuffer,
isDataView,
isKeyObject,
isPromise,
isRegExp,
isMap,
isSet,
isDate,
isWeakSet,
isWeakMap,
isSharedArrayBuffer,
isAnyArrayBuffer,
} = require('internal/util/types');
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
const { innerOk } = require('internal/assert/utils');
@@ -95,15 +61,16 @@ const CallTracker = require('internal/assert/calltracker');
const {
validateFunction,
} = require('internal/validators');
const { isURL } = require('internal/url');
let isDeepEqual;
let isDeepStrictEqual;
let isPartialStrictEqual;
function lazyLoadComparison() {
const comparison = require('internal/util/comparisons');
isDeepEqual = comparison.isDeepEqual;
isDeepStrictEqual = comparison.isDeepStrictEqual;
isPartialStrictEqual = comparison.isPartialStrictEqual;
}
let warned = false;
@@ -379,282 +346,6 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
}
};
function isSpecial(obj) {
return obj == null || typeof obj !== 'object' || isError(obj) || isRegExp(obj) || isDate(obj);
}
const typesToCallDeepStrictEqualWith = [
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer, isURL,
];
function partiallyCompareMaps(actual, expected, comparedObjects) {
if (MapPrototypeGetSize(expected) > MapPrototypeGetSize(actual)) {
return false;
}
comparedObjects ??= new SafeWeakSet();
const expectedIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], expected);
for (const { 0: key, 1: expectedValue } of expectedIterator) {
if (!MapPrototypeHas(actual, key)) {
return false;
}
const actualValue = MapPrototypeGet(actual, key);
if (!compareBranch(actualValue, expectedValue, comparedObjects)) {
return false;
}
}
return true;
}
function partiallyCompareArrayBuffersOrViews(actual, expected) {
let actualView, expectedView, expectedViewLength;
if (!ArrayBufferIsView(actual)) {
let actualViewLength;
if (isArrayBuffer(actual) && isArrayBuffer(expected)) {
actualViewLength = ArrayBufferPrototypeGetByteLength(actual);
expectedViewLength = ArrayBufferPrototypeGetByteLength(expected);
} else if (isSharedArrayBuffer(actual) && isSharedArrayBuffer(expected)) {
actualViewLength = actual.byteLength;
expectedViewLength = expected.byteLength;
} else {
// Cannot compare ArrayBuffers with SharedArrayBuffers
return false;
}
if (expectedViewLength > actualViewLength) {
return false;
}
actualView = new Uint8Array(actual);
expectedView = new Uint8Array(expected);
} else if (isDataView(actual)) {
if (!isDataView(expected)) {
return false;
}
const actualByteLength = DataViewPrototypeGetByteLength(actual);
expectedViewLength = DataViewPrototypeGetByteLength(expected);
if (expectedViewLength > actualByteLength) {
return false;
}
actualView = new Uint8Array(
DataViewPrototypeGetBuffer(actual),
DataViewPrototypeGetByteOffset(actual),
actualByteLength,
);
expectedView = new Uint8Array(
DataViewPrototypeGetBuffer(expected),
DataViewPrototypeGetByteOffset(expected),
expectedViewLength,
);
} else {
if (ObjectPrototypeToString(actual) !== ObjectPrototypeToString(expected)) {
return false;
}
actualView = actual;
expectedView = expected;
expectedViewLength = TypedArrayPrototypeGetLength(expected);
if (expectedViewLength > TypedArrayPrototypeGetLength(actual)) {
return false;
}
}
for (let i = 0; i < expectedViewLength; i++) {
if (!ObjectIs(actualView[i], expectedView[i])) {
return false;
}
}
return true;
}
function partiallyCompareSets(actual, expected, comparedObjects) {
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
return false; // `expected` can't be a subset if it has more elements
}
if (isDeepEqual === undefined) lazyLoadComparison();
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
const usedIndices = new SafeSet();
expectedIteration: for (const expectedItem of expectedIterator) {
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
usedIndices.add(actualIdx);
continue expectedIteration;
}
}
return false;
}
return true;
}
const minusZeroSymbol = Symbol('-0');
const zeroSymbol = Symbol('0');
// Helper function to get a unique key for 0, -0 to avoid collisions
function getZeroKey(item) {
if (item === 0) {
return ObjectIs(item, -0) ? minusZeroSymbol : zeroSymbol;
}
return item;
}
function partiallyCompareArrays(actual, expected, comparedObjects) {
if (expected.length > actual.length) {
return false;
}
if (isDeepEqual === undefined) lazyLoadComparison();
// Create a map to count occurrences of each element in the expected array
const expectedCounts = new SafeMap();
const safeExpected = new SafeArrayIterator(expected);
for (const expectedItem of safeExpected) {
// Check if the item is a zero or a -0, as these need to be handled separately
if (expectedItem === 0) {
const zeroKey = getZeroKey(expectedItem);
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1);
} else {
let found = false;
for (const { 0: key, 1: count } of expectedCounts) {
if (isDeepStrictEqual(key, expectedItem)) {
expectedCounts.set(key, count + 1);
found = true;
break;
}
}
if (!found) {
expectedCounts.set(expectedItem, 1);
}
}
}
const safeActual = new SafeArrayIterator(actual);
for (const actualItem of safeActual) {
// Check if the item is a zero or a -0, as these need to be handled separately
if (actualItem === 0) {
const zeroKey = getZeroKey(actualItem);
if (expectedCounts.has(zeroKey)) {
const count = expectedCounts.get(zeroKey);
if (count === 1) {
expectedCounts.delete(zeroKey);
} else {
expectedCounts.set(zeroKey, count - 1);
}
}
} else {
for (const { 0: expectedItem, 1: count } of expectedCounts) {
if (isDeepStrictEqual(expectedItem, actualItem)) {
if (count === 1) {
expectedCounts.delete(expectedItem);
} else {
expectedCounts.set(expectedItem, count - 1);
}
break;
}
}
}
}
return expectedCounts.size === 0;
}
/**
* Compares two objects or values recursively to check if they are equal.
* @param {any} actual - The actual value to compare.
* @param {any} expected - The expected value to compare.
* @param {Set} [comparedObjects=new Set()] - Set to track compared objects for handling circular references.
* @returns {boolean} - Returns `true` if the actual value matches the expected value, otherwise `false`.
* @example
* compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}); // true
*/
function compareBranch(
actual,
expected,
comparedObjects,
) {
// Checking for the simplest case possible.
if (actual === expected) {
return true;
}
// Check for Map object equality
if (isMap(actual) && isMap(expected)) {
return partiallyCompareMaps(actual, expected, comparedObjects);
}
if (
ArrayBufferIsView(actual) ||
isAnyArrayBuffer(actual) ||
ArrayBufferIsView(expected) ||
isAnyArrayBuffer(expected)
) {
return partiallyCompareArrayBuffersOrViews(actual, expected);
}
for (const type of typesToCallDeepStrictEqualWith) {
if (type(actual) || type(expected)) {
if (isDeepStrictEqual === undefined) lazyLoadComparison();
return isDeepStrictEqual(actual, expected);
}
}
// Check for Set object equality
if (isSet(actual) && isSet(expected)) {
return partiallyCompareSets(actual, expected, comparedObjects);
}
// Check if expected array is a subset of actual array
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
return partiallyCompareArrays(actual, expected, comparedObjects);
}
// Comparison done when at least one of the values is not an object
if (isSpecial(actual) || isSpecial(expected)) {
if (isDeepEqual === undefined) {
lazyLoadComparison();
}
return isDeepStrictEqual(actual, expected);
}
// Use Reflect.ownKeys() instead of Object.keys() to include symbol properties
const keysExpected = ReflectOwnKeys(expected);
comparedObjects ??= new SafeWeakSet();
// Handle circular references
if (comparedObjects.has(actual)) {
return true;
}
comparedObjects.add(actual);
// Check if all expected keys and values match
for (let i = 0; i < keysExpected.length; i++) {
const key = keysExpected[i];
if (!ReflectHas(actual, key)) {
return false;
}
if (!compareBranch(actual[key], expected[key], comparedObjects)) {
return false;
}
}
return true;
}
/**
* The strict equivalence assertion test between two objects
* @param {any} actual
@@ -671,8 +362,8 @@ assert.partialDeepStrictEqual = function partialDeepStrictEqual(
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
if (!compareBranch(actual, expected)) {
if (isDeepEqual === undefined) lazyLoadComparison();
if (!isPartialStrictEqual(actual, expected)) {
innerFail({
actual,
expected,
+257 -117
View File
@@ -59,8 +59,9 @@ const {
getOwnNonIndexProperties,
} = internalBinding('util');
const kStrict = true;
const kLoose = false;
const kStrict = 1;
const kLoose = 0;
const kPartial = 2;
const kNoIterator = 0;
const kIsArray = 1;
@@ -76,6 +77,35 @@ function areSimilarRegExps(a, b) {
a.lastIndex === b.lastIndex;
}
function isPartialUint8Array(a, b) {
const lenA = TypedArrayPrototypeGetByteLength(a);
const lenB = TypedArrayPrototypeGetByteLength(b);
if (lenA < lenB) {
return false;
}
let offsetA = 0;
for (let offsetB = 0; offsetB < lenB; offsetB++) {
while (!ObjectIs(a[offsetA], b[offsetB])) {
offsetA++;
if (offsetA > lenA - lenB + offsetB) {
return false;
}
}
offsetA++;
}
return true;
}
function isPartialArrayBufferView(a, b) {
if (a.byteLength < b.byteLength) {
return false;
}
return isPartialUint8Array(
new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength),
);
}
function areSimilarFloatArrays(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
@@ -140,14 +170,14 @@ function isEqualBoxedPrimitive(val1, val2) {
// a) The same built-in type tag.
// b) The same prototypes.
function innerDeepEqual(val1, val2, strict, memos) {
function innerDeepEqual(val1, val2, mode, memos) {
// All identical values are equivalent, as determined by ===.
if (val1 === val2) {
return val1 !== 0 || ObjectIs(val1, val2) || !strict;
return val1 !== 0 || ObjectIs(val1, val2) || mode === kLoose;
}
// Check more closely if val1 and val2 are equal.
if (strict) {
if (mode !== kLoose) {
if (typeof val1 === 'number') {
return NumberIsNaN(val1) && NumberIsNaN(val2);
}
@@ -155,7 +185,7 @@ function innerDeepEqual(val1, val2, strict, memos) {
typeof val1 !== 'object' ||
val1 === null ||
val2 === null ||
ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)) {
(mode === kStrict && ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))) {
return false;
}
} else {
@@ -179,18 +209,20 @@ function innerDeepEqual(val1, val2, strict, memos) {
if (ArrayIsArray(val1)) {
// Check for sparse arrays and general fast path
if (!ArrayIsArray(val2) || val1.length !== val2.length) {
if (!ArrayIsArray(val2) ||
(val1.length !== val2.length && (mode !== kPartial || val1.length < val2.length))) {
return false;
}
const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys1 = getOwnNonIndexProperties(val1, filter);
const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys2 = getOwnNonIndexProperties(val2, filter);
if (keys1.length !== keys2.length) {
if (mode !== kPartial &&
keys2.length !== getOwnNonIndexProperties(val1, filter).length) {
return false;
}
return keyCheck(val1, val2, strict, memos, kIsArray, keys1);
return keyCheck(val1, val2, mode, memos, kIsArray, keys2);
} else if (val1Tag === '[object Object]') {
return keyCheck(val1, val2, strict, memos, kNoIterator);
return keyCheck(val1, val2, mode, memos, kNoIterator);
} else if (isDate(val1)) {
if (!isDate(val2) ||
DatePrototypeGetTime(val1) !== DatePrototypeGetTime(val2)) {
@@ -205,7 +237,11 @@ function innerDeepEqual(val1, val2, strict, memos) {
TypedArrayPrototypeGetSymbolToStringTag(val2)) {
return false;
}
if (!strict && (isFloat32Array(val1) || isFloat64Array(val1))) {
if (mode === kPartial) {
if (!isPartialArrayBufferView(val1, val2)) {
return false;
}
} else if (mode === kLoose && (isFloat32Array(val1) || isFloat64Array(val1))) {
if (!areSimilarFloatArrays(val1, val2)) {
return false;
}
@@ -215,25 +251,35 @@ function innerDeepEqual(val1, val2, strict, memos) {
// Buffer.compare returns true, so val1.length === val2.length. If they both
// only contain numeric keys, we don't need to exam further than checking
// the symbols.
const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys1 = getOwnNonIndexProperties(val1, filter);
const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys2 = getOwnNonIndexProperties(val2, filter);
if (keys1.length !== keys2.length) {
if (mode !== kPartial &&
keys2.length !== getOwnNonIndexProperties(val1, filter).length) {
return false;
}
return keyCheck(val1, val2, strict, memos, kNoIterator, keys1);
return keyCheck(val1, val2, mode, memos, kNoIterator, keys2);
} else if (isSet(val1)) {
if (!isSet(val2) || val1.size !== val2.size) {
if (!isSet(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
return false;
}
return keyCheck(val1, val2, strict, memos, kIsSet);
const result = keyCheck(val1, val2, mode, memos, kIsSet);
return result;
} else if (isMap(val1)) {
if (!isMap(val2) || val1.size !== val2.size) {
if (!isMap(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
return false;
}
return keyCheck(val1, val2, strict, memos, kIsMap);
return keyCheck(val1, val2, mode, memos, kIsMap);
} else if (isAnyArrayBuffer(val1)) {
if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) {
if (!isAnyArrayBuffer(val2)) {
return false;
}
if (mode !== kPartial) {
if (!areEqualArrayBuffers(val1, val2)) {
return false;
}
} else if (!isPartialUint8Array(new Uint8Array(val1), new Uint8Array(val2))) {
return false;
}
} else if (isError(val1)) {
@@ -245,6 +291,7 @@ function innerDeepEqual(val1, val2, strict, memos) {
const message1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'message');
const name1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'name');
// TODO(BridgeAR): Adjust cause and errors properties for partial mode.
const cause1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'cause');
const errors1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'errors');
@@ -255,9 +302,9 @@ function innerDeepEqual(val1, val2, strict, memos) {
(cause1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'cause') ||
(!cause1Enumerable && (
ObjectPrototypeHasOwnProperty(val1, 'cause') !== ObjectPrototypeHasOwnProperty(val2, 'cause') ||
!innerDeepEqual(val1.cause, val2.cause, strict, memos)))) ||
!innerDeepEqual(val1.cause, val2.cause, mode, memos)))) ||
(errors1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'errors') ||
(!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, strict, memos)))) {
(!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, mode, memos)))) {
return false;
}
} else if (isBoxedPrimitive(val1)) {
@@ -283,9 +330,9 @@ function innerDeepEqual(val1, val2, strict, memos) {
kKeyObject ??= require('internal/crypto/util').kKeyObject;
if (!isCryptoKey(val2) ||
val1.extractable !== val2.extractable ||
!innerDeepEqual(val1.algorithm, val2.algorithm, strict, memos) ||
!innerDeepEqual(val1.usages, val2.usages, strict, memos) ||
!innerDeepEqual(val1[kKeyObject], val2[kKeyObject], strict, memos)
!innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) ||
!innerDeepEqual(val1.usages, val2.usages, mode, memos) ||
!innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos)
) {
return false;
}
@@ -297,7 +344,7 @@ function innerDeepEqual(val1, val2, strict, memos) {
}
}
return keyCheck(val1, val2, strict, memos, kNoIterator);
return keyCheck(val1, val2, mode, memos, kNoIterator);
}
function getEnumerables(val, keys) {
@@ -307,7 +354,7 @@ function getEnumerables(val, keys) {
);
}
function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
// For all remaining Object pairs, including Array, objects and Maps,
// equivalence is determined by having:
// a) The same number of owned enumerable properties
@@ -315,16 +362,16 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
// c) Equivalent values for every corresponding key/index
// d) For Sets and Maps, equal contents
// Note: this accounts for both named and indexed properties on Arrays.
const isArrayLikeObject = aKeys !== undefined;
const isArrayLikeObject = keys2 !== undefined;
if (aKeys === undefined) {
aKeys = ObjectKeys(val1);
if (keys2 === undefined) {
keys2 = ObjectKeys(val2);
}
// Cheap key test
if (aKeys.length > 0) {
for (const key of aKeys) {
if (!ObjectPrototypePropertyIsEnumerable(val2, key)) {
if (keys2.length > 0) {
for (const key of keys2) {
if (!ObjectPrototypePropertyIsEnumerable(val1, key)) {
return false;
}
}
@@ -332,11 +379,23 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
if (!isArrayLikeObject) {
// The pair must have the same number of owned properties.
if (aKeys.length !== ObjectKeys(val2).length) {
if (mode === kPartial) {
const symbolKeys = ObjectGetOwnPropertySymbols(val2);
if (symbolKeys.length !== 0) {
for (const key of symbolKeys) {
if (ObjectPrototypePropertyIsEnumerable(val2, key)) {
if (!ObjectPrototypePropertyIsEnumerable(val1, key)) {
return false;
}
ArrayPrototypePush(keys2, key);
}
}
}
} else if (keys2.length !== ObjectKeys(val1).length) {
return false;
}
if (strict) {
if (mode === kStrict) {
const symbolKeysA = ObjectGetOwnPropertySymbols(val1);
if (symbolKeysA.length !== 0) {
let count = 0;
@@ -345,7 +404,7 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
if (!ObjectPrototypePropertyIsEnumerable(val2, key)) {
return false;
}
ArrayPrototypePush(aKeys, key);
ArrayPrototypePush(keys2, key);
count++;
} else if (ObjectPrototypePropertyIsEnumerable(val2, key)) {
return false;
@@ -366,10 +425,10 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
}
}
if (aKeys.length === 0 &&
if (keys2.length === 0 &&
(iterationType === kNoIterator ||
(iterationType === kIsArray && val1.length === 0) ||
val1.size === 0)) {
(iterationType === kIsArray && val2.length === 0) ||
val2.size === 0)) {
return true;
}
@@ -384,7 +443,7 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
deep: false,
deleteFailures: false,
};
return objEquiv(val1, val2, strict, aKeys, memos, iterationType);
return objEquiv(val1, val2, mode, keys2, memos, iterationType);
}
if (memos.set === undefined) {
@@ -395,7 +454,7 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
memos.c = val1;
memos.d = val2;
memos.deep = true;
const result = objEquiv(val1, val2, strict, aKeys, memos, iterationType);
const result = objEquiv(val1, val2, mode, keys2, memos, iterationType);
memos.deep = false;
return result;
}
@@ -415,7 +474,7 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
return true;
}
const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType);
const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType);
if (areEq || memos.deleteFailures) {
set.delete(val1);
@@ -425,13 +484,13 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
return areEq;
}
function setHasEqualElement(set, val2, strict, memo) {
function setHasEqualElement(set, val1, mode, memo) {
const { deleteFailures } = memo;
memo.deleteFailures = true;
for (const val1 of set) {
if (innerDeepEqual(val1, val2, strict, memo)) {
for (const val2 of set) {
if (innerDeepEqual(val1, val2, mode, memo)) {
// Remove the matching element to make sure we do not check that again.
set.delete(val1);
set.delete(val2);
memo.deleteFailures = deleteFailures;
return true;
}
@@ -471,56 +530,72 @@ function setMightHaveLoosePrim(a, b, prim) {
if (altValue != null)
return altValue;
return b.has(altValue) && !a.has(altValue);
return !b.has(altValue) && a.has(altValue);
}
function mapMightHaveLoosePrim(a, b, prim, item, memo) {
function mapMightHaveLoosePrim(a, b, prim, item2, memo) {
const altValue = findLooseMatchingPrimitives(prim);
if (altValue != null) {
return altValue;
}
const curB = b.get(altValue);
if ((curB === undefined && !b.has(altValue)) ||
!innerDeepEqual(item, curB, false, memo)) {
const item1 = a.get(altValue);
if ((item1 === undefined && !a.has(altValue)) ||
!innerDeepEqual(item1, item2, kLoose, memo)) {
return false;
}
return !a.has(altValue) && innerDeepEqual(item, curB, false, memo);
return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo);
}
function setEquiv(a, b, strict, memo) {
function partialObjectSetEquiv(a, b, mode, set, memo) {
let aPos = 0;
for (const val of a) {
aPos++;
if (!b.has(val) && setHasEqualElement(set, val, mode, memo) && set.size === 0) {
return true;
}
if (a.size - aPos < set.size) {
return false;
}
}
}
function setEquiv(a, b, mode, memo) {
// This is a lazily initiated Set of entries which have to be compared
// pairwise.
let set = null;
for (const val of a) {
if (!b.has(val)) {
for (const val of b) {
if (!a.has(val)) {
if ((typeof val !== 'object' || val === null) &&
(strict || !setMightHaveLoosePrim(a, b, val))) {
(mode !== kLoose || !setMightHaveLoosePrim(a, b, val))) {
return false;
}
if (set === null) {
if (b.size === 1) {
return innerDeepEqual(val, b.values().next().value, strict, memo);
if (a.size === 1) {
return innerDeepEqual(a.values().next().value, val, mode, memo);
}
set = new SafeSet();
}
// If the specified value doesn't exist in the second set it's a object
// (or in loose mode: a non-matching primitive). Find the
// deep-(strict-)equal element in a set copy to reduce duplicate checks.
// deep-(mode-)equal element in a set copy to reduce duplicate checks.
set.add(val);
}
}
if (set !== null) {
for (const val of b) {
if (mode === kPartial) {
return partialObjectSetEquiv(a, b, mode, set, memo);
}
for (const val of a) {
// Primitive values have already been handled above.
if (typeof val === 'object' && val !== null) {
if (!a.has(val) && !setHasEqualElement(set, val, strict, memo)) {
if (!b.has(val) && !setHasEqualElement(set, val, mode, memo)) {
return false;
}
} else if (!strict &&
!a.has(val) &&
!setHasEqualElement(set, val, strict, memo)) {
} else if (mode === kLoose &&
!b.has(val) &&
!setHasEqualElement(set, val, mode, memo)) {
return false;
}
}
@@ -530,16 +605,16 @@ function setEquiv(a, b, strict, memo) {
return true;
}
function mapHasEqualEntry(set, map, key2, item2, strict, memo) {
function mapHasEqualEntry(set, map, key1, item1, mode, memo) {
// To be able to handle cases like:
// Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']])
// ... we need to consider *all* matching keys, not just the first we find.
const { deleteFailures } = memo;
memo.deleteFailures = true;
for (const key1 of set) {
if (innerDeepEqual(key1, key2, strict, memo) &&
innerDeepEqual(map.get(key1), item2, strict, memo)) {
set.delete(key1);
for (const key2 of set) {
if (innerDeepEqual(key1, key2, mode, memo) &&
innerDeepEqual(item1, map.get(key2), mode, memo)) {
set.delete(key2);
memo.deleteFailures = deleteFailures;
return true;
}
@@ -549,49 +624,68 @@ function mapHasEqualEntry(set, map, key2, item2, strict, memo) {
return false;
}
function mapEquiv(a, b, strict, memo) {
function partialObjectMapEquiv(a, b, mode, set, memo) {
let aPos = 0;
for (const { 0: key1, 1: item1 } of a) {
aPos++;
if (typeof key1 === 'object' &&
key1 !== null &&
mapHasEqualEntry(set, b, key1, item1, mode, memo) &&
set.size === 0) {
return true;
}
if (a.size - aPos < set.size) {
return false;
}
}
}
function mapEquiv(a, b, mode, memo) {
let set = null;
for (const { 0: key, 1: item1 } of a) {
if (typeof key === 'object' && key !== null) {
for (const { 0: key2, 1: item2 } of b) {
if (typeof key2 === 'object' && key2 !== null) {
if (set === null) {
if (b.size === 1) {
const { 0: key2, 1: item2 } = b.entries().next().value;
return innerDeepEqual(key, key2, strict, memo) &&
innerDeepEqual(item1, item2, strict, memo);
if (a.size === 1) {
const { 0: key1, 1: item1 } = a.entries().next().value;
return innerDeepEqual(key1, key2, mode, memo) &&
innerDeepEqual(item1, item2, mode, memo);
}
set = new SafeSet();
}
set.add(key);
set.add(key2);
} else {
// By directly retrieving the value we prevent another b.has(key) check in
// By directly retrieving the value we prevent another b.has(key2) check in
// almost all possible cases.
const item2 = b.get(key);
if (((item2 === undefined && !b.has(key)) ||
!innerDeepEqual(item1, item2, strict, memo))) {
if (strict)
const item1 = a.get(key2);
if (((item1 === undefined && !a.has(key2)) ||
!innerDeepEqual(item1, item2, mode, memo))) {
if (mode !== kLoose)
return false;
// Fast path to detect missing string, symbol, undefined and null
// keys.
if (!mapMightHaveLoosePrim(a, b, key, item1, memo))
if (!mapMightHaveLoosePrim(a, b, key2, item2, memo))
return false;
if (set === null) {
set = new SafeSet();
}
set.add(key);
set.add(key2);
}
}
}
if (set !== null) {
for (const { 0: key, 1: item } of b) {
if (typeof key === 'object' && key !== null) {
if (!mapHasEqualEntry(set, a, key, item, strict, memo))
if (mode === kPartial) {
return partialObjectMapEquiv(a, b, mode, set, memo);
}
for (const { 0: key1, 1: item1 } of a) {
if (typeof key1 === 'object' && key1 !== null) {
if (!mapHasEqualEntry(set, b, key1, item1, mode, memo))
return false;
} else if (!strict &&
(!a.has(key) ||
!innerDeepEqual(a.get(key), item, strict, memo)) &&
!mapHasEqualEntry(set, a, key, item, strict, memo)) {
} else if (mode === kLoose &&
(!b.has(key1) ||
!innerDeepEqual(item1, b.get(key1), mode, memo)) &&
!mapHasEqualEntry(set, b, key1, item1, mode, memo)) {
return false;
}
}
@@ -601,32 +695,79 @@ function mapEquiv(a, b, strict, memo) {
return true;
}
function objEquiv(a, b, strict, keys, memos, iterationType) {
function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
let aPos = 0;
const keysA = ObjectKeys(a).slice(startA);
const keysB = ObjectKeys(b).slice(startB);
if (keysA.length < keysB.length) {
return false;
}
for (let i = 0; i < keysB.length; i++) {
const keyB = keysB[i];
while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) {
aPos++;
if (aPos > keysA.length - keysB.length + i) {
return false;
}
}
aPos++;
}
return true;
}
function partialArrayEquiv(a, b, mode, memos) {
let aPos = 0;
for (let i = 0; i < b.length; i++) {
let isSparse = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i);
if (isSparse) {
return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
}
while (!(isSparse = a[aPos] === undefined && !ObjectPrototypeHasOwnProperty(a, aPos)) &&
!innerDeepEqual(a[aPos], b[i], mode, memos)) {
aPos++;
if (aPos > a.length - b.length + i) {
return false;
}
}
if (isSparse) {
return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
}
aPos++;
}
return true;
}
function objEquiv(a, b, mode, keys2, memos, iterationType) {
// The pair must have equivalent values for every corresponding key.
if (keys.length > 0) {
for (const key of keys) {
if (!innerDeepEqual(a[key], b[key], strict, memos)) {
if (keys2.length > 0) {
for (const key of keys2) {
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
return false;
}
}
}
if (iterationType === kIsArray) {
if (mode === kPartial) {
return partialArrayEquiv(a, b, mode, memos);
}
for (let i = 0; i < a.length; i++) {
if (ObjectPrototypeHasOwnProperty(a, i)) {
if (!ObjectPrototypeHasOwnProperty(b, i) ||
!innerDeepEqual(a[i], b[i], strict, memos)) {
return false;
}
} else if (ObjectPrototypeHasOwnProperty(b, i)) {
if (!innerDeepEqual(a[i], b[i], mode, memos)) {
return false;
} else {
}
const isOwnProperty = ObjectPrototypeHasOwnProperty(a, i);
if (isOwnProperty !== ObjectPrototypeHasOwnProperty(b, i)) {
return false;
}
if (!isOwnProperty) {
// Array is sparse.
// TODO(BridgeAR): Use internal method to only get index properties. The
// same applies to the partial implementation.
const keysA = ObjectKeys(a);
for (; i < keysA.length; i++) {
const key = keysA[i];
if (!ObjectPrototypeHasOwnProperty(b, key) ||
!innerDeepEqual(a[key], b[key], strict, memos)) {
!innerDeepEqual(a[key], b[key], mode, memos)) {
return false;
}
}
@@ -634,11 +775,11 @@ function objEquiv(a, b, strict, keys, memos, iterationType) {
}
}
} else if (iterationType === kIsSet) {
if (!setEquiv(a, b, strict, memos)) {
if (!setEquiv(a, b, mode, memos)) {
return false;
}
} else if (iterationType === kIsMap) {
if (!mapEquiv(a, b, strict, memos)) {
if (!mapEquiv(a, b, mode, memos)) {
return false;
}
}
@@ -646,15 +787,14 @@ function objEquiv(a, b, strict, keys, memos, iterationType) {
return true;
}
function isDeepEqual(val1, val2) {
return innerDeepEqual(val1, val2, kLoose);
}
function isDeepStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kStrict);
}
module.exports = {
isDeepEqual,
isDeepStrictEqual,
isDeepEqual(val1, val2) {
return innerDeepEqual(val1, val2, kLoose);
},
isDeepStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kStrict);
},
isPartialStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kPartial);
},
};
+23 -3
View File
@@ -198,12 +198,14 @@ test('deepEqual should pass for these weird cases', () => {
function assertDeepAndStrictEqual(a, b) {
assert.deepEqual(a, b);
assert.deepStrictEqual(a, b);
assert.partialDeepStrictEqual(a, b);
assert.deepEqual(b, a);
assert.deepStrictEqual(b, a);
assert.partialDeepStrictEqual(b, a);
}
function assertNotDeepOrStrict(a, b, err) {
function assertNotDeepOrStrict(a, b, err, options) {
assert.throws(
() => assert.deepEqual(a, b),
err || re`${a}\n\nshould loosely deep-equal\n\n${b}`
@@ -221,6 +223,15 @@ function assertNotDeepOrStrict(a, b, err) {
() => assert.deepStrictEqual(b, a),
err || { code: 'ERR_ASSERTION' }
);
const partial = () => {
assert.partialDeepStrictEqual(b, a);
assert.partialDeepStrictEqual(a, b);
};
if (options?.partial === 'pass') {
partial();
} else {
assert.throws(partial, err || { code: 'ERR_ASSERTION' });
}
}
function assertOnlyDeepEqual(a, b, err) {
@@ -598,16 +609,17 @@ test('Handle sparse arrays', () => {
/* eslint-disable no-sparse-arrays */
assertDeepAndStrictEqual([1, , , 3], [1, , , 3]);
assertNotDeepOrStrict([1, , , 3], [1, , , 3, , , ]);
assertNotDeepOrStrict([1, , , 3], [1, undefined, , 3]);
/* eslint-enable no-sparse-arrays */
const a = new Array(3);
const b = new Array(3);
a[2] = true;
b[1] = true;
assertNotDeepOrStrict(a, b);
assertNotDeepOrStrict(a, b, AssertionError, { partial: 'pass' });
b[2] = true;
assertNotDeepOrStrict(a, b);
a[0] = true;
assertNotDeepOrStrict(a, b);
assertNotDeepOrStrict(a, b, AssertionError, { partial: 'pass' });
});
test('Handle different error messages', () => {
@@ -1246,6 +1258,14 @@ test('Verify object types being identical on both sides', () => {
});
assertNotDeepOrStrict(a, b);
a = new ArrayBuffer(3);
b = new Uint8Array(3);
Object.setPrototypeOf(b, ArrayBuffer.prototype);
Object.defineProperty(b, Symbol.toStringTag, {
value: 'ArrayBuffer'
});
assertNotDeepOrStrict(a, b);
a = new Date(2000);
b = Object.create(
Object.getPrototypeOf(a),
+304 -2
View File
@@ -5,9 +5,12 @@ const vm = require('node:vm');
const assert = require('node:assert');
const { describe, it } = require('node:test');
const x = ['x'];
function createCircularObject() {
const obj = {};
obj.self = obj;
obj.set = new Set([x, ['y']]);
return obj;
}
@@ -43,6 +46,11 @@ describe('Object Comparison Tests', () => {
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,
@@ -225,6 +233,46 @@ describe('Object Comparison Tests', () => {
['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 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',
@@ -261,11 +309,124 @@ describe('Object Comparison Tests', () => {
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),
@@ -445,6 +606,44 @@ describe('Object Comparison Tests', () => {
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),
@@ -597,6 +796,62 @@ describe('Object Comparison Tests', () => {
['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 }],
@@ -617,6 +872,21 @@ describe('Object Comparison Tests', () => {
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']),
@@ -785,8 +1055,8 @@ describe('Object Comparison Tests', () => {
{
description:
'compares one subset array with another',
actual: [1, 2, 3],
expected: [2],
actual: [1, 2, 3, 4, 5, 6, 7, 8, 9],
expected: [2, 5, 6, 7, 8],
},
{
description: 'ensures that File extends Blob',
@@ -803,6 +1073,38 @@ describe('Object Comparison Tests', () => {
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: [] } },
],
}
}]
},
].forEach(({ description, actual, expected }) => {
it(description, () => {
assert.partialDeepStrictEqual(actual, expected);