|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Components.CompilerServices;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.Components.Test;
public class RendererTest
{
// 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 string EventActionsName = nameof(NestedAsyncComponent.EventActions);
private const string WhatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
private const string LogName = nameof(NestedAsyncComponent.Log);
[Fact]
public void CanRenderTopLevelComponents()
{
// Arrange
var renderer = new TestRenderer();
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();
// Assert
var batch = renderer.Batches.Single();
var diff = batch.DiffsByComponentId[componentId].Single();
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Element(batch.ReferenceFrames[0], "my element", 2);
AssertFrame.Text(batch.ReferenceFrames[1], "some text");
}
[Fact]
public void CanRenderNestedComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<MessageComponent>(1);
builder.AddComponentParameter(2, nameof(MessageComponent.Message), "Nested component output");
builder.CloseComponent();
});
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var componentFrame = batch.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentId = componentFrame.ComponentId;
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
// We rendered both components
Assert.Equal(2, batch.DiffsByComponentId.Count);
// The nested component exists
Assert.IsType<MessageComponent>(componentFrame.Component);
// The nested component was rendered as part of the batch
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Nested component output");
});
}
[Fact]
public void CanReRenderTopLevelComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new MessageComponent { Message = "Initial message" };
var componentId = renderer.AssignRootComponentId(component);
// Act/Assert: first render
component.TriggerRender();
var batch = renderer.Batches.Single();
var firstDiff = batch.DiffsByComponentId[componentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(batch.ReferenceFrames[0], "Initial message");
});
// Act/Assert: second render
component.Message = "Modified message";
component.TriggerRender();
var secondBatch = renderer.Batches.Skip(1).Single();
var secondDiff = secondBatch.DiffsByComponentId[componentId].Single();
Assert.Collection(secondDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Modified message");
});
}
[Fact]
public void CanReRenderNestedComponents()
{
// Arrange: parent component already rendered
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
builder.OpenComponent<MessageComponent>(0);
builder.CloseComponent();
});
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
parentComponent.TriggerRender();
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (MessageComponent)nestedComponentFrame.Component;
var nestedComponentId = nestedComponentFrame.ComponentId;
// Assert: initial render
nestedComponent.Message = "Render 1";
nestedComponent.TriggerRender();
var batch = renderer.Batches[1];
var firstDiff = batch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(batch.ReferenceFrames[0], "Render 1");
});
// Act/Assert: re-render
nestedComponent.Message = "Render 2";
nestedComponent.TriggerRender();
var secondBatch = renderer.Batches[2];
var secondDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(secondDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Render 2");
});
}
[Fact]
public async Task CanRenderAsyncTopLevelComponents()
{
// Arrange
var renderer = new TestRenderer();
var tcs = new TaskCompletionSource();
var component = new AsyncComponent(tcs.Task, 5); // Triggers n renders, the first one creating <p>n</p> and the n-1 renders asynchronously update the value.
// Act
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.False(renderTask.IsCompleted);
tcs.SetResult();
await renderTask;
Assert.Equal(5, renderer.Batches.Count);
// First render
var create = renderer.Batches[0];
var diff = create.DiffsByComponentId[componentId].Single();
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Element(create.ReferenceFrames[0], "p", 2);
AssertFrame.Text(create.ReferenceFrames[1], "5");
// Second render
for (var i = 1; i < 5; i++)
{
var update = renderer.Batches[i];
var updateDiff = update.DiffsByComponentId[componentId].Single();
Assert.Collection(updateDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.StepIn, edit.Type);
},
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
},
edit =>
{
Assert.Equal(RenderTreeEditType.StepOut, edit.Type);
});
AssertFrame.Text(update.ReferenceFrames[0], (5 - i).ToString(CultureInfo.InvariantCulture));
}
}
[Fact]
public async Task CanRenderAsyncNestedComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new NestedAsyncComponent();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
},
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
}
},
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>())
},
[LogName] = log
})));
var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray();
AssertStream(0, logForParent);
AssertStream(1, logForChild);
}
[Fact]
public void CanReRenderRootComponentsWithNewParameters()
{
// This differs from the other "CanReRender..." tests above in that the root component is being supplied
// with new parameters from outside, as opposed to making its own decision to re-render.
// Arrange
var renderer = new TestRenderer();
var component = new MessageComponent();
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(MessageComponent.Message)] = "Hello"
}));
// Assert 1: First render
var batch = renderer.Batches.Single();
var diff = batch.DiffsByComponentId[componentId].Single();
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(batch.ReferenceFrames[0], "Hello");
// Act 2: Update params
renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(MessageComponent.Message)] = "Goodbye"
}));
// Assert 2: Second render
var batch2 = renderer.Batches.Skip(1).Single();
var diff2 = batch2.DiffsByComponentId[componentId].Single();
Assert.Collection(diff2.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(batch2.ReferenceFrames[0], "Goodbye");
}
[Fact]
public async Task CanAddAndRenderNewRootComponentsWhileNotQuiescent()
{
// Arrange 1: An async root component
var renderer = new TestRenderer();
var tcs1 = new TaskCompletionSource();
var component1 = new AsyncComponent(tcs1.Task, 1);
var component1Id = renderer.AssignRootComponentId(component1);
// Act/Assert 1: Its SetParametersAsync task remains incomplete
var renderTask1 = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(component1Id));
Assert.False(renderTask1.IsCompleted);
// Arrange/Act 2: Can add a second root component while not quiescent
var tcs2 = new TaskCompletionSource();
var component2 = new AsyncComponent(tcs2.Task, 1);
var component2Id = renderer.AssignRootComponentId(component2);
var renderTask2 = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(component2Id));
// Assert 2
Assert.False(renderTask1.IsCompleted);
Assert.False(renderTask2.IsCompleted);
// Completing the first task isn't enough to consider the system quiescent, because there's now a second task
tcs1.SetResult();
// renderTask1 should not complete until we finish tcs2.
// We can't really prove that absolutely, but at least show it doesn't happen during a certain time period.
await Assert.ThrowsAsync<TimeoutException>(() => renderTask1.WaitAsync(TimeSpan.FromMilliseconds(250)));
Assert.False(renderTask1.IsCompleted);
Assert.False(renderTask2.IsCompleted);
// Completing the second task does finally complete both render tasks
tcs2.SetResult();
await Task.WhenAll(renderTask1, renderTask2);
}
[Fact]
public async Task AsyncComponentTriggeringRootReRenderDoesNotDeadlock()
{
// Arrange
var renderer = new TestRenderer();
var tcs = new TaskCompletionSource();
int? componentId = null;
var hasRendered = false;
var component = new CallbackDuringSetParametersAsyncComponent
{
Callback = async () =>
{
await tcs.Task;
if (!hasRendered)
{
hasRendered = true;
// If we were to await here, then it would deadlock, because the component would be saying it's not
// finished rendering until the rendering system has already finished. The point of this test is to
// show that, as long as we don't await quiescence here, nothing within the system will be doing so
// and hence the whole process can complete.
_ = renderer.RenderRootComponentAsync(componentId.Value, ParameterView.Empty);
}
}
};
componentId = renderer.AssignRootComponentId(component);
// Act
var renderTask = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId.Value));
// Assert
Assert.False(renderTask.IsCompleted);
tcs.SetResult();
await renderTask;
}
[Fact]
public async Task CanRenderAsyncComponentsWithSyncChildComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new NestedAsyncComponent();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
},
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
}
},
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>())
},
[LogName] = log
})));
var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray();
AssertStream(0, logForParent);
AssertStream(1, logForChild);
}
[Fact]
public async Task CanRenderAsyncComponentsWithAsyncChildInit()
{
// Arrange
var renderer = new TestRenderer();
var component = new NestedAsyncComponent();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
},
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
}
},
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>())
},
[LogName] = log
})));
var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray();
AssertStream(0, logForParent);
AssertStream(1, logForChild);
}
[Fact]
public async Task CanRenderAsyncComponentsWithMultipleAsyncChildren()
{
// Arrange
var renderer = new TestRenderer();
var component = new NestedAsyncComponent();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
},
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
},
[2] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
},
[3] = new List<NestedAsyncComponent.ExecutionAction>
{
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInit),
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSet),
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
}
},
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1, 2 }),
[1] = CreateRenderFactory(new[] { 3 }),
[2] = CreateRenderFactory(Array.Empty<int>()),
[3] = CreateRenderFactory(Array.Empty<int>())
},
[LogName] = log
})));
var logForParent = log.Where(l => l.id == 0).ToArray();
var logForFirstChild = log.Where(l => l.id == 1).ToArray();
var logForSecondChild = log.Where(l => l.id == 2).ToArray();
var logForThirdChild = log.Where(l => l.id == 3).ToArray();
AssertStream(0, logForParent);
AssertStream(1, logForFirstChild);
AssertStream(2, logForSecondChild);
AssertStream(3, logForThirdChild);
}
[Fact]
public void DispatchingEventsWithoutAsyncWorkShouldCompleteSynchronously()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
EventArgs receivedArgs = null;
var component = new EventComponent
{
OnTest = args => { receivedArgs = args; }
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// This should always be run synchronously
Assert.True(task.IsCompletedSuccessfully);
}
[Fact]
public void CanDispatchEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
EventArgs receivedArgs = null;
var component = new EventComponent
{
OnTest = args => { receivedArgs = args; }
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
[Fact]
public void CanGetEventArgsTypeForHandler()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
var component = new EventComponent
{
OnArbitraryDelegateEvent = (Func<DerivedEventArgs, Task>)(args => Task.CompletedTask),
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Can determine event args type
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
Assert.Same(typeof(DerivedEventArgs), eventArgsType);
}
[Fact]
public void CanGetEventArgsTypeForParameterlessHandler()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
var component = new EventComponent
{
OnArbitraryDelegateEvent = (Func<Task>)(() => Task.CompletedTask),
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Can determine event args type
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
Assert.Same(typeof(EventArgs), eventArgsType);
}
[Fact]
public void CannotGetEventArgsTypeForMultiParameterHandler()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
var component = new EventComponent
{
OnArbitraryDelegateEvent = (Action<EventArgs, string>)((x, y) => { }),
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Cannot determine event args type
var ex = Assert.Throws<InvalidOperationException>(() => renderer.GetEventArgsType(eventHandlerId));
Assert.Contains("declares more than one parameter", ex.Message);
}
[Fact]
public void CannotGetEventArgsTypeForHandlerWithNonEventArgsParameter()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
var component = new EventComponent
{
OnArbitraryDelegateEvent = (Action<DateTime>)(arg => { }),
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Cannot determine event args type
var ex = Assert.Throws<InvalidOperationException>(() => renderer.GetEventArgsType(eventHandlerId));
Assert.Contains($"must inherit from {typeof(EventArgs).FullName}", ex.Message);
}
[Fact]
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer
{
ShouldHandleExceptions = true
};
var component = new EventComponent
{
OnTest = args => throw new Exception("Error")
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Empty(renderer.HandledExceptions);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
var exception = Assert.Single(renderer.HandledExceptions);
Assert.Equal("Error", exception.Message);
}
[Fact]
public void CanDispatchTypedEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
DerivedEventArgs receivedArgs = null;
var component = new EventComponent
{
OnClick = args => { receivedArgs = args; }
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new DerivedEventArgs();
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
[Fact]
public void CanDispatchActionEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
object receivedArgs = null;
var component = new EventComponent
{
OnClickAction = () => { receivedArgs = new object(); }
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new DerivedEventArgs();
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.NotNull(receivedArgs);
}
[Fact]
public void CanDispatchEventsToNestedComponents()
{
EventArgs receivedArgs = null;
// Arrange: Render parent component
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
builder.OpenComponent<EventComponent>(0);
builder.CloseComponent();
});
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
parentComponent.TriggerRender();
// Arrange: Render nested component
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (EventComponent)nestedComponentFrame.Component;
nestedComponent.OnTest = args => { receivedArgs = args; };
var nestedComponentId = nestedComponentFrame.ComponentId;
nestedComponent.TriggerRender();
// Find nested component's event handler ID
var eventHandlerId = renderer.Batches[1]
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
[Fact]
public async Task CanAsyncDispatchEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
EventArgs receivedArgs = null;
var state = 0;
var tcs = new TaskCompletionSource();
var component = new EventComponent
{
OnTestAsync = async (args) =>
{
receivedArgs = args;
state = 1;
await tcs.Task;
state = 2;
},
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.Equal(1, state);
Assert.Same(eventArgs, receivedArgs);
tcs.SetResult();
await task;
Assert.Equal(2, state);
}
[Fact]
public async Task CanAsyncDispatchTypedEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
DerivedEventArgs receivedArgs = null;
var state = 0;
var tcs = new TaskCompletionSource();
var component = new EventComponent
{
OnClickAsync = async (args) =>
{
receivedArgs = args;
state = 1;
await tcs.Task;
state = 2;
}
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new DerivedEventArgs();
var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.Equal(1, state);
Assert.Same(eventArgs, receivedArgs);
tcs.SetResult();
await task;
Assert.Equal(2, state);
}
[Fact]
public async Task CanAsyncDispatchActionEventsToTopLevelComponents()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
object receivedArgs = null;
var state = 0;
var tcs = new TaskCompletionSource();
var component = new EventComponent
{
OnClickAsyncAction = async () =>
{
receivedArgs = new object();
state = 1;
await tcs.Task;
state = 2;
}
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new DerivedEventArgs();
var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.Equal(1, state);
Assert.NotNull(receivedArgs);
tcs.SetResult();
await task;
Assert.Equal(2, state);
}
[Fact]
public async Task CanAsyncDispatchEventsToNestedComponents()
{
EventArgs receivedArgs = null;
var state = 0;
var tcs = new TaskCompletionSource();
// Arrange: Render parent component
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
builder.OpenComponent<EventComponent>(0);
builder.CloseComponent();
});
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
parentComponent.TriggerRender();
// Arrange: Render nested component
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (EventComponent)nestedComponentFrame.Component;
nestedComponent.OnTestAsync = async (args) =>
{
receivedArgs = args;
state = 1;
await tcs.Task;
state = 2;
};
var nestedComponentId = nestedComponentFrame.ComponentId;
nestedComponent.TriggerRender();
// Find nested component's event handler ID
var eventHandlerId = renderer.Batches[1]
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new EventArgs();
var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
Assert.Equal(1, state);
Assert.Same(eventArgs, receivedArgs);
tcs.SetResult();
await task;
Assert.Equal(2, state);
}
// This tests the behaviour of dispatching an event when the event-handler
// delegate is a bound-delegate with a target that points to the parent component.
//
// This is a very common case when a component accepts a delegate parameter that
// will be hooked up to a DOM event handler. It's essential that this will dispatch
// to the parent component so that manual StateHasChanged calls are not necessary.
[Fact]
public async Task EventDispatching_DelegateParameter_MethodToDelegateConversion()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAction), (Action)parentComponent.SomeMethod);
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
// This is the inverse case of EventDispatching_DelegateParameter_MethodToDelegateConversion
// where the event-handling delegate has a target that is not a component.
//
// This is a degenerate case that we don't expect to occur in applications often,
// but it's important to verify the semantics.
[Fact]
public async Task EventDispatching_DelegateParameter_NoTargetLambda()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAction), (Action)(() =>
{
parentComponent.SomeMethod();
}));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(0, outerStateChangeCount);
}
// This is a similar case to EventDispatching_DelegateParameter_MethodToDelegateConversion
// but uses our event handling infrastructure to achieve the same effect. The call to CreateDelegate
// is not necessary for correctness in this case - it should just no op.
[Fact]
public async Task EventDispatching_EventCallback_MethodToDelegateConversion()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)parentComponent.SomeMethod));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
// This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses
// our event-handling infrastructure to avoid the need for a manual StateHasChanged()
[Fact]
public async Task EventDispatching_EventCallback_NoTargetLambda()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() =>
{
parentComponent.SomeMethod();
})));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
// This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses
// our event-handling infrastructure to avoid the need for a manual StateHasChanged()
[Fact]
public async Task EventDispatching_EventCallback_AsyncNoTargetLambda()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Func<Task>)(() =>
{
parentComponent.SomeMethod();
return Task.CompletedTask;
})));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
[Fact]
public async Task EventDispatching_EventCallbackOfT_MethodToDelegateConversion()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, (Action)parentComponent.SomeMethod));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
// This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses
// our event-handling infrastructure to avoid the need for a manual StateHasChanged()
[Fact]
public async Task EventDispatching_EventCallbackOfT_NoTargetLambda()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, (Action)(() =>
{
parentComponent.SomeMethod();
})));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
// This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses
// our event-handling infrastructure to avoid the need for a manual StateHasChanged()
[Fact]
public async Task EventDispatching_EventCallbackOfT_AsyncNoTargetLambda()
{
// Arrange
var outerStateChangeCount = 0;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, (Func<Task>)(() =>
{
parentComponent.SomeMethod();
return Task.CompletedTask;
})));
builder.CloseComponent();
};
parentComponent.OnEvent = () =>
{
outerStateChangeCount++;
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var eventArgs = new DerivedEventArgs();
await renderer.DispatchEventAsync(eventHandlerId, eventArgs);
// Assert
Assert.Equal(1, parentComponent.SomeMethodCallCount);
Assert.Equal(1, outerStateChangeCount);
}
[Fact]
public async Task DispatchEventAsync_Delegate_SynchronousCompletion()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAction), (Action)(() =>
{
// Do nothing.
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_EventCallback_SynchronousCompletion()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() =>
{
// Do nothing.
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_SynchronousCompletion()
{
// Arrange
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action<DerivedEventArgs>)((e) =>
{
arg = e;
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_Delegate_SynchronousCancellation()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAction), (Action)(() =>
{
throw new OperationCanceledException();
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallback_SynchronousCancellation()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() =>
{
throw new OperationCanceledException();
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_SynchronousCancellation()
{
// Arrange
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action<DerivedEventArgs>)((e) =>
{
arg = e;
throw new OperationCanceledException();
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_Delegate_SynchronousException()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAction), (Action)(() =>
{
throw new InvalidTimeZoneException();
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.Faulted, task.Status);
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallback_SynchronousException()
{
// Arrange
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() =>
{
throw new InvalidTimeZoneException();
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.Faulted, task.Status);
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_SynchronousException()
{
// Arrange
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, (Action<DerivedEventArgs>)((e) =>
{
arg = e;
throw new InvalidTimeZoneException();
})));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.Faulted, task.Status);
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_Delegate_AsynchronousCompletion()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAsyncAction), (Func<Task>)(async () =>
{
await tcs.Task;
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_EventCallback_AsynchronousCompletion()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () =>
{
await tcs.Task;
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousCompletion()
{
// Arrange
var tcs = new TaskCompletionSource();
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, async (e) =>
{
arg = e;
await tcs.Task;
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
}
[Fact]
public async Task DispatchEventAsync_Delegate_AsynchronousCancellation()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAsyncAction), (Func<Task>)(async () =>
{
await tcs.Task;
throw new TaskCanceledException();
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public async Task DispatchEventAsync_EventCallback_AsynchronousCancellation()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () =>
{
await tcs.Task;
throw new TaskCanceledException();
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousCancellation()
{
// Arrange
var tcs = new TaskCompletionSource();
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, async (e) =>
{
arg = e;
await tcs.Task;
throw new TaskCanceledException();
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await task; // Does not throw
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public async Task DispatchEventAsync_Delegate_AsynchronousException()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent
{
RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickAsyncAction), (Func<Task>)(async () =>
{
await tcs.Task;
throw new InvalidTimeZoneException();
}));
builder.CloseComponent();
}
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclickaction")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallback_AsynchronousException()
{
// Arrange
var tcs = new TaskCompletionSource();
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () =>
{
await tcs.Task;
throw new InvalidTimeZoneException();
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousException()
{
// Arrange
var tcs = new TaskCompletionSource();
DerivedEventArgs arg = null;
var renderer = new TestRenderer();
var parentComponent = new OuterEventComponent();
parentComponent.RenderFragment = (builder) =>
{
builder.OpenComponent<EventComponent>(0);
builder.AddComponentParameter(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create<DerivedEventArgs>(parentComponent, async (e) =>
{
arg = e;
await tcs.Task;
throw new InvalidTimeZoneException();
}));
builder.CloseComponent();
};
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
await parentComponent.TriggerRenderAsync();
var eventHandlerId = renderer.Batches[0]
.ReferenceFrames
.First(frame => frame.AttributeName == "onclick")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new DerivedEventArgs());
// Assert
Assert.NotNull(arg);
Assert.Equal(TaskStatus.WaitingForActivation, task.Status);
tcs.SetResult();
await Assert.ThrowsAsync<InvalidTimeZoneException>(() => task);
}
[Fact]
public async Task CannotDispatchEventsWithUnknownEventHandlers()
{
// Arrange
var renderer = new TestRenderer();
// Act/Assert
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(0, new EventArgs());
});
}
[Fact]
public void ComponentsCanBeAssociatedWithMultipleRenderers()
{
// Arrange
var renderer1 = new TestRenderer();
var renderer2 = new TestRenderer();
var component = new MultiRendererComponent();
var renderer1ComponentId = renderer1.AssignRootComponentId(component);
renderer2.AssignRootComponentId(new TestComponent(null)); // Just so they don't get the same IDs
var renderer2ComponentId = renderer2.AssignRootComponentId(component);
// Act/Assert
component.TriggerRender();
var renderer1Batch = renderer1.Batches.Single();
var renderer1Diff = renderer1Batch.DiffsByComponentId[renderer1ComponentId].Single();
Assert.Collection(renderer1Diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(renderer1Batch.ReferenceFrames[edit.ReferenceFrameIndex],
$"Hello from {nameof(MultiRendererComponent)}", 0);
});
var renderer2Batch = renderer2.Batches.Single();
var renderer2Diff = renderer2Batch.DiffsByComponentId[renderer2ComponentId].Single();
Assert.Collection(renderer2Diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(renderer2Batch.ReferenceFrames[edit.ReferenceFrameIndex],
$"Hello from {nameof(MultiRendererComponent)}", 0);
});
}
[Fact]
public void PreservesChildComponentInstancesWithNoAttributes()
{
// Arrange: First render, capturing child component instance
var renderer = new TestRenderer();
var message = "Hello";
var component = new TestComponent(builder =>
{
builder.AddContent(0, message);
builder.OpenComponent<MessageComponent>(1);
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentInstance = (MessageComponent)nestedComponentFrame.Component;
// Act: Second render
message = "Modified message";
component.TriggerRender();
// Assert
var batch = renderer.Batches[1];
var diff = batch.DiffsByComponentId[rootComponentId].Single();
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
}
[Fact]
public void UpdatesPropertiesOnRetainedChildComponentInstances()
{
// Arrange: First render, capturing child component instance
var renderer = new TestRenderer();
var objectThatWillNotChange = new object();
var firstRender = true;
var component = new TestComponent(builder =>
{
builder.OpenComponent<FakeComponent>(1);
builder.AddComponentParameter(2, nameof(FakeComponent.IntProperty), firstRender ? 123 : 256);
builder.AddComponentParameter(3, nameof(FakeComponent.ObjectProperty), objectThatWillNotChange);
builder.AddComponentParameter(4, nameof(FakeComponent.StringProperty), firstRender ? "String that will change" : "String that did change");
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var originalComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var childComponentInstance = (FakeComponent)originalComponentFrame.Component;
// Assert 1: properties were assigned
Assert.Equal(123, childComponentInstance.IntProperty);
Assert.Equal("String that will change", childComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert
Assert.Equal(256, childComponentInstance.IntProperty);
Assert.Equal("String that did change", childComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
}
[Fact]
public void ReRendersChildComponentsWhenPropertiesChange()
{
// Arrange: First render
var renderer = new TestRenderer();
var firstRender = true;
var component = new TestComponent(builder =>
{
builder.OpenComponent<MessageComponent>(1);
builder.AddComponentParameter(2, nameof(MessageComponent.Message), firstRender ? "first" : "second");
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var childComponentId = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.ComponentId;
// Act: Second render
firstRender = false;
component.TriggerRender();
var diff = renderer.Batches[1].DiffsByComponentId[childComponentId].Single();
// Assert
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(renderer.Batches[1].ReferenceFrames[0], "second");
}
[Fact]
public void ReRendersChildComponentWhenUnmatchedValuesChange()
{
// Arrange: First render
var renderer = new TestRenderer();
var firstRender = true;
var component = new TestComponent(builder =>
{
builder.OpenComponent<MyStrongComponent>(1);
builder.AddComponentParameter(1, "class", firstRender ? "first" : "second");
builder.AddComponentParameter(2, "id", "some_text");
builder.AddComponentParameter(3, nameof(MyStrongComponent.Text), "hi there.");
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var childComponentId = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.ComponentId;
// Act: Second render
firstRender = false;
component.TriggerRender();
var diff = renderer.Batches[1].DiffsByComponentId[childComponentId].Single();
// Assert
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Attribute(renderer.Batches[1].ReferenceFrames[0], "class", "second");
}
// This is a sanity check that diffs of "unmatched" values *just work* without any specialized
// code in the renderer to handle it. All of the data that's used in the diff is contained in
// the render tree, and the diff process does not need to inspect the state of the component.
[Fact]
public void ReRendersDoesNotReRenderChildComponentWhenUnmatchedValuesDoNotChange()
{
// Arrange: First render
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<MyStrongComponent>(1);
builder.AddComponentParameter(1, "class", "cool-beans");
builder.AddComponentParameter(2, "id", "some_text");
builder.AddComponentParameter(3, nameof(MyStrongComponent.Text), "hi there.");
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var childComponentId = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.ComponentId;
// Act: Second render
component.TriggerRender();
// Assert
Assert.False(renderer.Batches[1].DiffsByComponentId.ContainsKey(childComponentId));
}
[Fact]
public void RenderBatchIncludesListOfDisposedComponents()
{
// Arrange
var renderer = new TestRenderer();
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
// Nested descendants
builder.OpenComponent<ConditionalParentComponent<FakeComponent>>(100);
builder.AddComponentParameter(101, nameof(ConditionalParentComponent<FakeComponent>.IncludeChild), true);
builder.CloseComponent();
}
builder.OpenComponent<FakeComponent>(200);
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(component);
// Act/Assert 1: First render, capturing child component IDs
component.TriggerRender();
var batch = renderer.Batches.Single();
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
var childComponentIds = rootComponentDiff
.Edits
.Select(edit => batch.ReferenceFrames[edit.ReferenceFrameIndex])
.Where(frame => frame.FrameType == RenderTreeFrameType.Component)
.Select(frame => frame.ComponentId)
.ToList();
var childComponent3 = batch.ReferenceFrames.Where(f => f.ComponentId == 3)
.Single().Component;
Assert.Equal(new[] { 1, 2 }, childComponentIds);
Assert.IsType<FakeComponent>(childComponent3);
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
// Act/Assert: If a disposed component requests a render, it's a no-op
var renderHandle = ((FakeComponent)childComponent3).RenderHandle;
renderHandle.Dispatcher.InvokeAsync(() => renderHandle.Render(builder
=> throw new NotImplementedException("Should not be invoked")));
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public void RenderBatch_HandlesExceptionsFromAllDisposedComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new Exception();
var exception2 = new Exception();
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<DisposableComponent>(1);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1));
builder.CloseComponent();
builder.OpenComponent<DisposableComponent>(2);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception2));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, 2 }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
Assert.Contains(exception1, aex.InnerExceptions);
Assert.Contains(exception2, aex.InnerExceptions);
}
[Fact]
public void RenderBatch_HandlesSynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new InvalidOperationException();
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw exception1));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.Single(renderer.HandledExceptions);
Assert.Same(exception1, aex);
}
[Fact]
public void RenderBatch_CanDisposeSynchronousAsyncDisposableImplementations()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => default));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_CanDisposeAsynchronousAsyncDisposables()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer
{
ShouldHandleExceptions = true,
OnExceptionHandled = () => semaphore.Release()
};
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
Assert.False(semaphore.WaitOne(10));
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_HandlesAsynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer
{
ShouldHandleExceptions = true,
OnExceptionHandled = () => semaphore.Release()
};
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; throw exception1; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
semaphore.WaitOne();
var aex = Assert.IsType<InvalidOperationException>(Assert.Single(renderer.HandledExceptions));
Assert.Same(exception1, aex);
}
[Fact]
public void RenderBatch_ReportsSynchronousCancelationsAsErrors()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw new TaskCanceledException()));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.IsType<TaskCanceledException>(Assert.Single(renderer.HandledExceptions));
}
[Fact]
public void RenderBatch_ReportsAsynchronousCancelationsAsErrors()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled += () => semaphore.Release();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(
1,
nameof(AsyncDisposableComponent.AsyncDisposeAction),
(Func<ValueTask>)(() => new ValueTask(tcs.Task)));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Cancel execution
tcs.SetCanceled();
semaphore.WaitOne();
var aex = Assert.IsType<TaskCanceledException>(Assert.Single(renderer.HandledExceptions));
}
[Fact]
public void RenderBatch_DoesNotDisposeComponentMultipleTimes()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new Exception();
var exception2 = new Exception();
var count1 = 0;
var count2 = 0;
var count3 = 0;
var count4 = 0;
var count5 = 0;
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<DisposableComponent>(1);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count1++; }));
builder.CloseComponent();
builder.OpenComponent<DisposableComponent>(2);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count2++; throw exception1; }));
builder.CloseComponent();
builder.OpenComponent<DisposableComponent>(3);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count3++; }));
builder.CloseComponent();
}
builder.OpenComponent<DisposableComponent>(4);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count4++; throw exception2; }));
builder.CloseComponent();
builder.OpenComponent<DisposableComponent>(5);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count5++; }));
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, 2, 3 }, renderer.Batches[1].DisposedComponentIDs);
// Components "disposed" in the batch were all disposed, components that are still live were not disposed
Assert.Equal(1, count1);
Assert.Equal(1, count2);
Assert.Equal(1, count3);
Assert.Equal(0, count4);
Assert.Equal(0, count5);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var ex = Assert.IsType<Exception>(Assert.Single(renderer.HandledExceptions));
Assert.Same(exception1, ex);
// Act: Dispose renderer
renderer.Dispose();
Assert.Equal(2, renderer.HandledExceptions.Count);
ex = renderer.HandledExceptions[1];
Assert.Same(exception2, ex);
// Assert: Everything was disposed once.
Assert.Equal(1, count1);
Assert.Equal(1, count2);
Assert.Equal(1, count3);
Assert.Equal(1, count4);
Assert.Equal(1, count5);
Assert.True(component.Disposed);
}
[Fact]
public async Task DoesNotDispatchEventsAfterOwnerComponentIsDisposed()
{
// Arrange
var renderer = new TestRenderer();
var eventCount = 0;
Action<EventArgs> origEventHandler = args => { eventCount++; };
var component = new ConditionalParentComponent<EventComponent>
{
IncludeChild = true,
ChildParameters = new Dictionary<string, object>
{
{ nameof(EventComponent.OnTest), origEventHandler }
}
};
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
var rootComponentFrame = batch.ReferenceFrames[0];
var childComponentFrame = rootComponentDiff.Edits
.Select(e => batch.ReferenceFrames[e.ReferenceFrameIndex])
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Single();
var childComponentId = childComponentFrame.ComponentId;
var childComponentDiff = batch.DiffsByComponentId[childComponentFrame.ComponentId].Single();
var eventHandlerId = batch.ReferenceFrames
.Skip(childComponentDiff.Edits[0].ReferenceFrameIndex) // Search from where the child component frames start
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
var renderTask = renderer.DispatchEventAsync(eventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
await renderTask;
// Now remove the EventComponent, but without ever acknowledging the renderbatch, so the event handler doesn't get disposed
var disposalBatchAcknowledgementTcs = new TaskCompletionSource();
component.IncludeChild = false;
renderer.NextRenderResultTask = disposalBatchAcknowledgementTcs.Task;
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event. It's not an error but the delegate was not invoked.
await renderer.DispatchEventAsync(eventHandlerId, args: null);
Assert.Equal(1, eventCount);
}
[Fact]
public async Task DisposesEventHandlersWhenAttributeValueChanged()
{
// Arrange
var renderer = new TestRenderer();
var eventCount = 0;
Action<EventArgs> origEventHandler = args => { eventCount++; };
var component = new EventComponent { OnTest = origEventHandler };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
await renderTask;
// Now change the attribute value
var newEventCount = 0;
component.OnTest = args => { newEventCount++; };
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event, but can fire the new event
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
Assert.Equal(0, newEventCount);
renderTask = renderer.DispatchEventAsync(origEventHandlerId + 1, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, newEventCount);
await renderTask;
}
[Fact]
public async Task DisposesEventHandlersWhenAttributeRemoved()
{
// Arrange
var renderer = new TestRenderer();
var eventCount = 0;
Action<EventArgs> origEventHandler = args => { eventCount++; };
var component = new EventComponent { OnTest = origEventHandler };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
await renderTask;
// Now remove the event attribute
component.OnTest = null;
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
[Fact]
public async Task DisposesEventHandlersWhenOwnerComponentRemoved()
{
// Arrange
var renderer = new TestRenderer();
var eventCount = 0;
Action<EventArgs> origEventHandler = args => { eventCount++; };
var component = new ConditionalParentComponent<EventComponent>
{
IncludeChild = true,
ChildParameters = new Dictionary<string, object>
{
{ nameof(EventComponent.OnTest), origEventHandler }
}
};
var rootComponentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
var rootComponentFrame = batch.ReferenceFrames[0];
var childComponentFrame = rootComponentDiff.Edits
.Select(e => batch.ReferenceFrames[e.ReferenceFrameIndex])
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Single();
var childComponentId = childComponentFrame.ComponentId;
var childComponentDiff = batch.DiffsByComponentId[childComponentFrame.ComponentId].Single();
var eventHandlerId = batch.ReferenceFrames
.Skip(childComponentDiff.Edits[0].ReferenceFrameIndex) // Search from where the child component frames start
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
var renderTask = renderer.DispatchEventAsync(eventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
await renderTask;
// Now remove the EventComponent
component.IncludeChild = false;
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(eventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
[Fact]
public async Task DisposesEventHandlersWhenAncestorElementRemoved()
{
// Arrange
var renderer = new TestRenderer();
var eventCount = 0;
Action<EventArgs> origEventHandler = args => { eventCount++; };
var component = new EventComponent { OnTest = origEventHandler };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
await renderTask;
// Now remove the ancestor element
component.SkipElement = true;
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
[Fact]
public async Task AllRendersTriggeredSynchronouslyDuringEventHandlerAreHandledAsSingleBatch()
{
// Arrange: A root component with a child whose event handler explicitly queues
// a re-render of both the root component and the child
var renderer = new TestRenderer();
var eventCount = 0;
TestComponent rootComponent = null;
EventComponent childComponent = null;
rootComponent = new TestComponent(builder =>
{
builder.AddContent(0, "Child event count: " + eventCount);
builder.OpenComponent<EventComponent>(1);
builder.AddComponentParameter(2, nameof(EventComponent.OnTest), new Action<EventArgs>(args =>
{
eventCount++;
rootComponent.TriggerRender();
childComponent.TriggerRender();
}));
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var origBatchReferenceFrames = renderer.Batches.Single().ReferenceFrames;
var childComponentFrame = origBatchReferenceFrames
.Single(f => f.Component is EventComponent);
var childComponentId = childComponentFrame.ComponentId;
childComponent = (EventComponent)childComponentFrame.Component;
var origEventHandlerId = origBatchReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Last(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
Assert.Single(renderer.Batches);
// Act
var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null);
// Assert
Assert.True(renderTask.IsCompletedSuccessfully);
await renderTask;
Assert.Equal(2, renderer.Batches.Count);
var batch = renderer.Batches.Last();
Assert.Collection(batch.DiffsInOrder,
diff =>
{
// First we triggered the root component to re-render
Assert.Equal(rootComponentId, diff.ComponentId);
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Child event count: 1");
});
},
diff =>
{
// Then the root re-render will have triggered an update to the child
Assert.Equal(childComponentId, diff.ComponentId);
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Render count: 2");
});
},
diff =>
{
// Finally we explicitly requested a re-render of the child
Assert.Equal(childComponentId, diff.ComponentId);
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Render count: 3");
});
});
}
[Fact]
public void ComponentCannotTriggerRenderBeforeRenderHandleAssigned()
{
// Arrange
var component = new TestComponent(builder => { });
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(component.TriggerRender);
Assert.Equal("The render handle is not yet assigned.", ex.Message);
}
[Fact]
public void ComponentCanTriggerRenderWhenNoBatchIsInProgress()
{
// Arrange
var renderer = new TestRenderer();
var renderCount = 0;
var component = new TestComponent(builder =>
{
builder.AddContent(0, $"Render count: {++renderCount}");
});
var componentId = renderer.AssignRootComponentId(component);
// Act/Assert: Can trigger initial render
Assert.Equal(0, renderCount);
component.TriggerRender();
Assert.Equal(1, renderCount);
var batch1 = renderer.Batches.Single();
var edit1 = batch1.DiffsByComponentId[componentId].Single().Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, edit1.Type);
AssertFrame.Text(batch1.ReferenceFrames[edit1.ReferenceFrameIndex],
"Render count: 1", 0);
// Act/Assert: Can trigger subsequent render
component.TriggerRender();
Assert.Equal(2, renderCount);
var batch2 = renderer.Batches.Skip(1).Single();
var edit2 = batch2.DiffsByComponentId[componentId].Single().Edits.Single();
Assert.Equal(RenderTreeEditType.UpdateText, edit2.Type);
AssertFrame.Text(batch2.ReferenceFrames[edit2.ReferenceFrameIndex],
"Render count: 2", 0);
}
[Fact]
public void ComponentCanTriggerRenderWhenExistingBatchIsInProgress()
{
// Arrange
var renderer = new TestRenderer();
TestComponent parent = null;
var parentRenderCount = 0;
parent = new TestComponent(builder =>
{
builder.OpenComponent<ReRendersParentComponent>(0);
builder.AddComponentParameter(1, nameof(ReRendersParentComponent.Parent), parent);
builder.CloseComponent();
builder.AddContent(2, $"Parent render count: {++parentRenderCount}");
});
var parentComponentId = renderer.AssignRootComponentId(parent);
// Act
parent.TriggerRender();
// Assert
var batch = renderer.Batches.Single();
Assert.Equal(4, batch.DiffsInOrder.Count);
// First is the parent component's initial render
var diff1 = batch.DiffsInOrder[0];
Assert.Equal(parentComponentId, diff1.ComponentId);
Assert.Collection(diff1.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<ReRendersParentComponent>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Parent render count: 1");
});
// Second is the child component's single render
var diff2 = batch.DiffsInOrder[1];
Assert.NotEqual(parentComponentId, diff2.ComponentId);
var diff2edit = diff2.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, diff2edit.Type);
AssertFrame.Text(batch.ReferenceFrames[diff2edit.ReferenceFrameIndex],
"Child is here");
// Third is the parent's triggered render
var diff3 = batch.DiffsInOrder[2];
Assert.Equal(parentComponentId, diff3.ComponentId);
var diff3edit = diff3.Edits.Single();
Assert.Equal(RenderTreeEditType.UpdateText, diff3edit.Type);
AssertFrame.Text(batch.ReferenceFrames[diff3edit.ReferenceFrameIndex],
"Parent render count: 2");
// Fourth is child's rerender due to parent rendering
var diff4 = batch.DiffsInOrder[3];
Assert.NotEqual(parentComponentId, diff4.ComponentId);
Assert.Empty(diff4.Edits);
}
[Fact]
public void QueuedRenderIsSkippedIfComponentWasAlreadyDisposedInSameBatch()
{
// Arrange
var renderer = new TestRenderer();
var shouldRenderChild = true;
TestComponent component = null;
component = new TestComponent(builder =>
{
builder.AddContent(0, "Some frame so the child isn't at position zero");
if (shouldRenderChild)
{
builder.OpenComponent<RendersSelfAfterEventComponent>(1);
builder.AddComponentParameter(2, "onclick", (Action<object>)((object obj) =>
{
// First we queue (1) a re-render of the root component, then the child component
// will queue (2) its own re-render. But by the time (1) completes, the child will
// have been disposed, even though (2) is still in the queue
shouldRenderChild = false;
component.TriggerRender();
}));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var childComponentId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.ComponentId != 0)
.Single()
.ComponentId;
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmycustomevent")
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act
// The fact that there's no error here is the main thing we're testing
var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null);
// Assert: correct render result
Assert.True(renderTask.IsCompletedSuccessfully);
var newBatch = renderer.Batches.Skip(1).Single();
Assert.Single(newBatch.DisposedComponentIDs);
Assert.Single(newBatch.DiffsByComponentId);
Assert.Collection(newBatch.DiffsByComponentId[componentId].Single().Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
});
}
[Fact]
public async Task CanCombineBindAndConditionalAttribute()
{
// This test represents https://github.com/dotnet/blazor/issues/624
// Arrange: Rendered with textbox enabled
var renderer = new TestRenderer();
var component = new BindPlusConditionalAttributeComponent();
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var checkboxChangeEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act: Toggle the checkbox
var eventArgs = new ChangeEventArgs { Value = true };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
var latestBatch = renderer.Batches.Last();
var latestDiff = latestBatch.DiffsInOrder.Single();
var referenceFrames = latestBatch.ReferenceFrames;
// Assert: Textbox's "disabled" attribute was removed
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(componentId, latestDiff.ComponentId);
Assert.Contains(latestDiff.Edits, edit =>
edit.SiblingIndex == 1
&& edit.RemovedAttributeName == "disabled");
await renderTask;
}
[Fact]
public async Task BindWithSynchronousSetter_Lambda()
{
// Arrange
var renderer = new TestRenderer();
var component = new OuterEventComponent();
var value = "value";
component.RenderFragment = (builder) =>
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder(
component,
RuntimeHelpers.CreateInferredBindSetter(__value => value = __value, value),
value));
builder.CloseElement();
};
var componentId = renderer.AssignRootComponentId(component);
await component.TriggerRenderAsync();
var checkboxChangeEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act: Trigger change event
var eventArgs = new ChangeEventArgs { Value = "hello" };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
await renderTask;
// Assert
Assert.Equal("hello", value);
}
[Fact]
public async Task BindWithAsynchronousSetter_MethodGroupToDelegate()
{
// This test represents https://github.com/dotnet/blazor/issues/624
// Arrange: Rendered with textbox enabled
var renderer = new TestRenderer();
var component = new OuterEventComponent();
var value = "value";
async Task SetValue(string __value)
{
value = __value;
await Task.CompletedTask;
}
component.RenderFragment = (builder) =>
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder(
component,
RuntimeHelpers.CreateInferredBindSetter(SetValue, value),
value));
builder.CloseElement();
};
var componentId = renderer.AssignRootComponentId(component);
await component.TriggerRenderAsync();
var checkboxChangeEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act: Trigger change event
var eventArgs = new ChangeEventArgs { Value = "hello" };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
await renderTask;
// Assert
Assert.Equal("hello", value);
}
[Fact]
public async Task BindWithAfter()
{
// This test represents https://github.com/dotnet/blazor/issues/624
// Arrange: Rendered with textbox enabled
var renderer = new TestRenderer();
var component = new OuterEventComponent();
string value = "value";
component.RenderFragment = (builder) =>
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder(
component,
RuntimeHelpers.CreateInferredBindSetter(
__value =>
{
value = __value;
return RuntimeHelpers.InvokeAsynchronousDelegate(() => Task.CompletedTask);
},
value),
value));
builder.CloseElement();
};
var componentId = renderer.AssignRootComponentId(component);
await component.TriggerRenderAsync();
var checkboxChangeEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act: Trigger change event
var eventArgs = new ChangeEventArgs { Value = "hello" };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
await renderTask;
// Assert
Assert.Equal("hello", value);
}
[Fact]
public async Task BindWithAfter_Action()
{
// This test represents https://github.com/dotnet/blazor/issues/624
// Arrange: Rendered with textbox enabled
var renderer = new TestRenderer();
var component = new OuterEventComponent();
string value = "value";
component.RenderFragment = (builder) =>
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "onchange", EventCallback.Factory.CreateBinder(
component,
RuntimeHelpers.CreateInferredBindSetter(
__value =>
{
value = __value;
return RuntimeHelpers.InvokeAsynchronousDelegate(() => { });
},
value),
value));
builder.CloseElement();
};
var componentId = renderer.AssignRootComponentId(component);
await component.TriggerRenderAsync();
var checkboxChangeEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
// Act: Trigger change event
var eventArgs = new ChangeEventArgs { Value = "hello" };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
await renderTask;
// Assert
Assert.Equal("hello", value);
}
[Fact]
public void HandlesNestedElementCapturesDuringRefresh()
{
// This may seem like a very arbitrary test case, but at once stage there was a bug
// whereby the diff output was incorrect given a ref capture on an element whose
// parent element also had a ref capture
// Arrange
var attrValue = 0;
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "parent elem");
builder.AddAttribute(1, "parent elem attr", attrValue);
builder.AddElementReferenceCapture(2, _ => { });
builder.OpenElement(3, "child elem");
builder.AddElementReferenceCapture(4, _ => { });
builder.AddContent(5, "child text");
builder.CloseElement();
builder.CloseElement();
});
var renderer = new TestRenderer();
renderer.AssignRootComponentId(component);
// Act: Update the attribute value on the parent
component.TriggerRender();
attrValue++;
component.TriggerRender();
// Assert
var latestBatch = renderer.Batches.Skip(1).Single();
var latestDiff = latestBatch.DiffsInOrder.Single();
Assert.Collection(latestDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Attribute(latestBatch.ReferenceFrames[edit.ReferenceFrameIndex],
"parent elem attr", 1);
});
}
[Fact]
public void CallsAfterRenderOnEachRender()
{
// Arrange
var onAfterRenderCallCountLog = new List<int>();
var component = new AfterRenderCaptureComponent();
var renderer = new TestRenderer
{
OnUpdateDisplay = _ => onAfterRenderCallCountLog.Add(component.OnAfterRenderCallCount)
};
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
// Assert
// When the display was first updated, OnAfterRender had not yet been called
Assert.Equal(new[] { 0 }, onAfterRenderCallCountLog);
// But OnAfterRender was called since then
Assert.Equal(1, component.OnAfterRenderCallCount);
// Act/Assert 2: On a subsequent render, the same happens again
component.TriggerRender();
Assert.Equal(new[] { 0, 1 }, onAfterRenderCallCountLog);
Assert.Equal(2, component.OnAfterRenderCallCount);
}
[Fact]
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously()
{
// Arrange
var @event = new ManualResetEventSlim();
var tcs = new TaskCompletionSource();
var afterRenderTcs = new TaskCompletionSource();
var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task)
{
OnAfterRenderComplete = @event.Set,
};
var renderer = new AsyncUpdateTestRenderer()
{
OnUpdateDisplayAsync = _ => tcs.Task,
};
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
tcs.SetResult();
afterRenderTcs.SetResult();
// We need to wait here because the completions from SetResult will be scheduled.
@event.Wait(Timeout);
// Assert
Assert.True(component.Called);
}
[Fact]
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingSynchronously()
{
// Arrange
var @event = new ManualResetEventSlim();
var afterRenderTcs = new TaskCompletionSource();
var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task)
{
OnAfterRenderComplete = @event.Set,
};
var renderer = new AsyncUpdateTestRenderer()
{
OnUpdateDisplayAsync = _ => Task.CompletedTask
};
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
afterRenderTcs.SetResult();
// We need to wait here because the completions from SetResult will be scheduled.
@event.Wait(Timeout);
// Assert
Assert.True(component.Called);
}
[Fact]
public void DoesNotCallOnAfterRenderForComponentsNotRendered()
{
// Arrange
var showComponent3 = true;
var parentComponent = new TestComponent(builder =>
{
// First child will be re-rendered because we'll change its param
builder.OpenComponent<AfterRenderCaptureComponent>(0);
builder.AddComponentParameter(1, "some param", showComponent3);
builder.CloseComponent();
// Second child will not be re-rendered because nothing changes
builder.OpenComponent<AfterRenderCaptureComponent>(2);
builder.CloseComponent();
// Third component will be disposed
if (showComponent3)
{
builder.OpenComponent<AfterRenderCaptureComponent>(3);
builder.CloseComponent();
}
});
var renderer = new TestRenderer();
var parentComponentId = renderer.AssignRootComponentId(parentComponent);
// Act: First render
parentComponent.TriggerRender();
// Assert: All child components were notified of "after render"
var batch1 = renderer.Batches.Single();
var parentComponentEdits1 = batch1.DiffsByComponentId[parentComponentId].Single().Edits;
var childComponents = parentComponentEdits1
.Select(
edit => (AfterRenderCaptureComponent)batch1.ReferenceFrames[edit.ReferenceFrameIndex].Component)
.ToArray();
Assert.Equal(1, childComponents[0].OnAfterRenderCallCount);
Assert.Equal(1, childComponents[1].OnAfterRenderCallCount);
Assert.Equal(1, childComponents[2].OnAfterRenderCallCount);
// Act: Second render
showComponent3 = false;
parentComponent.TriggerRender();
// Assert: Only the re-rendered component was notified of "after render"
var batch2 = renderer.Batches.Skip(1).Single();
Assert.Equal(2, batch2.DiffsInOrder.Count); // Parent and first child
Assert.Single(batch2.DisposedComponentIDs); // Third child
Assert.Equal(2, childComponents[0].OnAfterRenderCallCount); // Retained and re-rendered
Assert.Equal(1, childComponents[1].OnAfterRenderCallCount); // Retained and not re-rendered
Assert.Equal(1, childComponents[2].OnAfterRenderCallCount); // Disposed
}
[Fact]
public void CanTriggerRenderingSynchronouslyFromInsideAfterRenderCallback()
{
// Arrange
AfterRenderCaptureComponent component = null;
component = new AfterRenderCaptureComponent
{
OnAfterRenderLogic = () =>
{
if (component.OnAfterRenderCallCount < 10)
{
component.TriggerRender();
}
}
};
var renderer = new TestRenderer();
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
// Assert
Assert.Equal(10, component.OnAfterRenderCallCount);
}
[Fact]
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
{
// This represents the scenario where the same event handler is being triggered
// rapidly, such as an input event while typing. It only applies to asynchronous
// batch updates, i.e., server-side Components.
// Sequence:
// 1. The client dispatches event X twice (say) in quick succession
// 2. The server receives the first instance, handles the event, and re-renders
// some component. The act of re-rendering causes the old event handler to be
// replaced by a new one, so the old one is flagged to be disposed.
// 3. The server receives the second instance. Even though the corresponding event
// handler is flagged to be disposed, we have to still be able to find and
// execute it without errors.
// Arrange
var renderer = new TestAsyncRenderer
{
NextUpdateDisplayReturnTask = Task.CompletedTask
};
var numEventsFired = 0;
EventComponent component = null;
Action<EventArgs> eventHandler = null;
eventHandler = _ =>
{
numEventsFired++;
// Replace the old event handler with a different one,
// (old the old handler ID will be disposed) then re-render.
component.OnTest = args => eventHandler(args);
component.TriggerRender();
};
component = new EventComponent { OnTest = eventHandler };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Act/Assert 1: Event can be fired for the first time
var render1TCS = new TaskCompletionSource();
renderer.NextUpdateDisplayReturnTask = render1TCS.Task;
await renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
Assert.Equal(1, numEventsFired);
// Act/Assert 2: *Same* event handler ID can be reused prior to completion of
// preceding UI update
var render2TCS = new TaskCompletionSource();
renderer.NextUpdateDisplayReturnTask = render2TCS.Task;
await renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
Assert.Equal(2, numEventsFired);
// Act/Assert 3: After we complete the first UI update in which a given
// event handler ID is disposed, we can no longer reuse that event handler ID
// From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough).
// There is a small chance in which the continuations from TaskCompletionSource run asynchronously.
// In that case we might not be able to see the results from RemoveEventHandlerIds as they might run asynchronously.
// For that case, we are going to queue a continuation on render1TCS.Task, include a 1s delay and await the resulting
// task to offer the best chance that we get to see the error in all cases.
var awaitableTask = render1TCS.Task.ContinueWith(_ => Task.Delay(1000)).Unwrap();
render1TCS.SetResult();
await awaitableTask;
var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
{
return renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
});
Assert.Contains($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", ex.Message);
Assert.Equal(2, numEventsFired);
}
[Fact]
public void ExceptionsThrownSynchronouslyCanBeHandledSynchronously()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var task = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => throw exception,
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(task.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsThrownSynchronouslyCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => throw exception,
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsReturnedUsingTaskFromExceptionCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => Task.FromException<(int, NestedAsyncComponent.EventType)>(exception),
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public async Task ExceptionsThrownAsynchronouslyDuringFirstRenderCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception;
}
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.False(renderTask.IsCompleted);
tcs.SetResult();
await renderTask;
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionsDispatchedOffSyncContextCanBeHandledAsync()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException("Error from outside the sync context.");
// Act
renderer.AssignRootComponentId(component);
await component.ExternalExceptionDispatch(exception);
// Assert
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionsThrownAsynchronouslyAfterFirstRenderCanBeHandled()
{
// This differs from the "during first render" case, because some aspects of the rendering
// code paths are special cased for the first render because of prerendering.
// Arrange
var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
ShouldHandleExceptions = true,
OnExceptionHandled = @event.Set,
};
var taskToAwait = Task.CompletedTask;
var component = new TestComponent(builder =>
{
builder.OpenComponent<ComponentThatAwaitsTask>(0);
builder.AddComponentParameter(1, nameof(ComponentThatAwaitsTask.TaskToAwait), taskToAwait);
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
await renderer.RenderRootComponentAsync(componentId); // Not throwing on first render
var asyncExceptionTcs = new TaskCompletionSource();
taskToAwait = asyncExceptionTcs.Task;
await renderer.Dispatcher.InvokeAsync(component.TriggerRender);
// Act
var exception = new InvalidOperationException();
@event.Reset();
asyncExceptionTcs.SetException(exception);
// We need to wait here because the continuations of SetException will be scheduled to run asynchronously.
@event.Wait(Timeout);
// Assert
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionsThrownAsynchronouslyFromMultipleComponentsCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception1 = new InvalidTimeZoneException();
var exception2 = new UriFormatException();
var tcs = new TaskCompletionSource();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception1;
}
},
},
[2] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception2;
}
},
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1, 2, }),
[1] = CreateRenderFactory(Array.Empty<int>()),
[2] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.False(renderTask.IsCompleted);
tcs.SetResult();
await renderTask;
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Contains(exception1, renderer.HandledExceptions);
Assert.Contains(exception2, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsThrownSynchronouslyFromMultipleComponentsCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception1 = new InvalidTimeZoneException();
var exception2 = new UriFormatException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () =>
{
throw exception1;
}
},
},
[2] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () =>
{
throw exception2;
}
},
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1, 2, }),
[1] = CreateRenderFactory(Array.Empty<int>()),
[2] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Contains(exception1, renderer.HandledExceptions);
Assert.Contains(exception2, renderer.HandledExceptions);
}
[Fact]
public async Task ExceptionsThrownFromHandleAfterRender_Sync_AreHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
var taskCompletionSource = new TaskCompletionSource();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
EventAction = () =>
{
throw exception;
},
}
},
[1] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
EventAction = () =>
{
taskCompletionSource.TrySetResult();
return Task.FromResult((1, NestedAsyncComponent.EventType.OnAfterRenderAsyncSync));
},
}
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
// OnAfterRenderAsync happens in the background. Make it more predictable, by gating it until we're ready to capture exceptions.
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionsThrownFromHandleAfterRender_Async_AreHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
var taskCompletionSource = new TaskCompletionSource();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
EventAction = async () =>
{
await Task.Yield();
throw exception;
},
}
},
[1] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
EventAction = async () =>
{
await Task.Yield();
taskCompletionSource.TrySetResult();
return (1, NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync);
},
}
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
// OnAfterRenderAsync happens in the background. Make it more predictable, by gating it until we're ready to capture exceptions.
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionThrownFromConstructor()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new TestComponent(builder =>
{
builder.OpenComponent<ConstructorThrowingComponent>(0);
builder.CloseComponent();
});
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId);
await renderTask;
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(ConstructorThrowingComponent.Exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
private class ConstructorThrowingComponent : IComponent
{
public static readonly Exception Exception = new InvalidTimeZoneException();
public ConstructorThrowingComponent()
{
throw Exception;
}
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
[Fact]
public async Task ExceptionThrownFromAttach()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new TestComponent(builder =>
{
builder.OpenComponent<AttachThrowingComponent>(0);
builder.CloseComponent();
});
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId);
await renderTask;
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(AttachThrowingComponent.Exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
private class AttachThrowingComponent : IComponent
{
public static readonly Exception Exception = new InvalidTimeZoneException();
public void Attach(RenderHandle renderHandle)
{
throw Exception;
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
[Fact]
public void SynchronousCancelledTasks_HandleAfterRender_Works()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
tcs.TrySetCanceled();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
EventAction = () => tcs.Task,
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
// Rendering should finish synchronously
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void AsynchronousCancelledTasks_HandleAfterRender_Works()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
EventAction = () => tcs.Task,
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
// Rendering should be complete.
Assert.True(renderTask.IsCompletedSuccessfully);
tcs.TrySetCanceled();
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public async Task CanceledTasksInHandleAfterRender_AreIgnored()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var taskCompletionSource = new TaskCompletionSource();
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
await renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
EventAction = () =>
{
taskCompletionSource.TrySetResult();
cancellationTokenSource.Token.ThrowIfCancellationRequested();
return default;
},
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void DisposingRenderer_DisposesTopLevelComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new DisposableComponent();
renderer.AssignRootComponentId(component);
// Act
renderer.Dispose();
// Assert
Assert.True(component.Disposed);
}
[Fact]
public void DisposingRenderer_DisregardsAttemptsToStartMoreRenderBatches()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "my element");
builder.AddContent(1, "some text");
builder.CloseElement();
});
// Act
renderer.AssignRootComponentId(component);
renderer.Dispose();
component.TriggerRender();
// Assert
Assert.Empty(renderer.Batches);
}
[Fact]
public void WhenRendererIsDisposed_ComponentRenderRequestsAreSkipped()
{
// The important point of this is that user code in components may continue to call
// StateHasChanged (e.g., after an async task completion), and we don't want that to
// show up as an error. In general, components should skip rendering after disposal.
// This test shows that we don't add any new entries to the render queue after disposal.
// There's a different test showing that if the render queue entry was already added
// before a component got individually disposed, that render queue entry gets skipped.
// Arrange
var renderer = new TestRenderer();
var component = new DisposableComponent();
renderer.AssignRootComponentId(component);
// Act
renderer.Dispose();
component.TriggerRender();
// Assert: no exception, no batch produced
Assert.Empty(renderer.Batches);
}
[Fact]
public void DisposingRenderer_DisposesNestedComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<DisposableComponent>(1);
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var componentFrame = batch.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = Assert.IsType<DisposableComponent>(componentFrame.Component);
// Act
renderer.Dispose();
// Assert
Assert.True(component.Disposed);
Assert.True(nestedComponent.Disposed);
}
[Fact]
public void DisposingRenderer_CapturesExceptionsFromAllRegisteredComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new Exception();
var exception2 = new Exception();
var component = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<DisposableComponent>(1);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1));
builder.CloseComponent();
builder.OpenComponent<DisposableComponent>(2);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception2));
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act &A Assert
renderer.Dispose();
// All components must be disposed even if some throw as part of being disposed.
Assert.True(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
Assert.Contains(exception1, aex.InnerExceptions);
Assert.Contains(exception2, aex.InnerExceptions);
}
[Fact]
public async Task DisposingRenderer_CapturesSyncExceptionsFromAllRegisteredAsyncDisposableComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new InvalidOperationException();
var disposed = false;
var component = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => { disposed = true; throw exception1; }));
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act
await renderer.DisposeAsync();
// Assert
Assert.True(disposed);
var handledException = Assert.Single(renderer.HandledExceptions);
Assert.Same(exception1, handledException);
}
[Fact]
public async Task DisposingRenderer_CapturesAsyncExceptionsFromAllRegisteredAsyncDisposableComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new InvalidOperationException();
var disposed = false;
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var component = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; disposed = true; throw exception1; }));
builder.CloseComponent();
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act
var disposal = renderer.DisposeAsync();
Assert.False(disposed);
Assert.False(disposal.IsCompleted);
tcs.TrySetResult();
await disposal;
// Assert
Assert.True(disposed);
var handledException = Assert.Single(renderer.HandledExceptions);
Assert.Same(exception1, handledException);
}
[Theory]
[InlineData(null)] // No existing attribute to update
[InlineData("old property value")] // Has existing attribute to update
public void EventFieldInfoCanPatchTreeSoDiffDoesNotUpdateAttribute(string oldValue)
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
var component = new BoundPropertyComponent { BoundString = oldValue };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
.AttributeEventHandlerId;
// Act: Fire event and re-render
var eventFieldInfo = new EventFieldInfo
{
FieldValue = "new property value",
ComponentId = componentId
};
var dispatchEventTask = renderer.DispatchEventAsync(eventHandlerId, eventFieldInfo, new ChangeEventArgs
{
Value = "new property value"
});
Assert.True(dispatchEventTask.IsCompletedSuccessfully);
// Assert: Property was updated, but the diff doesn't include changing the
// element attribute, since we told it the element attribute was already updated
Assert.Equal("new property value", component.BoundString);
Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1];
Assert.Collection(batch2.DiffsInOrder.Single().Edits.ToArray(), edit =>
{
// The only edit is updating the event handler ID, since the test component
// deliberately uses a capturing lambda. The whole point of this test is to
// show that the diff does *not* update the BoundString value attribute.
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
var attributeFrame = batch2.ReferenceFrames[edit.ReferenceFrameIndex];
AssertFrame.Attribute(attributeFrame, "ontestevent", typeof(Action<ChangeEventArgs>));
Assert.NotEqual(default, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(eventHandlerId, attributeFrame.AttributeEventHandlerId);
});
}
[Fact]
public void EventFieldInfoWorksWhenEventHandlerIdWasSuperseded()
{
// Arrange: Render a component with an event handler
// We want the renderer to think none of the "UpdateDisplay" calls ever complete, because we
// want to keep reusing the same eventHandlerId and not let it get disposed
var renderCompletedTcs = new TaskCompletionSource();
var renderer = new TestRenderer { NextRenderResultTask = renderCompletedTcs.Task };
var component = new BoundPropertyComponent { BoundString = "old property value" };
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
.AttributeEventHandlerId;
// Act: Fire event and re-render *repeatedly*, without changing to use a newer event handler ID,
// even though we know the event handler ID is getting updated in successive diffs
for (var i = 0; i < 10; i++)
{
var newPropertyValue = $"new property value {i}";
var fieldInfo = new EventFieldInfo
{
ComponentId = componentId,
FieldValue = newPropertyValue,
};
var dispatchEventTask = renderer.DispatchEventAsync(eventHandlerId, fieldInfo, new ChangeEventArgs
{
Value = newPropertyValue
});
Assert.True(dispatchEventTask.IsCompletedSuccessfully);
// Assert: Property was updated, but the diff doesn't include changing the
// element attribute, since we told it the element attribute was already updated
Assert.Equal(newPropertyValue, component.BoundString);
Assert.Equal(i + 2, renderer.Batches.Count);
var latestBatch = renderer.Batches.Last();
Assert.Collection(latestBatch.DiffsInOrder.Single().Edits.ToArray(), edit =>
{
// The only edit is updating the event handler ID, since the test component
// deliberately uses a capturing lambda. The whole point of this test is to
// show that the diff does *not* update the BoundString value attribute.
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
var attributeFrame = latestBatch.ReferenceFrames[edit.ReferenceFrameIndex];
AssertFrame.Attribute(attributeFrame, "ontestevent", typeof(Action<ChangeEventArgs>));
Assert.NotEqual(default, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(eventHandlerId, attributeFrame.AttributeEventHandlerId);
});
}
}
[Fact]
public void CannotStartOverlappingBatches()
{
// Arrange
var renderer = new InvalidRecursiveRenderer();
var component = new CallbackOnRenderComponent(renderer.ProcessPendingRender);
var componentId = renderer.AssignRootComponentId(component);
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(
() => renderer.RenderRootComponent(componentId));
Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message);
}
[Fact]
public void CannotAccessParameterViewAfterSynchronousReturn()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = new TestComponent(builder =>
{
builder.OpenComponent<ParameterViewIllegalCapturingComponent>(0);
builder.AddComponentParameter(1, nameof(ParameterViewIllegalCapturingComponent.SomeParam), 0);
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
// Note that we're not waiting for the async render to complete, since we want to assert
// about the situation immediately after the component yields the thread
renderer.RenderRootComponentAsync(rootComponentId);
// Act/Assert
var capturingComponent = (ParameterViewIllegalCapturingComponent)renderer.GetCurrentRenderTreeFrames(rootComponentId).Array[0].Component;
var parameterView = capturingComponent.CapturedParameterView;
// All public APIs on capturingComponent should be electrified now
// Internal APIs don't have to be, because we won't call them at the wrong time
Assert.Throws<InvalidOperationException>(() => parameterView.GetEnumerator());
Assert.Throws<InvalidOperationException>(() => parameterView.GetValueOrDefault<object>("anything"));
Assert.Throws<InvalidOperationException>(() => parameterView.SetParameterProperties(new object()));
Assert.Throws<InvalidOperationException>(parameterView.ToDictionary);
var ex = Assert.Throws<InvalidOperationException>(() => parameterView.TryGetValue<object>("anything", out _));
// It's enough to assert about one of the messages
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
}
[Fact]
public async Task CanSetComponentParameter_WhenParameterTypeHasImplicitConversionToString()
{
// Arrange
var renderer = new TestRenderer();
var parameterValue = new ImplicitlyConvertsToString("Hello");
var rootComponent = new TestComponent(builder =>
{
builder.OpenComponent<ImplicitConversionComponent>(0);
builder.AddComponentParameter(1, nameof(ImplicitConversionComponent.SomeParam), parameterValue);
builder.CloseComponent();
});
// Act
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
await renderer.RenderRootComponentAsync(rootComponentId);
var capturingComponent = (ImplicitConversionComponent)renderer.GetCurrentRenderTreeFrames(rootComponentId).Array[0].Component;
// Assert
Assert.Same(parameterValue, capturingComponent.SomeParam);
}
[Fact]
public void CanUseCustomComponentActivatorFromConstructorParameter()
{
// Arrange
var serviceProvider = new TestServiceProvider();
var componentActivator = new TestComponentActivator<MessageComponent>();
var renderer = new TestRenderer(serviceProvider, componentActivator);
// Act: Ask for TestComponent
var suppliedComponent = renderer.InstantiateComponent<TestComponent>();
// Assert: We actually receive MessageComponent
Assert.IsType<MessageComponent>(suppliedComponent);
Assert.Collection(componentActivator.RequestedComponentTypes,
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
}
[Fact]
public void CanUseCustomComponentActivatorFromServiceProvider()
{
// Arrange
var serviceProvider = new TestServiceProvider();
var componentActivator = new TestComponentActivator<MessageComponent>();
serviceProvider.AddService<IComponentActivator>(componentActivator);
var renderer = new TestRenderer(serviceProvider);
// Act: Ask for TestComponent
var suppliedComponent = renderer.InstantiateComponent<TestComponent>();
// Assert: We actually receive MessageComponent
Assert.IsType<MessageComponent>(suppliedComponent);
Assert.Collection(componentActivator.RequestedComponentTypes,
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
}
[Fact]
public async Task ThrowsIfComponentProducesInvalidRenderTree()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "myElem");
});
var rootComponentId = renderer.AssignRootComponentId(component);
// Act/Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderRootComponentAsync(rootComponentId));
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Element' was left unclosed.", ex.Message);
}
[Fact]
public void RenderingExceptionsCanBeHandledByClosestErrorBoundary()
{
// Arrange
var renderer = new TestRenderer();
var exception = new InvalidTimeZoneException("Error during render");
var rootComponentId = renderer.AssignRootComponentId(new TestComponent(builder =>
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringRender), exception);
builder.CloseComponent();
});
}));
// Act
renderer.RenderRootComponent(rootComponentId);
// Assert
var batch = renderer.Batches.Single();
var errorThrowingComponentId = batch.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
var componentFrames = batch.GetComponentFrames<TestErrorBoundary>();
Assert.Collection(componentFrames.Select(f => (TestErrorBoundary)f.Component),
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
// The failed subtree is disposed
Assert.Equal(errorThrowingComponentId, batch.DisposedComponentIDs.Single());
}
[Fact]
public void SetParametersAsyncExceptionsCanBeHandledByClosestErrorBoundary_Sync()
{
// Arrange
var renderer = new TestRenderer();
Exception exception = null;
var rootComponent = new TestComponent(builder =>
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringParameterSettingSync), exception);
builder.CloseComponent();
});
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
renderer.RenderRootComponent(rootComponentId);
var errorBoundaries = renderer.Batches.Single().GetComponentFrames<TestErrorBoundary>()
.Select(f => (TestErrorBoundary)f.Component);
var errorThrowingComponentId = renderer.Batches.Single()
.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
// Act
exception = new InvalidTimeZoneException("Error during SetParametersAsync");
rootComponent.TriggerRender();
// Assert
Assert.Equal(2, renderer.Batches.Count);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
// The failed subtree is disposed
Assert.Equal(errorThrowingComponentId, renderer.Batches[1].DisposedComponentIDs.Single());
}
[Fact]
public async Task SetParametersAsyncExceptionsCanBeHandledByClosestErrorBoundary_Async()
{
// Arrange
var renderer = new TestRenderer();
var exception = new InvalidTimeZoneException("Error during SetParametersAsync");
TaskCompletionSource exceptionTcs = null;
var rootComponent = new TestComponent(builder =>
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringParameterSettingAsync), exceptionTcs?.Task);
builder.CloseComponent();
});
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
renderer.RenderRootComponent(rootComponentId);
var errorBoundaries = renderer.Batches.Single().GetComponentFrames<TestErrorBoundary>()
.Select(f => (TestErrorBoundary)f.Component).ToArray();
var errorThrowingComponentId = renderer.Batches.Single()
.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
// Act/Assert 1: No synchronous errors
exceptionTcs = new TaskCompletionSource();
rootComponent.TriggerRender();
Assert.Equal(2, renderer.Batches.Count);
// Act/Assert 2: Asynchronous error
exceptionTcs.SetException(exception);
await errorBoundaries[1].ReceivedErrorTask;
Assert.Equal(3, renderer.Batches.Count);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
// The failed subtree is disposed
Assert.Equal(errorThrowingComponentId, renderer.Batches[2].DisposedComponentIDs.Single());
}
[Fact]
public void EventDispatchExceptionsCanBeHandledByClosestErrorBoundary_Sync()
{
// Arrange
var renderer = new TestRenderer();
var exception = new InvalidTimeZoneException("Error during event");
var rootComponentId = renderer.AssignRootComponentId(new TestComponent(builder =>
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringEventSync), exception);
builder.CloseComponent();
});
}));
renderer.RenderRootComponent(rootComponentId);
var errorBoundaries = renderer.Batches.Single().GetComponentFrames<TestErrorBoundary>()
.Select(f => (TestErrorBoundary)f.Component);
var errorThrowingComponentId = renderer.Batches.Single()
.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
var eventHandlerId = renderer.Batches.Single().ReferenceFrames
.Single(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmakeerror")
.AttributeEventHandlerId;
// Act
var task = renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
// Assert
Assert.True(task.IsCompletedSuccessfully);
Assert.Equal(2, renderer.Batches.Count);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
// The failed subtree is disposed
Assert.Equal(errorThrowingComponentId, renderer.Batches[1].DisposedComponentIDs.Single());
}
[Fact]
public async Task EventDispatchExceptionsCanBeHandledByClosestErrorBoundary_Async()
{
// Arrange
var renderer = new TestRenderer();
var exception = new InvalidTimeZoneException("Error during event");
var exceptionTcs = new TaskCompletionSource();
var rootComponentId = renderer.AssignRootComponentId(new TestComponent(builder =>
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringEventAsync), exceptionTcs.Task);
builder.CloseComponent();
});
}));
renderer.RenderRootComponent(rootComponentId);
var errorBoundaries = renderer.Batches.Single().GetComponentFrames<TestErrorBoundary>()
.Select(f => (TestErrorBoundary)f.Component);
var errorThrowingComponentId = renderer.Batches.Single()
.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
var eventHandlerId = renderer.Batches.Single().ReferenceFrames
.Single(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmakeerror")
.AttributeEventHandlerId;
// Act/Assert 1: No error synchronously
var dispatchEventTask = renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
Assert.Single(renderer.Batches);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Null(component.ReceivedException));
// Act/Assert 2: Error is handled asynchronously
exceptionTcs.SetException(exception);
await dispatchEventTask;
Assert.Equal(2, renderer.Batches.Count);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
// The failed subtree is disposed
Assert.Equal(errorThrowingComponentId, renderer.Batches[1].DisposedComponentIDs.Single());
}
[Fact]
public async Task EventDispatchExceptionsCanBeHandledByClosestErrorBoundary_AfterDisposal()
{
// Arrange
var renderer = new TestRenderer();
var disposeChildren = false;
var exception = new InvalidTimeZoneException("Error during event");
var exceptionTcs = new TaskCompletionSource();
var rootComponent = new TestComponent(builder =>
{
if (!disposeChildren)
{
TestErrorBoundary.RenderNestedErrorBoundaries(builder, builder =>
{
builder.OpenComponent<ErrorThrowingComponent>(0);
builder.AddComponentParameter(1, nameof(ErrorThrowingComponent.ThrowDuringEventAsync), exceptionTcs.Task);
builder.CloseComponent();
});
}
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
renderer.RenderRootComponent(rootComponentId);
var errorBoundaries = renderer.Batches.Single().GetComponentFrames<TestErrorBoundary>()
.Select(f => (TestErrorBoundary)f.Component);
var errorThrowingComponentId = renderer.Batches.Single()
.GetComponentFrames<ErrorThrowingComponent>().Single().ComponentId;
var eventHandlerId = renderer.Batches.Single().ReferenceFrames
.Single(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmakeerror")
.AttributeEventHandlerId;
// Act/Assert 1: No error synchronously
var dispatchEventTask = renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
Assert.Single(renderer.Batches);
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Null(component.ReceivedException));
// Act 2: Before the async error occurs, dispose the hierarchy containing the error boundary and erroring component
disposeChildren = true;
rootComponent.TriggerRender();
Assert.Equal(2, renderer.Batches.Count);
Assert.Contains(errorThrowingComponentId, renderer.Batches.Last().DisposedComponentIDs);
// Assert 2: Error is still handled
exceptionTcs.SetException(exception);
await dispatchEventTask;
Assert.Equal(2, renderer.Batches.Count); // Didn't re-render as the error boundary was already gone
Assert.Collection(errorBoundaries,
component => Assert.Null(component.ReceivedException),
component => Assert.Same(exception, component.ReceivedException));
}
[Fact]
public async Task CanRemoveRootComponents()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = new TestComponent(builder =>
{
builder.OpenComponent<DisposableComponent>(0);
builder.CloseComponent();
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.CloseComponent();
});
var unrelatedComponent = new DisposableComponent();
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
var unrelatedRootComponentId = renderer.AssignRootComponentId(unrelatedComponent);
rootComponent.TriggerRender();
unrelatedComponent.TriggerRender();
Assert.Equal(2, renderer.Batches.Count);
var nestedDisposableComponentFrame = renderer.Batches[0]
.GetComponentFrames<DisposableComponent>().Single();
var nestedAsyncDisposableComponentFrame = renderer.Batches[0]
.GetComponentFrames<AsyncDisposableComponent>().Single();
// Act
_ = renderer.Dispatcher.InvokeAsync(() => renderer.RemoveRootComponent(rootComponentId));
// Assert: we disposed the specified root component and its descendants, but not
// the other root component
Assert.Equal(3, renderer.Batches.Count);
var batch = renderer.Batches.Last();
Assert.Equal(new[]
{
rootComponentId,
nestedDisposableComponentFrame.ComponentId,
nestedAsyncDisposableComponentFrame.ComponentId,
}, batch.DisposedComponentIDs);
// Assert: component instances were disposed properly
Assert.True(((DisposableComponent)nestedDisposableComponentFrame.Component).Disposed);
Assert.True(((AsyncDisposableComponent)nestedAsyncDisposableComponentFrame.Component).Disposed);
// Assert: it's no longer known as a component
await renderer.Dispatcher.InvokeAsync(() =>
{
var ex = Assert.Throws<ArgumentException>(() =>
renderer.RemoveRootComponent(rootComponentId));
Assert.Equal($"The renderer does not have a component with ID {rootComponentId}.", ex.Message);
});
}
[Fact]
public async Task CannotRemoveSameRootComponentMultipleTimesSynchronously()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = new AsyncDisposableComponent
{
// Show that, even if the component tries to delay its disposal by returning
// a task that never completes, it still gets removed from the renderer synchronously
AsyncDisposeAction = () => new ValueTask(new TaskCompletionSource().Task)
};
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
// Act/Assert
var didRunTestLogic = false; // Don't just trust the dispatcher here - verify it runs our callback
await renderer.Dispatcher.InvokeAsync(() =>
{
renderer.RemoveRootComponent(rootComponentId);
// Even though we didn't await anything, it's synchronously unavailable for re-removal
var ex = Assert.Throws<ArgumentException>(() =>
renderer.RemoveRootComponent(rootComponentId));
Assert.Equal($"The renderer does not have a component with ID {rootComponentId}.", ex.Message);
didRunTestLogic = true;
});
Assert.True(didRunTestLogic);
}
[Fact]
public async Task CannotRemoveNonRootComponentsDirectly()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = new TestComponent(builder =>
{
builder.OpenComponent<DisposableComponent>(0);
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var nestedComponentFrame = renderer.Batches[0]
.GetComponentFrames<DisposableComponent>().Single();
var nestedComponent = (DisposableComponent)nestedComponentFrame.Component;
// Act/Assert
await renderer.Dispatcher.InvokeAsync(() =>
{
var ex = Assert.Throws<InvalidOperationException>(() =>
renderer.RemoveRootComponent(nestedComponentFrame.ComponentId));
Assert.Equal("The specified component is not a root component", ex.Message);
});
Assert.False(nestedComponent.Disposed);
}
[Fact]
public void RemoveRootComponentHandlesDisposalExceptions()
{
// Arrange
var autoResetEvent = new AutoResetEvent(false);
var renderer = new TestRenderer
{
ShouldHandleExceptions = true,
OnExceptionHandled = () => autoResetEvent.Set()
};
var exception1 = new InvalidTimeZoneException();
var exception2Tcs = new TaskCompletionSource();
var rootComponent = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<DisposableComponent>(1);
builder.AddComponentParameter(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1));
builder.CloseComponent();
builder.OpenComponent<AsyncDisposableComponent>(2);
builder.AddComponentParameter(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => await exception2Tcs.Task));
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
Assert.Single(renderer.Batches);
var nestedDisposableComponentFrame = renderer.Batches[0]
.GetComponentFrames<DisposableComponent>().Single();
var nestedAsyncDisposableComponentFrame = renderer.Batches[0]
.GetComponentFrames<AsyncDisposableComponent>().Single();
// Act
renderer.Dispatcher.InvokeAsync(() => renderer.RemoveRootComponent(rootComponentId));
// Assert: we get the synchronous exception synchronously
Assert.Same(exception1, Assert.Single(renderer.HandledExceptions));
// Assert: we get the asynchronous exception asynchronously
var exception2 = new InvalidTimeZoneException();
autoResetEvent.Reset();
exception2Tcs.SetException(exception2);
autoResetEvent.WaitOne();
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Same(exception2, renderer.HandledExceptions[1]);
}
[Fact]
public void DisposeCallsComponentDisposeOnSyncContext()
{
// Arrange
var renderer = new TestRenderer();
var wasOnSyncContext = false;
var component = new DisposableComponent
{
DisposeAction = () =>
{
wasOnSyncContext = renderer.Dispatcher.CheckAccess();
}
};
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.Dispose();
// Assert
Assert.True(wasOnSyncContext);
}
[Fact]
public async Task DisposeAsyncCallsComponentDisposeAsyncOnSyncContext()
{
// Arrange
var renderer = new TestRenderer();
var wasOnSyncContext = false;
var component = new AsyncDisposableComponent
{
AsyncDisposeAction = () =>
{
wasOnSyncContext = renderer.Dispatcher.CheckAccess();
return ValueTask.CompletedTask;
}
};
// Act
var componentId = renderer.AssignRootComponentId(component);
await renderer.DisposeAsync();
// Assert
Assert.True(wasOnSyncContext);
}
[Fact]
public async Task NoHotReloadListenersAreRegistered_WhenMetadataUpdatesAreNotSupported()
{
// Arrange
await using var renderer = new TestRenderer();
var hotReloadManager = new HotReloadManager { MetadataUpdateSupported = false };
renderer.HotReloadManager = hotReloadManager;
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "h2");
builder.AddContent(1, "some text");
builder.CloseElement();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
Assert.False(hotReloadManager.IsSubscribedTo);
await renderer.DisposeAsync();
}
[Fact]
public async Task DisposingRenderer_UnsubsribesFromHotReloadManager()
{
// Arrange
var renderer = new TestRenderer();
var hotReloadManager = new HotReloadManager { MetadataUpdateSupported = true };
renderer.HotReloadManager = hotReloadManager;
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "h2");
builder.AddContent(1, "some text");
builder.CloseElement();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
Assert.True(hotReloadManager.IsSubscribedTo);
await renderer.DisposeAsync();
// Assert
Assert.False(hotReloadManager.IsSubscribedTo);
}
[Fact]
public void ThrowsForUnknownRenderMode_OnComponentType()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<ComponentWithUnknownRenderMode>(0);
builder.CloseComponent();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
var ex = Assert.Throws<NotSupportedException>(() => component.TriggerRender());
Assert.Contains($"Cannot supply a component of type '{typeof(ComponentWithUnknownRenderMode)}' because the current platform does not support the render mode '{typeof(ComponentWithUnknownRenderMode.UnknownRenderMode)}'.", ex.Message);
}
[Fact]
public void ThrowsForUnknownRenderMode_AtCallSite()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddComponentRenderMode(new ComponentWithUnknownRenderMode.UnknownRenderMode());
builder.CloseComponent();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
var ex = Assert.Throws<NotSupportedException>(component.TriggerRender);
Assert.Contains($"Cannot supply a component of type '{typeof(TestComponent)}' because the current platform does not support the render mode '{typeof(ComponentWithUnknownRenderMode.UnknownRenderMode)}'.", ex.Message);
}
[Fact]
public void RenderModeResolverCanSupplyComponent_WithComponentTypeRenderMode()
{
// Arrange
var renderer = new RendererWithRenderModeResolver();
var component = new TestComponent(builder =>
{
builder.OpenComponent<ComponentWithRenderMode>(0);
builder.AddComponentParameter(1, nameof(MessageComponent.Message), "Some message");
builder.CloseComponent();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Assert
var batch = renderer.Batches.Single();
var componentFrames = batch.GetComponentFrames<MessageComponent>();
var resolvedComponent = (MessageComponent)componentFrames.Single().Component;
Assert.Equal("Some message", resolvedComponent.Message);
}
[Fact]
public void RenderModeResolverCanSupplyComponent_CallSiteRenderMode()
{
// Arrange
var renderer = new RendererWithRenderModeResolver();
var component = new TestComponent(builder =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddComponentParameter(1, nameof(MessageComponent.Message), "Some message");
builder.AddComponentRenderMode(new SubstituteComponentRenderMode());
builder.CloseComponent();
});
// Act
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Assert
var batch = renderer.Batches.Single();
var componentFrames = batch.GetComponentFrames<MessageComponent>();
var resolvedComponent = (MessageComponent)componentFrames.Single().Component;
Assert.Equal("Some message", resolvedComponent.Message);
}
[HasSubstituteComponentRenderMode]
private class ComponentWithRenderMode : IComponent
{
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
public class HasSubstituteComponentRenderMode : RenderModeAttribute
{
public override IComponentRenderMode Mode => new SubstituteComponentRenderMode();
}
}
[HasUnknownRenderMode]
private class ComponentWithUnknownRenderMode : IComponent
{
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
public class HasUnknownRenderMode : RenderModeAttribute
{
public override IComponentRenderMode Mode => new UnknownRenderMode();
}
public class UnknownRenderMode : IComponentRenderMode { }
}
private class RendererWithRenderModeResolver : TestRenderer
{
protected internal override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
{
return renderMode switch
{
SubstituteComponentRenderMode => componentActivator.CreateInstance(typeof(MessageComponent)),
var other => throw new NotSupportedException($"{nameof(RendererWithRenderModeResolver)} should not have received rendermode {other}"),
};
}
}
private class SubstituteComponentRenderMode : IComponentRenderMode { }
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
public IComponent CreateInstance(Type componentType)
{
RequestedComponentTypes.Add(componentType);
return new TResult();
}
}
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}
private class TestComponent : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private readonly RenderFragment _renderFragment;
public TestComponent(RenderFragment renderFragment)
{
_renderFragment = renderFragment;
}
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
TriggerRender();
return Task.CompletedTask;
}
public void TriggerRender()
{
var t = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(_renderFragment));
// This should always be run synchronously
Assert.True(t.IsCompleted);
if (t.IsFaulted)
{
var exception = t.Exception.Flatten().InnerException;
while (exception is AggregateException e)
{
exception = e.InnerException;
}
ExceptionDispatchInfo.Capture(exception).Throw();
}
}
public bool Disposed { get; private set; }
void IDisposable.Dispose() => Disposed = true;
}
private class MessageComponent : AutoRenderComponent
{
[Parameter]
public string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, Message);
}
}
private class MyStrongComponent : AutoRenderComponent
{
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Attributes { get; set; }
[Parameter] public string Text { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "strong");
builder.AddMultipleAttributes(1, Attributes);
builder.AddContent(2, Text);
builder.CloseElement();
}
}
private class FakeComponent : IComponent
{
[Parameter]
public int IntProperty { get; set; }
[Parameter]
public string StringProperty { get; set; }
[Parameter]
public object ObjectProperty { get; set; }
public RenderHandle RenderHandle { get; private set; }
public void Attach(RenderHandle renderHandle)
=> RenderHandle = renderHandle;
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
return Task.CompletedTask;
}
}
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
{
[Parameter]
public Action<EventArgs> OnTest { get; set; }
[Parameter]
public Func<EventArgs, Task> OnTestAsync { get; set; }
[Parameter]
public Action<DerivedEventArgs> OnClick { get; set; }
[Parameter]
public Func<DerivedEventArgs, Task> OnClickAsync { get; set; }
[Parameter]
public Action OnClickAction { get; set; }
[Parameter]
public Func<Task> OnClickAsyncAction { get; set; }
[Parameter]
public EventCallback OnClickEventCallback { get; set; }
[Parameter]
public EventCallback<DerivedEventArgs> OnClickEventCallbackOfT { get; set; }
[Parameter]
public Delegate OnArbitraryDelegateEvent { get; set; }
public bool SkipElement { get; set; }
private int renderCount = 0;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "grandparent");
if (!SkipElement)
{
builder.OpenElement(1, "parent");
builder.OpenElement(2, "some element");
if (OnTest != null)
{
builder.AddAttribute(3, "ontest", OnTest);
}
else if (OnTestAsync != null)
{
builder.AddAttribute(3, "ontest", OnTestAsync);
}
if (OnClick != null)
{
builder.AddAttribute(4, "onclick", OnClick);
}
else if (OnClickAsync != null)
{
builder.AddAttribute(4, "onclick", OnClickAsync);
}
else if (OnClickEventCallback.HasDelegate)
{
builder.AddAttribute(4, "onclick", OnClickEventCallback);
}
else if (OnClickEventCallbackOfT.HasDelegate)
{
builder.AddAttribute(4, "onclick", OnClickEventCallbackOfT);
}
if (OnClickAction != null)
{
builder.AddAttribute(5, "onclickaction", OnClickAction);
}
else if (OnClickAsyncAction != null)
{
builder.AddAttribute(5, "onclickaction", OnClickAsyncAction);
}
if (OnArbitraryDelegateEvent != null)
{
builder.AddAttribute(6, "onarbitrarydelegateevent", OnArbitraryDelegateEvent);
}
builder.CloseElement();
builder.CloseElement();
}
builder.CloseElement();
builder.AddContent(6, $"Render count: {++renderCount}");
}
public Task HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
// Notice, we don't re-render.
return callback.InvokeAsync(arg);
}
}
private class ConditionalParentComponent<T> : AutoRenderComponent where T : IComponent
{
[Parameter]
public bool IncludeChild { get; set; }
[Parameter]
public IDictionary<string, object> ChildParameters { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "Parent here");
if (IncludeChild)
{
builder.OpenComponent<T>(1);
if (ChildParameters != null)
{
foreach (var kvp in ChildParameters)
{
builder.AddComponentParameter(2, kvp.Key, kvp.Value);
}
}
builder.CloseComponent();
}
}
}
private class ReRendersParentComponent : AutoRenderComponent
{
[Parameter]
public TestComponent Parent { get; set; }
private bool _isFirstTime = true;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (_isFirstTime) // Don't want an infinite loop
{
_isFirstTime = false;
Parent.TriggerRender();
}
builder.AddContent(0, "Child is here");
}
}
private class RendersSelfAfterEventComponent : IComponent, IHandleEvent
{
[Parameter]
public Action<object> OnClick { get; set; }
private RenderHandle _renderHandle;
public void Attach(RenderHandle renderHandle)
=> _renderHandle = renderHandle;
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}
public Task HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
var task = callback.InvokeAsync(arg);
Render();
return task;
}
private void Render()
=> _renderHandle.Render(builder =>
{
builder.OpenElement(0, "my button");
builder.AddAttribute(1, "onmycustomevent", EventCallback.Factory.Create(this, eventArgs => OnClick(eventArgs)));
builder.CloseElement();
});
}
private class MultiRendererComponent : IComponent
{
private readonly List<RenderHandle> _renderHandles
= new List<RenderHandle>();
public void Attach(RenderHandle renderHandle)
=> _renderHandles.Add(renderHandle);
public Task SetParametersAsync(ParameterView parameters)
{
return Task.CompletedTask;
}
public void TriggerRender()
{
foreach (var renderHandle in _renderHandles)
{
renderHandle.Dispatcher.InvokeAsync(() => renderHandle.Render(builder =>
{
builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
}));
}
}
}
private class BindPlusConditionalAttributeComponent : AutoRenderComponent, IHandleEvent
{
public bool CheckboxEnabled;
public string SomeStringProperty;
public Task HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
var task = callback.InvokeAsync(arg);
TriggerRender();
return task;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "type", "checkbox");
builder.AddAttribute(2, "value", BindConverter.FormatValue(CheckboxEnabled));
builder.AddAttribute(3, "onchange", EventCallback.Factory.CreateBinder<bool>(this, __value => CheckboxEnabled = __value, CheckboxEnabled));
builder.CloseElement();
builder.OpenElement(4, "input");
builder.AddAttribute(5, "value", BindConverter.FormatValue(SomeStringProperty));
builder.AddAttribute(6, "onchange", EventCallback.Factory.CreateBinder<string>(this, __value => SomeStringProperty = __value, SomeStringProperty));
builder.AddAttribute(7, "disabled", !CheckboxEnabled);
builder.CloseElement();
}
}
private class AfterRenderCaptureComponent : AutoRenderComponent, IComponent, IHandleAfterRender
{
public Action OnAfterRenderLogic { get; set; }
public int OnAfterRenderCallCount { get; private set; }
public Task OnAfterRenderAsync()
{
OnAfterRenderCallCount++;
OnAfterRenderLogic?.Invoke();
return Task.CompletedTask;
}
Task IComponent.SetParametersAsync(ParameterView parameters)
{
TriggerRender();
return Task.CompletedTask;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
private class DisposableComponent : AutoRenderComponent, IDisposable
{
public bool Disposed { get; private set; }
[Parameter]
public Action DisposeAction { get; set; }
public void Dispose()
{
Disposed = true;
DisposeAction?.Invoke();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
private class AsyncDisposableComponent : AutoRenderComponent, IAsyncDisposable
{
public bool Disposed { get; private set; }
[Parameter]
public Func<ValueTask> AsyncDisposeAction { get; set; }
public ValueTask DisposeAsync()
{
Disposed = true;
return AsyncDisposeAction == null ? default : AsyncDisposeAction.Invoke();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
class TestAsyncRenderer : TestRenderer
{
public Task NextUpdateDisplayReturnTask { get; set; }
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
base.UpdateDisplayAsync(renderBatch);
return NextUpdateDisplayReturnTask;
}
}
private class AsyncComponent : IComponent
{
private RenderHandle _renderHandler;
public AsyncComponent(Task taskToAwait, int number)
{
_taskToAwait = taskToAwait;
Number = number;
}
private readonly Task _taskToAwait;
public int Number { get; set; }
public void Attach(RenderHandle renderHandle)
{
_renderHandler = renderHandle;
}
public async Task SetParametersAsync(ParameterView parameters)
{
int n;
while (Number > 0)
{
n = Number;
_renderHandler.Render(CreateFragment);
Number--;
await _taskToAwait;
};
// Cheap closure
void CreateFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, n);
builder.CloseElement();
}
}
}
private class OuterEventComponent : IComponent, IHandleEvent
{
private RenderHandle _renderHandle;
public RenderFragment RenderFragment { get; set; }
public Action OnEvent { get; set; }
public int SomeMethodCallCount { get; set; }
public void SomeMethod()
{
SomeMethodCallCount++;
}
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
var task = callback.InvokeAsync(arg);
OnEvent?.Invoke();
return task;
}
public Task SetParametersAsync(ParameterView parameters)
{
return TriggerRenderAsync();
}
public Task TriggerRenderAsync() => _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(RenderFragment));
}
private void AssertStream(int expectedId, (int id, NestedAsyncComponent.EventType @event)[] logStream)
{
// OnInit runs first
Assert.Equal((expectedId, NestedAsyncComponent.EventType.OnInit), logStream[0]);
// OnInit async completes
Assert.Single(logStream.Skip(1),
e => e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncAsync) || e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncSync));
var parametersSetEvent = logStream.Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSet)).ToArray();
// OnParametersSet gets called at least once
Assert.NotEmpty(parametersSetEvent);
var parametersSetAsyncEvent = logStream
.Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync) ||
le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync))
.ToArray();
// OnParametersSetAsync async gets called at least once
Assert.NotEmpty(parametersSetAsyncEvent);
// The same number of OnParametersSet and OnParametersSetAsync get produced
Assert.Equal(parametersSetEvent.Length, parametersSetAsyncEvent.Length);
// The log ends with an OnParametersSetAsync event
Assert.True(logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync) ||
logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync));
}
private Func<NestedAsyncComponent, RenderFragment> CreateRenderFactory(int[] childrenToRender)
{
// For some reason nameof doesn't work inside a nested lambda, so capturing the value here.
var eventActionsName = nameof(NestedAsyncComponent.EventActions);
var whatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
var testIdName = nameof(NestedAsyncComponent.TestId);
var logName = nameof(NestedAsyncComponent.Log);
return component => builder =>
{
builder.OpenElement(0, "div");
builder.AddContent(1, $"Id: {component.TestId} BuildRenderTree, {Guid.NewGuid()}");
foreach (var child in childrenToRender)
{
builder.OpenComponent<NestedAsyncComponent>(2);
builder.AddComponentParameter(3, eventActionsName, component.EventActions);
builder.AddComponentParameter(4, whatToRenderName, component.WhatToRender);
builder.AddComponentParameter(5, testIdName, child);
builder.AddComponentParameter(6, logName, component.Log);
builder.CloseComponent();
}
builder.CloseElement();
};
}
private class NestedAsyncComponent : ComponentBase
{
[Parameter] public IDictionary<int, IList<ExecutionAction>> EventActions { get; set; }
[Parameter] public IDictionary<int, Func<NestedAsyncComponent, RenderFragment>> WhatToRender { get; set; }
[Parameter] public int TestId { get; set; }
[Parameter] public ConcurrentQueue<(int testId, EventType @event)> Log { get; set; }
protected override void OnInitialized()
{
if (TryGetEntry(EventType.OnInit, out var entry))
{
var result = entry.EventAction();
Assert.True(result.IsCompleted, "Task must complete synchronously.");
LogResult(result.Result);
}
}
protected override async Task OnInitializedAsync()
{
if (TryGetEntry(EventType.OnInitAsyncSync, out var entrySync))
{
var result = entrySync.EventAction();
Assert.True(result.IsCompleted, "Task must complete synchronously.");
LogResult(result.Result);
}
else if (TryGetEntry(EventType.OnInitAsyncAsync, out var entryAsync))
{
var result = await entryAsync.EventAction();
LogResult(result);
}
}
protected override void OnParametersSet()
{
if (TryGetEntry(EventType.OnParametersSet, out var entry))
{
var result = entry.EventAction();
Assert.True(result.IsCompleted, "Task must complete synchronously.");
LogResult(result.Result);
}
base.OnParametersSet();
}
protected override async Task OnParametersSetAsync()
{
if (TryGetEntry(EventType.OnParametersSetAsyncSync, out var entrySync))
{
var result = entrySync.EventAction();
Assert.True(result.IsCompleted, "Task must complete synchronously.");
LogResult(result.Result);
}
else if (TryGetEntry(EventType.OnParametersSetAsyncAsync, out var entryAsync))
{
var result = await entryAsync.EventAction();
LogResult(result);
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var renderFactory = WhatToRender[TestId];
renderFactory(this)(builder);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (TryGetEntry(EventType.OnAfterRenderAsyncSync, out var entrySync))
{
var result = entrySync.EventAction();
Assert.True(result.IsCompleted, "Task must complete synchronously.");
LogResult(result.Result);
}
if (TryGetEntry(EventType.OnAfterRenderAsyncAsync, out var entryAsync))
{
var result = await entryAsync.EventAction();
LogResult(result);
}
}
private bool TryGetEntry(EventType eventType, out ExecutionAction entry)
{
var entries = EventActions[TestId];
if (entries == null)
{
throw new InvalidOperationException("Failed to find entries for component with Id: " + TestId);
}
entry = entries.FirstOrDefault(e => e.Event == eventType);
return entry != null;
}
private void LogResult((int, EventType) entry)
{
Log?.Enqueue(entry);
}
public class ExecutionAction
{
public EventType Event { get; set; }
public Func<Task<(int id, EventType @event)>> EventAction { get; set; }
public static ExecutionAction On(int id, EventType @event, bool async = false)
{
if (!async)
{
return new ExecutionAction
{
Event = @event,
EventAction = () => Task.FromResult((id, @event))
};
}
else
{
return new ExecutionAction
{
Event = @event,
EventAction = async () =>
{
await Task.Yield();
return (id, @event);
}
};
}
}
}
public enum EventType
{
OnInit,
OnInitAsyncSync,
OnInitAsyncAsync,
OnParametersSet,
OnParametersSetAsyncSync,
OnParametersSetAsyncAsync,
OnAfterRenderAsyncSync,
OnAfterRenderAsyncAsync,
}
public Task ExternalExceptionDispatch(Exception exception)
{
var tcs = new TaskCompletionSource();
Task.Run(async () =>
{
// Inside Task.Run, we're outside the call stack or task chain of the lifecycle method, so
// DispatchExceptionAsync is needed to get an exception back into the component
await DispatchExceptionAsync(exception);
tcs.SetResult();
});
return tcs.Task;
}
}
private class ComponentThatAwaitsTask : ComponentBase
{
[Parameter] public Task TaskToAwait { get; set; }
protected override async Task OnParametersSetAsync()
{
await TaskToAwait;
}
}
private class AsyncUpdateTestRenderer : TestRenderer
{
public Func<RenderBatch, Task> OnUpdateDisplayAsync { get; set; }
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
return OnUpdateDisplayAsync(renderBatch);
}
}
private class AsyncAfterRenderComponent : AutoRenderComponent, IHandleAfterRender
{
private readonly Task _task;
public AsyncAfterRenderComponent(Task task)
{
_task = task;
}
public bool Called { get; private set; }
public Action OnAfterRenderComplete { get; set; }
public async Task OnAfterRenderAsync()
{
await _task;
Called = true;
OnAfterRenderComplete?.Invoke();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.CloseElement();
}
}
class BoundPropertyComponent : AutoRenderComponent
{
public string BoundString { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var unrelatedThingToMakeTheLambdaCapture = new object();
builder.OpenElement(0, "element with event");
builder.AddAttribute(1, nameof(BoundString), BoundString);
builder.AddAttribute(2, "ontestevent", new Action<ChangeEventArgs>((ChangeEventArgs eventArgs) =>
{
BoundString = (string)eventArgs.Value;
TriggerRender();
GC.KeepAlive(unrelatedThingToMakeTheLambdaCapture);
}));
builder.SetUpdatesAttributeName(nameof(BoundString));
builder.CloseElement();
}
}
private class DerivedEventArgs : EventArgs
{
}
class CallbackOnRenderComponent : AutoRenderComponent
{
private readonly Action _callback;
public CallbackOnRenderComponent(Action callback)
{
_callback = callback;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> _callback();
}
class InvalidRecursiveRenderer : TestRenderer
{
public new void ProcessPendingRender()
=> base.ProcessPendingRender();
}
class ParameterViewIllegalCapturingComponent : IComponent
{
public ParameterView CapturedParameterView { get; private set; }
[Parameter] public int SomeParam { get; set; }
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters)
{
CapturedParameterView = parameters;
// Return a task that never completes to show that access is forbidden
// after the synchronous return, not just after the returned task completes
return new TaskCompletionSource().Task;
}
}
private class TestErrorBoundary : AutoRenderComponent, IErrorBoundary
{
private readonly TaskCompletionSource receivedErrorTaskCompletionSource = new();
public Exception ReceivedException { get; private set; }
public Task ReceivedErrorTask => receivedErrorTaskCompletionSource.Task;
[Parameter] public RenderFragment ChildContent { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> ChildContent(builder);
public void HandleException(Exception error)
{
ReceivedException = error;
receivedErrorTaskCompletionSource.SetResult();
}
public static void RenderNestedErrorBoundaries(RenderTreeBuilder builder, RenderFragment innerContent)
{
// Create an error boundary
builder.OpenComponent<TestErrorBoundary>(0);
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), (RenderFragment)(builder =>
{
// ... containing another error boundary, containing the content
builder.OpenComponent<TestErrorBoundary>(0);
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), innerContent);
builder.CloseComponent();
}));
builder.CloseComponent();
}
}
private class ErrorThrowingComponent : AutoRenderComponent, IHandleEvent
{
[Parameter] public Exception ThrowDuringRender { get; set; }
[Parameter] public Exception ThrowDuringEventSync { get; set; }
[Parameter] public Task ThrowDuringEventAsync { get; set; }
[Parameter] public Exception ThrowDuringParameterSettingSync { get; set; }
[Parameter] public Task ThrowDuringParameterSettingAsync { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
_ = base.SetParametersAsync(parameters);
if (ThrowDuringParameterSettingSync is not null)
{
throw ThrowDuringParameterSettingSync;
}
if (ThrowDuringParameterSettingAsync is not null)
{
await ThrowDuringParameterSettingAsync;
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (ThrowDuringRender is not null)
{
throw ThrowDuringRender;
}
builder.OpenElement(0, "someelem");
builder.AddAttribute(1, "onmakeerror", EventCallback.Factory.Create(this, () => { }));
builder.AddContent(1, "Hello");
builder.CloseElement();
}
public async Task HandleEventAsync(EventCallbackWorkItem item, object arg)
{
if (ThrowDuringEventSync is not null)
{
throw ThrowDuringEventSync;
}
if (ThrowDuringEventAsync is not null)
{
await ThrowDuringEventAsync;
}
}
}
private class CallbackDuringSetParametersAsyncComponent : AutoRenderComponent
{
public int RenderCount { get; private set; }
public Func<Task> Callback { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
await Callback();
await base.SetParametersAsync(parameters);
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
RenderCount++;
}
}
private sealed class ImplicitConversionComponent : IComponent
{
[Parameter]
public ImplicitlyConvertsToString SomeParam { get; set; }
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters)
{
foreach (var parameter in parameters)
{
if (parameter.Name.Equals(nameof(SomeParam), StringComparison.OrdinalIgnoreCase))
{
// 'SomeParam' will be assigned to null if an implicit conversion changed the
// parameter type.
SomeParam = parameter.Value as ImplicitlyConvertsToString;
}
}
return Task.CompletedTask;
}
}
private sealed class ImplicitlyConvertsToString
{
private readonly string _value;
public ImplicitlyConvertsToString(string value)
{
_value = value;
}
public static implicit operator string(ImplicitlyConvertsToString value) => value._value;
}
}
|