@@ -29,15 +29,24 @@ const {
29
29
Error,
30
30
ErrorCaptureStackTrace,
31
31
FunctionPrototypeBind,
32
+ FunctionPrototypeCall,
33
+ MapPrototypeGet,
34
+ MapPrototypeHas,
32
35
NumberIsNaN,
33
36
ObjectAssign,
37
+ ObjectGetPrototypeOf,
34
38
ObjectIs,
35
39
ObjectKeys,
40
+ ObjectPrototype,
36
41
ObjectPrototypeIsPrototypeOf,
42
+ ObjectPrototypeToString,
37
43
ReflectApply,
44
+ ReflectOwnKeys,
38
45
RegExpPrototypeExec,
39
46
RegExpPrototypeSymbolReplace,
40
47
SafeMap,
48
+ SafeSet,
49
+ SetPrototypeHas,
41
50
String,
42
51
StringPrototypeCharCodeAt,
43
52
StringPrototypeIncludes,
@@ -46,6 +55,7 @@ const {
46
55
StringPrototypeSlice,
47
56
StringPrototypeSplit,
48
57
StringPrototypeStartsWith,
58
+ SymbolIterator,
49
59
} = primordials ;
50
60
51
61
const { Buffer } = require ( 'buffer' ) ;
@@ -63,7 +73,7 @@ const {
63
73
const AssertionError = require ( 'internal/assert/assertion_error' ) ;
64
74
const { openSync, closeSync, readSync } = require ( 'fs' ) ;
65
75
const { inspect } = require ( 'internal/util/inspect' ) ;
66
- const { isPromise, isRegExp } = require ( 'internal/util/types' ) ;
76
+ const { isPromise, isRegExp, isMap , isSet } = require ( 'internal/util/types' ) ;
67
77
const { EOL } = require ( 'internal/constants' ) ;
68
78
const { BuiltinModule } = require ( 'internal/bootstrap/realm' ) ;
69
79
const { isError, deprecate } = require ( 'internal/util' ) ;
@@ -608,6 +618,144 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
608
618
}
609
619
} ;
610
620
621
+ /**
622
+ * Compares two objects or values recursively to check if they are equal.
623
+ * @param {any } actual - The actual value to compare.
624
+ * @param {any } expected - The expected value to compare.
625
+ * @param {boolean } [loose=false] - Whether to use loose comparison (==) or strict comparison (===). Defaults to false.
626
+ * @param {Set } [comparedObjects=new Set()] - Set to track compared objects for handling circular references.
627
+ * @returns {boolean } - Returns `true` if the actual value matches the expected value, otherwise `false`.
628
+ * @example
629
+ * // Loose comparison (default)
630
+ * compareBranch({a: 1, b: 2}, {a: 1, b: '2'}); // true
631
+ *
632
+ * // Strict comparison
633
+ * compareBranch({a: 1, b: 2}, {a: 1, b: 2}, true); // true
634
+ */
635
+ function compareBranch (
636
+ actual ,
637
+ expected ,
638
+ loose = false ,
639
+ comparedObjects = new SafeSet ( ) ,
640
+ ) {
641
+ function isPlainObject ( obj ) {
642
+ if ( typeof obj !== 'object' || obj === null ) return false ;
643
+ const proto = ObjectGetPrototypeOf ( obj ) ;
644
+ return proto === ObjectPrototype || proto === null || ObjectPrototypeToString ( obj ) === '[object Object]' ;
645
+ }
646
+
647
+ // Check for Map object equality
648
+ if ( isMap ( actual ) && isMap ( expected ) ) {
649
+ if ( actual . size !== expected . size ) return false ;
650
+ const safeIterator = FunctionPrototypeCall ( SafeMap . prototype [ SymbolIterator ] , actual ) ;
651
+ for ( const { 0 : key , 1 : val } of safeIterator ) {
652
+ if ( ! MapPrototypeHas ( expected , key ) ) return false ;
653
+ if ( ! compareBranch ( val , MapPrototypeGet ( expected , key ) , loose , comparedObjects ) )
654
+ return false ;
655
+ }
656
+ return true ;
657
+ }
658
+
659
+ // Check for Set object equality
660
+ if ( isSet ( actual ) && isSet ( expected ) ) {
661
+ if ( actual . size !== expected . size ) return false ;
662
+ const safeIterator = FunctionPrototypeCall ( SafeSet . prototype [ SymbolIterator ] , actual ) ;
663
+ for ( const item of safeIterator ) {
664
+ if ( ! SetPrototypeHas ( expected , item ) ) return false ;
665
+ }
666
+ return true ;
667
+ }
668
+
669
+ // Check non object types equality
670
+ if ( ! isPlainObject ( actual ) || ! isPlainObject ( expected ) ) {
671
+ if ( isDeepEqual === undefined ) lazyLoadComparison ( ) ;
672
+ return loose ? isDeepEqual ( actual , expected ) : isDeepStrictEqual ( actual , expected ) ;
673
+ }
674
+
675
+ // Check if actual and expected are null or not objects
676
+ if ( actual == null || expected == null ) {
677
+ return false ;
678
+ }
679
+
680
+ // Use Reflect.ownKeys() instead of Object.keys() to include symbol properties
681
+ const keysActual = ReflectOwnKeys ( actual ) ;
682
+ const keysExpected = ReflectOwnKeys ( expected ) ;
683
+
684
+ // Check if number of keys are equal
685
+ if ( keysActual . length !== keysExpected . length ) {
686
+ return false ;
687
+ }
688
+
689
+ // Handle circular references
690
+ if ( comparedObjects . has ( actual ) ) {
691
+ return true ;
692
+ }
693
+ comparedObjects . add ( actual ) ;
694
+
695
+ // Check if all keys and values recursively match
696
+ for ( let i = 0 ; i < keysActual . length ; i ++ ) {
697
+ const key = keysActual [ i ] ;
698
+ if (
699
+ ! keysExpected . includes ( key ) ||
700
+ ! compareBranch ( actual [ key ] , expected [ key ] , loose , comparedObjects )
701
+ ) {
702
+ return false ;
703
+ }
704
+ }
705
+
706
+ return true ;
707
+ }
708
+
709
+ /**
710
+ * The strict equivalence assertion test between two objects
711
+ * @param {any } actual
712
+ * @param {any } expected
713
+ * @param {string | Error } [message]
714
+ * @returns {void }
715
+ */
716
+ assert . matchObjectStrict = function matchObjectStrict (
717
+ actual ,
718
+ expected ,
719
+ message ,
720
+ ) {
721
+ if ( arguments . length < 2 ) {
722
+ throw new ERR_MISSING_ARGS ( 'actual' , 'expected' ) ;
723
+ }
724
+
725
+ if ( ! compareBranch ( actual , expected ) ) {
726
+ innerFail ( {
727
+ actual,
728
+ expected,
729
+ message,
730
+ operator : 'matchObjectStrict' ,
731
+ stackStartFn : matchObjectStrict ,
732
+ } ) ;
733
+ }
734
+ } ;
735
+
736
+ /**
737
+ * The equivalence assertion test between two objects
738
+ * @param {any } actual
739
+ * @param {any } expected
740
+ * @param {string | Error } [message]
741
+ * @returns {void }
742
+ */
743
+ assert . matchObject = function matchObject ( actual , expected , message ) {
744
+ if ( arguments . length < 2 ) {
745
+ throw new ERR_MISSING_ARGS ( 'actual' , 'expected' ) ;
746
+ }
747
+
748
+ if ( ! compareBranch ( actual , expected , true ) ) {
749
+ innerFail ( {
750
+ actual,
751
+ expected,
752
+ message,
753
+ operator : 'matchObject' ,
754
+ stackStartFn : matchObject ,
755
+ } ) ;
756
+ }
757
+ } ;
758
+
611
759
class Comparison {
612
760
constructor ( obj , keys , actual ) {
613
761
for ( const key of keys ) {
0 commit comments