@@ -666,6 +666,322 @@ describe('decodePath', () => {
666666 } )
667667} )
668668
669+ /**
670+ * Tests for getEnumerableOwnKeys behavior (internal function).
671+ * Tested indirectly through replaceEqualDeep since getEnumerableOwnKeys is not exported.
672+ *
673+ * getEnumerableOwnKeys should:
674+ * 1. Return array of all enumerable own keys (strings + symbols)
675+ * 2. Return false if any property is non-enumerable
676+ * 3. Handle objects with no symbols efficiently (optimization target)
677+ */
678+ describe ( 'getEnumerableOwnKeys behavior (via replaceEqualDeep)' , ( ) => {
679+ describe ( 'plain objects with string keys only' , ( ) => {
680+ it ( 'should handle empty objects' , ( ) => {
681+ const prev = { }
682+ const next = { }
683+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
684+ } )
685+
686+ it ( 'should handle objects with single key' , ( ) => {
687+ const prev = { a : 1 }
688+ const next = { a : 1 }
689+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
690+ } )
691+
692+ it ( 'should handle objects with many keys' , ( ) => {
693+ const prev = {
694+ a : 1 ,
695+ b : 2 ,
696+ c : 3 ,
697+ d : 4 ,
698+ e : 5 ,
699+ f : 6 ,
700+ g : 7 ,
701+ h : 8 ,
702+ i : 9 ,
703+ j : 10 ,
704+ }
705+ const next = {
706+ a : 1 ,
707+ b : 2 ,
708+ c : 3 ,
709+ d : 4 ,
710+ e : 5 ,
711+ f : 6 ,
712+ g : 7 ,
713+ h : 8 ,
714+ i : 9 ,
715+ j : 10 ,
716+ }
717+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
718+ } )
719+
720+ it ( 'should handle objects with numeric string keys' , ( ) => {
721+ const prev = { '0' : 'a' , '1' : 'b' , '2' : 'c' }
722+ const next = { '0' : 'a' , '1' : 'b' , '2' : 'c' }
723+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
724+ } )
725+
726+ it ( 'should handle objects with special string keys' , ( ) => {
727+ const prev = {
728+ 'key-with-dash' : 1 ,
729+ 'key.with.dot' : 2 ,
730+ 'key with space' : 3 ,
731+ }
732+ const next = {
733+ 'key-with-dash' : 1 ,
734+ 'key.with.dot' : 2 ,
735+ 'key with space' : 3 ,
736+ }
737+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
738+ } )
739+
740+ it ( 'should detect differences in objects with string keys' , ( ) => {
741+ const prev = { a : 1 , b : 2 , c : 3 }
742+ const next = { a : 1 , b : 99 , c : 3 }
743+ const result = replaceEqualDeep ( prev , next )
744+ expect ( result ) . not . toBe ( prev )
745+ expect ( result ) . toEqual ( next )
746+ } )
747+ } )
748+
749+ describe ( 'objects with symbol keys' , ( ) => {
750+ it ( 'should handle objects with single symbol key' , ( ) => {
751+ const sym = Symbol ( 'test' )
752+ const prev = { [ sym ] : 1 }
753+ const next = { [ sym ] : 1 }
754+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
755+ } )
756+
757+ it ( 'should handle objects with multiple symbol keys' , ( ) => {
758+ const sym1 = Symbol ( 'a' )
759+ const sym2 = Symbol ( 'b' )
760+ const sym3 = Symbol ( 'c' )
761+ const prev = { [ sym1 ] : 1 , [ sym2 ] : 2 , [ sym3 ] : 3 }
762+ const next = { [ sym1 ] : 1 , [ sym2 ] : 2 , [ sym3 ] : 3 }
763+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
764+ } )
765+
766+ it ( 'should detect differences in symbol values' , ( ) => {
767+ const sym = Symbol ( 'test' )
768+ const prev = { [ sym ] : 1 }
769+ const next = { [ sym ] : 2 }
770+ const result = replaceEqualDeep ( prev , next )
771+ expect ( result ) . not . toBe ( prev )
772+ expect ( result [ sym ] ) . toBe ( 2 )
773+ } )
774+
775+ it ( 'should handle global symbols' , ( ) => {
776+ const sym = Symbol . for ( 'global.test.key' )
777+ const prev = { [ sym ] : 'value' }
778+ const next = { [ sym ] : 'value' }
779+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
780+ } )
781+ } )
782+
783+ describe ( 'objects with mixed string and symbol keys' , ( ) => {
784+ it ( 'should handle objects with both string and symbol keys' , ( ) => {
785+ const sym = Symbol ( 'test' )
786+ const prev = { a : 1 , b : 2 , [ sym ] : 3 }
787+ const next = { a : 1 , b : 2 , [ sym ] : 3 }
788+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
789+ } )
790+
791+ it ( 'should detect differences in string keys when symbols present' , ( ) => {
792+ const sym = Symbol ( 'test' )
793+ const prev = { a : 1 , b : 2 , [ sym ] : 3 }
794+ const next = { a : 1 , b : 99 , [ sym ] : 3 }
795+ const result = replaceEqualDeep ( prev , next )
796+ expect ( result ) . not . toBe ( prev )
797+ expect ( result . b ) . toBe ( 99 )
798+ expect ( result [ sym ] ) . toBe ( 3 )
799+ } )
800+
801+ it ( 'should detect differences in symbol keys when strings present' , ( ) => {
802+ const sym = Symbol ( 'test' )
803+ const prev = { a : 1 , b : 2 , [ sym ] : 3 }
804+ const next = { a : 1 , b : 2 , [ sym ] : 99 }
805+ const result = replaceEqualDeep ( prev , next )
806+ expect ( result ) . not . toBe ( prev )
807+ expect ( result . a ) . toBe ( 1 )
808+ expect ( result [ sym ] ) . toBe ( 99 )
809+ } )
810+
811+ it ( 'should handle complex nested objects with symbols' , ( ) => {
812+ const sym = Symbol ( 'nested' )
813+ const prev = { outer : { inner : 1 , [ sym ] : { deep : 'value' } } }
814+ const next = { outer : { inner : 1 , [ sym ] : { deep : 'value' } } }
815+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
816+ } )
817+ } )
818+
819+ describe ( 'non-enumerable properties' , ( ) => {
820+ it ( 'should treat objects with non-enumerable string property as non-plain' , ( ) => {
821+ const prev : Record < string , number > = { a : 1 }
822+ Object . defineProperty ( prev , 'hidden' , { value : 2 , enumerable : false } )
823+ const next : Record < string , number > = { a : 1 }
824+ Object . defineProperty ( next , 'hidden' , { value : 2 , enumerable : false } )
825+
826+ // Non-plain objects should return next, not prev (no structural sharing)
827+ const result = replaceEqualDeep ( prev , next )
828+ expect ( result ) . toBe ( next )
829+ } )
830+
831+ it ( 'should treat objects with non-enumerable symbol property as non-plain' , ( ) => {
832+ const sym = Symbol ( 'hidden' )
833+ const prev : Record < string | symbol , number > = { a : 1 }
834+ Object . defineProperty ( prev , sym , { value : 2 , enumerable : false } )
835+ const next : Record < string | symbol , number > = { a : 1 }
836+ Object . defineProperty ( next , sym , { value : 2 , enumerable : false } )
837+
838+ // Non-plain objects should return next, not prev
839+ const result = replaceEqualDeep ( prev , next )
840+ expect ( result ) . toBe ( next )
841+ } )
842+
843+ it ( 'should handle mix of enumerable and non-enumerable properties' , ( ) => {
844+ const prev : Record < string , number > = { visible : 1 }
845+ Object . defineProperty ( prev , 'hidden' , { value : 2 , enumerable : false } )
846+ const next = { visible : 1 }
847+
848+ // prev is non-plain (has non-enumerable), next is plain
849+ const result = replaceEqualDeep ( prev , next )
850+ expect ( result ) . toBe ( next )
851+ } )
852+
853+ it ( 'should handle non-enumerable property that shadows a string key' , ( ) => {
854+ const prev = Object . create ( null )
855+ prev . a = 1
856+ Object . defineProperty ( prev , 'b' , { value : 2 , enumerable : false } )
857+
858+ const next = Object . create ( null )
859+ next . a = 1
860+ next . b = 2 // enumerable version
861+
862+ const result = replaceEqualDeep ( prev , next )
863+ expect ( result ) . toBe ( next )
864+ } )
865+ } )
866+
867+ describe ( 'edge cases for key enumeration' , ( ) => {
868+ it ( 'should handle frozen objects as non-plain (configurable is false)' , ( ) => {
869+ const prev = Object . freeze ( { a : 1 , b : 2 } )
870+ const next = Object . freeze ( { a : 1 , b : 2 } )
871+
872+ // Frozen objects have all properties as non-configurable but still enumerable
873+ // They should still work with replaceEqualDeep
874+ const result = replaceEqualDeep ( prev , next )
875+ expect ( result ) . toBe ( prev )
876+ } )
877+
878+ it ( 'should handle sealed objects' , ( ) => {
879+ const prev = Object . seal ( { a : 1 , b : 2 } )
880+ const next = Object . seal ( { a : 1 , b : 2 } )
881+
882+ const result = replaceEqualDeep ( prev , next )
883+ expect ( result ) . toBe ( prev )
884+ } )
885+
886+ it ( 'should handle objects created with Object.create(null)' , ( ) => {
887+ const prev = Object . create ( null )
888+ prev . a = 1
889+ prev . b = 2
890+
891+ const next = Object . create ( null )
892+ next . a = 1
893+ next . b = 2
894+
895+ const result = replaceEqualDeep ( prev , next )
896+ expect ( result ) . toBe ( prev )
897+ } )
898+
899+ it ( 'should handle objects with inherited properties (only own props checked)' , ( ) => {
900+ const proto = { inherited : 'value' }
901+ const prev = Object . create ( proto )
902+ prev . own = 1
903+
904+ const next = Object . create ( proto )
905+ next . own = 1
906+
907+ const result = replaceEqualDeep ( prev , next )
908+ expect ( result ) . toBe ( prev )
909+ } )
910+
911+ it ( 'should not be confused by Object.prototype properties' , ( ) => {
912+ // Ensure hasOwnProperty, toString, etc. don't interfere
913+ const prev = { hasOwnProperty : 1 , toString : 2 , valueOf : 3 }
914+ const next = { hasOwnProperty : 1 , toString : 2 , valueOf : 3 }
915+ const result = replaceEqualDeep ( prev , next )
916+ expect ( result ) . toBe ( prev )
917+ } )
918+ } )
919+
920+ describe ( 'performance-critical scenarios (typical router state)' , ( ) => {
921+ it ( 'should efficiently handle typical router location object' , ( ) => {
922+ const prev = {
923+ pathname : '/users/123' ,
924+ search : '?tab=settings' ,
925+ hash : '#section' ,
926+ state : { key : 'abc123' } ,
927+ }
928+ const next = {
929+ pathname : '/users/123' ,
930+ search : '?tab=settings' ,
931+ hash : '#section' ,
932+ state : { key : 'abc123' } ,
933+ }
934+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
935+ } )
936+
937+ it ( 'should efficiently handle typical router match object' , ( ) => {
938+ const prev = {
939+ id : 'route-1' ,
940+ routeId : '/users/$userId' ,
941+ pathname : '/users/123' ,
942+ params : { userId : '123' } ,
943+ search : { } ,
944+ fullPath : '/users/$userId' ,
945+ loaderData : { user : { name : 'John' } } ,
946+ }
947+ const next = {
948+ id : 'route-1' ,
949+ routeId : '/users/$userId' ,
950+ pathname : '/users/123' ,
951+ params : { userId : '123' } ,
952+ search : { } ,
953+ fullPath : '/users/$userId' ,
954+ loaderData : { user : { name : 'John' } } ,
955+ }
956+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
957+ } )
958+
959+ it ( 'should efficiently handle array of matches' , ( ) => {
960+ const prev = [
961+ { id : '1' , routeId : '__root__' , pathname : '/' , params : { } } ,
962+ { id : '2' , routeId : '/users' , pathname : '/users' , params : { } } ,
963+ {
964+ id : '3' ,
965+ routeId : '/users/$userId' ,
966+ pathname : '/users/123' ,
967+ params : { userId : '123' } ,
968+ } ,
969+ ]
970+ const next = [
971+ { id : '1' , routeId : '__root__' , pathname : '/' , params : { } } ,
972+ { id : '2' , routeId : '/users' , pathname : '/users' , params : { } } ,
973+ {
974+ id : '3' ,
975+ routeId : '/users/$userId' ,
976+ pathname : '/users/123' ,
977+ params : { userId : '123' } ,
978+ } ,
979+ ]
980+ expect ( replaceEqualDeep ( prev , next ) ) . toBe ( prev )
981+ } )
982+ } )
983+ } )
984+
669985describe ( 'escapeHtml' , ( ) => {
670986 it ( 'should escape less-than sign' , ( ) => {
671987 expect ( escapeHtml ( '<' ) ) . toBe ( '\\u003c' )
0 commit comments