Skip to content

Commit f54388f

Browse files
Copilotjaviercn
andcommitted
Rewrite persistent state tests to properly simulate component recreation scenarios
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
1 parent 381d854 commit f54388f

File tree

1 file changed

+119
-128
lines changed

1 file changed

+119
-128
lines changed

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 119 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)