Skip to content

Commit 66a2fe2

Browse files
committed
Add circuit host tests
1 parent 6e77a57 commit 66a2fe2

File tree

4 files changed

+126
-13
lines changed

4 files changed

+126
-13
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ private async Task TryNotifyClientErrorAsync(IClientProxy client, string error,
760760

761761
internal Task UpdateRootComponents(
762762
RootComponentOperationBatch operationBatch,
763-
ProtectedPrerenderComponentApplicationStore store,
763+
IClearableStore store,
764764
bool isRestore,
765765
CancellationToken cancellation)
766766
{
@@ -858,7 +858,7 @@ internal Task UpdateRootComponents(
858858
// At this point all components have successfully produced an initial render and we can clear the contents of the component
859859
// application state store. This ensures the memory that was not used during the initial render of these components gets
860860
// reclaimed since no-one else is holding on to it any longer.
861-
store.ExistingState.Clear();
861+
store.Clear();
862862
}
863863
}
864864
});

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
}

src/Components/Server/test/Circuits/TestCircuitHost.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,29 @@ public static CircuitHost Create(
3030
CircuitHandler[] handlers = null,
3131
CircuitClientProxy clientProxy = null)
3232
{
33-
serviceScope = serviceScope ?? new AsyncServiceScope(Mock.Of<IServiceScope>());
3433
clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of<ISingleClientProxy>(), Guid.NewGuid().ToString());
3534
var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
3635
var navigationManager = new RemoteNavigationManager(Mock.Of<ILogger<RemoteNavigationManager>>());
3736
var componentsActivitySource = new ComponentsActivitySource();
3837
var circuitActivitySource = new CircuitActivitySource();
39-
var serviceProvider = new Mock<IServiceProvider>();
40-
serviceProvider
41-
.Setup(services => services.GetService(typeof(IJSRuntime)))
42-
.Returns(jsRuntime);
43-
serviceProvider
44-
.Setup(services => services.GetService(typeof(ComponentsActivitySource)))
45-
.Returns(componentsActivitySource);
38+
var persistenceManager = new ComponentStatePersistenceManager(
39+
NullLogger<ComponentStatePersistenceManager>.Instance,
40+
new ServiceCollection().BuildServiceProvider());
41+
var serviceProvider = new ServiceCollection()
42+
.AddSingleton<IJSRuntime>(jsRuntime)
43+
.AddSingleton(componentsActivitySource)
44+
.AddSingleton(persistenceManager)
45+
.AddSingleton(circuitActivitySource)
46+
.BuildServiceProvider();
47+
serviceScope ??= serviceProvider.CreateAsyncScope();
48+
4649
var serverComponentDeserializer = Mock.Of<IServerComponentDeserializer>();
4750
var circuitMetrics = new CircuitMetrics(new TestMeterFactory());
4851

4952
if (remoteRenderer == null)
5053
{
5154
remoteRenderer = new RemoteRenderer(
52-
serviceProvider.Object,
55+
serviceProvider,
5356
NullLoggerFactory.Instance,
5457
new CircuitOptions(),
5558
clientProxy,

src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Microsoft.AspNetCore.Components;
88

9-
internal sealed class ProtectedPrerenderComponentApplicationStore : PrerenderComponentApplicationStore
9+
internal sealed class ProtectedPrerenderComponentApplicationStore : PrerenderComponentApplicationStore, IClearableStore
1010
{
1111
private IDataProtector _protector = default!; // Assigned in all constructor paths
1212

@@ -39,4 +39,14 @@ private void CreateProtector(IDataProtectionProvider dataProtectionProvider) =>
3939
public override bool SupportsRenderMode(IComponentRenderMode renderMode) =>
4040
renderMode is null ||
4141
renderMode is InteractiveServerRenderMode || renderMode is InteractiveAutoRenderMode;
42+
43+
public void Clear()
44+
{
45+
ExistingState.Clear();
46+
}
47+
}
48+
49+
internal interface IClearableStore : IPersistentComponentStateStore
50+
{
51+
void Clear();
4252
}

0 commit comments

Comments
 (0)