File: src\Components\Shared\test\TestRenderer.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.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.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Microsoft.AspNetCore.Components.Test.Helpers;
 
public class TestRenderer : Renderer
{
    public TestRenderer() : this(new TestServiceProvider())
    {
    }
 
    public TestRenderer(Dispatcher dispatcher) : base(new TestServiceProvider(), NullLoggerFactory.Instance)
    {
        Dispatcher = dispatcher;
    }
 
    public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, NullLoggerFactory.Instance)
    {
        Dispatcher = Dispatcher.CreateDefault();
    }
 
    public TestRenderer(IServiceProvider serviceProvider, IComponentActivator componentActivator)
        : base(serviceProvider, NullLoggerFactory.Instance, componentActivator)
    {
        Dispatcher = Dispatcher.CreateDefault();
    }
 
    public override Dispatcher Dispatcher { get; }
 
    public Action OnExceptionHandled { get; set; }
 
    public Action<RenderBatch> OnUpdateDisplay { get; set; }
 
    public Action OnUpdateDisplayComplete { get; set; }
 
    public List<CapturedBatch> Batches { get; }
        = new List<CapturedBatch>();
 
    public List<Exception> HandledExceptions { get; } = new List<Exception>();
 
    public bool ShouldHandleExceptions { get; set; }
 
    public Task NextRenderResultTask { get; set; } = Task.CompletedTask;
 
    private HashSet<TestRendererComponentState> UndisposedComponentStates { get; } = new();
 
    public new int AssignRootComponentId(IComponent component)
        => base.AssignRootComponentId(component);
 
    public new void RemoveRootComponent(int componentId)
        => base.RemoveRootComponent(componentId);
 
    public new ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId)
        => base.GetCurrentRenderTreeFrames(componentId);
 
    public void RenderRootComponent(int componentId, ParameterView? parameters = default)
    {
        var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty));
        UnwrapTask(task);
    }
 
    public new Task RenderRootComponentAsync(int componentId)
        => Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId));
 
    public new Task RenderRootComponentAsync(int componentId, ParameterView parameters)
        => Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
 
    public Task DispatchEventAsync(ulong eventHandlerId, EventArgs args)
        => Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, null, args));
 
    public new Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs args)
        => Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, eventFieldInfo, args));
 
    private static Task UnwrapTask(Task task)
    {
        // This should always be run synchronously
        Assert.True(task.IsCompleted);
        if (task.IsFaulted)
        {
            var exception = task.Exception.Flatten().InnerException;
            while (exception is AggregateException e)
            {
                exception = e.InnerException;
            }
 
            ExceptionDispatchInfo.Capture(exception).Throw();
        }
 
        return task;
    }
 
    public IComponent InstantiateComponent<T>()
        => InstantiateComponent(typeof(T));
 
    protected override void HandleException(Exception exception)
    {
        if (!ShouldHandleExceptions)
        {
            ExceptionDispatchInfo.Capture(exception).Throw();
        }
 
        HandledExceptions.Add(exception);
        OnExceptionHandled?.Invoke();
    }
 
    protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
    {
        OnUpdateDisplay?.Invoke(renderBatch);
 
        var capturedBatch = new CapturedBatch();
        Batches.Add(capturedBatch);
 
        for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
        {
            ref var renderTreeDiff = ref renderBatch.UpdatedComponents.Array[i];
            capturedBatch.AddDiff(renderTreeDiff);
        }
 
        // Clone other data, as underlying storage will get reused by later batches
        capturedBatch.ReferenceFrames = renderBatch.ReferenceFrames.AsEnumerable().ToArray();
        capturedBatch.DisposedComponentIDs = renderBatch.DisposedComponentIDs.AsEnumerable().ToList();
 
        // This renderer updates the UI synchronously, like the WebAssembly one.
        // To test async UI updates, subclass TestRenderer and override UpdateDisplayAsync.
 
        OnUpdateDisplayComplete?.Invoke();
        return NextRenderResultTask;
    }
 
    public new void ProcessPendingRender()
        => base.ProcessPendingRender();
 
    protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState parentComponentState)
        => new TestRendererComponentState(this, componentId, component, parentComponentState);
 
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
 
        if (UndisposedComponentStates.Count > 0)
        {
            throw new InvalidOperationException("Did not dispose all the ComponentState instances. This could lead to ArrayBuffer not returning buffers to its pool.");
        }
    }
 
    class TestRendererComponentState : ComponentState, IAsyncDisposable
    {
        private readonly TestRenderer _renderer;
 
        public TestRendererComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState)
            : base(renderer, componentId, component, parentComponentState)
        {
            _renderer = (TestRenderer)renderer;
            _renderer.UndisposedComponentStates.Add(this);
        }
 
        public override ValueTask DisposeAsync()
        {
            _renderer.UndisposedComponentStates.Remove(this);
            return base.DisposeAsync();
        }
    }
}