@@ -710,165 +710,156 @@ public void Constructor_WorksCorrectly_ForPublicProperty()
710710 }
711711
712712 [ Fact ]
713- public void RestoreProperty_SetsIgnoreComponentPropertyValueUnconditionally_WhenRestoringFromState ( )
713+ public async Task ComponentRecreation_PreservesPersistedState_WhenComponentIsRecreatedDuringNavigation ( )
714714 {
715+ // This test simulates the scenario where a component is destroyed and recreated (like during navigation)
716+ // and verifies that the persisted state is correctly restored in the new component instance
717+
715718 // Arrange
716- var initialState = new Dictionary < string , byte [ ] > ( ) ;
717- var state = new PersistentComponentState ( initialState , [ ] , [ ] ) ;
718- var renderer = new TestRenderer ( ) ;
719- var component = new TestComponent { State = "initial-value" } ;
720- var componentState = CreateComponentState ( renderer , component , null , null ) ;
721-
722- // Pre-populate the state with serialized data
723- var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState , nameof ( TestComponent . State ) ) ;
724- initialState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value" , JsonSerializerOptions . Web ) ;
725- state . InitializeExistingState ( initialState , RestoreContext . LastSnapshot ) ;
726-
719+ var appState = new Dictionary < string , byte [ ] > ( ) ;
720+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
721+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
722+ . AddSingleton ( manager )
723+ . AddSingleton ( manager . State )
724+ . AddFakeLogging ( )
725+ . BuildServiceProvider ( ) ;
726+ var renderer = new TestRenderer ( serviceProvider ) ;
727+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
727728 var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
728- var serviceProvider = new ServiceCollection ( ) . BuildServiceProvider ( ) ;
729- var logger = NullLogger . Instance ;
730729
731- var subscription = new PersistentValueProviderComponentSubscription (
732- state , componentState , cascadingParameterInfo , serviceProvider , logger ) ;
730+ // Setup initial persisted state
731+ var component1 = new TestComponent { State = "initial-property-value" } ;
732+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
733+ var componentState1 = renderer . GetComponentState ( component1 ) ;
734+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
735+
736+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value-from-previous-session" , JsonSerializerOptions . Web ) ;
737+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
733738
734- // Initialize the subscription so it has a _lastValue set
735- subscription . GetOrComputeLastValue ( ) ;
739+ // Act & Assert - First component instance should get the persisted value
740+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
741+ Assert . Equal ( "persisted-value-from-previous-session" , component1 . State ) ;
736742
737- // Act - Call RestoreProperty to simulate restoration after component re-creation
738- var restoreMethod = typeof ( PersistentValueProviderComponentSubscription )
739- . GetMethod ( "RestoreProperty" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
740- restoreMethod . Invoke ( subscription , null ) ;
743+ // Simulate component destruction (like during navigation away)
744+ renderer . RemoveRootComponent ( componentId1 ) ;
741745
742- // Assert - The ignoreComponentPropertyValue flag should cause the restored value to be returned
743- var result = subscription . GetOrComputeLastValue ( ) ;
744- Assert . Equal ( "persisted-value" , result ) ;
746+ // Simulate component recreation (like during navigation back) - NEW SUBSCRIPTION CREATED
747+ var component2 = new TestComponent { State = "new-component-initial-value" } ;
748+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
749+ var componentState2 = renderer . GetComponentState ( component2 ) ;
745750
746- subscription . Dispose ( ) ;
751+ // Verify the key is the same (important for components without @key)
752+ var key2 = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState2 , nameof ( TestComponent . State ) ) ;
753+ Assert . Equal ( key , key2 ) ;
754+
755+ // The state should still be available for restoration
756+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
757+
758+ // Assert - The new component instance should get the same persisted value
759+ Assert . Equal ( "persisted-value-from-previous-session" , component2 . State ) ;
747760 }
748761
749762 [ Fact ]
750- public void GetOrComputeLastValue_ReturnsRestoredValue_AfterComponentRecreation ( )
763+ public async Task ComponentRecreation_WithStateUpdates_PreservesCorrectValueTransitionSequence ( )
751764 {
765+ // This test simulates the full lifecycle with component recreation and state updates
766+ // following the pattern from GetOrComputeLastValue_FollowsCorrectValueTransitionSequence
767+ // but with subscription recreation between state restorations
768+
752769 // Arrange
753- var initialState = new Dictionary < string , byte [ ] > ( ) ;
754- var state = new PersistentComponentState ( initialState , [ ] , [ ] ) ;
755- var renderer = new TestRenderer ( ) ;
756- var component = new TestComponent { State = "component-property-value" } ;
757- var componentState = CreateComponentState ( renderer , component , null , null ) ;
758-
759- // Pre-populate the state with serialized data
760- var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState , nameof ( TestComponent . State ) ) ;
761- initialState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "restored-value" , JsonSerializerOptions . Web ) ;
762- state . InitializeExistingState ( initialState , RestoreContext . LastSnapshot ) ;
763-
770+ var appState = new Dictionary < string , byte [ ] > ( ) ;
771+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
772+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
773+ . AddSingleton ( manager )
774+ . AddSingleton ( manager . State )
775+ . AddFakeLogging ( )
776+ . BuildServiceProvider ( ) ;
777+ var renderer = new TestRenderer ( serviceProvider ) ;
778+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
764779 var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
765- var serviceProvider = new ServiceCollection ( ) . BuildServiceProvider ( ) ;
766- var logger = NullLogger . Instance ;
767-
768- var subscription = new PersistentValueProviderComponentSubscription (
769- state , componentState , cascadingParameterInfo , serviceProvider , logger ) ;
770-
771- // Act - First call initializes and restores from persistent state
772- var firstResult = subscription . GetOrComputeLastValue ( ) ;
773-
774- // Assert - Should return the restored value, not the component's property value
775- Assert . Equal ( "restored-value" , firstResult ) ;
776-
777- // Simulate component property being updated after restoration
778- component . State = "updated-component-value" ;
779-
780- // Second call should return the updated component value since the flag was reset
781- var secondResult = subscription . GetOrComputeLastValue ( ) ;
782- Assert . Equal ( "updated-component-value" , secondResult ) ;
783-
784- subscription . Dispose ( ) ;
785- }
786-
787- [ Fact ]
788- public void RestoreProperty_WorksCorrectly_ForComponentsWithoutKey ( )
789- {
790- // Arrange - This test simulates components being added/removed without @key
791- var initialState = new Dictionary < string , byte [ ] > ( ) ;
792- var state = new PersistentComponentState ( initialState , [ ] , [ ] ) ;
793- var renderer = new TestRenderer ( ) ;
794-
795- // First component instance
796- var component1 = new TestComponent { State = "initial-value-1" } ;
797- var componentState1 = CreateComponentState ( renderer , component1 , null , null ) ;
798780
781+ // First component lifecycle
782+ var component1 = new TestComponent { State = "initial-property-value" } ;
783+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
784+ var componentState1 = renderer . GetComponentState ( component1 ) ;
799785 var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
800- initialState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value-from-previous-session" , JsonSerializerOptions . Web ) ;
801- state . InitializeExistingState ( initialState , RestoreContext . LastSnapshot ) ;
802-
803- var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
804- var serviceProvider = new ServiceCollection ( ) . BuildServiceProvider ( ) ;
805- var logger = NullLogger . Instance ;
806-
807- var subscription1 = new PersistentValueProviderComponentSubscription (
808- state , componentState1 , cascadingParameterInfo , serviceProvider , logger ) ;
809786
810- // Act - Simulate component being destroyed and recreated (like during navigation)
811- var result1 = subscription1 . GetOrComputeLastValue ( ) ;
812- subscription1 . Dispose ( ) ;
787+ // Pre-populate with first persisted value
788+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "first-restored-value" , JsonSerializerOptions . Web ) ;
789+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
813790
814- // Simulate a new component instance being created (like after navigation back)
815- var component2 = new TestComponent { State = "initial-value-2" } ;
816- var componentState2 = CreateComponentState ( renderer , component2 , null , null ) ;
791+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
792+
793+ // Act & Assert - First component gets restored value
794+ Assert . Equal ( "first-restored-value" , component1 . State ) ;
817795
818- // Re-populate state for the new component instance
819- initialState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value-from-previous-session" , JsonSerializerOptions . Web ) ;
820- state . InitializeExistingState ( initialState , RestoreContext . LastSnapshot ) ;
796+ // Update component property
797+ component1 . State = "updated-by-component-1" ;
798+ Assert . Equal ( "updated-by-component-1" , provider . GetCurrentValue ( componentState1 , cascadingParameterInfo ) ) ;
821799
822- var subscription2 = new PersistentValueProviderComponentSubscription (
823- state , componentState2 , cascadingParameterInfo , serviceProvider , logger ) ;
800+ // Simulate component destruction and recreation (NEW SUBSCRIPTION CREATED)
801+ renderer . RemoveRootComponent ( componentId1 ) ;
802+
803+ var component2 = new TestComponent { State = "new-component-initial-value" } ;
804+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
805+ var componentState2 = renderer . GetComponentState ( component2 ) ;
824806
825- var result2 = subscription2 . GetOrComputeLastValue ( ) ;
807+ // Restore state with a different value
808+ appState . Clear ( ) ;
809+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "second-restored-value" , JsonSerializerOptions . Web ) ;
810+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . ValueUpdate ) ;
826811
827- // Assert - Both instances should restore the persisted value correctly
828- Assert . Equal ( "persisted-value-from-previous-session" , result1 ) ;
829- Assert . Equal ( "persisted-value-from-previous-session" , result2 ) ;
812+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
813+
814+ // Assert - New component gets the updated restored value
815+ Assert . Equal ( "second-restored-value" , component2 . State ) ;
830816
831- subscription2 . Dispose ( ) ;
817+ // Continue with property updates on the new component
818+ component2 . State = "updated-by-component-2" ;
819+ Assert . Equal ( "updated-by-component-2" , provider . GetCurrentValue ( componentState2 , cascadingParameterInfo ) ) ;
832820 }
833821
834822 [ Fact ]
835- public void RestoreProperty_WithSkipNotifications_StillSetsIgnoreComponentPropertyValue ( )
823+ public async Task ComponentRecreation_WithSkipNotifications_StillRestoresCorrectly ( )
836824 {
837- // This test verifies that the fix works even when skipNotifications is true,
838- // which is the scenario that was broken before our fix
825+ // This test verifies that the fix works even when skipNotifications is true during component recreation ,
826+ // which is the core scenario that was broken before our fix
839827
840- // Arrange
841- var initialState = new Dictionary < string , byte [ ] > ( ) ;
842- var state = new PersistentComponentState ( initialState , [ ] , [ ] ) ;
843- var renderer = new TestRenderer ( ) ;
844- var component = new TestComponent { State = "component-value" } ;
845- var componentState = CreateComponentState ( renderer , component , null , null ) ;
846-
847- var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState , nameof ( TestComponent . State ) ) ;
848- initialState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value" , JsonSerializerOptions . Web ) ;
849- state . InitializeExistingState ( initialState , RestoreContext . LastSnapshot ) ;
850-
828+ // Arrange
829+ var appState = new Dictionary < string , byte [ ] > ( ) ;
830+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
831+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
832+ . AddSingleton ( manager )
833+ . AddSingleton ( manager . State )
834+ . AddFakeLogging ( )
835+ . BuildServiceProvider ( ) ;
836+ var renderer = new TestRenderer ( serviceProvider ) ;
851837 var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
852- var serviceProvider = new ServiceCollection ( ) . BuildServiceProvider ( ) ;
853- var logger = NullLogger . Instance ;
854-
855- var subscription = new PersistentValueProviderComponentSubscription (
856- state , componentState , cascadingParameterInfo , serviceProvider , logger ) ;
857838
858- // Mark the subscription as having pending initial value to trigger skipNotifications = true
859- var pendingField = typeof ( PersistentValueProviderComponentSubscription )
860- . GetField ( "_hasPendingInitialValue" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
861- pendingField . SetValue ( subscription , true ) ;
862-
863- // Act - Call RestoreProperty which should skipNotifications but still set _ignoreComponentPropertyValue
864- var restoreMethod = typeof ( PersistentValueProviderComponentSubscription )
865- . GetMethod ( "RestoreProperty" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
866- restoreMethod . Invoke ( subscription , null ) ;
839+ // Setup persisted state
840+ var component1 = new TestComponent { State = "component-initial-value" } ;
841+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
842+ var componentState1 = renderer . GetComponentState ( component1 ) ;
843+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
844+
845+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value" , JsonSerializerOptions . Web ) ;
846+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
867847
868- // Assert - Even with skipNotifications = true, the next GetOrComputeLastValue should return the restored value
869- var result = subscription . GetOrComputeLastValue ( ) ;
870- Assert . Equal ( "persisted-value" , result ) ;
848+ // First component gets the persisted value
849+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
850+ Assert . Equal ( "persisted-value" , component1 . State ) ;
871851
872- subscription . Dispose ( ) ;
852+ // Destroy and recreate component (simulating navigation or component without @key)
853+ renderer . RemoveRootComponent ( componentId1 ) ;
854+
855+ // Create new component instance - this will create a NEW SUBSCRIPTION
856+ var component2 = new TestComponent { State = "different-initial-value" } ;
857+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
858+
859+ // Render the new component - this should restore the persisted value even if skipNotifications is true
860+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
861+
862+ // Assert - The new component should get the persisted value, not its initial property value
863+ Assert . Equal ( "persisted-value" , component2 . State ) ;
873864 }
874865}
0 commit comments