2121'use strict' ;
2222
2323const {
24+ ArrayFrom,
25+ ArrayIsArray,
2426 ArrayPrototypeIndexOf,
2527 ArrayPrototypeJoin,
2628 ArrayPrototypePush,
2729 ArrayPrototypeSlice,
2830 Error,
31+ FunctionPrototypeCall,
32+ MapPrototypeDelete,
33+ MapPrototypeGet,
34+ MapPrototypeHas,
35+ MapPrototypeSet,
2936 NumberIsNaN,
3037 ObjectAssign,
3138 ObjectIs,
3239 ObjectKeys,
3340 ObjectPrototypeIsPrototypeOf,
3441 ReflectApply,
42+ ReflectHas,
43+ ReflectOwnKeys,
3544 RegExpPrototypeExec,
45+ SafeMap,
46+ SafeSet,
47+ SafeWeakSet,
3648 String,
3749 StringPrototypeIndexOf,
3850 StringPrototypeSlice,
3951 StringPrototypeSplit,
52+ SymbolIterator,
4053} = primordials ;
4154
4255const {
@@ -50,8 +63,18 @@ const {
5063} = require ( 'internal/errors' ) ;
5164const AssertionError = require ( 'internal/assert/assertion_error' ) ;
5265const { 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' ) ;
5578const { innerOk } = require ( 'internal/assert/utils' ) ;
5679
5780const CallTracker = require ( 'internal/assert/calltracker' ) ;
@@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
341364 }
342365} ;
343366
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+
344552class Comparison {
345553 constructor ( obj , keys , actual ) {
346554 for ( const key of keys ) {
0 commit comments