Skip to content

Commit db2a683

Browse files
Copilotjaviercn
andcommitted
Fix unit tests for persistent state restoration and improve test reliability
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
1 parent 6b82560 commit db2a683

File tree

3 files changed

+211
-766
lines changed

3 files changed

+211
-766
lines changed

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence()
263263
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
264264

265265
// Act & Assert - First call: Returns restored value from state
266+
var firstCall = provider.GetCurrentValue(componentState, cascadingParameterInfo);
267+
Assert.Equal("first-restored-value", firstCall);
266268
Assert.Equal("first-restored-value", component.State);
267269

268270
// Change the component's property value
@@ -708,4 +710,213 @@ public void Constructor_WorksCorrectly_ForPublicProperty()
708710
Assert.NotNull(subscription);
709711
subscription.Dispose();
710712
}
713+
714+
[Fact]
715+
public async Task ComponentRecreation_PreservesPersistedState_WhenComponentIsRecreatedDuringNavigation()
716+
{
717+
// This test simulates the scenario where a component is destroyed and recreated (like during navigation)
718+
// and verifies that the persisted state is correctly restored in the new component instance
719+
720+
// Arrange
721+
var appState = new Dictionary<string, byte[]>();
722+
var manager = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
723+
var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection())
724+
.AddSingleton(manager)
725+
.AddSingleton(manager.State)
726+
.AddFakeLogging()
727+
.BuildServiceProvider();
728+
var renderer = new TestRenderer(serviceProvider);
729+
var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
730+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
731+
732+
// Setup initial persisted state
733+
var component1 = new TestComponent { State = "initial-property-value" };
734+
var componentId1 = renderer.AssignRootComponentId(component1);
735+
var componentState1 = renderer.GetComponentState(component1);
736+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State));
737+
738+
appState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value-from-previous-session", JsonSerializerOptions.Web);
739+
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue);
740+
741+
// Act & Assert - First component instance should get the persisted value
742+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty));
743+
Assert.Equal("persisted-value-from-previous-session", component1.State);
744+
745+
// Simulate component destruction (like during navigation away)
746+
renderer.RemoveRootComponent(componentId1);
747+
748+
// Simulate component recreation (like during navigation back) - NEW SUBSCRIPTION CREATED
749+
var component2 = new TestComponent { State = "new-component-initial-value" };
750+
var componentId2 = renderer.AssignRootComponentId(component2);
751+
var componentState2 = renderer.GetComponentState(component2);
752+
753+
// Verify the key is the same (important for components without @key)
754+
var key2 = PersistentStateValueProviderKeyResolver.ComputeKey(componentState2, nameof(TestComponent.State));
755+
Assert.Equal(key, key2);
756+
757+
// The state should still be available for restoration
758+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty));
759+
760+
// Assert - The new component instance should get the same persisted value
761+
var providerForSecondComponent = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
762+
var cascadingParameterInfoForSecondComponent = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
763+
var restoredCall = providerForSecondComponent.GetCurrentValue(componentState2, cascadingParameterInfoForSecondComponent);
764+
Assert.Equal("persisted-value-from-previous-session", restoredCall);
765+
Assert.Equal("persisted-value-from-previous-session", component2.State);
766+
}
767+
768+
[Fact]
769+
public async Task ComponentRecreation_WithStateUpdates_PreservesCorrectValueTransitionSequence()
770+
{
771+
// This test simulates the full lifecycle with component recreation and state updates
772+
// following the pattern from GetOrComputeLastValue_FollowsCorrectValueTransitionSequence
773+
// but with subscription recreation between state restorations
774+
775+
// Arrange
776+
var appState = new Dictionary<string, byte[]>();
777+
var manager = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
778+
var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection())
779+
.AddSingleton(manager)
780+
.AddSingleton(manager.State)
781+
.AddFakeLogging()
782+
.BuildServiceProvider();
783+
var renderer = new TestRenderer(serviceProvider);
784+
var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
785+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
786+
787+
// First component lifecycle
788+
var component1 = new TestComponent { State = "initial-property-value" };
789+
var componentId1 = renderer.AssignRootComponentId(component1);
790+
var componentState1 = renderer.GetComponentState(component1);
791+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State));
792+
793+
// Pre-populate with first persisted value
794+
appState[key] = JsonSerializer.SerializeToUtf8Bytes("first-restored-value", JsonSerializerOptions.Web);
795+
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue);
796+
797+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty));
798+
799+
// Act & Assert - First component gets restored value
800+
var firstCall = provider.GetCurrentValue(componentState1, cascadingParameterInfo);
801+
Assert.Equal("first-restored-value", firstCall);
802+
Assert.Equal("first-restored-value", component1.State);
803+
804+
// Update component property
805+
component1.State = "updated-by-component-1";
806+
Assert.Equal("updated-by-component-1", provider.GetCurrentValue(componentState1, cascadingParameterInfo));
807+
808+
// Simulate component destruction and recreation (NEW SUBSCRIPTION CREATED)
809+
renderer.RemoveRootComponent(componentId1);
810+
811+
var component2 = new TestComponent { State = "new-component-initial-value" };
812+
var componentId2 = renderer.AssignRootComponentId(component2);
813+
var componentState2 = renderer.GetComponentState(component2);
814+
815+
// Restore state with a different value
816+
appState.Clear();
817+
appState[key] = JsonSerializer.SerializeToUtf8Bytes("second-restored-value", JsonSerializerOptions.Web);
818+
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.ValueUpdate);
819+
820+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty));
821+
822+
// Assert - New component gets the updated restored value
823+
var secondComponentCall = provider.GetCurrentValue(componentState2, cascadingParameterInfo);
824+
Assert.Equal("second-restored-value", secondComponentCall);
825+
Assert.Equal("second-restored-value", component2.State);
826+
827+
// Continue with property updates on the new component
828+
component2.State = "updated-by-component-2";
829+
Assert.Equal("updated-by-component-2", provider.GetCurrentValue(componentState2, cascadingParameterInfo));
830+
}
831+
832+
[Fact]
833+
public async Task ComponentRecreation_WithSkipNotifications_StillRestoresCorrectly()
834+
{
835+
// This test verifies that the fix works even when skipNotifications is true during component recreation,
836+
// which is the core scenario that was broken before our fix
837+
838+
// Arrange
839+
var appState = new Dictionary<string, byte[]>();
840+
var manager = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
841+
var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection())
842+
.AddSingleton(manager)
843+
.AddSingleton(manager.State)
844+
.AddFakeLogging()
845+
.BuildServiceProvider();
846+
var renderer = new TestRenderer(serviceProvider);
847+
var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
848+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
849+
850+
// Setup persisted state
851+
var component1 = new TestComponent { State = "component-initial-value" };
852+
var componentId1 = renderer.AssignRootComponentId(component1);
853+
var componentState1 = renderer.GetComponentState(component1);
854+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State));
855+
856+
appState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value", JsonSerializerOptions.Web);
857+
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue);
858+
859+
// First component gets the persisted value
860+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty));
861+
var firstCall = provider.GetCurrentValue(componentState1, cascadingParameterInfo);
862+
Assert.Equal("persisted-value", firstCall);
863+
Assert.Equal("persisted-value", component1.State);
864+
865+
// Destroy and recreate component (simulating navigation or component without @key)
866+
renderer.RemoveRootComponent(componentId1);
867+
868+
// Create new component instance - this will create a NEW SUBSCRIPTION
869+
var component2 = new TestComponent { State = "different-initial-value" };
870+
var componentId2 = renderer.AssignRootComponentId(component2);
871+
var componentState2 = renderer.GetComponentState(component2);
872+
873+
// Render the new component - this should restore the persisted value even if skipNotifications is true
874+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty));
875+
876+
// Assert - The new component should get the persisted value, not its initial property value
877+
var providerForLastComponent = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
878+
var cascadingParameterInfoForLastComponent = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
879+
var restoredCall2 = providerForLastComponent.GetCurrentValue(componentState2, cascadingParameterInfoForLastComponent);
880+
Assert.Equal("persisted-value", restoredCall2);
881+
Assert.Equal("persisted-value", component2.State);
882+
}
883+
884+
[Fact]
885+
public async Task DebugTest_UnderstandIgnoreComponentPropertyValueFlag()
886+
{
887+
// Simple test to understand the _ignoreComponentPropertyValue flag behavior
888+
var appState = new Dictionary<string, byte[]>();
889+
var manager = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
890+
var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection())
891+
.AddSingleton(manager)
892+
.AddSingleton(manager.State)
893+
.AddFakeLogging()
894+
.BuildServiceProvider();
895+
var renderer = new TestRenderer(serviceProvider);
896+
var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single();
897+
var component = new TestComponent { State = "initial-property-value" };
898+
var componentId = renderer.AssignRootComponentId(component);
899+
var componentState = renderer.GetComponentState(component);
900+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
901+
902+
// Set up state to restore
903+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState, nameof(TestComponent.State));
904+
appState[key] = JsonSerializer.SerializeToUtf8Bytes("restored-value", JsonSerializerOptions.Web);
905+
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue);
906+
907+
// Render component - this should restore the value
908+
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.Empty));
909+
910+
// First call should return restored value
911+
var firstCall = provider.GetCurrentValue(componentState, cascadingParameterInfo);
912+
Assert.Equal("restored-value", firstCall);
913+
Assert.Equal("restored-value", component.State);
914+
915+
// Update the component's property manually
916+
component.State = "manually-updated-value";
917+
918+
// Second call should return the manually updated value
919+
var secondCall = provider.GetCurrentValue(componentState, cascadingParameterInfo);
920+
Assert.Equal("manually-updated-value", secondCall);
921+
}
711922
}

0 commit comments

Comments
 (0)