@@ -303,7 +303,7 @@ describe('Function: deepMerge', () => {
303303 expect ( ( result . arr as unknown [ ] ) [ 1 ] ) . toBe ( 2 ) ;
304304 } ) ;
305305
306- it ( 'skips already-seen arrays to prevent duplication ' , ( ) => {
306+ it ( 'merges shared array references into all properties ' , ( ) => {
307307 // Prepare
308308 const target = { } ;
309309 const sharedArray = [ 1 , 2 , 3 ] ;
@@ -315,16 +315,14 @@ describe('Function: deepMerge', () => {
315315 // Act
316316 const result = deepMerge ( target , source ) ;
317317
318- // Assess - shared array is copied on first occurrence,
319- // skipped on subsequent occurrences
318+ // Assess - shared (non-circular) array is merged into both properties
320319 expect ( result ) . toEqual ( {
321320 first : [ 1 , 2 , 3 ] ,
322- second : undefined ,
321+ second : [ 1 , 2 , 3 ] ,
323322 } ) ;
324- expect ( result ) . not . toHaveProperty ( 'second' ) ;
325323 } ) ;
326324
327- it ( 'skips already-seen objects within arrays' , ( ) => {
325+ it ( 'merges shared objects within arrays' , ( ) => {
328326 // Prepare
329327 const sharedObj = { shared : true } ;
330328 const target = {
@@ -338,17 +336,85 @@ describe('Function: deepMerge', () => {
338336 // Act
339337 const result = deepMerge ( target , source ) ;
340338
341- // Assess - sharedObj is merged into prop first, then skipped when
342- // encountered again in the array merge
339+ // Assess - sharedObj is merged into both prop and arr[0]
343340 expect ( result . prop ) . toEqual ( { shared : true } ) ;
344- expect ( ( result . arr as Record < string , unknown > [ ] ) [ 0 ] ) . toEqual ( { a : 1 } ) ;
341+ expect ( ( result . arr as Record < string , unknown > [ ] ) [ 0 ] ) . toEqual ( {
342+ a : 1 ,
343+ shared : true ,
344+ } ) ;
345+ expect ( ( result . arr as Record < string , unknown > [ ] ) [ 1 ] ) . toEqual ( {
346+ b : 2 ,
347+ c : 3 ,
348+ } ) ;
349+ } ) ;
350+
351+ it ( 'merges shared objects into all referencing properties' , ( ) => {
352+ // Prepare
353+ const target = { } ;
354+ const shared = { value : 42 } ;
355+ const source = {
356+ first : shared ,
357+ second : { nested : shared } ,
358+ } ;
359+
360+ // Act
361+ const result = deepMerge ( target , source ) ;
362+
363+ // Assess - shared (non-circular) object appears in both locations
364+ expect ( result ) . toEqual ( {
365+ first : { value : 42 } ,
366+ second : { nested : { value : 42 } } ,
367+ } ) ;
368+ } ) ;
369+ } ) ;
370+
371+ describe ( 'Shared (non-circular) references' , ( ) => {
372+ it ( 'correctly merges shared array references into both properties' , ( ) => {
373+ // Prepare
374+ const target = { } ;
375+ const sharedArray = [ 1 , 2 , 3 ] ;
376+ const source = {
377+ first : sharedArray ,
378+ second : sharedArray ,
379+ } ;
380+
381+ // Act
382+ const result = deepMerge ( target , source ) ;
383+
384+ // Assess - shared array should be merged into both properties
385+ expect ( result ) . toEqual ( {
386+ first : [ 1 , 2 , 3 ] ,
387+ second : [ 1 , 2 , 3 ] ,
388+ } ) ;
389+ } ) ;
390+
391+ it ( 'correctly merges shared objects referenced in arrays' , ( ) => {
392+ // Prepare
393+ const sharedObj = { shared : true } ;
394+ const target = {
395+ arr : [ { a : 1 } , { b : 2 } ] ,
396+ } ;
397+ const source = {
398+ prop : sharedObj ,
399+ arr : [ sharedObj , { c : 3 } ] ,
400+ } ;
401+
402+ // Act
403+ const result = deepMerge ( target , source ) ;
404+
405+ // Assess - sharedObj should be merged into both prop and arr[0]
406+ expect ( result . prop ) . toEqual ( { shared : true } ) ;
407+ expect ( ( result . arr as Record < string , unknown > [ ] ) [ 0 ] ) . toEqual ( {
408+ a : 1 ,
409+ shared : true ,
410+ } ) ;
345411 expect ( ( result . arr as Record < string , unknown > [ ] ) [ 1 ] ) . toEqual ( {
346412 b : 2 ,
347413 c : 3 ,
348414 } ) ;
349415 } ) ;
350416
351- it ( 'skips already-seen objects to prevent duplication ' , ( ) => {
417+ it ( 'correctly merges shared objects into all referencing properties ' , ( ) => {
352418 // Prepare
353419 const target = { } ;
354420 const shared = { value : 42 } ;
@@ -360,11 +426,81 @@ describe('Function: deepMerge', () => {
360426 // Act
361427 const result = deepMerge ( target , source ) ;
362428
363- // Assess - shared object is merged on first occurrence,
364- // skipped on subsequent occurrences to prevent infinite recursion
429+ // Assess - shared object should appear in both locations
365430 expect ( result ) . toEqual ( {
366431 first : { value : 42 } ,
367- second : { } ,
432+ second : { nested : { value : 42 } } ,
433+ } ) ;
434+ } ) ;
435+
436+ it ( 'correctly merges shared objects across multiple sources' , ( ) => {
437+ // Prepare
438+ const shared = { x : 1 } ;
439+ const target = { } ;
440+ const source1 = { a : shared } ;
441+ const source2 = { b : shared } ;
442+
443+ // Act
444+ const result = deepMerge ( target , source1 , source2 ) ;
445+
446+ // Assess
447+ expect ( result ) . toEqual ( { a : { x : 1 } , b : { x : 1 } } ) ;
448+ } ) ;
449+
450+ it ( 'correctly merges diamond-shaped shared references' , ( ) => {
451+ // Prepare
452+ const shared = { value : 'shared' } ;
453+ const target = { } ;
454+ const source = {
455+ branch1 : { leaf : shared } ,
456+ branch2 : { leaf : shared } ,
457+ } ;
458+
459+ // Act
460+ const result = deepMerge ( target , source ) ;
461+
462+ // Assess - same object at different depths in two branches
463+ expect ( result ) . toEqual ( {
464+ branch1 : { leaf : { value : 'shared' } } ,
465+ branch2 : { leaf : { value : 'shared' } } ,
466+ } ) ;
467+ } ) ;
468+
469+ it ( 'merges a shared object that itself contains a circular reference' , ( ) => {
470+ // Prepare
471+ const shared : Record < string , unknown > = { value : 1 } ;
472+ shared . self = shared ;
473+ const target = { } ;
474+ const source = {
475+ first : shared ,
476+ second : { nested : shared } ,
477+ } ;
478+
479+ // Act
480+ const result = deepMerge ( target , source ) ;
481+
482+ // Assess - shared object is merged in both locations,
483+ // but the circular self-reference within it is skipped
484+ expect ( result ) . toEqual ( {
485+ first : { value : 1 } ,
486+ second : { nested : { value : 1 } } ,
487+ } ) ;
488+ } ) ;
489+
490+ it ( 'merges when source references an object from the target' , ( ) => {
491+ // Prepare
492+ const inner = { x : 1 } ;
493+ const target : Record < string , unknown > = { a : inner } ;
494+ const source = { b : inner } ;
495+
496+ // Act
497+ const result = deepMerge ( target , source ) ;
498+
499+ // Assess - inner is not in the ancestor chain when processing source,
500+ // so it should be merged into both properties
501+ expect ( result ) . toEqual ( {
502+ a : { x : 1 } ,
503+ b : { x : 1 } ,
368504 } ) ;
369505 } ) ;
370506 } ) ;
0 commit comments