From 0ee83d4b99684f3ab5a60ecad07ee4c473d2d265 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Thu, 29 Aug 2024 14:24:05 +0200 Subject: [PATCH] assert: adds deepMatch, deepMatchStrict, includes, includesStrict Fixes: https://github.com/nodejs/node/issues/50399 Co-Authored-By: Cristian Barlutiu --- doc/api/assert.md | 239 ++++++++++ lib/assert.js | 228 +++++++++- test/parallel/test-assert-objects.js | 631 +++++++++++++++++++++++++++ 3 files changed, 1097 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-assert-objects.js diff --git a/doc/api/assert.md b/doc/api/assert.md index 70f4ac6c6db5bd..c1b2f8d728eea7 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -2548,6 +2548,241 @@ assert.throws(throwingFirst, /Second$/); Due to the confusing error-prone notation, avoid a string as the second argument. +## `assert.deepMatch(actual, expected[, message])` + + + +* `actual` {any} +* `expected` {any} +* `message` {string|Error} + +[`assert.deepMatch()`][] evaluates the equivalence between the `actual` and `expected` parameters by +performing a deep comparison. This function ensures that all properties defined +in the `expected` parameter match those in the `actual` parameter in +both value and type, allowing type coercion. The main difference with [`assert.deepEqual()`][] is that +[`assert.deepMatch()`][] does not require all properties in the `actual` parameter to be present in the +`expected` parameter. + +```mjs +import assert from 'node:assert'; + +assert.deepMatch({ a: 1, b: '2' }, { a: 1, b: 2 }); +// OK + +assert.deepMatch({ a: 1, b: '2', c: 3 }, { a: 1, b: 2 }); +// OK + +assert.deepMatch({ a: { b: { c: '1' } } }, { a: { b: { c: 1 } } }); +// OK + +assert.deepMatch({ a: 1 }, { a: 1, b: 2 }); +// AssertionError + +assert.deepMatch({ a: 1, b: true }, { a: 1, b: 'true' }); +// AssertionError + +assert.deepMatch({ a: { b: 2 } }, { a: { b: 2, c: 3 } }); +// AssertionError +``` + +```cjs +const assert = require('node:assert'); + +assert.deepMatch({ a: 1, b: '2' }, { a: 1, b: 2 }); +// OK + +assert.deepMatch({ a: 1, b: '2', c: 3 }, { a: 1, b: 2 }); +// OK + +assert.deepMatch({ a: { b: { c: '1' } } }, { a: { b: { c: 1 } } }); +// OK + +assert.deepMatch({ a: 1 }, { a: 1, b: 2 }); +// AssertionError: Expected key b + +assert.deepMatch({ a: 1, b: true }, { a: 1, b: 'true' }); +// AssertionError + +assert.deepMatch({ a: { b: 2, d: 4 } }, { a: { b: 2, c: 3 } }); +// AssertionError: Expected key c +``` + +If the values or keys are not equal in the `expected` parameter, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of an [`Error`][] then it will be thrown instead of the +`AssertionError`. + +## `assert.deepMatchStrict(actual, expected[, message])` + + + +* `actual` {any} +* `expected` {any} +* `message` {string|Error} + +[`assert.deepMatchStrict()`][] Assesses 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.deepMatchStrict()`][] does not require all +properties in the `actual` parameter to be present in the `expected` parameter. + +```mjs +import assert from 'node:assert'; + +assert.deepMatchStrict({ a: 1, b: 2 }, { a: 1, b: 2 }); +// OK + +assert.deepMatchStrict({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); +// OK + +assert.deepMatchStrict({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); +// OK + +assert.deepMatchStrict({ a: 1 }, { a: 1, b: 2 }); +// AssertionError + +assert.deepMatchStrict({ a: 1, b: '2' }, { a: 1, b: 2 }); +// AssertionError + +assert.deepMatchStrict({ a: { b: 2 } }, { a: { b: '2' } }); +// AssertionError +``` + +```cjs +const assert = require('node:assert'); + +assert.deepMatchStrict({ a: 1, b: 2 }, { a: 1, b: 2 }); +// OK + +assert.deepMatchStrict({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); +// OK + +assert.deepMatchStrict({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); +// OK + +assert.deepMatchStrict({ a: 1 }, { a: 1, b: 2 }); +// AssertionError + +assert.deepMatchStrict({ a: 1, b: '2' }, { a: 1, b: 2 }); +// AssertionError + +assert.deepMatchStrict({ a: { b: 2 } }, { a: { b: '2' } }); +// AssertionError +``` + +## `assert.includes(actual, expected[, message])` + + + +* `actual` {Array | string} +* `expected` {Array | string} +* `message` {string|Error} + +[`assert.includes()`][] compares the `actual` and `expected` parameters to determine if the `expected` +parameter is included in the `actual` parameter; the comparison is done with type coercion. +The `actual` and the `expected` parameters can be +either an array or a string. If the `actual` parameter is an array, +the `expected` parameter must be an array and vice versa for strings. + +```mjs +import assert from 'node:assert'; + +assert.includes([1, 2, 3], ['2', 3]); +// OK + +assert.includes('Hello World!', 'World'); +// OK + +assert.includes([1, 2, 3], [2, 4]); +// AssertionError + +assert.includes('Hello World!', 'Node.js'); +// AssertionError +``` + +```cjs +const assert = require('node:assert'); + +assert.includes([1, 2, 3], [2, 3]); +// OK + +assert.includes('Hello World!', 'World'); +// OK + +assert.includes([1, 2, 3], [2, 4]); +// AssertionError + +assert.includes('Hello World!', 'Node.js'); +// AssertionError +``` + +If the assertion fails, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of an [`Error`][] then it will be thrown instead of the +`AssertionError`. + +## `assert.includesStrict(actual, expected[, message])` + + + +* `actual` {Array | string} +* `expected` {Array | string} +* `message` {string|Error} + +[`assert.includesStrict()`][] compares the `actual` and `expected` parameters to determine if the `expected` +parameter is included in the `actual` parameter; the comparison is done without type coercion. +The `actual` and the `expected` parameters can be +either an array or a string. If the `actual` parameter is an array, +the `expected` parameter must be an array and vice versa for strings. + +```mjs +import assert from 'node:assert'; + +assert.includesStrict([1, 2, 3], [2, 3]); +// OK + +assert.includesStrict('Hello World!', 'World'); +// OK + +assert.includesStrict([1, 2, 3], [2, 4]); +// AssertionError + +assert.includesStrict('Hello World!', 'Node.js'); +// AssertionError +``` + +```cjs +const assert = require('node:assert'); + +assert.includesStrict([1, 2, 3], [2, 3]); +// OK + +assert.includesStrict('Hello World!', 'World'); +// OK + +assert.includesStrict([1, 2, 3], [2, 4]); +// AssertionError + +assert.includesStrict('Hello World!', 'Node.js'); +// AssertionError +``` + +If the assertion fails, an [`AssertionError`][] is thrown with a `message` +property set equal to the value of the `message` parameter. If the `message` +parameter is undefined, a default error message is assigned. If the `message` +parameter is an instance of an [`Error`][] then it will be thrown instead of the +`AssertionError`. + [Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript [Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring [`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality @@ -2568,9 +2803,13 @@ argument. [`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap [`WeakSet`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet [`assert.deepEqual()`]: #assertdeepequalactual-expected-message +[`assert.deepMatch()`]: #assertdeepmatchactual-expected-message +[`assert.deepMatchStrict()`]: #assertdeepmatchstrictactual-expected-message [`assert.deepStrictEqual()`]: #assertdeepstrictequalactual-expected-message [`assert.doesNotThrow()`]: #assertdoesnotthrowfn-error-message [`assert.equal()`]: #assertequalactual-expected-message +[`assert.includes()`]: #assertincludesactual-expected-message +[`assert.includesStrict()`]: #assertincludesstrictactual-expected-message [`assert.notDeepEqual()`]: #assertnotdeepequalactual-expected-message [`assert.notDeepStrictEqual()`]: #assertnotdeepstrictequalactual-expected-message [`assert.notEqual()`]: #assertnotequalactual-expected-message diff --git a/lib/assert.js b/lib/assert.js index eadc3844c20128..65fe84bbd0a895 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -21,6 +21,7 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypePush, @@ -29,15 +30,25 @@ const { Error, ErrorCaptureStackTrace, FunctionPrototypeBind, + FunctionPrototypeCall, + MapPrototypeGet, + MapPrototypeHas, NumberIsNaN, ObjectAssign, + ObjectGetPrototypeOf, ObjectIs, ObjectKeys, + ObjectPrototype, ObjectPrototypeIsPrototypeOf, + ObjectPrototypeToString, ReflectApply, + ReflectHas, + ReflectOwnKeys, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, SafeMap, + SafeSet, + SetPrototypeHas, String, StringPrototypeCharCodeAt, StringPrototypeIncludes, @@ -46,6 +57,7 @@ const { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, + SymbolIterator, } = primordials; const { Buffer } = require('buffer'); @@ -63,7 +75,7 @@ const { const AssertionError = require('internal/assert/assertion_error'); const { openSync, closeSync, readSync } = require('fs'); const { inspect } = require('internal/util/inspect'); -const { isPromise, isRegExp } = require('internal/util/types'); +const { isPromise, isRegExp, isMap, isSet } = require('internal/util/types'); const { EOL } = require('internal/constants'); const { BuiltinModule } = require('internal/bootstrap/realm'); const { isError, deprecate } = require('internal/util'); @@ -608,6 +620,218 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) { } }; +function isPlainObject(obj) { + if (typeof obj !== 'object' || obj === null) { + return false; + } + const proto = ObjectGetPrototypeOf(obj); + return proto === ObjectPrototype || proto === null || ObjectPrototypeToString(obj) === '[object Object]'; +} + +function assertIncludes(actual, expected, loose) { + if (expected.length > actual.length) { + return false; + } + if (typeof actual === 'string') { + return StringPrototypeIncludes(actual, expected); + } + + return expected.every((item) => actual.some((actualItem) => { + // eslint-disable-next-line eqeqeq + return loose ? actualItem == item : actualItem === item; + })); +} + +/** + * 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 {boolean} [loose=false] - Whether to use loose comparison (==) or strict comparison (===). Defaults to false. + * @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 + * // Loose comparison (default) + * compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: '2'}); // true + * + * // Strict comparison + * compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}, true); // true + */ +function compareBranch( + actual, + expected, + loose = false, + comparedObjects = new SafeSet(), +) { + // Check for Map object equality + if (isMap(actual) && isMap(expected)) { + if (actual.size !== expected.size) { + return false; + } + const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual); + for (const { 0: key, 1: val } of safeIterator) { + if (!MapPrototypeHas(expected, key)) { + return false; + } + if (!compareBranch(val, MapPrototypeGet(expected, key), loose, comparedObjects)) { + return false; + } + } + return true; + } + + // Check for Set object equality + if (isSet(actual) && isSet(expected)) { + if (actual.size !== expected.size) { + return false; + } + const safeIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual); + for (const item of safeIterator) { + if (!SetPrototypeHas(expected, item)) { + return false; + } + } + return true; + } + + // Check if expected array is a subset of actual array + if (ArrayIsArray(actual) && ArrayIsArray(expected)) { + return assertIncludes(actual, expected, loose); + } + + // Check non object types equality + if (!isPlainObject(actual) || !isPlainObject(expected)) { + if (isDeepEqual === undefined) { + lazyLoadComparison(); + } + return loose ? isDeepEqual(actual, expected) : isDeepStrictEqual(actual, expected); + } + + // Check if actual and expected are null or not objects + if (actual == null || expected == null) { + return false; + } + + // Use Reflect.ownKeys() instead of Object.keys() to include symbol properties + const keysExpected = ReflectOwnKeys(expected); + + // 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]; + assert( + ReflectHas(actual, key), + new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }), + ); + if (!compareBranch(actual[key], expected[key], loose, comparedObjects)) { + return false; + } + } + + return true; +} + +/** + * The strict equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.deepMatchStrict = function deepMatchStrict( + actual, + expected, + message, +) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + + if (!compareBranch(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'deepMatchStrict', + stackStartFn: deepMatchStrict, + }); + } +}; + +/** + * The equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.deepMatch = function deepMatch(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + + if (!compareBranch(actual, expected, true)) { + innerFail({ + actual, + expected, + message, + operator: 'deepMatch', + stackStartFn: deepMatch, + }); + } +}; + +/** + * The inclusion assertion test between two arrays or strings + * @param {Array | string} actual + * @param {Array | string} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.includes = function includes(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + + if (!assertIncludes(actual, expected, true)) { + innerFail({ + actual, + expected, + message, + operator: 'includes', + stackStartFn: includes, + }); + } +}; + + +/** + * The strict inclusion assertion test between two arrays or strings + * @param {Array | string} actual + * @param {Array | string} expected + * @param {string | Error} [message] + * @returns {void} + */ +assert.includesStrict = function includesStrict(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + + if (!assertIncludes(actual, expected, false)) { + innerFail({ + actual, + expected, + message, + operator: 'includesStrict', + stackStartFn: includesStrict, + }); + } +}; + class Comparison { constructor(obj, keys, actual) { for (const key of keys) { @@ -1074,6 +1298,8 @@ assert.strict = ObjectAssign(strict, assert, { deepEqual: assert.deepStrictEqual, notEqual: assert.notStrictEqual, notDeepEqual: assert.notDeepStrictEqual, + deepMatch: assert.deepMatchStrict, + includes: assert.includesStrict, }); assert.strict.strict = assert.strict; diff --git a/test/parallel/test-assert-objects.js b/test/parallel/test-assert-objects.js new file mode 100644 index 00000000000000..91e1474c0a8eec --- /dev/null +++ b/test/parallel/test-assert-objects.js @@ -0,0 +1,631 @@ +'use strict'; + +require('../common'); +const vm = require('node:vm'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const { KeyObject } = require('node:crypto'); + +const { subtle } = globalThis.crypto; + +function createCircularObject() { + const obj = {}; + obj.self = obj; + return obj; +} + +function createDeepNestedObject() { + return { level1: { level2: { level3: 'deepValue' } } }; +} + +async function generateCryptoKey() { + const cryptoKey = await subtle.generateKey( + { + name: 'HMAC', + hash: 'SHA-256', + length: 256, + }, + true, + ['sign', 'verify'] + ); + + const keyObject = KeyObject.from(cryptoKey); + + return { cryptoKey, keyObject }; +} + +describe('Object Comparison Tests', () => { + describe('deepMatchStrict', () => { + describe('throws an error', () => { + [ + { + description: 'throws when only one argument is provided', + actual: { a: 1 }, + expected: undefined, + }, + { + 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 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 instances', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 2') }, + }, + { + 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 comparing two Map objects with different length', + actual: new Map([ + ['key1', 'value1'], + ['key2', 'value2'], + ]), + expected: new Map([['key1', 'value1']]), + }, + { + 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 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: + 'throws when comparing two objects with different CryptoKey instances objects', + actual: async () => { + return generateCryptoKey(); + }, + expected: async () => { + return generateCryptoKey(); + }, + }, + { + 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'], + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.throws(() => assert.deepMatchStrict(actual, expected), Error); + }); + }); + }); + }); + + describe('does not throw an error', () => { + const sym = Symbol('test'); + const func = () => {}; + + [ + { + 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 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 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 objects with the same TypedArray instance', + actual: { typedArray: new Uint8Array([1, 2, 3]) }, + expected: { typedArray: new Uint8Array([1, 2, 3]) }, + }, + { + 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 Set objects with identical values', + actual: new Set(['value1', 'value2']), + 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 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, does not throw an error', + actual: { a: 1, b: 2, c: 3 }, + expected: { b: 2 }, + }, + { + description: + 'compares one subset array with another, does not throw an error', + actual: [1, 2, 3], + expected: [2], + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.deepMatchStrict(actual, expected); + }); + }); + }); + + describe('deepMatch', () => { + describe('throws an error', () => { + [ + { + description: + 'throws because the expected value is longer than the actual value', + actual: [1, 2, 3], + expected: [1, 2, 3, 4], + }, + { + description: + 'deepMatch throws when comparing two objects with null and empty object', + actual: { a: null }, + expected: { a: {} }, + }, + { + description: 'throws because only one argument is provided', + actual: { a: 1 }, + expected: undefined, + }, + { + description: 'throws because the first argument is null', + actual: null, + expected: { a: 1 }, + }, + { + description: 'throws because the second argument is null', + actual: { a: 1 }, + expected: null, + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.throws(() => assert.deepMatch(actual, expected), Error); + }); + }); + }); + + describe('does not throw an error', () => { + [ + { + description: 'compares two objects with Buffers', + actual: { buf: Buffer.from('Node.js') }, + expected: { buf: Buffer.from('Node.js') }, + }, + { + 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, bigint', + actual: 1n, + expected: 1n, + }, + + { + description: 'compares two non identical simple objects', + actual: { a: 1, b: 'foo', c: '1' }, + expected: { a: 1, c: 1 }, + }, + { + description: 'compares two similar objects with type coercion', + actual: { a: 1, b: '2' }, + expected: { a: 1, b: 2 }, + }, + { + description: + 'compares two objects with nested objects and type coercion', + actual: { level1: { level2: { level3: '42' } } }, + expected: { level1: { level2: { level3: 42 } } }, + }, + { + description: 'compares two objects with circular references', + actual: createCircularObject(), + expected: createCircularObject(), + }, + { + description: 'compares two arrays with type coercion', + actual: [1, '2', true], + expected: [1, 2, 1], + }, + { + description: 'compares two objects with numeric string and number', + actual: { a: '100' }, + expected: { a: 100 }, + }, + { + description: + 'compares two objects with boolean and numeric representations', + actual: { a: 1, b: 0 }, + expected: { a: true, b: false }, + }, + { + description: + 'compares two objects with undefined and missing properties', + actual: { a: undefined }, + expected: {}, + }, + { + 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: [true], + expected: [1], + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.deepMatch(actual, expected); + }); + }); + }); + }); + describe('includesStrict', () => { + describe('throws an error', () => { + [ + { + description: 'throws because only one argument is provided', + actual: () => assert.includesStrict({ a: 1 }), + expected: Error, + }, + { + description: + 'throws when comparing one subset array with another', + actual: () => assert.includesStrict([1, 2, 3], ['2']), + expected: Error, + }, + { + description: + 'throws when comparing one subset string with another', + actual: () => assert.includesStrict('abc', '2'), + expected: Error, + }, + { + description: + 'throws because the expected value is longer than the actual value', + actual: () => assert.includesStrict('abc', 'abcd'), + expected: Error, + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.throws(actual, expected); + }); + }); + }); + + describe('does not throw an error', () => { + [ + { + description: + 'compares one subset array with another, it does not throw an error', + actual: [1, 2, 3], + expected: [2], + }, + { + description: + 'compares one subset string with another, it does not throw an error', + actual: 'abc', + expected: 'b', + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.includesStrict(actual, expected); + }); + }); + }); + }); + + describe('includes', () => { + describe('throws an error', () => { + [ + { + description: 'throws because only one argument is provided', + actual: () => assert.includes({ a: 1 }), + expected: Error, + }, + { + description: + 'throws because using includes with a string and an array', + actual: () => assert.includes('abc', ['a', 'b', 'c']), + expected: Error, + }, + { + description: + 'throws because the expected value is longer than the actual value', + actual: () => assert.includes('abc', 'abcd'), + expected: Error, + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.throws(actual, expected); + }); + }); + }); + + describe('does not throw an error', () => { + [ + { + description: 'compares one subset array with another', + actual: [1, 2, 3], + expected: ['2'], + }, + { + description: 'compares one subset string with another', + actual: 'abc', + expected: 'b', + }, + ].forEach(({ description, actual, expected }) => { + it(description, () => { + assert.includes(actual, expected); + }); + }); + }); + }); +});