@@ -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 non object types equality
648
+ if (!isPlainObject(actual) || !isPlainObject(expected)) {
649
+ if (isDeepEqual === undefined) lazyLoadComparison();
650
+ return loose ? isDeepEqual(actual, expected) : isDeepStrictEqual(actual, expected);
651
+ }
652
+
653
+ // Check if actual and expected are null or not objects
654
+ if (actual == null || expected == null) {
655
+ return false;
656
+ }
657
+
658
+ // Check for Map object equality
659
+ if (isMap(actual) && isMap(expected)) {
660
+ if (actual.size !== expected.size) return false;
661
+ const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
662
+ for (const { 0: key, 1: val } of safeIterator) {
663
+ if (!MapPrototypeHas(expected, key)) return false;
664
+ if (!compareBranch(val, MapPrototypeGet(expected, key), loose, comparedObjects))
665
+ return false;
666
+ }
667
+ return true;
668
+ }
669
+
670
+ // Check for Set object equality
671
+ if (isSet(actual) && isSet(expected)) {
672
+ if (actual.size !== expected.size) return false;
673
+ const safeIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual);
674
+ for (const item of safeIterator) {
675
+ if (!SetPrototypeHas(expected, item)) return false;
676
+ }
677
+ return true;
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