This commit is contained in:
parent
3da9c7cff7
commit
8ba7c7b457
|
|
@ -69,6 +69,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
CurrentRenderTree.Clear();
|
||||
renderFragment(CurrentRenderTree);
|
||||
|
||||
CurrentRenderTree.AssertTreeIsValid(Component);
|
||||
|
||||
var diff = RenderTreeDiffBuilder.ComputeDiff(
|
||||
_renderer,
|
||||
batchBuilder,
|
||||
|
|
|
|||
|
|
@ -685,6 +685,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public ArrayRange<RenderTreeFrame> GetFrames() =>
|
||||
_entries.ToRange();
|
||||
|
||||
internal void AssertTreeIsValid(IComponent component)
|
||||
{
|
||||
if (_openElementIndices.Count > 0)
|
||||
{
|
||||
// It's never valid to leave an element/component/region unclosed. Doing so
|
||||
// could cause undefined behavior in diffing.
|
||||
ref var invalidFrame = ref _entries.Buffer[_openElementIndices.Peek()];
|
||||
throw new InvalidOperationException($"Render output is invalid for component of type '{component.GetType().FullName}'. A frame of type '{invalidFrame.FrameType}' was left unclosed. Do not use try/catch inside rendering logic, because partial output cannot be undone.");
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal void ProcessDuplicateAttributes(int first)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4001,6 +4001,22 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
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);
|
||||
}
|
||||
|
||||
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
|
||||
{
|
||||
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
|
||||
|
|
|
|||
|
|
@ -1831,6 +1831,69 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
f => AssertFrame.Attribute(f, "3", "see ya"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsClosedFramesAsValid()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new RenderTreeBuilder();
|
||||
var component = new TestComponent();
|
||||
builder.OpenElement(0, "myElem");
|
||||
builder.OpenRegion(1);
|
||||
builder.OpenComponent<OtherComponent>(2);
|
||||
builder.CloseComponent();
|
||||
builder.CloseRegion();
|
||||
builder.CloseElement();
|
||||
|
||||
// Act/Assert
|
||||
// Lack of exception is success
|
||||
builder.AssertTreeIsValid(component);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportsUnclosedElementAsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new RenderTreeBuilder();
|
||||
var component = new TestComponent();
|
||||
builder.OpenElement(0, "outerElem");
|
||||
builder.OpenElement(1, "innerElem");
|
||||
builder.CloseElement();
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
|
||||
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 ReportsUnclosedComponentAsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new RenderTreeBuilder();
|
||||
var component = new TestComponent();
|
||||
builder.OpenComponent<OtherComponent>(0);
|
||||
builder.OpenComponent<OtherComponent>(1);
|
||||
builder.CloseComponent();
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
|
||||
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Component' was left unclosed.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportsUnclosedRegionAsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new RenderTreeBuilder();
|
||||
var component = new TestComponent();
|
||||
builder.OpenRegion(0);
|
||||
builder.OpenRegion(1);
|
||||
builder.CloseRegion();
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
|
||||
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Region' was left unclosed.", ex.Message);
|
||||
}
|
||||
|
||||
private class TestComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle) { }
|
||||
|
|
@ -1839,6 +1902,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class OtherComponent : TestComponent
|
||||
{
|
||||
}
|
||||
|
||||
private class TestRenderer : Renderer
|
||||
{
|
||||
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
|
||||
|
|
|
|||
Loading…
Reference in New Issue