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