Skip to content

Commit fed1dac

Browse files
miguelmarcondesftargos
authored andcommitted
lib: update isDeepStrictEqual to support options
PR-URL: #59762 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent cbec4fd commit fed1dac

7 files changed

Lines changed: 315 additions & 13 deletions

File tree

‎doc/api/assert.md‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,20 @@ The `Assert` class allows creating independent assertion instances with custom o
227227

228228
### `new assert.Assert([options])`
229229

230+
<!-- YAML
231+
changes:
232+
- version: REPLACEME
233+
pr-url: https://github.com/nodejs/node/pull/59762
234+
description: Added `skipPrototype` option.
235+
-->
236+
230237
*`options` {Object}
231238
*`diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`.
232239
Accepted values: `'simple'`, `'full'`.
233240
*`strict` {boolean} If set to `true`, non-strict methods behave like their
234241
corresponding strict methods. Defaults to `true`.
242+
*`skipPrototype` {boolean} If set to `true`, skips prototype and constructor
243+
comparison in deep equality checks. Defaults to `false`.
235244

236245
Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages.
237246

@@ -243,7 +252,8 @@ assertInstance.deepStrictEqual({ a: 1 }, { a: 2 });
243252
```
244253

245254
**Important**: When destructuring assertion methods from an `Assert` instance,
246-
the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings).
255+
the methods lose their connection to the instance's configuration options (such
256+
as `diff`, `strict`, and `skipPrototype` settings).
247257
The destructured methods will fall back to default behavior instead.
248258

249259
```js
@@ -257,6 +267,33 @@ const { strictEqual } = myAssert;
257267
strictEqual({ a:1 }, { b: { c:1 } });
258268
```
259269

270+
The `skipPrototype` option affects all deep equality methods:
271+
272+
```js
273+
classFoo {
274+
constructor(a) {
275+
this.a= a;
276+
}
277+
}
278+
279+
classBar {
280+
constructor(a) {
281+
this.a= a;
282+
}
283+
}
284+
285+
constfoo=newFoo(1);
286+
constbar=newBar(1);
287+
288+
// Default behavior - fails due to different constructors
289+
constassert1=newAssert();
290+
assert1.deepStrictEqual(foo, bar); // AssertionError
291+
292+
// Skip prototype comparison - passes if properties are equal
293+
constassert2=newAssert({ skipPrototype:true });
294+
assert2.deepStrictEqual(foo, bar); // OK
295+
```
296+
260297
When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
261298
(diff: 'simple', non-strict mode).
262299
To maintain custom options when using destructured methods, avoid

‎doc/api/util.md‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1551,19 +1551,56 @@ inspect.defaultOptions.maxArrayLength = null;
15511551
console.log(arr); // logs the full array
15521552
```
15531553

1554-
## `util.isDeepStrictEqual(val1, val2)`
1554+
## `util.isDeepStrictEqual(val1, val2[, options])`
15551555

15561556
<!-- YAML
15571557
added: v9.0.0
1558+
changes:
1559+
- version: REPLACEME
1560+
pr-url: https://github.com/nodejs/node/pull/59762
1561+
description: Added `options` parameter to allow skipping prototype comparison.
15581562
-->
15591563

15601564
*`val1` {any}
15611565
*`val2` {any}
1566+
*`skipPrototype` {boolean} If `true`, prototype and constructor
1567+
comparison is skipped during deep strict equality check. **Default:**`false`.
15621568
* Returns: {boolean}
15631569

15641570
Returns `true` if there is deep strict equality between `val1` and `val2`.
15651571
Otherwise, returns `false`.
15661572

1573+
By default, deep strict equality includes comparison of object prototypes and
1574+
constructors. When `skipPrototype` is `true`, objects with
1575+
different prototypes or constructors can still be considered equal if their
1576+
enumerable properties are deeply strictly equal.
1577+
1578+
```js
1579+
constutil=require('node:util');
1580+
1581+
classFoo {
1582+
constructor(a) {
1583+
this.a= a;
1584+
}
1585+
}
1586+
1587+
classBar {
1588+
constructor(a) {
1589+
this.a= a;
1590+
}
1591+
}
1592+
1593+
constfoo=newFoo(1);
1594+
constbar=newBar(1);
1595+
1596+
// Different constructors, same properties
1597+
console.log(util.isDeepStrictEqual(foo, bar));
1598+
// false
1599+
1600+
console.log(util.isDeepStrictEqual(foo, bar, true));
1601+
// true
1602+
```
1603+
15671604
See [`assert.deepStrictEqual()`][] for more information about deep strict
15681605
equality.
15691606

‎lib/assert.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ const NO_EXCEPTION_SENTINEL = {};
9696
* @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors.
9797
* @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding
9898
* strict methods.
99+
* @property {boolean} [skipPrototype=false] - If set to true, skips comparing prototypes
100+
* in deep equality checks.
99101
*/
100102

101103
/**
@@ -108,7 +110,7 @@ function Assert(options) {
108110
thrownewERR_CONSTRUCT_CALL_REQUIRED('Assert');
109111
}
110112

111-
options=ObjectAssign({__proto__: null,strict: true},options);
113+
options=ObjectAssign({__proto__: null,strict: true,skipPrototype: false},options);
112114

113115
constallowedDiffs=['simple','full'];
114116
if(options.diff!==undefined){
@@ -336,7 +338,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me
336338
thrownewERR_MISSING_ARGS('actual','expected');
337339
}
338340
if(isDeepEqual===undefined)lazyLoadComparison();
339-
if(!isDeepStrictEqual(actual,expected)){
341+
if(!isDeepStrictEqual(actual,expected,this?.[kOptions]?.skipPrototype)){
340342
innerFail({
341343
actual,
342344
expected,
@@ -362,7 +364,7 @@ function notDeepStrictEqual(actual, expected, message) {
362364
thrownewERR_MISSING_ARGS('actual','expected');
363365
}
364366
if(isDeepEqual===undefined)lazyLoadComparison();
365-
if(isDeepStrictEqual(actual,expected)){
367+
if(isDeepStrictEqual(actual,expected,this?.[kOptions]?.skipPrototype)){
366368
innerFail({
367369
actual,
368370
expected,

‎lib/internal/util/comparisons.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,10 @@ const {
126126
getOwnNonIndexProperties,
127127
}=internalBinding('util');
128128

129-
constkStrict=1;
129+
constkStrict=2;
130+
constkStrictWithoutPrototypes=3;
130131
constkLoose=0;
131-
constkPartial=2;
132+
constkPartial=1;
132133

133134
constkNoIterator=0;
134135
constkIsArray=1;
@@ -452,7 +453,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
452453
}
453454
}elseif(keys2.length!==(keys1=ObjectKeys(val1)).length){
454455
returnfalse;
455-
}elseif(mode===kStrict){
456+
}elseif(mode===kStrict||mode===kStrictWithoutPrototypes){
456457
constsymbolKeysA=getOwnSymbols(val1);
457458
if(symbolKeysA.length!==0){
458459
letcount=0;
@@ -1021,7 +1022,10 @@ module.exports = {
10211022
isDeepEqual(val1,val2){
10221023
returndetectCycles(val1,val2,kLoose);
10231024
},
1024-
isDeepStrictEqual(val1,val2){
1025+
isDeepStrictEqual(val1,val2,skipPrototype){
1026+
if(skipPrototype){
1027+
returndetectCycles(val1,val2,kStrictWithoutPrototypes);
1028+
}
10251029
returndetectCycles(val1,val2,kStrict);
10261030
},
10271031
isPartialStrictEqual(val1,val2){

‎lib/util.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,12 +487,11 @@ module.exports = {
487487
isArray: deprecate(ArrayIsArray,
488488
'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.',
489489
'DEP0044'),
490-
isDeepStrictEqual(a,b){
490+
isDeepStrictEqual(a,b,skipPrototype){
491491
if(internalDeepEqual===undefined){
492-
internalDeepEqual=require('internal/util/comparisons')
493-
.isDeepStrictEqual;
492+
internalDeepEqual=require('internal/util/comparisons').isDeepStrictEqual;
494493
}
495-
returninternalDeepEqual(a,b);
494+
returninternalDeepEqual(a,b,skipPrototype);
496495
},
497496
promisify,
498497
stripVTControlCharacters,

‎test/parallel/test-assert-class.js‎

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,163 @@ test('Assert class non strict with simple diff', () => {
478478
);
479479
}
480480
});
481+
482+
// Shared setup for skipPrototype tests
483+
{
484+
constmessage='Expected values to be strictly deep-equal:\n'+
485+
'+ actual - expected\n'+
486+
'\n'+
487+
' [\n'+
488+
' 1,\n'+
489+
' 2,\n'+
490+
' 3,\n'+
491+
' 4,\n'+
492+
' 5,\n'+
493+
'+ 6,\n'+
494+
'- 9,\n'+
495+
' 7\n'+
496+
' ]\n';
497+
498+
functionCoolClass(name){this.name=name;}
499+
500+
functionAwesomeClass(name){this.name=name;}
501+
502+
classModern{constructor(value){this.value=value;}}
503+
classLegacy{constructor(value){this.value=value;}}
504+
505+
constcool=newCoolClass('Assert is inspiring');
506+
constawesome=newAwesomeClass('Assert is inspiring');
507+
constmodern=newModern(42);
508+
constlegacy=newLegacy(42);
509+
510+
test('Assert class strict with skipPrototype',()=>{
511+
constassertInstance=newAssert({skipPrototype: true});
512+
513+
assert.throws(
514+
()=>assertInstance.deepEqual([1,2,3,4,5,6,7],[1,2,3,4,5,9,7]),
515+
{ message }
516+
);
517+
518+
assertInstance.deepEqual(cool,awesome);
519+
assertInstance.deepStrictEqual(cool,awesome);
520+
assertInstance.deepEqual(modern,legacy);
521+
assertInstance.deepStrictEqual(modern,legacy);
522+
523+
constcool2=newCoolClass('Soooo coooool');
524+
assert.throws(
525+
()=>assertInstance.deepStrictEqual(cool,cool2),
526+
{code: 'ERR_ASSERTION'}
527+
);
528+
529+
constnested1={obj: newCoolClass('test'),arr: [1,2,3]};
530+
constnested2={obj: newAwesomeClass('test'),arr: [1,2,3]};
531+
assertInstance.deepStrictEqual(nested1,nested2);
532+
533+
constarr=newUint8Array([1,2,3]);
534+
constbuf=Buffer.from([1,2,3]);
535+
assertInstance.deepStrictEqual(arr,buf);
536+
});
537+
538+
test('Assert class non strict with skipPrototype',()=>{
539+
constassertInstance=newAssert({strict: false,skipPrototype: true});
540+
541+
assert.throws(
542+
()=>assertInstance.deepStrictEqual([1,2,3,4,5,6,7],[1,2,3,4,5,9,7]),
543+
{ message }
544+
);
545+
546+
assertInstance.deepStrictEqual(cool,awesome);
547+
assertInstance.deepStrictEqual(modern,legacy);
548+
});
549+
550+
test('Assert class skipPrototype with complex objects',()=>{
551+
constassertInstance=newAssert({skipPrototype: true});
552+
553+
functionComplexAwesomeClass(name,age){
554+
this.name=name;
555+
this.age=age;
556+
this.settings={
557+
theme: 'dark',
558+
lang: 'en'
559+
};
560+
}
561+
562+
functionComplexCoolClass(name,age){
563+
this.name=name;
564+
this.age=age;
565+
this.settings={
566+
theme: 'dark',
567+
lang: 'en'
568+
};
569+
}
570+
571+
constawesome1=newComplexAwesomeClass('Foo',30);
572+
constcool1=newComplexCoolClass('Foo',30);
573+
574+
assertInstance.deepStrictEqual(awesome1,cool1);
575+
576+
constcool2=newComplexCoolClass('Foo',30);
577+
cool2.settings.theme='light';
578+
579+
assert.throws(
580+
()=>assertInstance.deepStrictEqual(awesome1,cool2),
581+
{code: 'ERR_ASSERTION'}
582+
);
583+
});
584+
585+
test('Assert class skipPrototype with arrays and special objects',()=>{
586+
constassertInstance=newAssert({skipPrototype: true});
587+
588+
constarr1=[1,2,3];
589+
constarr2=newArray(1,2,3);
590+
assertInstance.deepStrictEqual(arr1,arr2);
591+
592+
constdate1=newDate('2023-01-01');
593+
constdate2=newDate('2023-01-01');
594+
assertInstance.deepStrictEqual(date1,date2);
595+
596+
constregex1=/test/g;
597+
constregex2=newRegExp('test','g');
598+
assertInstance.deepStrictEqual(regex1,regex2);
599+
600+
constdate3=newDate('2023-01-02');
601+
assert.throws(
602+
()=>assertInstance.deepStrictEqual(date1,date3),
603+
{code: 'ERR_ASSERTION'}
604+
);
605+
});
606+
607+
test('Assert class skipPrototype with notDeepStrictEqual',()=>{
608+
constassertInstance=newAssert({skipPrototype: true});
609+
610+
assert.throws(
611+
()=>assertInstance.notDeepStrictEqual(cool,awesome),
612+
{code: 'ERR_ASSERTION'}
613+
);
614+
615+
constnotAwesome=newAwesomeClass('Not so awesome');
616+
assertInstance.notDeepStrictEqual(cool,notAwesome);
617+
618+
constdefaultAssertInstance=newAssert({skipPrototype: false});
619+
defaultAssertInstance.notDeepStrictEqual(cool,awesome);
620+
});
621+
622+
test('Assert class skipPrototype with mixed types',()=>{
623+
constassertInstance=newAssert({skipPrototype: true});
624+
625+
constobj1={value: 42,nested: {prop: 'test'}};
626+
627+
functionCustomObj(value,nested){
628+
this.value=value;
629+
this.nested=nested;
630+
}
631+
632+
constobj2=newCustomObj(42,{prop: 'test'});
633+
assertInstance.deepStrictEqual(obj1,obj2);
634+
635+
assert.throws(
636+
()=>assertInstance.deepStrictEqual({num: 42},{num: '42'}),
637+
{code: 'ERR_ASSERTION'}
638+
);
639+
});
640+
}

0 commit comments

Comments
 (0)