diff --git a/CHANGELOG.md b/CHANGELOG.md index e732dd9da..97b676064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad ## [Unreleased] +### Fixed + +- An element returned by `Find()` could be `null` for a moment when the component re-rendered on another thread, so using it threw a `NullReferenceException`, for example from `GetBunitContext` in `ClickAsync`. Reported and fixed by [@psu-prog](https://github.com/psu-prog). + ## [2.11.3] - 2026-09-13 ### Fixed diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs index 8dd4d5963..2fc1a14ba 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs @@ -26,10 +26,8 @@ public TElement WrappedElement [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (element is null) - element = elementFactory.GetElement(); - - return element; + // Read the field once: a re-render on another thread can reset it to null between two reads. + return element ??= elementFactory.GetElement(); } } diff --git a/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs b/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs index 2f8cc5e05..92a546836 100644 --- a/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs +++ b/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs @@ -27,10 +27,8 @@ public TElement WrappedElement [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (element is null) - element = elementFactory.GetElement(); - - return element; + // Read the field once: a re-render on another thread can reset it to null between two reads. + return element ??= elementFactory.GetElement(); } } diff --git a/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs b/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs index 4f1602435..efbd901b6 100644 --- a/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs +++ b/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs @@ -76,6 +76,30 @@ public void Test031() Should.Throw(() => btn.TextContent); } + [Fact(DisplayName = "Found element is never null while the component re-renders on another thread")] + public async Task Test040() + { + var cut = Render(); + var elm = cut.Find("span"); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + var renders = Task.Run(async () => + { + while (!cts.IsCancellationRequested) + await cut.InvokeAsync(cut.Instance.Increment); + }); + + try + { + while (!cts.IsCancellationRequested) + _ = elm.TextContent; + } + finally + { + await cts.CancelAsync(); + await renders; + } + } + private sealed class Markup : ComponentBase { [Parameter] public string Base { get; set; } = string.Empty; @@ -89,4 +113,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddMarkupContent(1, Optional); } } + + private sealed class Counter : ComponentBase + { + private int count; + + public void Increment() + { + count++; + StateHasChanged(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "span"); + builder.AddContent(1, count); + builder.CloseElement(); + } + } }