@@ -693,6 +693,90 @@ public async Task UpdateRootComponents_CanRemoveExistingRootComponent()
693693 ( ( TestRemoteRenderer ) circuitHost . Renderer ) . GetTestComponentState ( 0 ) ) ;
694694 }
695695
696+ [ Fact ]
697+ public async Task UpdateRootComponents_ValidatesOperationSequencingDuringValueUpdateRestore ( )
698+ {
699+ // Arrange
700+ var testRenderer = GetRemoteRenderer ( ) ;
701+ var circuitHost = TestCircuitHost . Create (
702+ remoteRenderer : testRenderer ) ;
703+
704+ // Set up initial components for subsequent operations
705+ await AddComponentAsync < DynamicallyAddedComponent > ( circuitHost , 0 , new Dictionary < string , object >
706+ {
707+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "Component 0"
708+ } ) ;
709+ await AddComponentAsync < DynamicallyAddedComponent > ( circuitHost , 1 , new Dictionary < string , object >
710+ {
711+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "Component 1"
712+ } ) ;
713+
714+ Assert . Equal ( 2 , testRenderer . GetOrCreateWebRootComponentManager ( ) . GetRootComponents ( ) . Count ( ) ) ;
715+ var store = new TestComponentApplicationStore (
716+ new Dictionary < string , byte [ ] > { [ "test" ] = [ 1 , 2 , 3 ] } ) ;
717+
718+ var operations = new RootComponentOperation [ ]
719+ {
720+ new ( )
721+ {
722+ Type = RootComponentOperationType . Add ,
723+ SsrComponentId = 2 ,
724+ Marker = CreateMarker ( typeof ( DynamicallyAddedComponent ) , "2" , new Dictionary < string , object >
725+ {
726+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "New Component 2"
727+ } ) ,
728+ Descriptor = new (
729+ componentType : typeof ( DynamicallyAddedComponent ) ,
730+ parameters : CreateWebRootComponentParameters ( new Dictionary < string , object >
731+ {
732+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "New Component 2"
733+ } ) ) ,
734+ } ,
735+
736+ new ( )
737+ {
738+ Type = RootComponentOperationType . Remove ,
739+ SsrComponentId = 0 ,
740+ } ,
741+
742+ new ( )
743+ {
744+ Type = RootComponentOperationType . Update ,
745+ SsrComponentId = 1 ,
746+ Marker = CreateMarker ( typeof ( DynamicallyAddedComponent ) , "1" , new Dictionary < string , object >
747+ {
748+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "Replaced Component 1"
749+ } ) ,
750+ Descriptor = new (
751+ componentType : typeof ( DynamicallyAddedComponent ) ,
752+ parameters : CreateWebRootComponentParameters ( new Dictionary < string , object >
753+ {
754+ [ nameof ( DynamicallyAddedComponent . Message ) ] = "Replaced Component 1"
755+ } ) ) ,
756+ } ,
757+ } ;
758+
759+ var batch = new RootComponentOperationBatch
760+ {
761+ BatchId = 1 ,
762+ Operations = operations
763+ } ;
764+
765+ var updateTask = circuitHost . UpdateRootComponents ( batch , store , false , CancellationToken . None ) ;
766+ Assert . Equal ( 2 , testRenderer . GetOrCreateWebRootComponentManager ( ) . GetRootComponents ( ) . Count ( ) ) ;
767+ Assert . Equal ( "Default message" , Assert . IsType < DynamicallyAddedComponent > ( testRenderer . GetTestComponentState ( 3 ) . Component ) . Message ) ;
768+ Assert . Equal ( "Default message" , Assert . IsType < DynamicallyAddedComponent > ( testRenderer . GetTestComponentState ( 2 ) . Component ) . Message ) ;
769+ store . Continue ( ) ;
770+ await updateTask ;
771+
772+ // Enqueue a callback to indirectly await for the remaining operations to complete.
773+ await testRenderer . Dispatcher . InvokeAsync ( ( ) => { } ) ;
774+ Assert . Equal ( "Replaced Component 1" , Assert . IsType < DynamicallyAddedComponent > ( testRenderer . GetTestComponentState ( 3 ) . Component ) . Message ) ;
775+ Assert . Equal ( "New Component 2" , Assert . IsType < DynamicallyAddedComponent > ( testRenderer . GetTestComponentState ( 2 ) . Component ) . Message ) ;
776+
777+ Assert . Equal ( 2 , testRenderer . GetOrCreateWebRootComponentManager ( ) . GetRootComponents ( ) . Count ( ) ) ;
778+ }
779+
696780 private async Task AddComponentAsync < TComponent > ( CircuitHost circuitHost , int ssrComponentId , Dictionary < string , object > parameters = null , string componentKey = "" )
697781 where TComponent : IComponent
698782 {
@@ -823,7 +907,7 @@ public TestRemoteRenderer(IServiceProvider serviceProvider, ISingleClientProxy c
823907 NullLogger . Instance ,
824908 CreateJSRuntime ( new CircuitOptions ( ) ) ,
825909 new CircuitJSComponentInterop ( new CircuitOptions ( ) ) )
826- {
910+ {
827911 }
828912
829913 public ComponentState GetTestComponentState ( int id )
@@ -1051,4 +1135,20 @@ public void TriggerRender()
10511135 Assert . True ( task . IsCompletedSuccessfully ) ;
10521136 }
10531137 }
1138+
1139+ private class TestComponentApplicationStore ( Dictionary < string , byte [ ] > dictionary ) : IPersistentComponentStateStore , IClearableStore
1140+ {
1141+ private readonly TaskCompletionSource _tcs = new ( ) ;
1142+
1143+ public void Clear ( ) => dictionary . Clear ( ) ;
1144+
1145+ public async Task < IDictionary < string , byte [ ] > > GetPersistedStateAsync ( )
1146+ {
1147+ await _tcs . Task ;
1148+ return dictionary ;
1149+ }
1150+
1151+ public Task PersistStateAsync ( IReadOnlyDictionary < string , byte [ ] > state ) => throw new NotImplementedException ( ) ;
1152+ internal void Continue ( ) => _tcs . SetResult ( ) ;
1153+ }
10541154}
0 commit comments