Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Components/Components/src/ComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,10 @@ private async Task RunInitAndSetParametersAsync()
// to defer calling StateHasChanged up until the first bit of async code happens or until
// the end. Additionally, we want to avoid calling StateHasChanged if no
// async work is to be performed.
StateHasChanged();
if (task.Status != TaskStatus.Faulted)
{
StateHasChanged();
}

try
{
Expand Down
83 changes: 80 additions & 3 deletions src/Components/Components/test/ComponentBaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,37 @@ public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationT
Assert.NotEmpty(renderer.Batches);
}

[Fact]
public async Task ErrorBoundaryHandlesOnInitializedAsyncReturnFaultedTask()
{
// Arrange
var renderer = new TestRenderer();
TestErrorBoundary capturedBoundary = null;

// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
var rootComponent = new TestComponent();
rootComponent.ChildContent = builder =>
{
builder.OpenComponent<TestErrorBoundary>(0);
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), (RenderFragment)(childBuilder =>
{
childBuilder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
childBuilder.CloseComponent();
}));
builder.AddComponentReferenceCapture(2, inst => capturedBoundary = (TestErrorBoundary)inst);
builder.CloseComponent();
};

// Act
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
await renderer.RenderRootComponentAsync(rootComponentId);

// Assert
Assert.NotNull(capturedBoundary);
Assert.NotNull(capturedBoundary!.ReceivedException);
Assert.Equal(typeof(InvalidTimeZoneException), capturedBoundary!.ReceivedException.GetType());
}

[Fact]
public async Task DoesNotRenderAfterOnParametersSetAsyncTaskIsCanceled()
{
Expand Down Expand Up @@ -491,11 +522,20 @@ private class TestComponent : ComponentBase

public int Counter { get; set; }

public RenderFragment ChildContent { get; set; }

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, Counter);
builder.CloseElement();
if (ChildContent != null)
{
builder.AddContent(0, ChildContent);
}
else
{
builder.OpenElement(0, "p");
builder.AddContent(1, Counter);
builder.CloseElement();
}
}

protected override void OnInitialized()
Expand Down Expand Up @@ -570,4 +610,41 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}
}
}

private class TestErrorBoundary : ErrorBoundaryBase
{
public Exception ReceivedException => CurrentException;

protected override Task OnErrorAsync(Exception exception)
{
return Task.CompletedTask;
}

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (CurrentException == null)
{
builder.AddContent(0, ChildContent);
}
else
{
builder.OpenElement(2, "div");
builder.AddAttribute(3, "class", "blazor-error-boundary");
builder.CloseElement();
}
}
}

private class TestComponentErrorBuildRenderTree : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
throw new InvalidOperationException("Error in BuildRenderTree");
}

protected override Task OnInitializedAsync()
{
return Task.FromException(new InvalidTimeZoneException());
}
}
}
11 changes: 11 additions & 0 deletions src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ public void CanHandleErrorsAfterDisposingComponent()
AssertGlobalErrorState(false);
}

[Fact]
public void CanHandleErrorsAfterDisposingErrorBoundaryComponent()
{
var container = Browser.Exists(By.Id("multiple-errors-at-once-test"));
container.FindElement(By.ClassName("throw-miltiple-errors")).Click();
// The error boundary is still there, so we see the error message
Browser.Collection(() => container.FindElements(By.ClassName("error-message")),
elem => Assert.Equal("OnInitializedAsyncError", elem.Text));
AssertGlobalErrorState(false);
}

[Fact]
public async Task CanHandleErrorsAfterDisposingErrorBoundary()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@
<button class="throw-in-errorcontent" @onclick="@(() => throwInErrorContent = true)">Throw in error content (causing infinite error loop)</button>
</div>

<hr/>
<h2>Two errors in child</h2>
<div id="multiple-errors-at-once-test">
@if (twoErrorsInChild) {
<ErrorBoundary>
<ChildContent>
<MultipleErrorsChild />
</ChildContent>
<ErrorContent>
<p class="error-message">@context.Message</p>
</ErrorContent>
</ErrorBoundary>
}
<button class="throw-miltiple-errors" @onclick="@(() => twoErrorsInChild = true)">Throw multiple errors in child</button>
</div>

<hr />
<h2>Errors after disposal</h2>
<p>Long-running tasks could fail after the component has been removed from the tree. We still want these failures to be captured by the error boundary they were inside when the task began, even if that error boundary itself has also since been disposed. Otherwise, error handling would behave differently based on whether the user has navigated away while processing was in flight, which would be very unexpected and hard to handle.</p>
Expand Down Expand Up @@ -122,6 +138,7 @@
private bool disposalTestRemoveErrorBoundary;
private bool disposalTestBeginDelayedError;
private bool multipleChildrenBeginDelayedError;
private bool twoErrorsInChild;

void EventHandlerErrorSync()
=> throw new InvalidTimeZoneException("Synchronous error from event handler");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h3>MultipleErrorsChild</h3>

@{
throw new Exception("BuildRenderTreeError");
}

@code {
protected override Task OnInitializedAsync()
{
return Task.FromException(new Exception("OnInitializedAsyncError"));
}
}
Loading