21
21
'use strict' ;
22
22
23
23
const {
24
+ ArrayFrom,
25
+ ArrayIsArray,
24
26
ArrayPrototypeIndexOf,
25
27
ArrayPrototypeJoin,
26
28
ArrayPrototypePush,
27
29
ArrayPrototypeSlice,
28
30
Error,
31
+ FunctionPrototypeCall,
32
+ MapPrototypeDelete,
33
+ MapPrototypeGet,
34
+ MapPrototypeHas,
35
+ MapPrototypeSet,
29
36
NumberIsNaN,
30
37
ObjectAssign,
31
38
ObjectIs,
32
39
ObjectKeys,
33
40
ObjectPrototypeIsPrototypeOf,
34
41
ReflectApply,
42
+ ReflectHas,
43
+ ReflectOwnKeys,
35
44
RegExpPrototypeExec,
45
+ SafeMap,
46
+ SafeSet,
47
+ SafeWeakSet,
36
48
String,
37
49
StringPrototypeIndexOf,
38
50
StringPrototypeSlice,
39
51
StringPrototypeSplit,
52
+ SymbolIterator,
40
53
} = primordials ;
41
54
42
55
const {
@@ -50,8 +63,18 @@ const {
50
63
} = require ( 'internal/errors' ) ;
51
64
const AssertionError = require ( 'internal/assert/assertion_error' ) ;
52
65
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' ) ;
55
78
const { innerOk } = require ( 'internal/assert/utils' ) ;
56
79
57
80
const CallTracker = require ( 'internal/assert/calltracker' ) ;
@@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
341
364
}
342
365
} ;
343
366
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
+
344
552
class Comparison {
345
553
constructor ( obj , keys , actual ) {
346
554
for ( const key of keys ) {
0 commit comments