Skip to content

Commit 4aae250

Browse files
committed
Fixed the ComponentBase faulted task bug
1 parent e656076 commit 4aae250

File tree

5 files changed

+124
-4
lines changed

5 files changed

+124
-4
lines changed

src/Components/Components/src/ComponentBase.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,10 @@ private async Task RunInitAndSetParametersAsync()
283283
// to defer calling StateHasChanged up until the first bit of async code happens or until
284284
// the end. Additionally, we want to avoid calling StateHasChanged if no
285285
// async work is to be performed.
286-
StateHasChanged();
286+
if (task.Status != TaskStatus.Faulted)
287+
{
288+
StateHasChanged();
289+
}
287290

288291
try
289292
{

src/Components/Components/test/ComponentBaseTest.cs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,37 @@ public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationT
372372
Assert.NotEmpty(renderer.Batches);
373373
}
374374

375+
[Fact]
376+
public async Task ErrorBoundaryHandlesOnInitializedAsyncReturnFaultedTask()
377+
{
378+
// Arrange
379+
var renderer = new TestRenderer();
380+
TestErrorBoundary capturedBoundary = null;
381+
382+
// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
383+
var rootComponent = new TestComponent();
384+
rootComponent.ChildContent = builder =>
385+
{
386+
builder.OpenComponent<TestErrorBoundary>(0);
387+
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), (RenderFragment)(childBuilder =>
388+
{
389+
childBuilder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
390+
childBuilder.CloseComponent();
391+
}));
392+
builder.AddComponentReferenceCapture(2, inst => capturedBoundary = (TestErrorBoundary)inst);
393+
builder.CloseComponent();
394+
};
395+
396+
// Act
397+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
398+
await renderer.RenderRootComponentAsync(rootComponentId);
399+
400+
// Assert
401+
Assert.NotNull(capturedBoundary);
402+
Assert.NotNull(capturedBoundary!.ReceivedException);
403+
Assert.Equal(typeof(InvalidTimeZoneException), capturedBoundary!.ReceivedException.GetType());
404+
}
405+
375406
[Fact]
376407
public async Task DoesNotRenderAfterOnParametersSetAsyncTaskIsCanceled()
377408
{
@@ -491,11 +522,20 @@ private class TestComponent : ComponentBase
491522

492523
public int Counter { get; set; }
493524

525+
public RenderFragment ChildContent { get; set; }
526+
494527
protected override void BuildRenderTree(RenderTreeBuilder builder)
495528
{
496-
builder.OpenElement(0, "p");
497-
builder.AddContent(1, Counter);
498-
builder.CloseElement();
529+
if (ChildContent != null)
530+
{
531+
builder.AddContent(0, ChildContent);
532+
}
533+
else
534+
{
535+
builder.OpenElement(0, "p");
536+
builder.AddContent(1, Counter);
537+
builder.CloseElement();
538+
}
499539
}
500540

501541
protected override void OnInitialized()
@@ -570,4 +610,41 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
570610
}
571611
}
572612
}
613+
614+
private class TestErrorBoundary : ErrorBoundaryBase
615+
{
616+
public Exception ReceivedException => CurrentException;
617+
618+
protected override Task OnErrorAsync(Exception exception)
619+
{
620+
return Task.CompletedTask;
621+
}
622+
623+
protected override void BuildRenderTree(RenderTreeBuilder builder)
624+
{
625+
if (CurrentException == null)
626+
{
627+
builder.AddContent(0, ChildContent);
628+
}
629+
else
630+
{
631+
builder.OpenElement(2, "div");
632+
builder.AddAttribute(3, "class", "blazor-error-boundary");
633+
builder.CloseElement();
634+
}
635+
}
636+
}
637+
638+
private class TestComponentErrorBuildRenderTree : ComponentBase
639+
{
640+
protected override void BuildRenderTree(RenderTreeBuilder builder)
641+
{
642+
throw new InvalidOperationException("Error in BuildRenderTree");
643+
}
644+
645+
protected override Task OnInitializedAsync()
646+
{
647+
return Task.FromException(new InvalidTimeZoneException());
648+
}
649+
}
573650
}

src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ public void CanHandleErrorsAfterDisposingComponent()
126126
AssertGlobalErrorState(false);
127127
}
128128

129+
[Fact]
130+
public void CanHandleErrorsAfterDisposingErrorBoundaryComponent()
131+
{
132+
var container = Browser.Exists(By.Id("multiple-errors-at-once-test"));
133+
container.FindElement(By.ClassName("throw-miltiple-errors")).Click();
134+
// The error boundary is still there, so we see the error message
135+
Browser.Collection(() => container.FindElements(By.ClassName("error-message")),
136+
elem => Assert.Equal("OnInitializedAsyncError", elem.Text));
137+
AssertGlobalErrorState(false);
138+
}
139+
129140
[Fact]
130141
public async Task CanHandleErrorsAfterDisposingErrorBoundary()
131142
{

src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@
7070
<button class="throw-in-errorcontent" @onclick="@(() => throwInErrorContent = true)">Throw in error content (causing infinite error loop)</button>
7171
</div>
7272

73+
<hr/>
74+
<h2>Two errors in child</h2>
75+
<div id="multiple-errors-at-once-test">
76+
@if (twoErrorsInChild) {
77+
<ErrorBoundary>
78+
<ChildContent>
79+
<MultipleErrorsChild />
80+
</ChildContent>
81+
<ErrorContent>
82+
<p class="error-message">@context.Message</p>
83+
</ErrorContent>
84+
</ErrorBoundary>
85+
}
86+
<button class="throw-miltiple-errors" @onclick="@(() => twoErrorsInChild = true)">Throw multiple errors in child</button>
87+
</div>
88+
7389
<hr />
7490
<h2>Errors after disposal</h2>
7591
<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>
@@ -122,6 +138,7 @@
122138
private bool disposalTestRemoveErrorBoundary;
123139
private bool disposalTestBeginDelayedError;
124140
private bool multipleChildrenBeginDelayedError;
141+
private bool twoErrorsInChild;
125142

126143
void EventHandlerErrorSync()
127144
=> throw new InvalidTimeZoneException("Synchronous error from event handler");
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h3>MultipleErrorsChild</h3>
2+
3+
@{
4+
throw new Exception("BuildRenderTreeError");
5+
}
6+
7+
@code {
8+
protected override Task OnInitializedAsync()
9+
{
10+
return Task.FromException(new Exception("OnInitializedAsyncError"));
11+
}
12+
}

0 commit comments

Comments
 (0)