Skip to content

Commit a4f57f0

Browse files
Giovanni Buccisynapse
authored andcommitted
assert: add partialDeepStrictEqual
Fixes: #50399 Co-Authored-By: Cristian Barlutiu <[email protected]> PR-URL: #54630 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Jithil P Ponnan <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent 98f8f4a commit a4f57f0

File tree

4 files changed

+803
-2
lines changed

4 files changed

+803
-2
lines changed

‎doc/api/assert.md‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,6 +2548,96 @@ assert.throws(throwingFirst, /Second$/);
25482548
Due to the confusing error-prone notation, avoid a string as the second
25492549
argument.
25502550

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

‎lib/assert.js‎

Lines 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
constAssertionError=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
constCallTracker=require('internal/assert/calltracker');
@@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message){
341364
}
342365
};
343366

367+
functionisSpecial(obj){
368+
returnobj==null||typeofobj!=='object'||isError(obj)||isRegExp(obj)||isDate(obj);
369+
}
370+
371+
consttypesToCallDeepStrictEqualWith=[
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+
functioncompareBranch(
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+
returnfalse;
393+
}
394+
constsafeIterator=FunctionPrototypeCall(SafeMap.prototype[SymbolIterator],actual);
395+
396+
comparedObjects??=newSafeWeakSet();
397+
398+
for(const{0: key,1: val}ofsafeIterator){
399+
if(!MapPrototypeHas(expected,key)){
400+
returnfalse;
401+
}
402+
if(!compareBranch(val,MapPrototypeGet(expected,key),comparedObjects)){
403+
returnfalse;
404+
}
405+
}
406+
returntrue;
407+
}
408+
409+
for(consttypeoftypesToCallDeepStrictEqualWith){
410+
if(type(actual)||type(expected)){
411+
if(isDeepStrictEqual===undefined)lazyLoadComparison();
412+
returnisDeepStrictEqual(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+
returnfalse;// `expected` can't be a subset if it has more elements
421+
}
422+
423+
if(isDeepEqual===undefined)lazyLoadComparison();
424+
425+
constactualArray=ArrayFrom(actual);
426+
constexpectedArray=ArrayFrom(expected);
427+
constusedIndices=newSafeSet();
428+
429+
for(letexpectedIdx=0;expectedIdx<expectedArray.length;expectedIdx++){
430+
constexpectedItem=expectedArray[expectedIdx];
431+
letfound=false;
432+
433+
for(letactualIdx=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+
returnfalse;
443+
}
444+
}
445+
446+
returntrue;
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+
returnfalse;
453+
}
454+
455+
if(isDeepEqual===undefined)lazyLoadComparison();
456+
457+
// Create a map to count occurrences of each element in the expected array
458+
constexpectedCounts=newSafeMap();
459+
for(constexpectedItemofexpected){
460+
letfound=false;
461+
for(const{0: key,1: count}ofexpectedCounts){
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(constactualItemofactual){
475+
for(const{0: key,1: count}ofexpectedCounts){
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+
returnisDeepStrictEqual(actual,expected);
496+
}
497+
498+
// Use Reflect.ownKeys() instead of Object.keys() to include symbol properties
499+
constkeysExpected=ReflectOwnKeys(expected);
500+
501+
comparedObjects??=newSafeWeakSet();
502+
503+
// Handle circular references
504+
if(comparedObjects.has(actual)){
505+
returntrue;
506+
}
507+
comparedObjects.add(actual);
508+
509+
// Check if all expected keys and values match
510+
for(leti=0;i<keysExpected.length;i++){
511+
constkey=keysExpected[i];
512+
assert(
513+
ReflectHas(actual,key),
514+
newAssertionError({message: `Expected key ${String(key)} not found in actual object`}),
515+
);
516+
if(!compareBranch(actual[key],expected[key],comparedObjects)){
517+
returnfalse;
518+
}
519+
}
520+
521+
returntrue;
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=functionpartialDeepStrictEqual(
532+
actual,
533+
expected,
534+
message,
535+
){
536+
emitExperimentalWarning('assert.partialDeepStrictEqual');
537+
if(arguments.length<2){
538+
thrownewERR_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
classComparison{
345553
constructor(obj,keys,actual){
346554
for(constkeyofkeys){

‎lib/internal/test_runner/test.js‎

Lines 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)