Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
"rollForward": false
},
"docfx": {
"version": "2.78.5",
"version": "2.81.0",
"commands": [
"docfx"
],
"rollForward": false
},
"dotnet-dump": {
"version": "10.0.745401",
"version": "10.0.750501",
"commands": [
"dotnet-dump"
],
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to **bUnit** will be documented in this file. The project ad

## [Unreleased]

### Fixed

- `WrapperBase.WrappedElement` could return `null` and throw `NullReferenceException` when an element was accessed concurrently with component re-renders. Reported by [@psu-prog](https://github.com/psu-prog) in #1937. Fixed by [@Nacl-HCl](https://github.com/Nacl-HCl).
- `DisposeComponentsAsync()` could dispose nothing when the renderer's dispatcher was busy, e.g. with a timer calling `InvokeAsync`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1939. Fixed by [@linkdotnet](https://github.com/linkdotnet).
- Markup generation and `FindComponents` could recurse until the test host crashed with a stack overflow when a render threw while instantiating a child component. Reported in #1935. Fixed by [@linkdotnet](https://github.com/linkdotnet).
- `Find()` could keep returning DOM nodes from the previous render when a render, e.g. from a timer, happened while the nodes were parsed, causing `UnknownEventHandlerIdException`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1938. Fixed by [@linkdotnet](https://github.com/linkdotnet).

## [2.11.3] - 2026-09-13

### Fixed
Expand Down
107 changes: 54 additions & 53 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<GlobalPackageReference Include="AsyncFixer" Version="2.1.0" PrivateAssets="All" IncludeAssets="Runtime;Build;Native;contentFiles;Analyzers"/>
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="10.29.0.143774" PrivateAssets="All" IncludeAssets="Runtime;Build;Native;contentFiles;Analyzers"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Meziantou.Polyfill" Version="1.0.165" />
<PackageVersion Include="Meziantou.Polyfill" Version="1.0.166" />
</ItemGroup>

<ItemGroup Label="Shared">
Expand All @@ -18,8 +18,8 @@
<PackageVersion Include="Serilog" Version="4.4.0"/>
<PackageVersion Include="Serilog.Expressions" Version="5.0.0"/>
<PackageVersion Include="AngleSharp.Diffing" Version="1.1.1"/>
<PackageVersion Include="AngleSharp" Version="1.8.1"/>
<PackageVersion Include="AngleSharp.Css" Version="1.1.2"/>
<PackageVersion Include="AngleSharp" Version="1.8.5"/>
<PackageVersion Include="AngleSharp.Css" Version="1.1.3"/>
</ItemGroup>

<ItemGroup Label="Package Versioning">
Expand All @@ -29,86 +29,86 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.32"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.29"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.32"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.32"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="8.0.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="8.0.32"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.32"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.32"/>

<PackageVersion Include="System.Text.Json" Version="8.0.5"/>
<PackageVersion Include="System.Text.Json" Version="8.0.6"/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.12"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.12"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.12"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.12"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.12"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.12"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="9.0.12"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.12"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.12"/>

<PackageVersion Include="System.Text.Json" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.21"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.21"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.21"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.21"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.21"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.21"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="9.0.21"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.21"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.21"/>

<PackageVersion Include="System.Text.Json" Version="9.0.21"/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.10"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.13"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.13"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.10"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.10"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.10"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.10"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.10"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.13"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.13"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.13"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.13"/>

<PackageVersion Include="System.Text.Json" Version="10.0.0"/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net11.0'">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="11.0.0-rc.1.26425.128"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="11.0.0-rc.1.26425.128"/>

<PackageVersion Include="System.Text.Json" Version="11.0.0-rc.1.26425.128"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="11.0.0-rc.2.26504.105"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="11.0.0-rc.2.26504.105"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="11.0.0-rc.2.26504.105"/>

<PackageVersion Include="System.Text.Json" Version="11.0.0-rc.2.26504.105"/>
</ItemGroup>

<ItemGroup Label="Test Dependencies">
<PackageVersion Include="AutoFixture" Version="4.18.1"/>
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0"/>
<PackageVersion Include="MSTest.TestAdapter" Version="3.6.0" />
<PackageVersion Include="MSTest.TestFramework" Version="3.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.0"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0"/>
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NSubstitute" Version="6.2.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="5.0.0" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Shouldly" Version="4.3.0"/>
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0"/>
<PackageVersion Include="Verify.XunitV3" Version="32.0.0"/>
<PackageVersion Include="Xunit.Combinatorial" Version="2.0.24"/>
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.1" />
<PackageVersion Include="nunit" Version="4.4.0" />
<PackageVersion Include="Verify.XunitV3" Version="33.3.4"/>
<PackageVersion Include="Xunit.Combinatorial" Version="2.1.43"/>
<PackageVersion Include="coverlet.collector" Version="10.1.0" />
<PackageVersion Include="coverlet.msbuild" Version="10.1.0" />
<PackageVersion Include="nunit" Version="4.6.1" />
<PackageVersion Include="xunit" Version="2.9.3"/>
<PackageVersion Include="xunit.v3" Version="4.0.0"/>
<PackageVersion Include="xunit.v3.extensibility.core" Version="4.0.0" />
<PackageVersion Include="xunit.v3.assert" Version="4.0.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0"/>
<PackageVersion Include="xunit.v3" Version="4.0.2"/>
<PackageVersion Include="xunit.v3.extensibility.core" Version="4.0.2" />
<PackageVersion Include="xunit.v3.assert" Version="4.0.2"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.1"/>
</ItemGroup>

<ItemGroup Label="Source Code Generators">
Expand All @@ -117,5 +117,6 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.4" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="9.0.21" />
</ItemGroup>
</Project>
8 changes: 1 addition & 7 deletions src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@ public abstract class WrapperBase<TElement> : IElementWrapper<TElement>
public TElement WrappedElement
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (element is null)
element = elementFactory.GetElement<TElement>();

return element;
}
get => element ??= elementFactory.GetElement<TElement>();
}

/// <summary>
Expand Down
43 changes: 13 additions & 30 deletions src/bunit/Rendering/BunitRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,27 +281,21 @@ public IReadOnlyList<IRenderedComponent<TComponent>> FindComponents<TComponent>(
public Task DisposeComponents()
{
ObjectDisposedException.ThrowIf(disposed, this);
Task? returnTask;

lock (renderTreeUpdateLock)
return Dispatcher.InvokeAsync(async () =>
{
returnTask = Dispatcher.InvokeAsync(async () =>
{
ResetUnhandledException();

foreach (var root in rootComponents)
{
root.Detach();
}

await Task.WhenAll(disposalTasks).ConfigureAwait(false);
disposalTasks.Clear();
});
ResetUnhandledException();

var roots = rootComponents.ToArray();
rootComponents.Clear();
}

return returnTask;
foreach (var root in roots)
{
root.Detach();
}

await Task.WhenAll(disposalTasks.ToArray()).ConfigureAwait(false);
});
}

/// <inheritdoc/>
Expand Down Expand Up @@ -425,21 +419,11 @@ protected override void AddPendingTask(ComponentState? componentState, Task task
if (componentState is null)
{
ArgumentNullException.ThrowIfNull(task);
AddDisposalTaskToQueue();
disposalTasks.RemoveAll(static t => t.IsCompleted);
disposalTasks.Add(task);
}

base.AddPendingTask(componentState, task);

void AddDisposalTaskToQueue()
{
var t = task;
t = task.ContinueWith(_ =>
{
disposalTasks.Remove(t);
}, TaskScheduler.Current);

disposalTasks.Add(t);
}
}

internal Task SetDirectParametersAsync<TComponent>(IRenderedComponent<TComponent> renderedComponent, ParameterView parameters)
Expand Down Expand Up @@ -602,7 +586,6 @@ protected override void Dispose(bool disposing)
if (disposing)
{
returnedRenderedComponentIds.Clear();
disposalTasks.Clear();
unhandledExceptionTsc.TrySetCanceled();
}

Expand Down Expand Up @@ -678,7 +661,7 @@ void FindComponentsInRenderTree(int componentId)
for (var i = 0; i < frames.Count; i++)
{
ref var frame = ref frames.Array[i];
if (frame.FrameType == RenderTreeFrameType.Component)
if (frame.FrameType == RenderTreeFrameType.Component && frame.Component is not null)
{
if (frame.Component is TComponent)
{
Expand Down
8 changes: 8 additions & 0 deletions src/bunit/Rendering/Internal/Htmlizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ int position
)
{
var frame = frames.Array[position];

// A component frame is left uninstantiated (ComponentId 0) when the render that added it threw mid-diff.
// Following that id would recurse into the first root component, which can be an ancestor.
if (frame.Component is null)
{
return position + frame.ComponentSubtreeLength;
}

var childFrames = context.GetRenderTreeFrames(frame.ComponentId);
RenderFrames(context, childFrames, 0, childFrames.Count);
return position + frame.ComponentSubtreeLength;
Expand Down
16 changes: 14 additions & 2 deletions src/bunit/Rendering/RenderedComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal sealed class RenderedComponent<TComponent> : ComponentState, IRenderedC
private readonly BunitHtmlParser htmlParser;

private string markup = string.Empty;
private INodeList? latestRenderNodes;
private MarkupNodes? latestRenderNodes;

/// <summary>
/// Gets the component under test.
Expand Down Expand Up @@ -68,7 +68,17 @@ public INodeList Nodes
get
{
EnsureComponentNotDisposed();
return latestRenderNodes ??= htmlParser.Parse(Markup);
// A render can finish while Markup is parsed; keying on the markup reference stops that stale parse from being cached (#1938).
// Not needed in v3: the shared render tree (#1910) binds nodes to an immutable markup snapshot.
var currentMarkup = Markup;
var cached = latestRenderNodes;
if (cached is null || !ReferenceEquals(cached.Markup, currentMarkup))
{
cached = new MarkupNodes(currentMarkup, htmlParser.Parse(currentMarkup));
latestRenderNodes = cached;
}

return cached.Nodes;
}
}

Expand Down Expand Up @@ -169,4 +179,6 @@ public override ValueTask DisposeAsync()
Dispose();
return base.DisposeAsync();
}

private sealed record MarkupNodes(string Markup, INodeList Nodes);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//HintName: WrapperBase.g.cs
//HintName: WrapperBase.g.cs
#nullable enable
using System.CodeDom.Compiler;
using System.Diagnostics;
Expand All @@ -25,13 +25,7 @@ public abstract class WrapperBase<TElement> : IElementWrapper<TElement>
public TElement WrappedElement
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (element is null)
element = elementFactory.GetElement<TElement>();

return element;
}
get => element ??= elementFactory.GetElement<TElement>();
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions tests/bunit.generators.tests/bunit.generators.tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
<Verify_SponsorshipExemption>OpenSource</Verify_SponsorshipExemption>
<Verify_SponsorshipExemptionUntil>2027-10</Verify_SponsorshipExemptionUntil>
<RootNamespace>Bunit</RootNamespace>
<AssemblyName>Bunit.Generator.Tests</AssemblyName>
<ImplicitUsings>true</ImplicitUsings>
Expand All @@ -21,6 +23,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
<PackageReference Include="System.Security.Cryptography.Pkcs" />
</ItemGroup>

<ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions tests/bunit.testassets/.globalconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Razor SDK 11 moves component declarations into *_razor.decl.g.cs, where Sonar flags @implements IDisposable.
# .editorconfig path sections do not match generated files, so this must be global.
is_global = true

dotnet_diagnostic.S3881.severity = none
Loading
Loading