Skip to content

feat(ErrorLogger): redesign ErrorLogger component#7558

Merged
ArgoZhang merged 28 commits intomainfrom
fix-error
Jan 22, 2026
Merged

feat(ErrorLogger): redesign ErrorLogger component#7558
ArgoZhang merged 28 commits intomainfrom
fix-error

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Jan 21, 2026

Link issues

fixes #7557

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Redesign the error logging and rendering pipeline to centralize exception display logic and simplify ErrorLogger integration across components.

New Features:

  • Introduce an ErrorRender component to standardize how exceptions are rendered based on configuration.

Enhancements:

  • Refactor BootstrapBlazorErrorBoundary to delegate logging to an injected IErrorBoundaryLogger and simplify its render logic.
  • Update ErrorLogger to handle exceptions directly via registered handlers and an optional callback, removing its dependency on BootstrapBlazorErrorBoundary internals.
  • Adjust Tab error handling to ensure a default error rendering path when no custom handler is provided.
  • Tidy up sample components and unit tests related to error handling and logging behavior.

Copilot AI review requested due to automatic review settings January 21, 2026 10:30
@bb-auto bb-auto bot added the enhancement New feature or request label Jan 21, 2026
@bb-auto bb-auto bot added this to the v10.2.0 milestone Jan 21, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

Refactors the ErrorLogger and BootstrapBlazorErrorBoundary error-handling pipeline to use a dedicated ErrorRender component and an injected IErrorBoundaryLogger, simplifying exception rendering and callback signatures, and updates call sites and tests accordingly.

Sequence diagram for BootstrapBlazorErrorBoundary error handling with ErrorRender

sequenceDiagram
    participant Component as FailingComponent
    participant Boundary as BootstrapBlazorErrorBoundary
    participant BoundaryLogger as IErrorBoundaryLogger
    participant ErrorView as ErrorRender

    Component->>Boundary: lifecycle method throws Exception
    Boundary->>Boundary: OnErrorAsync(exception)
    Boundary->>BoundaryLogger: LogErrorAsync(exception)
    BoundaryLogger-->>Boundary: Task completed

    Boundary->>Boundary: BuildRenderTree(builder)
    alt CurrentException is null
        Boundary->>Boundary: render ChildContent
    else ErrorContent is not null
        Boundary->>Boundary: builder.AddContent(ErrorContent(CurrentException))
    else page lifecycle exception
        Boundary->>Boundary: IsPageException(CurrentException)
        alt IsPageException returns true
            Boundary->>Boundary: RenderException(builder, CurrentException)
            alt OnErrorHandleAsync is not null
                Boundary->>Boundary: OnErrorHandleAsync(Logger, exception)
                Boundary-->>Boundary: continue after callback
            else OnErrorHandleAsync is null
                Boundary->>ErrorView: instantiate component
                Boundary->>ErrorView: set Exception parameter
                ErrorView->>ErrorView: BuildRenderTree(builder)
                ErrorView-->>Boundary: rendered error markup
            end
            Boundary->>Boundary: ResetException()
        else not a page exception
            Boundary->>Boundary: render ChildContent
            Boundary->>Boundary: ResetException()
        end
    end
Loading

Sequence diagram for ErrorLogger.HandlerExceptionAsync pipeline

sequenceDiagram
    participant Caller as CallerComponent
    participant Logger as ErrorLogger
    participant Handler as IHandlerException
    participant ErrorView as ErrorRender

    Caller->>Logger: HandlerExceptionAsync(exception)
    activate Logger
    Logger->>Logger: handler = _cache.LastOrDefault()
    alt handler is not null
        Logger->>Handler: HandlerExceptionAsync(exception, exceptionContent)
        activate Handler
        note over Logger,Handler: exceptionContent builds ErrorRender with Exception
        Handler-->>Logger: Task completed
        deactivate Handler
    else handler is null
        Logger->>Logger: skip handler invocation
    end

    alt OnErrorHandleAsync is not null
        Logger->>Logger: OnErrorHandleAsync(exception)
        Logger-->>Caller: Task completed
    else no external callback
        Logger-->>Caller: Task completed
    end
    deactivate Logger
Loading

Class diagram for redesigned ErrorLogger and error boundary pipeline

classDiagram
    direction LR

    class BootstrapBlazorErrorBoundary {
        +bool ShowToast
        +bool EnableILogger
        +string ToastTitle
        +Func~ILogger, Exception, Task~ OnErrorHandleAsync
        -ILogger~BootstrapBlazorErrorBoundary~ Logger
        -IErrorBoundaryLogger ErrorBoundaryLogger
        -NavigationManager NavigationManager
        -IToastService ToastService
        -PropertyInfo _currentExceptionPropertyInfo
        +Task OnErrorAsync(Exception exception)
        +void BuildRenderTree(RenderTreeBuilder builder)
        -void RenderException(RenderTreeBuilder builder, Exception ex)
        -static bool IsPageException(Exception ex)
        -void ResetException()
    }

    class ErrorLogger {
        +RenderFragment? ErrorContent
        +RenderFragment? ChildContent
        +bool EnableILogger
        +Func~IErrorLogger, Task~? OnInitializedCallback
        +Func~Exception, Task~? OnErrorHandleAsync
        -List~IHandlerException~ _cache
        +Task HandlerExceptionAsync(Exception exception)
        +void Register(IHandlerException handler)
        +void UnRegister(IHandlerException handler)
        +RenderFragment BuildRenderTree(RenderTreeBuilder builder)
    }

    class ErrorRender {
        -IConfiguration Configuration
        -RenderHandle _renderHandle
        -Exception _ex
        -bool? _errorDetails
        +Task SetParametersAsync(ParameterView parameters)
        -void BuildRenderTree(RenderTreeBuilder builder)
        -MarkupString GetErrorContentMarkupString(Exception ex)
    }

    class IErrorLogger {
        <<interface>>
        +Task HandlerExceptionAsync(Exception exception)
        +void Register(IHandlerException handler)
        +void UnRegister(IHandlerException handler)
    }

    class IHandlerException {
        <<interface>>
        +Task HandlerExceptionAsync(Exception exception, RenderFragment exceptionContent)
    }

    class IErrorBoundaryLogger {
        <<interface>>
        +Task LogErrorAsync(Exception exception)
    }

    class TabItemContent {
        -ErrorLogger _logger
        -bool _detailedErrorsLoaded
        -bool _showDetailedErrors
        -Task HandlerErrorCoreAsync(ILogger logger, Exception ex)
        +void Render()
    }

    ErrorLogger ..|> IErrorLogger
    ErrorRender ..|> IComponent
    BootstrapBlazorErrorBoundary ..|> ErrorBoundaryBase

    ErrorLogger o-- IHandlerException : caches
    ErrorLogger --> IErrorLogger : OnInitializedCallback
    ErrorLogger --> IHandlerException : HandlerExceptionAsync uses

    BootstrapBlazorErrorBoundary --> IErrorBoundaryLogger : uses
    BootstrapBlazorErrorBoundary --> ErrorRender : renders on page exception

    TabItemContent --> ErrorLogger : uses
    TabItemContent --> ILogger : HandlerErrorCoreAsync

    IErrorLogger <.. ErrorLogger : implements
    IHandlerException <.. ErrorLogger : calls
    IErrorBoundaryLogger <.. BootstrapBlazorErrorBoundary : injected
    ErrorRender --> IConfiguration : uses for details
Loading

File-Level Changes

Change Details Files
Refactor BootstrapBlazorErrorBoundary to delegate logging to IErrorBoundaryLogger and use ErrorRender for exception UI while simplifying render logic.
  • Replace IConfiguration injection with IErrorBoundaryLogger and call LogErrorAsync in OnErrorAsync
  • Simplify BuildRenderTree to branch on CurrentException, distinguish page vs component exceptions, and reset the base CurrentException via reflection
  • Move exception UI rendering into a new RenderException method that uses ErrorRender or OnErrorHandleAsync
  • Remove toast-based error UI and internal exception/handler plumbing
src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs
Redesign ErrorLogger to directly invoke registered IHandlerException instances and OnErrorHandleAsync without coupling to BootstrapBlazorErrorBoundary.
  • Remove stored reference to BootstrapBlazorErrorBoundary and its RenderException method
  • Implement HandlerExceptionAsync to call the last registered handler with a standard ErrorRender-based fragment
  • Invoke OnErrorHandleAsync with only the exception argument
  • Keep a simple handler cache for registration
src/BootstrapBlazor/Components/ErrorLogger/ErrorLogger.cs
Introduce ErrorRender component that encapsulates configuration-driven exception rendering.
  • Implement ErrorRender as a low-level IComponent that renders the configured exception
  • Inject IConfiguration to decide between detailed stack trace and message-only output
  • Expose an Exception parameter via ParameterView and render a div.error-stack accordingly
src/BootstrapBlazor/Components/ErrorLogger/ErrorRender.cs
Align TabItemContent, sample pages, and tests with the new error-handling and callback signatures.
  • Update TabItemContent to provide a default HandlerErrorCoreAsync that uses ErrorRender and matches the new OnErrorHandleAsync signature
  • Tidy minor sample code in TreeViews and GlobalException files
  • Adjust Layout and ErrorLogger unit tests to use the new OnErrorHandleAsync(Exception) signature and updated button click triggering
  • Normalize Error page file formatting
src/BootstrapBlazor/Components/Tab/TabItemContent.cs
src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs
src/BootstrapBlazor.Server/Components/Pages/Error.razor
src/BootstrapBlazor.Server/Components/Samples/GlobalException.razor.cs
test/UnitTest/Components/LayoutTest.cs
test/UnitTest/Components/ErrorLoggerTest.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#7557 Redesign the ErrorLogger component and its associated error boundary to change how errors are captured, logged, and rendered.
#7557 Introduce and wire up a dedicated error rendering component and update all usages/tests to the new ErrorLogger API.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues, and left some high level feedback:

  • In both BootstrapBlazorErrorBoundary.RenderException and ErrorLogger.HandlerExceptionAsync the render fragments call builder.OpenComponent<ErrorRender>(0); but then use builder.CloseElement(); instead of builder.CloseComponent();, which will generate an invalid render tree.
  • The new ErrorRender class implements IComponent but relies on [Inject] IConfiguration Configuration { get; set; }; property injection is only performed for ComponentBase-derived components, so Configuration will remain null unless you refactor this to derive from ComponentBase or inject IConfiguration via some other explicit mechanism.
  • The delegate type for ErrorLogger.OnErrorHandleAsync appears to have changed to Func<Exception, Task>, but TabItemContent.HandlerErrorCoreAsync still has the signature (ILogger logger, Exception ex), which will not match the expected delegate type and should be updated to accept only the exception.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In both `BootstrapBlazorErrorBoundary.RenderException` and `ErrorLogger.HandlerExceptionAsync` the render fragments call `builder.OpenComponent<ErrorRender>(0);` but then use `builder.CloseElement();` instead of `builder.CloseComponent();`, which will generate an invalid render tree.
- The new `ErrorRender` class implements `IComponent` but relies on `[Inject] IConfiguration Configuration { get; set; }`; property injection is only performed for `ComponentBase`-derived components, so `Configuration` will remain null unless you refactor this to derive from `ComponentBase` or inject `IConfiguration` via some other explicit mechanism.
- The delegate type for `ErrorLogger.OnErrorHandleAsync` appears to have changed to `Func<Exception, Task>`, but `TabItemContent.HandlerErrorCoreAsync` still has the signature `(ILogger logger, Exception ex)`, which will not match the expected delegate type and should be updated to accept only the exception.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs:101-76` </location>
<code_context>
-    private Exception? _exception = null;
-
-    private RenderFragment<Exception> ExceptionContent => ex => builder =>
+    private void RenderException(RenderTreeBuilder builder, Exception ex)
     {
-        if (ErrorContent != null)
+        if (OnErrorHandleAsync is not null)
         {
-            builder.AddContent(0, ErrorContent(ex));
+            var renderTask = OnErrorHandleAsync(Logger, ex);
         }
         else
         {
</code_context>

<issue_to_address>
**issue (bug_risk):** RenderException does not render any content when OnErrorHandleAsync is set and also ignores the returned Task.

With `OnErrorHandleAsync` set, this path only calls the callback and never writes to the render tree, so the error results in no UI. It also assigns the callback’s `Task` to `renderTask` but never awaits or observes it, risking unobserved exceptions. Please either (a) have the callback return a fragment that you render here, or (b) use the callback only for side effects and still render fallback UI, and in either case make this path properly async/await or clearly treat it as fire-and-forget with appropriate exception handling.
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs:109-111` </location>
<code_context>
-            builder.OpenElement(0, "div");
-            builder.AddAttribute(1, "class", "error-stack");
-            builder.AddContent(2, GetErrorContentMarkupString(ex));
+            builder.OpenComponent<ErrorRender>(0);
+            builder.AddAttribute(1, "Exception", ex);
             builder.CloseElement();
         }
-    };
</code_context>

<issue_to_address>
**issue (bug_risk):** CloseElement is used for a component, which should be closed with CloseComponent instead.

`OpenComponent<ErrorRender>(0)` must be paired with `CloseComponent()`, not `CloseElement()`. Using `CloseElement()` here will cause an `InvalidOperationException` at runtime when the renderer validates the render tree calls.
</issue_to_address>

### Comment 3
<location> `src/BootstrapBlazor/Components/ErrorLogger/ErrorLogger.cs:142-146` </location>
<code_context>
+        var handler = _cache.LastOrDefault();
+        if (handler is not null)
+        {
+            await handler.HandlerExceptionAsync(exception, ex => builder =>
+            {
+                builder.OpenComponent<ErrorRender>(0);
+                builder.AddAttribute(1, "Exception", ex);
+                builder.CloseElement();
+            });
+        }
</code_context>

<issue_to_address>
**issue (bug_risk):** ErrorLogger’s error fragment also closes a component with CloseElement instead of CloseComponent.

Here you open an `ErrorRender` with `builder.OpenComponent<ErrorRender>(0);` but close it with `builder.CloseElement();`. This mismatch will throw at render time. Use `builder.CloseComponent();` to correctly close the component.
</issue_to_address>

### Comment 4
<location> `src/BootstrapBlazor/Components/Tab/TabItemContent.cs:83` </location>
<code_context>
             return Task.CompletedTask;
         }));
-        builder.AddAttribute(7, nameof(ErrorLogger.OnErrorHandleAsync), TabSet.OnErrorHandleAsync);
+        builder.AddAttribute(7, nameof(ErrorLogger.OnErrorHandleAsync), TabSet.OnErrorHandleAsync ?? HandlerErrorCoreAsync);
         builder.CloseComponent();
     }
</code_context>

<issue_to_address>
**issue (bug_risk):** TabItemContent wires OnErrorHandleAsync to a (ILogger, Exception) handler, which may not match ErrorLogger’s parameter type.

`TabSet.OnErrorHandleAsync ?? HandlerErrorCoreAsync` means `ErrorLogger.OnErrorHandleAsync` is treated as a `(ILogger logger, Exception ex) => Task` delegate, but `ErrorLogger.HandlerExceptionAsync` invokes `OnErrorHandleAsync` with only an `Exception`. Please align the `OnErrorHandleAsync` delegate signature and its invocation (and any public surface using it) so they consistently match, then adjust either this binding or the call site.
</issue_to_address>

### Comment 5
<location> `src/BootstrapBlazor/Components/ErrorLogger/ErrorRender.cs:11-15` </location>
<code_context>
+
+namespace BootstrapBlazor.Components;
+
+class ErrorRender : IComponent
+{
+    [Inject]
+    [NotNull]
+    private IConfiguration? Configuration { get; set; }
+
+    private RenderHandle _renderHandle;
</code_context>

<issue_to_address>
**issue (bug_risk):** Using [Inject] on an IComponent implementation is unlikely to work and can lead to null Configuration at runtime.

Blazor only applies `[Inject]` to `ComponentBase`-derived components; it does not perform property injection on a raw `IComponent`, so this property will never be set. Because `GetErrorContentMarkupString` dereferences `Configuration` without a null check, this can result in a `NullReferenceException`. Consider either deriving `ErrorRender` from `ComponentBase` so DI works as expected, or passing `IConfiguration` explicitly (e.g., via a `[Parameter]`).
</issue_to_address>

### Comment 6
<location> `test/UnitTest/Components/ErrorLoggerTest.cs:89-90` </location>
<code_context>
             });
         });
         var button = cut.Find("button");
-        button.TriggerEvent("onclick", EventArgs.Empty);
+        await cut.InvokeAsync(() => button.Click());
         var result = await tcs.Task;
         Assert.True(result);
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for the new ErrorLogger.HandlerExceptionAsync behavior (handler and callback interactions)

`ErrorLogger.HandlerExceptionAsync` now (1) uses the last registered `IHandlerException` to render via `ErrorRender`, and (2) always calls `OnErrorHandleAsync(exception)` without the logger. This test only checks that `OnErrorHandleAsync` runs on button click.

Please add tests that:
- Register a fake `IHandlerException` and assert `HandlerExceptionAsync` calls the handler exactly once with the thrown exception and a non-null `RenderFragment`.
- Cover the case where no handlers are registered and confirm `OnErrorHandleAsync` is still invoked.
- Optionally assert that the handler’s render fragment uses `ErrorRender` (e.g., `.error-stack` wrapper or the exception message when `DetailedErrors` is disabled).

This will more fully exercise the new error pipeline and `ErrorRender` integration.

Suggested implementation:

```csharp
            });
        });
        var button = cut.Find("button");
        await cut.InvokeAsync(() => button.Click());
        var result = await tcs.Task;
        Assert.True(result);
    }

    [Fact]
    public async Task HandlerExceptionAsync_UsesLastRegisteredHandler_WithExceptionAndRenderFragment()
    {
        using var ctx = new TestContext();

        var thrownException = new InvalidOperationException("boom");
        Exception? receivedException = null;
        RenderFragment? receivedRenderFragment = null;

        var handlerMock = new Mock<IHandlerException>();
        handlerMock
            .Setup(h => h.HandlerExceptionAsync(It.IsAny<Exception>(), It.IsAny<RenderFragment>()))
            .Returns<Exception, RenderFragment>((ex, fragment) =>
            {
                receivedException = ex;
                receivedRenderFragment = fragment;
                return Task.CompletedTask;
            });

        // Register multiple handlers to ensure the last one is used
        ctx.Services.AddSingleton<IHandlerException, NoopHandlerException>();
        ctx.Services.AddSingleton<IHandlerException>(handlerMock.Object);

        var cut = ctx.RenderComponent<ErrorLoggerTestHost>(parameters => parameters
            .Add(p => p.ThrowOnClick, thrownException));

        // Act
        await cut.InvokeAsync(() => cut.Find("button").Click());

        // Assert
        handlerMock.Verify(
            h => h.HandlerExceptionAsync(It.IsAny<Exception>(), It.IsAny<RenderFragment>()),
            Times.Once);
        Assert.Same(thrownException, receivedException);
        Assert.NotNull(receivedRenderFragment);
    }

    [Fact]
    public async Task HandlerExceptionAsync_WhenNoHandlersRegistered_StillInvokesOnErrorHandleAsync()
    {
        using var ctx = new TestContext();

        var tcs = new TaskCompletionSource<Exception>();

        var cut = ctx.RenderComponent<ErrorLoggerTestHost>(parameters => parameters
            .Add(p => p.OnErrorHandleAsync, (Exception ex) =>
            {
                tcs.TrySetResult(ex);
                return Task.CompletedTask;
            }));

        // Act
        await cut.InvokeAsync(() => cut.Find("button").Click());

        // Assert
        var exceptionFromCallback = await tcs.Task;
        Assert.NotNull(exceptionFromCallback);
    }

    [Fact]
    public async Task HandlerExceptionAsync_UsesErrorRender_ForHandlerRenderFragment()
    {
        using var ctx = new TestContext();

        var thrownException = new InvalidOperationException("render error");
        RenderFragment? receivedRenderFragment = null;

        var handlerMock = new Mock<IHandlerException>();
        handlerMock
            .Setup(h => h.HandlerExceptionAsync(It.IsAny<Exception>(), It.IsAny<RenderFragment>()))
            .Returns<Exception, RenderFragment>((ex, fragment) =>
            {
                receivedRenderFragment = fragment;
                return Task.CompletedTask;
            });

        ctx.Services.AddSingleton<IHandlerException>(handlerMock.Object);

        var cut = ctx.RenderComponent<ErrorLoggerTestHost>(parameters => parameters
            .Add(p => p.ThrowOnClick, thrownException));

        // Act
        await cut.InvokeAsync(() => cut.Find("button").Click());

        // Assert - fragment is provided
        Assert.NotNull(receivedRenderFragment);

        // Render the fragment and assert ErrorRender integration
        var fragmentHost = ctx.Render(receivedRenderFragment);
        var markup = fragmentHost.Markup;

        // When DetailedErrors is enabled, ErrorRender typically wraps stack/details in a container,
        // e.g. `.error-stack`. When disabled, at least the message should be present.
        Assert.True(
            markup.Contains("error-stack", StringComparison.OrdinalIgnoreCase) ||
            markup.Contains(thrownException.Message, StringComparison.OrdinalIgnoreCase),
            "The handler render fragment should be produced by ErrorRender (wrapper or message).");
    }

    /// <summary>
    /// Simple no-op handler used to verify that the *last* registered IHandlerException is used.
    /// </summary>
    private sealed class NoopHandlerException : IHandlerException
    {
        public Task HandlerExceptionAsync(Exception exception, RenderFragment renderFragment)
            => Task.CompletedTask;
    }

```

These tests assume the following are already available in the project:
- `ErrorLoggerTestHost` (or equivalent host component used in the existing tests) with parameters:
  - `Exception ThrowOnClick` (or similar) that causes the exception to be thrown when the button is clicked.
  - `Func<Exception, Task> OnErrorHandleAsync` callback used by `ErrorLogger`.
- `IHandlerException` interface with `Task HandlerExceptionAsync(Exception exception, RenderFragment renderFragment)` signature.
- Usings and packages:
  - `using Bunit;`
  - `using Microsoft.AspNetCore.Components;`
  - `using Moq;`
  - `using Microsoft.Extensions.DependencyInjection;`
  - `using System;`
  - `using System.Threading.Tasks;`
If any of these differ (e.g. host component name, parameter names, or the handler method signature), adjust the parameter/property names and the `HandlerExceptionAsync` signature in the tests accordingly. Also, if `ErrorRender` produces different CSS classes/markup than `.error-stack`, adapt the `markup.Contains(...)` assertions to match the actual rendered output.
</issue_to_address>

### Comment 7
<location> `test/UnitTest/Components/LayoutTest.cs:613-611` </location>
<code_context>
             {
                 pb.Add(a => a.UseTabSet, false);
-                pb.Add(a => a.OnErrorHandleAsync, (logger, ex) =>
+                pb.Add(a => a.OnErrorHandleAsync, ex =>
                 {
                     ex1 = ex;
                     return Task.CompletedTask;
</code_context>

<issue_to_address>
**suggestion (testing):** Add coverage for the default Tab error handling path when no custom OnErrorHandleAsync is provided

The layout tests now match the new `OnErrorHandleAsync(Exception ex)` signature, but they only cover the case where a custom callback is provided. The default behavior in `TabItemContent` (`TabSet.OnErrorHandleAsync ?? HandlerErrorCoreAsync`) isn’t exercised.

Please add a test that:
- Uses a `TabSet` without `OnErrorHandleAsync` so `HandlerErrorCoreAsync` runs.
- Triggers an exception in the tab content.
- Verifies the error is rendered via `ErrorRender` (e.g., `class="error-stack"` or the exception text in the DOM) and that the normal tab content is not rendered in the error state.

This will validate the default error path and guard against regressions when `OnErrorHandleAsync` is not set.

Suggested implementation:

```csharp
            pb.AddChildContent<Layout>(pb =>
            {
                pb.Add(a => a.UseTabSet, false);
                pb.Add(a => a.OnErrorHandleAsync, ex =>
                {
                    ex1 = ex;
                    return Task.CompletedTask;
            pb.Add(a => a.EnableErrorLogger, true);
            pb.AddChildContent<Layout>(pb =>
            {
                pb.Add(a => a.OnErrorHandleAsync, ex =>
                {
                    ex1 = ex;
                    return Task.CompletedTask;

```

To implement the requested coverage for the default `TabSet` error handling path (when `OnErrorHandleAsync` is not set), add a new test method to `LayoutTest.cs` similar to the existing Tab error-handling tests. Place it alongside the other layout/tab tests inside the `LayoutTest` class.

Here is a concrete example you can paste into the class:

```csharp
[Fact]
public void Layout_TabSet_DefaultErrorHandler_RendersErrorAndHidesContent()
{
    // Arrange
    using var ctx = new TestContext();
    ctx.JSInterop.Mode = JSRuntimeMode.Loose;

    var cut = ctx.RenderComponent<Layout>(pb =>
    {
        // Use TabSet with default error handler – do NOT set OnErrorHandleAsync here
        pb.Add(a => a.UseTabSet, true);
        pb.Add(a => a.EnableErrorLogger, true);
        pb.AddChildContent<Tabs>(tabPb =>
        {
            tabPb.AddChildContent<TabItem>(itemPb =>
            {
                itemPb.Add(a => a.Text, "Tab 1");
                itemPb.AddChildContent(builder =>
                {
                    // This will throw during render to trigger the Tab error path
                    throw new InvalidOperationException("Tab content failure");
                });
            });
        });
    });

    // Act
    // Force a render that evaluates the tab content and triggers the exception.
    // The exception should be handled by TabSet.HandlerErrorCoreAsync via
    // TabItemContent (TabSet.OnErrorHandleAsync ?? HandlerErrorCoreAsync).
    cut.Render();

    // Assert
    // Verify error UI is rendered by the default TabSet handler
    cut.MarkupMatches(markup =>
        markup.Contains("error-stack", StringComparison.OrdinalIgnoreCase) ||
        markup.Contains("Tab content failure", StringComparison.OrdinalIgnoreCase));

    // Optionally, assert that the normal tab content is not present when in error state.
    // Replace "Tab 1 content" with any known normal content text that would appear in a non-error render.
    cut.MarkupDoesNotContain("Tab 1 content");
}
```

You may need to align:
1. The component types (`Layout`, `Tabs`, `TabItem`) and parameter names (`UseTabSet`, `EnableErrorLogger`, `Text`) with the actual components in your project.
2. The error container selector: instead of checking for `"error-stack"` via `Contains`, you might use your existing pattern, e.g. `cut.Find(".error-stack")` if the tests normally use CSS selectors.
3. The way you assert markup. If you use `cut.Markup.Contains(...)` or FluentAssertions-style helpers already in `LayoutTest.cs`, adjust the assertions to match the existing convention.
4. The normal tab content text you assert is absent (replace `"Tab 1 content"` with something your Layout/TabItem would normally render on success).

This test ensures that when `OnErrorHandleAsync` is not provided, the default `HandlerErrorCoreAsync` path is exercised, the error is rendered via `ErrorRender`, and the original tab content is hidden in the error state.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs Outdated
Comment thread src/BootstrapBlazor/Components/Tab/TabItemContent.cs Outdated
Comment thread src/BootstrapBlazor/Components/ErrorLogger/ErrorRender.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR redesigns the ErrorLogger component by refactoring the error handling architecture. The main goal is to simplify the error handling callback signature and introduce a new ErrorRender component for displaying exceptions.

Changes:

  • Refactored OnErrorHandleAsync callback to remove the ILogger parameter (changed from 2 parameters to 1)
  • Introduced new ErrorRender component to encapsulate error rendering logic
  • Simplified BootstrapBlazorErrorBoundary by delegating logging to IErrorBoundaryLogger
  • Updated tests to use the new API signature and modern bUnit testing patterns

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/BootstrapBlazor/Components/ErrorLogger/ErrorLogger.cs Modified HandlerExceptionAsync implementation and removed _errorBoundary field; introduced direct rendering approach
src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs Refactored error handling logic; replaced Configuration with IErrorBoundaryLogger; simplified BuildRenderTree
src/BootstrapBlazor/Components/ErrorLogger/ErrorRender.cs New component to encapsulate exception rendering logic with configuration-based detail display
src/BootstrapBlazor/Components/Tab/TabItemContent.cs Added HandlerErrorCoreAsync method and integrated ErrorRender component
test/UnitTest/Components/LayoutTest.cs Updated tests to use new single-parameter OnErrorHandleAsync signature
test/UnitTest/Components/ErrorLoggerTest.cs Modernized test to use Click() instead of TriggerEvent
src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs Minor formatting: added spaces in log message
src/BootstrapBlazor.Server/Components/Samples/GlobalException.razor.cs Removed BOM character from file
src/BootstrapBlazor.Server/Components/Pages/Error.razor Formatting cleanup: removed extra blank lines

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/BootstrapBlazor/Components/ErrorLogger/ErrorLogger.cs Outdated
Comment thread src/BootstrapBlazor/Components/ErrorLogger/ErrorLogger.cs Outdated
Comment thread src/BootstrapBlazor/Components/Tab/TabItemContent.cs Outdated
Comment thread src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs Outdated
Comment thread src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs Outdated
Comment thread src/BootstrapBlazor/Components/ErrorLogger/BootstrapBlazorErrorBoundary.cs Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c237053) to head (d43af10).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7558   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          748       749    +1     
  Lines        33004     32974   -30     
  Branches      4589      4580    -9     
=========================================
- Hits         33004     32974   -30     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ArgoZhang ArgoZhang merged commit 4169c45 into main Jan 22, 2026
3 checks passed
@ArgoZhang ArgoZhang deleted the fix-error branch January 22, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ErrorLogger): redesign ErrorLogger component

2 participants