File: Circuits\RemoteRendererTest.cs
Web Access
Project: src\src\Components\Server\test\Microsoft.AspNetCore.Components.Server.Tests.csproj (Microsoft.AspNetCore.Components.Server.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using Moq;
 
namespace Microsoft.AspNetCore.Components.Web.Rendering;
 
public class RemoteRendererTest
{
    // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
    // failures.
    private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
 
    private const int MaxInteractiveServerRootComponentCount = 3;
 
    private readonly IDataProtectionProvider _ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider();
 
    [Fact]
    public void WritesAreBufferedWhenTheClientIsOffline()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
        var component = new TestComponent(builder =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        });
 
        // Act
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        component.TriggerRender();
 
        // Assert
        Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count);
    }
 
    [Fact]
    public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit()
    {
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
        var component = new TestComponent(builder =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        });
 
        // Act
        var componentId = renderer.AssignRootComponentId(component);
        for (int i = 0; i < 20; i++)
        {
            component.TriggerRender();
 
        }
 
        // Assert
        Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
    }
 
    [Fact]
    public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents()
    {
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
        var component = new TestComponent(builder =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        });
 
        // Act
        var componentId = renderer.AssignRootComponentId(component);
        for (var i = 0; i < 10; i++)
        {
            component.TriggerRender();
        }
 
        await renderer.OnRenderCompletedAsync(2, null);
 
        // Assert
        Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count);
    }
 
    [Fact]
    public async Task ProducesNewBatch_WhenABatchGetsAcknowledged()
    {
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
        var i = 0;
        var component = new TestComponent(builder =>
        {
            builder.AddContent(0, $"Value {i}");
        });
 
        // Act
        var componentId = renderer.AssignRootComponentId(component);
        for (i = 0; i < 20; i++)
        {
            component.TriggerRender();
        }
        Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
 
        await renderer.OnRenderCompletedAsync(2, null);
 
        // Assert
        Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
    }
 
    [Fact]
    public async Task ProcessBufferedRenderBatches_WritesRenders()
    {
        // Arrange
        var @event = new ManualResetEventSlim();
        var serviceProvider = CreateServiceProvider();
        var renderIds = new List<long>();
 
        var firstBatchTCS = new TaskCompletionSource();
        var secondBatchTCS = new TaskCompletionSource();
        var thirdBatchTCS = new TaskCompletionSource();
 
        var initialClient = new Mock<IClientProxy>();
        initialClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0]))
            .Returns(firstBatchTCS.Task);
        var circuitClient = new CircuitClientProxy(initialClient.Object, "connection0");
        var renderer = GetRemoteRenderer(serviceProvider, circuitClient);
        var component = new TestComponent(builder =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        });
 
        var client = new Mock<IClientProxy>();
        client.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0]))
            .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[0] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task);
 
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        _ = renderer.OnRenderCompletedAsync(2, null);
 
        @event.Reset();
        firstBatchTCS.SetResult();
 
        // Waiting is required here because the continuations of SetResult will not execute synchronously.
        @event.Wait(Timeout);
 
        circuitClient.SetDisconnected();
        component.TriggerRender();
        component.TriggerRender();
 
        // Act
        circuitClient.Transfer(client.Object, "new-connection");
        var task = renderer.ProcessBufferedRenderBatches();
 
        foreach (var id in renderIds.ToArray())
        {
            _ = renderer.OnRenderCompletedAsync(id, null);
        }
 
        secondBatchTCS.SetResult();
        thirdBatchTCS.SetResult();
 
        // Assert
        Assert.Equal(new long[] { 2, 3, 4 }, renderIds);
#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
        Assert.True(task.Wait(3000), "One or more render batches weren't acknowledged");
#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
 
        await task;
    }
 
    [Fact]
    public async Task OnRenderCompletedAsync_DoesNotThrowWhenReceivedDuplicateAcks()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var firstBatchTCS = new TaskCompletionSource();
        var secondBatchTCS = new TaskCompletionSource();
        var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
        offlineClient.SetDisconnected();
        var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
        RenderFragment initialContent = (builder) =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        };
        var trigger = new Trigger();
        var renderIds = new List<long>();
        var onlineClient = new Mock<IClientProxy>();
        onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
            .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
 
        // This produces the initial batch (id = 2)
        await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<AutoParameterTestComponent>(
            ParameterView.FromDictionary(new Dictionary<string, object>
            {
                [nameof(AutoParameterTestComponent.Content)] = initialContent,
                [nameof(AutoParameterTestComponent.Trigger)] = trigger
            })));
        trigger.Component.Content = (builder) =>
        {
            builder.OpenElement(0, "offline element");
            builder.AddContent(1, "offline text");
            builder.CloseElement();
        };
        // This produces an additional batch (id = 3)
        trigger.TriggerRender();
        var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
 
        // Act
        offlineClient.Transfer(onlineClient.Object, "new-connection");
        var task = renderer.ProcessBufferedRenderBatches();
        var exceptions = new List<Exception>();
        renderer.UnhandledException += (sender, e) =>
        {
            exceptions.Add(e);
        };
 
        // Receive the ack for the initial batch
        _ = renderer.OnRenderCompletedAsync(2, null);
        // Receive the ack for the second batch
        _ = renderer.OnRenderCompletedAsync(3, null);
 
        firstBatchTCS.SetResult();
        secondBatchTCS.SetResult();
        // Repeat the ack for the third batch
        _ = renderer.OnRenderCompletedAsync(3, null);
 
        // Assert
        Assert.Empty(exceptions);
    }
 
    [Fact]
    public async Task OnRenderCompletedAsync_DoesNotThrowWhenThereAreNoPendingBatchesToAck()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var firstBatchTCS = new TaskCompletionSource();
        var secondBatchTCS = new TaskCompletionSource();
        var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
        offlineClient.SetDisconnected();
        var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
        RenderFragment initialContent = (builder) =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        };
        var trigger = new Trigger();
        var renderIds = new List<long>();
        var onlineClient = new Mock<IClientProxy>();
        onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
            .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
 
        // This produces the initial batch (id = 2)
        await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<AutoParameterTestComponent>(
            ParameterView.FromDictionary(new Dictionary<string, object>
            {
                [nameof(AutoParameterTestComponent.Content)] = initialContent,
                [nameof(AutoParameterTestComponent.Trigger)] = trigger
            })));
        trigger.Component.Content = (builder) =>
        {
            builder.OpenElement(0, "offline element");
            builder.AddContent(1, "offline text");
            builder.CloseElement();
        };
        // This produces an additional batch (id = 3)
        trigger.TriggerRender();
        var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
 
        // Act
        offlineClient.Transfer(onlineClient.Object, "new-connection");
        var task = renderer.ProcessBufferedRenderBatches();
        var exceptions = new List<Exception>();
        renderer.UnhandledException += (sender, e) =>
        {
            exceptions.Add(e);
        };
 
        // Receive the ack for the initial batch
        _ = renderer.OnRenderCompletedAsync(2, null);
        // Receive the ack for the second batch
        _ = renderer.OnRenderCompletedAsync(2, null);
 
        firstBatchTCS.SetResult();
        secondBatchTCS.SetResult();
        // Repeat the ack for the third batch
        _ = renderer.OnRenderCompletedAsync(3, null);
 
        // Assert
        Assert.Empty(exceptions);
    }
 
    [Fact]
    public async Task ConsumesAllPendingBatchesWhenReceivingAHigherSequenceBatchId()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var firstBatchTCS = new TaskCompletionSource();
        var secondBatchTCS = new TaskCompletionSource();
        var renderIds = new List<long>();
 
        var onlineClient = new Mock<IClientProxy>();
        onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
            .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
 
        var renderer = GetRemoteRenderer(serviceProvider, new CircuitClientProxy(onlineClient.Object, "online-client"));
        RenderFragment initialContent = (builder) =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        };
        var trigger = new Trigger();
 
        // This produces the initial batch (id = 2)
        await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<AutoParameterTestComponent>(
            ParameterView.FromDictionary(new Dictionary<string, object>
            {
                [nameof(AutoParameterTestComponent.Content)] = initialContent,
                [nameof(AutoParameterTestComponent.Trigger)] = trigger
            })));
        trigger.Component.Content = (builder) =>
        {
            builder.OpenElement(0, "offline element");
            builder.AddContent(1, "offline text");
            builder.CloseElement();
        };
        // This produces an additional batch (id = 3)
        trigger.TriggerRender();
        var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
 
        // Act
        var exceptions = new List<Exception>();
        renderer.UnhandledException += (sender, e) =>
        {
            exceptions.Add(e);
        };
 
        // Pretend that we missed the ack for the initial batch
        _ = renderer.OnRenderCompletedAsync(3, null);
        firstBatchTCS.SetResult();
        secondBatchTCS.SetResult();
 
        // Assert
        Assert.Empty(exceptions);
        Assert.Empty(renderer._unacknowledgedRenderBatches);
    }
 
    [Fact]
    public async Task ThrowsIfWeReceivedAnAcknowledgeForANeverProducedBatch()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var firstBatchTCS = new TaskCompletionSource();
        var secondBatchTCS = new TaskCompletionSource();
        var renderIds = new List<long>();
 
        var onlineClient = new Mock<IClientProxy>();
        onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
            .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
            .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
 
        var renderer = GetRemoteRenderer(serviceProvider, new CircuitClientProxy(onlineClient.Object, "online-client"));
        RenderFragment initialContent = (builder) =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        };
        var trigger = new Trigger();
 
        // This produces the initial batch (id = 2)
        await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<AutoParameterTestComponent>(
            ParameterView.FromDictionary(new Dictionary<string, object>
            {
                [nameof(AutoParameterTestComponent.Content)] = initialContent,
                [nameof(AutoParameterTestComponent.Trigger)] = trigger
            })));
        trigger.Component.Content = (builder) =>
        {
            builder.OpenElement(0, "offline element");
            builder.AddContent(1, "offline text");
            builder.CloseElement();
        };
        // This produces an additional batch (id = 3)
        trigger.TriggerRender();
        var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
 
        // Act
        var exceptions = new List<Exception>();
        renderer.UnhandledException += (sender, e) =>
        {
            exceptions.Add(e);
        };
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.OnRenderCompletedAsync(4, null));
        firstBatchTCS.SetResult();
        secondBatchTCS.SetResult();
 
        // Assert
        Assert.Equal(
            "Received an acknowledgement for batch with id '4' when the last batch produced was '3'.",
            exception.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_AddRootComponentAsync_Throws_IfMaxInteractiveServerComponentCountIsExceeded()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        for (var i = 0; i < MaxInteractiveServerRootComponentCount; i++)
        {
            await AddWebRootComponentAsync(renderer, i);
        }
 
        // Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => AddWebRootComponentAsync(renderer, MaxInteractiveServerRootComponentCount));
 
        Assert.Equal("Exceeded the maximum number of allowed server interactive root components.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_AddRootComponentAsync_Throws_IfDuplicateSsrComponentIdIsProvided()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        await AddWebRootComponentAsync(renderer, 0);
 
        // Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => AddWebRootComponentAsync(renderer, 0));
 
        Assert.Equal("A root component with SSR component ID 0 already exists.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_AddRootComponentAsync_Throws_IfKeyIsInvalid()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act/assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            await webRootComponentManager.AddRootComponentAsync(
                0,
                typeof(TestComponent),
                default, // Invalid key
                WebRootComponentParameters.Empty);
        });
 
        Assert.Equal("An invalid component marker key was provided.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_AddRootComponentAsync_CanAddAndRenderRootComponent()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        await AddWebRootComponentAsync(renderer, 0);
 
        // Assert
        Assert.Single(renderer._unacknowledgedRenderBatches);
    }
 
    [Fact]
    public async Task WebRootComponentManager_UpdateRootComponentAsync_Throws_IfSsrComponentIdIsInvalid()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0);
 
        // Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            await webRootComponentManager.UpdateRootComponentAsync(1, typeof(TestComponent), key, WebRootComponentParameters.Empty);
        });
 
        Assert.Equal($"No root component exists with SSR component ID 1.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_UpdateRootComponentAsync_Throws_IfKeyDoesNotMatch()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        await AddWebRootComponentAsync(renderer, 0);
 
        // Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            await webRootComponentManager.UpdateRootComponentAsync(0, typeof(TestComponent), new("1", null), WebRootComponentParameters.Empty);
        });
 
        Assert.Equal("Cannot update components with mismatching keys.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_UpdateRootComponentAsync_Works_IfComponentKeyWasSupplied()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0, "mykey");
        await renderer.Dispatcher.InvokeAsync(() =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            var parameters = new Dictionary<string, object> { ["Name"] = "value" };
            webRootComponentManager.UpdateRootComponentAsync(0, typeof(TestComponent), key, CreateWebRootComponentParameters(parameters));
        });
 
        // Assert
        Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); // Initial render, re-render
    }
 
    [Fact]
    public async Task WebRootComponentManager_UpdateRootComponentAsync_DoesNothing_IfNoComponentKeyWasSuppliedAndParametersDidNotChange()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0);
        await renderer.Dispatcher.InvokeAsync(() =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            webRootComponentManager.UpdateRootComponentAsync(0, typeof(TestComponent), key, WebRootComponentParameters.Empty);
        });
 
        // Assert
        Assert.Single(renderer._unacknowledgedRenderBatches);
    }
 
    [Fact]
    public async Task WebRootComponentManager_UpdateRootComponentAsync_ReinitializesComponent_IfNoComponentKeyWasSuppliedAndParameterChanged()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0);
        await renderer.Dispatcher.InvokeAsync(() =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            var parameters = new Dictionary<string, object> { ["Name"] = "value" };
            webRootComponentManager.UpdateRootComponentAsync(0, typeof(TestComponent), key, CreateWebRootComponentParameters(parameters));
        });
 
        // Assert
        Assert.Equal(3, renderer._unacknowledgedRenderBatches.Count); // Initial render, dispose, and re-initialize
    }
 
    [Fact]
    public async Task WebRootComponentManager_RemoveRootComponent_Throws_IfSsrComponentIdIsInvalid()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0);
 
        // Assert
        var ex = Assert.Throws<InvalidOperationException>(() => renderer.GetOrCreateWebRootComponentManager().RemoveRootComponent(1));
 
        Assert.Equal($"No root component exists with SSR component ID 1.", ex.Message);
    }
 
    [Fact]
    public async Task WebRootComponentManager_RemoveRootComponent_Works()
    {
        // Arrange
        var serviceProvider = CreateServiceProvider();
        var renderer = GetRemoteRenderer(serviceProvider);
 
        // Act
        var key = await AddWebRootComponentAsync(renderer, 0);
        await renderer.Dispatcher.InvokeAsync(() =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            webRootComponentManager.RemoveRootComponent(0);
        });
 
        // Assert
        Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); // Initial render, dispose
    }
 
    private IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton(new Mock<IJSRuntime>().Object);
        return serviceCollection.BuildServiceProvider();
    }
 
    private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null)
    {
        var serverComponentDeserializer = new ServerComponentDeserializer(
            _ephemeralDataProtectionProvider,
            NullLogger<ServerComponentDeserializer>.Instance,
            new RootComponentTypeCache(),
            new ComponentParameterDeserializer(
                NullLogger<ComponentParameterDeserializer>.Instance,
                new ComponentParametersTypeCache()));
 
        return new TestRemoteRenderer(
            serviceProvider,
            NullLoggerFactory.Instance,
            new CircuitOptions
            {
                RootComponents =
                {
                    MaxJSRootComponents = MaxInteractiveServerRootComponentCount
                },
            },
            circuitClient ?? new CircuitClientProxy(),
            serverComponentDeserializer,
            NullLogger.Instance);
    }
 
    private static Task<ComponentMarkerKey> AddWebRootComponentAsync(RemoteRenderer renderer, int ssrComponentId, string componentKey = null)
        => renderer.Dispatcher.InvokeAsync(async () =>
        {
            var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
            var componentMarkerKey = new ComponentMarkerKey()
            {
                LocationHash = ssrComponentId.ToString(CultureInfo.CurrentCulture),
                FormattedComponentKey = componentKey,
            };
            await webRootComponentManager.AddRootComponentAsync(
                ssrComponentId,
                typeof(TestComponent),
                componentMarkerKey,
                WebRootComponentParameters.Empty);
            return componentMarkerKey;
        });
 
    private static WebRootComponentParameters CreateWebRootComponentParameters(IDictionary<string, object> parameters)
    {
        var parameterView = ParameterView.FromDictionary(parameters);
        var (parameterDefinitions, parameterValues) = ComponentParameter.FromParameterView(parameterView);
        for (var i = 0; i < parameterValues.Count; i++)
        {
            // WebRootComponentParameters expects serialized parameter values to be JsonElements.
            var jsonElement = JsonSerializer.SerializeToElement(parameterValues[i]);
            parameterValues[i] = jsonElement;
        }
        return new WebRootComponentParameters(
            parameterView,
            parameterDefinitions.AsReadOnly(),
            parameterValues.AsReadOnly());
    }
 
    private class TestRemoteRenderer : RemoteRenderer
    {
        public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, CircuitClientProxy client, IServerComponentDeserializer serverComponentDeserializer, ILogger logger)
            : base(serviceProvider, loggerFactory, options, client, serverComponentDeserializer, logger, CreateJSRuntime(options), new CircuitJSComponentInterop(options))
        {
        }
 
        public async Task RenderComponentAsync<TComponent>(ParameterView initialParameters)
        {
            var component = InstantiateComponent(typeof(TComponent));
            var componentId = AssignRootComponentId(component);
            await RenderRootComponentAsync(componentId, initialParameters);
        }
 
        protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
        {
        }
 
        public new ComponentState GetComponentState(int componentId)
        {
            return base.GetComponentState(componentId);
        }
 
        private static RemoteJSRuntime CreateJSRuntime(CircuitOptions options)
            => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions<ComponentHub>()), null);
    }
 
    private class TestComponent : IComponent, IHandleAfterRender
    {
        private RenderHandle _renderHandle;
        private readonly RenderFragment _renderFragment = (builder) =>
        {
            builder.OpenElement(0, "my element");
            builder.AddContent(1, "some text");
            builder.CloseElement();
        };
 
        public TestComponent()
        {
        }
 
        internal TestComponent(RenderFragment renderFragment)
        {
            _renderFragment = renderFragment;
        }
 
        public Action OnAfterRenderComplete { get; set; }
 
        public void Attach(RenderHandle renderHandle)
        {
            _renderHandle = renderHandle;
        }
 
        public Task OnAfterRenderAsync()
        {
            OnAfterRenderComplete?.Invoke();
            return Task.CompletedTask;
        }
 
        public Task SetParametersAsync(ParameterView parameters)
        {
            TriggerRender();
            return Task.CompletedTask;
        }
 
        public void TriggerRender()
        {
            var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(_renderFragment));
            Assert.True(task.IsCompletedSuccessfully);
        }
    }
 
    private class AutoParameterTestComponent : IComponent
    {
        private RenderHandle _renderHandle;
 
        [Parameter] public RenderFragment Content { get; set; }
 
        [Parameter] public Trigger Trigger { get; set; }
 
        public void Attach(RenderHandle renderHandle)
        {
            _renderHandle = renderHandle;
        }
 
        public Task SetParametersAsync(ParameterView parameters)
        {
            Content = parameters.GetValueOrDefault<RenderFragment>(nameof(Content));
            Trigger ??= parameters.GetValueOrDefault<Trigger>(nameof(Trigger));
            Trigger.Component = this;
            TriggerRender();
            return Task.CompletedTask;
        }
 
        public void TriggerRender()
        {
            var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(Content));
            Assert.True(task.IsCompletedSuccessfully);
        }
    }
 
    private class Trigger
    {
        public AutoParameterTestComponent Component { get; set; }
        public void TriggerRender()
        {
            Component.TriggerRender();
        }
    }
}