Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 05d6227

Browse filesBrowse files
Giovanni Buccisynapse
authored andcommitted
assert: add partialDeepStrictEqual
Fixes: #50399 Co-Authored-By: Cristian Barlutiu <cristian.barlutiu@gmail.com> PR-URL: #54630 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Jithil P Ponnan <jithil@outlook.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent 6352604 commit 05d6227
Copy full SHA for 05d6227

File tree

Expand file treeCollapse file tree

4 files changed

+793
-2
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+793
-2
lines changed
Open diff view settings
Collapse file

‎doc/api/assert.md‎

Copy file name to clipboardExpand all lines: doc/api/assert.md
+91Lines changed: 91 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -2543,6 +2543,96 @@ assert.throws(throwingFirst, /Second$/);
25432543
Due to the confusing error-prone notation, avoid a string as the second
25442544
argument.
25452545

2546+
## `assert.partialDeepStrictEqual(actual, expected[, message])`
2547+
2548+
<!-- YAML
2549+
added: REPLACEME
2550+
-->
2551+
2552+
> Stability: 1.0 - Early development
2553+
2554+
* `actual` {any}
2555+
* `expected` {any}
2556+
* `message` {string|Error}
2557+
2558+
[`assert.partialDeepStrictEqual()`][] Asserts the equivalence between the `actual` and `expected` parameters through a
2559+
deep comparison, ensuring that all properties in the `expected` parameter are
2560+
present in the `actual` parameter with equivalent values, not allowing type coercion.
2561+
The main difference with [`assert.deepStrictEqual()`][] is that [`assert.partialDeepStrictEqual()`][] does not require
2562+
all properties in the `actual` parameter to be present in the `expected` parameter.
2563+
This method should always pass the same test cases as [`assert.deepStrictEqual()`][], behaving as a super set of it.
2564+
2565+
```mjs
2566+
import assert from 'node:assert';
2567+
2568+
assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
2569+
// OK
2570+
2571+
assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } });
2572+
// OK
2573+
2574+
assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
2575+
// OK
2576+
2577+
assert.partialDeepStrictEqual(new Set(['value1', 'value2']), new Set(['value1', 'value2']));
2578+
// OK
2579+
2580+
assert.partialDeepStrictEqual(new Map([['key1', 'value1']]), new Map([['key1', 'value1']]));
2581+
// OK
2582+
2583+
assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]));
2584+
// OK
2585+
2586+
assert.partialDeepStrictEqual(/abc/, /abc/);
2587+
// OK
2588+
2589+
assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]);
2590+
// OK
2591+
2592+
assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }]));
2593+
// OK
2594+
2595+
assert.partialDeepStrictEqual(new Date(0), new Date(0));
2596+
// OK
2597+
2598+
assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 });
2599+
// AssertionError
2600+
2601+
assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 });
2602+
// AssertionError
2603+
2604+
assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } });
2605+
// AssertionError
2606+
```
2607+
2608+
```cjs
2609+
const assert = require('node:assert');
2610+
2611+
assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
2612+
// OK
2613+
2614+
assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } });
2615+
// OK
2616+
2617+
assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
2618+
// OK
2619+
2620+
assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]);
2621+
// OK
2622+
2623+
assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }]));
2624+
// OK
2625+
2626+
assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 });
2627+
// AssertionError
2628+
2629+
assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 });
2630+
// AssertionError
2631+
2632+
assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } });
2633+
// AssertionError
2634+
```
2635+
25462636
[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript
25472637
[Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring
25482638
[`!=` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality
@@ -2571,6 +2661,7 @@ argument.
25712661
[`assert.notEqual()`]: #assertnotequalactual-expected-message
25722662
[`assert.notStrictEqual()`]: #assertnotstrictequalactual-expected-message
25732663
[`assert.ok()`]: #assertokvalue-message
2664+
[`assert.partialDeepStrictEqual()`]: #assertpartialdeepstrictequalactual-expected-message
25742665
[`assert.strictEqual()`]: #assertstrictequalactual-expected-message
25752666
[`assert.throws()`]: #assertthrowsfn-error-message
25762667
[`getColorDepth()`]: tty.md#writestreamgetcolordepthenv
Collapse file

‎lib/assert.js‎

Copy file name to clipboardExpand all lines: lib/assert.js
+210-2Lines changed: 210 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,35 @@
2121
'use strict';
2222

2323
const {
24+
ArrayFrom,
25+
ArrayIsArray,
2426
ArrayPrototypeIndexOf,
2527
ArrayPrototypeJoin,
2628
ArrayPrototypePush,
2729
ArrayPrototypeSlice,
2830
Error,
31+
FunctionPrototypeCall,
32+
MapPrototypeDelete,
33+
MapPrototypeGet,
34+
MapPrototypeHas,
35+
MapPrototypeSet,
2936
NumberIsNaN,
3037
ObjectAssign,
3138
ObjectIs,
3239
ObjectKeys,
3340
ObjectPrototypeIsPrototypeOf,
3441
ReflectApply,
42+
ReflectHas,
43+
ReflectOwnKeys,
3544
RegExpPrototypeExec,
45+
SafeMap,
46+
SafeSet,
47+
SafeWeakSet,
3648
String,
3749
StringPrototypeIndexOf,
3850
StringPrototypeSlice,
3951
StringPrototypeSplit,
52+
SymbolIterator,
4053
} = primordials;
4154

4255
const {
@@ -50,8 +63,18 @@ const {
5063
} = require('internal/errors');
5164
const AssertionError = require('internal/assert/assertion_error');
5265
const { inspect } = require('internal/util/inspect');
53-
const { isPromise, isRegExp } = require('internal/util/types');
54-
const { isError, deprecate } = require('internal/util');
66+
const { Buffer } = require('buffer');
67+
const {
68+
isKeyObject,
69+
isPromise,
70+
isRegExp,
71+
isMap,
72+
isSet,
73+
isDate,
74+
isWeakSet,
75+
isWeakMap,
76+
} = require('internal/util/types');
77+
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
5578
const { innerOk } = require('internal/assert/utils');
5679

5780
const CallTracker = require('internal/assert/calltracker');
@@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
341364
}
342365
};
343366

367+
function isSpecial(obj) {
368+
return obj == null || typeof obj !== 'object' || isError(obj) || isRegExp(obj) || isDate(obj);
369+
}
370+
371+
const typesToCallDeepStrictEqualWith = [
372+
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
373+
];
374+
375+
/**
376+
* Compares two objects or values recursively to check if they are equal.
377+
* @param {any} actual - The actual value to compare.
378+
* @param {any} expected - The expected value to compare.
379+
* @param {Set} [comparedObjects=new Set()] - Set to track compared objects for handling circular references.
380+
* @returns {boolean} - Returns `true` if the actual value matches the expected value, otherwise `false`.
381+
* @example
382+
* compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}); // true
383+
*/
384+
function compareBranch(
385+
actual,
386+
expected,
387+
comparedObjects,
388+
) {
389+
// Check for Map object equality
390+
if (isMap(actual) && isMap(expected)) {
391+
if (actual.size !== expected.size) {
392+
return false;
393+
}
394+
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
395+
396+
comparedObjects ??= new SafeWeakSet();
397+
398+
for (const { 0: key, 1: val } of safeIterator) {
399+
if (!MapPrototypeHas(expected, key)) {
400+
return false;
401+
}
402+
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
403+
return false;
404+
}
405+
}
406+
return true;
407+
}
408+
409+
for (const type of typesToCallDeepStrictEqualWith) {
410+
if (type(actual) || type(expected)) {
411+
if (isDeepStrictEqual === undefined) lazyLoadComparison();
412+
return isDeepStrictEqual(actual, expected);
413+
}
414+
}
415+
416+
// Check for Set object equality
417+
// TODO(aduh95): switch to `SetPrototypeIsSubsetOf` when it's available
418+
if (isSet(actual) && isSet(expected)) {
419+
if (expected.size > actual.size) {
420+
return false; // `expected` can't be a subset if it has more elements
421+
}
422+
423+
if (isDeepEqual === undefined) lazyLoadComparison();
424+
425+
const actualArray = ArrayFrom(actual);
426+
const expectedArray = ArrayFrom(expected);
427+
const usedIndices = new SafeSet();
428+
429+
for (let expectedIdx = 0; expectedIdx < expectedArray.length; expectedIdx++) {
430+
const expectedItem = expectedArray[expectedIdx];
431+
let found = false;
432+
433+
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
434+
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
435+
usedIndices.add(actualIdx);
436+
found = true;
437+
break;
438+
}
439+
}
440+
441+
if (!found) {
442+
return false;
443+
}
444+
}
445+
446+
return true;
447+
}
448+
449+
// Check if expected array is a subset of actual array
450+
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
451+
if (expected.length > actual.length) {
452+
return false;
453+
}
454+
455+
if (isDeepEqual === undefined) lazyLoadComparison();
456+
457+
// Create a map to count occurrences of each element in the expected array
458+
const expectedCounts = new SafeMap();
459+
for (const expectedItem of expected) {
460+
let found = false;
461+
for (const { 0: key, 1: count } of expectedCounts) {
462+
if (isDeepStrictEqual(key, expectedItem)) {
463+
MapPrototypeSet(expectedCounts, key, count + 1);
464+
found = true;
465+
break;
466+
}
467+
}
468+
if (!found) {
469+
MapPrototypeSet(expectedCounts, expectedItem, 1);
470+
}
471+
}
472+
473+
// Create a map to count occurrences of relevant elements in the actual array
474+
for (const actualItem of actual) {
475+
for (const { 0: key, 1: count } of expectedCounts) {
476+
if (isDeepStrictEqual(key, actualItem)) {
477+
if (count === 1) {
478+
MapPrototypeDelete(expectedCounts, key);
479+
} else {
480+
MapPrototypeSet(expectedCounts, key, count - 1);
481+
}
482+
break;
483+
}
484+
}
485+
}
486+
487+
return !expectedCounts.size;
488+
}
489+
490+
// Comparison done when at least one of the values is not an object
491+
if (isSpecial(actual) || isSpecial(expected)) {
492+
if (isDeepEqual === undefined) {
493+
lazyLoadComparison();
494+
}
495+
return isDeepStrictEqual(actual, expected);
496+
}
497+
498+
// Use Reflect.ownKeys() instead of Object.keys() to include symbol properties
499+
const keysExpected = ReflectOwnKeys(expected);
500+
501+
comparedObjects ??= new SafeWeakSet();
502+
503+
// Handle circular references
504+
if (comparedObjects.has(actual)) {
505+
return true;
506+
}
507+
comparedObjects.add(actual);
508+
509+
// Check if all expected keys and values match
510+
for (let i = 0; i < keysExpected.length; i++) {
511+
const key = keysExpected[i];
512+
assert(
513+
ReflectHas(actual, key),
514+
new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }),
515+
);
516+
if (!compareBranch(actual[key], expected[key], comparedObjects)) {
517+
return false;
518+
}
519+
}
520+
521+
return true;
522+
}
523+
524+
/**
525+
* The strict equivalence assertion test between two objects
526+
* @param {any} actual
527+
* @param {any} expected
528+
* @param {string | Error} [message]
529+
* @returns {void}
530+
*/
531+
assert.partialDeepStrictEqual = function partialDeepStrictEqual(
532+
actual,
533+
expected,
534+
message,
535+
) {
536+
emitExperimentalWarning('assert.partialDeepStrictEqual');
537+
if (arguments.length < 2) {
538+
throw new ERR_MISSING_ARGS('actual', 'expected');
539+
}
540+
541+
if (!compareBranch(actual, expected)) {
542+
innerFail({
543+
actual,
544+
expected,
545+
message,
546+
operator: 'partialDeepStrictEqual',
547+
stackStartFn: partialDeepStrictEqual,
548+
});
549+
}
550+
};
551+
344552
class Comparison {
345553
constructor(obj, keys, actual) {
346554
for (const key of keys) {
Collapse file

‎lib/internal/test_runner/test.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function lazyAssertObject(harness) {
119119
'notDeepStrictEqual',
120120
'notEqual',
121121
'notStrictEqual',
122+
'partialDeepStrictEqual',
122123
'rejects',
123124
'strictEqual',
124125
'throws',

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.