From 61588ca76eb4580f5375301990371d1c0aac1f44 Mon Sep 17 00:00:00 2001 From: bUnit bot Date: Tue, 31 Mar 2026 13:13:24 +0000 Subject: [PATCH 01/26] Set version to '2.8-preview' --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index af3733a1b..035a06cb3 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.7-preview", + "version": "2.8-preview", "assemblyVersion": { "precision": "revision" }, From 1effdb10609caa8c707e705ffaeaca358b4a0269 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 19 Apr 2026 10:41:38 +0200 Subject: [PATCH 02/26] chore: Update packages and next net11 preview --- .editorconfig | 1 + Directory.Packages.props | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.editorconfig b/.editorconfig index cfdab13ec..1fbb86aae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -479,6 +479,7 @@ csharp_style_pattern_local_over_anonymous_function = false # IDE0039: Use local # AsyncFixer # http://www.asyncfixer.com dotnet_diagnostic.AsyncFixer01.severity = none # AsyncFixer01: Unnecessary async/await usage +dotnet_diagnostic.AsyncFixer04.severity = none # 'cts' is not awaited inside a using block - the resource may be disposed before the async operation completes. Add 'await' to ensure proper resource lifetime. # Meziantou # https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a478743b..6583421e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,17 +5,17 @@ - + - + - + @@ -72,18 +72,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -116,6 +116,6 @@ - + From 8203cfdda5111da4355a856144cbb3c9c124c515 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 19 Apr 2026 10:45:00 +0200 Subject: [PATCH 03/26] chore: Update Sonar --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6583421e2..ad9882d5a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + From 4e58a5eebfee040265729f823ca5e22336564cc3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 29 May 2026 21:20:19 +0200 Subject: [PATCH 04/26] chore: Update deps --- .config/dotnet-tools.json | 6 +++--- .github/workflows/ci.yml | 8 ++++---- .github/workflows/docs-deploy.yml | 2 +- .github/workflows/prepare-release.yml | 4 ++-- .github/workflows/release.yml | 2 +- Directory.Packages.props | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 294e26e0b..a2ee9a494 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,21 +3,21 @@ "isRoot": true, "tools": { "dotnet-serve": { - "version": "1.10.175", + "version": "1.10.194", "commands": [ "dotnet-serve" ], "rollForward": false }, "docfx": { - "version": "2.78.2", + "version": "2.78.5", "commands": [ "docfx" ], "rollForward": false }, "dotnet-dump": { - "version": "9.0.553101", + "version": "9.0.661903", "commands": [ "dotnet-dump" ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed97f5e09..96d6cbafb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: 11.0.x - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 id: nbgv with: setAllVars: true @@ -169,7 +169,7 @@ jobs: path: ${{ env.NUGET_DIRECTORY }} - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 with: setAllVars: true @@ -227,7 +227,7 @@ jobs: 11.0.x - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 id: nbgv with: setAllVars: true @@ -269,7 +269,7 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v6 - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@v5 release-preview: if: github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2') diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 75a18d275..6379d8e49 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -50,7 +50,7 @@ jobs: git config --global commit.gpgsign true - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 id: nbgv with: setAllVars: true diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index ed15be861..a96183015 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -34,7 +34,7 @@ jobs: path: ./CHANGELOG.md - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 id: nbgv with: setAllVars: true @@ -60,7 +60,7 @@ jobs: git config --global commit.gpgsign true - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 with: setAllVars: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4dd478931..5441e7b85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: git config --global commit.gpgsign true - name: ⚙️ Setup GIT versioning - uses: dotnet/nbgv@v0.5.1 + uses: dotnet/nbgv@v0.5.2 id: nbgv with: setAllVars: true diff --git a/Directory.Packages.props b/Directory.Packages.props index ad9882d5a..94b06fbf9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + @@ -101,8 +101,8 @@ - - + + From 91b7da07d94dd697b0e431c38f95b4f9a1299ba8 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 3 Jun 2026 08:13:33 +0200 Subject: [PATCH 05/26] chore: Update packages --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 94b06fbf9..49415e0b5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + @@ -19,7 +19,7 @@ - + @@ -116,6 +116,6 @@ - + From 2708f36a8465af950b511defb6716633a5b605c4 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 29 May 2026 19:36:10 +0200 Subject: [PATCH 06/26] feat: Add async overloads --- CHANGELOG.md | 4 + ...entWaitForHelperExtensions.WaitForState.cs | 50 +++++++ .../WaitForHelpers/WaitForAssertionHelper.cs | 24 +++ .../WaitForHelpers/WaitForHelper.cs | 138 ++++++++++++++---- .../WaitForHelpers/WaitForStateHelper.cs | 17 +++ ...enderedComponentWaitForHelperExtensions.cs | 112 ++++++++++++++ 6 files changed, 315 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141be3761..62999cca5 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] +### Added + +- New overloads to WaitForHelpers to have async assertions and predicates. Reported by [@radmorecameron](https://github.com/radmorecameron) in #1833. Fixed by [@linkdotnet](https://github.com/linkdotnet). + ## [2.7.2] - 2026-03-31 ### Fixed diff --git a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForState.cs b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForState.cs index 17e6f2c57..8f66fd195 100644 --- a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForState.cs +++ b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForState.cs @@ -56,6 +56,31 @@ public static async Task WaitForStateAsync(this IRenderedComponent + /// Wait until the provided asynchronous returns true, + /// or the is reached (default is one second). + /// + /// The is evaluated initially, and then each time + /// the renders. + /// + /// The render fragment or component to attempt to verify state against. + /// The asynchronous predicate to invoke after each render, which must return true when the desired state has been reached. + /// The maximum time to wait for the desired state. + /// Throws if the throw an exception during invocation, or if the timeout has been reached. See the inner exception for details. + /// + /// The predicate is awaited on the renderer's dispatcher. Because awaiting yields the dispatcher, + /// a render may interleave at each await point, so the predicate is not guaranteed an atomic + /// view of the DOM across awaits. Use this overload when the check itself must await (for + /// example an async service or JavaScript interop call); prefer the synchronous + /// overload for pure DOM or markup checks. + /// + public static async Task WaitForStateAsync(this IRenderedComponent renderedComponent, Func> statePredicate, TimeSpan? timeout = null) + where TComponent : IComponent + { + using var waiter = new WaitForStateHelper(renderedComponent, statePredicate, timeout); + await waiter.WaitTask; + } + /// /// Wait until the provided passes (i.e. does not throw an /// exception), or the is reached (default is one second). @@ -99,4 +124,29 @@ public static async Task WaitForAssertionAsync(this IRenderedCompone using var waiter = new WaitForAssertionHelper(renderedComponent, assertion, timeout); await waiter.WaitTask; } + + /// + /// Wait until the provided asynchronous passes (i.e. does not throw an + /// exception), or the is reached (default is one second). + /// + /// The is attempted initially, and then each time the renders. + /// + /// The rendered fragment to wait for renders from and assert against. + /// The asynchronous verification or assertion to perform. + /// The maximum time to attempt the verification. + /// Thrown if the timeout has been reached. See the inner exception to see the captured assertion exception. + /// + /// The assertion is awaited on the renderer's dispatcher. Because awaiting yields the dispatcher, + /// a render may interleave at each await point, so the assertion is not guaranteed an atomic + /// view of the DOM across awaits. Use this overload when the assertion itself must await (for + /// example an async service or JavaScript interop call); prefer the synchronous + /// overload for pure DOM or markup assertions. + /// + [AssertionMethod] + public static async Task WaitForAssertionAsync(this IRenderedComponent renderedComponent, Func assertion, TimeSpan? timeout = null) + where TComponent : IComponent + { + using var waiter = new WaitForAssertionHelper(renderedComponent, assertion, timeout); + await waiter.WaitTask; + } } diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForAssertionHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForAssertionHelper.cs index d93663e6b..b1e145862 100644 --- a/src/bunit/Extensions/WaitForHelpers/WaitForAssertionHelper.cs +++ b/src/bunit/Extensions/WaitForHelpers/WaitForAssertionHelper.cs @@ -37,4 +37,28 @@ public WaitForAssertionHelper(IRenderedComponent renderedComponent, }, timeout) { } + + /// + /// Initializes a new instance of the class, + /// which will wait until the provided asynchronous passes (i.e. does not throw an + /// exception), or the is reached (default is one second). + /// + /// The is attempted initially, and then each time the renders. + /// + /// The rendered fragment to wait for renders from and assert against. + /// The asynchronous verification or assertion to perform. + /// The maximum time to attempt the verification. + /// + /// If a debugger is attached the timeout is set to , giving the possibility to debug without the timeout triggering. + /// + public WaitForAssertionHelper(IRenderedComponent renderedComponent, Func assertion, TimeSpan? timeout = null) + : base( + renderedComponent, + async () => + { + await assertion(); + return (true, default); + }, + timeout) + { } } diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs index ec6e8e247..d8e5111e1 100644 --- a/src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs +++ b/src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs @@ -12,12 +12,14 @@ internal abstract class WaitForHelper : IDisposable where TComponent : IComponent { private readonly TaskCompletionSource checkPassedCompletionSource; - private readonly Func<(bool CheckPassed, T Content)> completeChecker; + private readonly Func> completeChecker; private readonly IRenderedComponent renderedComponent; private readonly ILogger> logger; private readonly BunitRenderer renderer; private readonly Timer? timer; private bool isDisposed; + private bool isChecking; + private bool isDirty; private int checkCount; private Exception? capturedException; @@ -44,12 +46,31 @@ internal abstract class WaitForHelper : IDisposable public Task WaitTask { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// with a synchronous check. The check is wrapped into a completed , + /// so it is still evaluated synchronously (with no render interleaving) on the renderer's dispatcher. /// protected WaitForHelper( IRenderedComponent renderedComponent, Func<(bool CheckPassed, T Content)> completeChecker, TimeSpan? timeout = null) + : this(renderedComponent, WrapSynchronousChecker(completeChecker), timeout) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The is awaited on the renderer's dispatcher after each render. + /// A synchronous check (see the other constructor) wraps into an already-completed + /// and therefore resumes synchronously without yielding the dispatcher; a genuinely asynchronous check yields the + /// dispatcher at each await, so a render may interleave and the check is not guaranteed an atomic view of the DOM. + /// + protected WaitForHelper( + IRenderedComponent renderedComponent, + Func> completeChecker, + TimeSpan? timeout = null) { this.renderedComponent = renderedComponent ?? throw new ArgumentNullException(nameof(renderedComponent)); this.completeChecker = completeChecker ?? throw new ArgumentNullException(nameof(completeChecker)); @@ -166,40 +187,87 @@ private void OnAfterRender(object? sender, EventArgs args) if (isDisposed || WaitTask.IsCompleted) return; - try + // The check may be asynchronous, so it cannot be awaited inside this synchronous + // event handler. Kick it off without blocking the dispatcher. A synchronous check + // completes inline (see RunCheckAsync). If a check is already in flight, mark the + // state dirty so the running check re-evaluates once it completes, ensuring the + // final render is never missed. + if (isChecking) { - logger.LogCheckingWaitCondition(renderedComponent.ComponentId); + isDirty = true; + return; + } - var checkResult = completeChecker(); - checkCount++; - if (checkResult.CheckPassed) - { - checkPassedCompletionSource.TrySetResult(checkResult.Content); - logger.LogCheckCompleted(renderedComponent.ComponentId); - Dispose(); - } - else + isChecking = true; + _ = RunCheckAsync(); + } + + private async Task RunCheckAsync() + { + // Runs on the renderer's dispatcher. A synchronous check wraps into an already-completed + // ValueTask, so the await below resumes inline without yielding - preserving the atomic, + // no-render-interleaving behaviour synchronous checks rely on. A genuinely asynchronous + // check yields the dispatcher; the default await captures and resumes on the dispatcher's + // synchronization context, so isChecking/isDirty are only ever touched on the single + // dispatcher thread and need no locking. + try + { + do { - logger.LogCheckFailed(renderedComponent.ComponentId); + isDirty = false; + + if (isDisposed || WaitTask.IsCompleted) + return; + + try + { + logger.LogCheckingWaitCondition(renderedComponent.ComponentId); + + var checkResult = await completeChecker(); + checkCount++; + + if (isDisposed || WaitTask.IsCompleted) + return; + + if (checkResult.CheckPassed) + { + checkPassedCompletionSource.TrySetResult(checkResult.Content); + logger.LogCheckCompleted(renderedComponent.ComponentId); + Dispose(); + return; + } + + logger.LogCheckFailed(renderedComponent.ComponentId); + } + catch (Exception ex) + { + checkCount++; + capturedException = ex; + logger.LogCheckThrow(renderedComponent.ComponentId, ex); + + if (StopWaitingOnCheckException) + { + if (!isDisposed && !WaitTask.IsCompleted) + { + checkPassedCompletionSource.TrySetException( + new WaitForFailedException( + CheckThrowErrorMessage ?? string.Empty, + checkCount, + renderedComponent.RenderCount, + renderer.RenderCount, + capturedException)); + Dispose(); + } + + return; + } + } } + while (isDirty && !isDisposed && !WaitTask.IsCompleted); } - catch (Exception ex) + finally { - checkCount++; - capturedException = ex; - logger.LogCheckThrow(renderedComponent.ComponentId, ex); - - if (StopWaitingOnCheckException) - { - checkPassedCompletionSource.TrySetException( - new WaitForFailedException( - CheckThrowErrorMessage ?? string.Empty, - checkCount, - renderedComponent.RenderCount, - renderer.RenderCount, - capturedException)); - Dispose(); - } + isChecking = false; } } @@ -218,4 +286,14 @@ private static TimeSpan GetRuntimeTimeout(TimeSpan? timeout) ? Timeout.InfiniteTimeSpan : timeout ?? BunitContext.DefaultWaitTimeout; } + + private static Func> WrapSynchronousChecker(Func<(bool CheckPassed, T Content)> completeChecker) + { + ArgumentNullException.ThrowIfNull(completeChecker); + + // Wrap the synchronous check in an already-completed ValueTask so the single check + // pipeline can await it. Awaiting a completed ValueTask resumes inline, so the check + // still runs synchronously on the dispatcher with no render interleaving. + return () => new ValueTask<(bool CheckPassed, T Content)>(completeChecker()); + } } diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForStateHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForStateHelper.cs index ad603c0c0..4216cf7b1 100644 --- a/src/bunit/Extensions/WaitForHelpers/WaitForStateHelper.cs +++ b/src/bunit/Extensions/WaitForHelpers/WaitForStateHelper.cs @@ -34,4 +34,21 @@ public WaitForStateHelper(IRenderedComponent renderedComponent, Func : base(renderedComponent, () => (statePredicate(), default), timeout) { } + + /// + /// Initializes a new instance of the class, + /// which will wait until the provided asynchronous returns true, + /// or the is reached (default is one second). + /// + /// + /// The is evaluated initially, and then each time the renders. + /// + /// The render fragment or component to attempt to verify state against. + /// The asynchronous predicate to invoke after each render, which must return true when the desired state has been reached. + /// The maximum time to wait for the desired state. + /// Thrown if the throw an exception during invocation, or if the timeout has been reached. See the inner exception for details. + public WaitForStateHelper(IRenderedComponent renderedComponent, Func> statePredicate, TimeSpan? timeout = null) + : base(renderedComponent, async () => (await statePredicate(), default), timeout) + { + } } diff --git a/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.cs b/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.cs index 9b5a96be6..0d0044ea4 100644 --- a/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.cs +++ b/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.cs @@ -128,6 +128,118 @@ public void Test301() () => cut.WaitForState(() => false, TimeSpan.FromSeconds(5))); } + [Fact(DisplayName = "WaitForStateAsync with an async predicate can wait for multiple renders and changes to occur")] + public async Task Test400() + { + var cut = Render(); + cut.Instance.Counter.ShouldBe(0); + + // Clicking 'tick' changes the counter, and starts a task + cut.Find("#tick").Click(); + cut.Instance.Counter.ShouldBe(1); + + // Clicking 'tock' completes the task, which updates the counter + // This click causes two renders, thus something is needed to await here. + cut.Find("#tock").Click(); + await cut.WaitForStateAsync(async () => + { + await Task.Yield(); + return cut.Instance.Counter == 2; + }); + + cut.Instance.Counter.ShouldBe(2); + } + + [Fact(DisplayName = "WaitForAssertionAsync with an async assertion can wait for multiple renders and changes to occur")] + public async Task Test401() + { + // Initial state is stopped + var cut = Render(); + cut.Find("#state").TextContent.ShouldBe("Stopped"); + + // Clicking 'tick' changes the state, and starts a task + cut.Find("#tick").Click(); + cut.Find("#state").TextContent.ShouldBe("Started"); + + // Clicking 'tock' completes the task, which updates the state + // This click causes two renders, thus something is needed to await here. + cut.Find("#tock").Click(); + await cut.WaitForAssertionAsync(async () => + { + await Task.Yield(); + cut.Find("#state").TextContent.ShouldBe("Stopped"); + }); + } + + [Fact(DisplayName = "WaitForStateAsync throws WaitForFailedException after timeout when the async predicate never passes")] + public async Task Test402() + { + var cut = Render(); + + var expected = await Should.ThrowAsync(async () => + await cut.WaitForStateAsync(async () => + { + await Task.Yield(); + return false; + }, TimeSpan.FromMilliseconds(50))); + + expected.Message.ShouldStartWith(WaitForStateHelper.TimeoutBeforePassMessage); + } + + [Fact(DisplayName = "WaitForStateAsync throws WaitForFailedException if the async predicate throws")] + public async Task Test403() + { + const string expectedInnerMessage = "INNER ASYNC MESSAGE"; + var cut = Render(); + + var expected = await Should.ThrowAsync(async () => + await cut.WaitForStateAsync(async () => + { + await Task.Yield(); + throw new InvalidOperationException(expectedInnerMessage); + })); + + expected.Message.ShouldStartWith(WaitForStateHelper.ExceptionInPredicateMessage); + expected.InnerException.ShouldBeOfType() + .Message.ShouldBe(expectedInnerMessage); + } + + [Fact(DisplayName = "WaitForStateAsync does not deadlock when the async predicate awaits work on the renderer dispatcher")] + public async Task Test405() + { + var cut = Render(); + + // Clicking 'tick' then 'tock' causes the counter to reach 2 across two renders. + cut.Find("#tick").Click(); + cut.Find("#tock").Click(); + + // The predicate awaits work routed through the renderer's dispatcher. A naive + // sync-over-async implementation (e.g. blocking the dispatcher with .GetResult()) + // would deadlock here; awaiting on the dispatcher does not. + await cut.WaitForStateAsync(async () => + { + var counter = await cut.InvokeAsync(() => cut.Instance.Counter); + return counter == 2; + }); + + cut.Instance.Counter.ShouldBe(2); + } + + [Fact(DisplayName = "WaitForAssertionAsync throws WaitForFailedException after timeout when the async assertion never passes")] + public async Task Test404() + { + var cut = Render(); + + var expected = await Should.ThrowAsync(async () => + await cut.WaitForAssertionAsync(async () => + { + await Task.Yield(); + cut.Markup.ShouldBeEmpty(); + }, TimeSpan.FromMilliseconds(50))); + + expected.Message.ShouldStartWith(WaitForAssertionHelper.TimeoutMessage); + } + private sealed class ThrowsAfterAsyncOperation : ComponentBase { protected override async Task OnInitializedAsync() From c18e175548c9dc1a2347bd51914aa85579208cc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:26:39 +0000 Subject: [PATCH 07/26] Bump SonarAnalyzer.CSharp from 10.23.0.137933 to 10.27.0.140913 --- updated-dependencies: - dependency-name: SonarAnalyzer.CSharp dependency-version: 10.27.0.140913 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 49415e0b5..3088856be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + From e22964f482c85b113e17b399696408793ddf6f74 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 4 Jun 2026 08:06:01 +0200 Subject: [PATCH 08/26] chore: Update packages --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3088856be..a22935522 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -91,7 +91,7 @@ - + @@ -99,15 +99,15 @@ - + - - - + + + From 6d5850b96b0e4e3ec520cbec05cfb06723080b30 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 4 Jun 2026 08:08:51 +0200 Subject: [PATCH 09/26] chore: Update template packages --- .../template/Company.BlazorTests1.csproj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/bunit.template/template/Company.BlazorTests1.csproj b/src/bunit.template/template/Company.BlazorTests1.csproj index fe62ba21c..65257789e 100644 --- a/src/bunit.template/template/Company.BlazorTests1.csproj +++ b/src/bunit.template/template/Company.BlazorTests1.csproj @@ -18,8 +18,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -27,28 +27,28 @@ - + all runtime; build; native; contentfiles; analyzers - - + + all runtime; build; native; contentfiles; analyzers - - + + - - + + From 2f9878d18e1895fbd5b7fe28ad33bae4eccc56b3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 4 Jun 2026 08:18:14 +0200 Subject: [PATCH 10/26] fix: Wrong SDK version --- src/bunit.template/template/Company.BlazorTests1.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bunit.template/template/Company.BlazorTests1.csproj b/src/bunit.template/template/Company.BlazorTests1.csproj index 65257789e..a6870e2ad 100644 --- a/src/bunit.template/template/Company.BlazorTests1.csproj +++ b/src/bunit.template/template/Company.BlazorTests1.csproj @@ -18,7 +18,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 6cb4df6dd8baa705990bf9ef652b1f62770ca67a Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 5 Jun 2026 20:06:21 +0200 Subject: [PATCH 11/26] feat: Add Asset handling --- CHANGELOG.md | 1 + .../docs/providing-input/seeding-assets.md | 51 +++++++++ docs/site/docs/toc.md | 1 + src/bunit/BunitContext.cs | 17 +++ src/bunit/Rendering/BunitRenderer.cs | 48 ++++++++ .../Assets/AssetsIndexerComponent.razor | 7 ++ .../Assets/AssetsIterationComponent.razor | 21 ++++ .../Assets/AssetsPropertiesComponent.razor | 15 +++ tests/bunit.tests/Rendering/AssetsTest.razor | 106 ++++++++++++++++++ 9 files changed, 267 insertions(+) create mode 100644 docs/site/docs/providing-input/seeding-assets.md create mode 100644 tests/bunit.testassets/Assets/AssetsIndexerComponent.razor create mode 100644 tests/bunit.testassets/Assets/AssetsIterationComponent.razor create mode 100644 tests/bunit.testassets/Assets/AssetsPropertiesComponent.razor create mode 100644 tests/bunit.tests/Rendering/AssetsTest.razor diff --git a/CHANGELOG.md b/CHANGELOG.md index 62999cca5..3783df78d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to **bUnit** will be documented in this file. The project ad ### Added - New overloads to WaitForHelpers to have async assertions and predicates. Reported by [@radmorecameron](https://github.com/radmorecameron) in #1833. Fixed by [@linkdotnet](https://github.com/linkdotnet). +- `AddAsset` to `BunitContext` to seed the `ResourceAssetCollection` exposed via `ComponentBase.Assets`. Reported by [LasseHerget](https://github.com/LasseHerget) in #1846. Implemented by [@linkdotnet](https://github.com/linkdotnet). ## [2.7.2] - 2026-03-31 diff --git a/docs/site/docs/providing-input/seeding-assets.md b/docs/site/docs/providing-input/seeding-assets.md new file mode 100644 index 000000000..af523698a --- /dev/null +++ b/docs/site/docs/providing-input/seeding-assets.md @@ -0,0 +1,51 @@ +--- +uid: seeding-assets +title: Seeding static assets (Assets) +--- + +# Seeding static assets (`Assets`) + +This article explains how to seed the `Assets` property of components under test in bUnit. This is supported for .NET 9 and later. + +Since .NET 9, components can access static assets mapped by [`MapStaticAssets`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/map-static-files?view=aspnetcore-9.0) through the [`ComponentBase.Assets`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assets?view=aspnetcore-9.0) property, e.g. to resolve fingerprinted URLs: + +```razor + +``` + +By default, bUnit's renderer returns an empty [`ResourceAssetCollection`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.resourceassetcollection?view=aspnetcore-9.0), which matches an app that does not use `MapStaticAssets`. The indexer then returns the passed-in key unchanged, i.e. `Assets["img.png"]` returns `"img.png"`, and iterating over `Assets` yields no items. + +## Adding assets + +Use the `AddAsset` method on `BunitContext` to add assets before rendering a component. Passing a `label` maps the stable asset key to its (fingerprinted) URL: + +```csharp +[Fact] +public void Image_uses_fingerprinted_url() +{ + AddAsset("img.abc123.png", label: "img.png"); + + var cut = Render(); + + cut.MarkupMatches(@""); +} +``` + +Assets can also carry additional properties, which components can read when iterating the collection: + +```csharp +[Fact] +public void Component_lists_subresources() +{ + AddAsset("css/app.abc123.css", label: "css/app.css"); + AddAsset("js/app.def456.js", label: "js/app.js", new ResourceAssetProperty("integrity", "sha256-...")); + + var cut = Render(); + + // component iterates Assets and reads each asset's "label" property + cut.MarkupMatches(@"css/app.cssjs/app.js"); +} +``` + +> [!NOTE] +> The `"label"` property is the convention `ResourceAssetCollection` uses to build its key → URL mapping, just like `MapStaticAssets` does in production. An asset added without a label is part of the collection when iterating, but the indexer will not map any key to it. diff --git a/docs/site/docs/toc.md b/docs/site/docs/toc.md index 2f4bebfea..53fe7e811 100644 --- a/docs/site/docs/toc.md +++ b/docs/site/docs/toc.md @@ -8,6 +8,7 @@ ## [Controlling the root render tree](xref:root-render-tree) ## [Substituting (mocking) component](xref:substituting-components) ## [Configure 3rd party libraries](xref:configure-3rd-party-libs) +## [Seeding static assets](xref:seeding-assets) # [Interaction](xref:interaction) ## [Trigger event handlers](xref:trigger-event-handlers) diff --git a/src/bunit/BunitContext.cs b/src/bunit/BunitContext.cs index cb3aea4d9..6783f63c8 100644 --- a/src/bunit/BunitContext.cs +++ b/src/bunit/BunitContext.cs @@ -184,6 +184,23 @@ public void SetRendererInfo(RendererInfo? rendererInfo) { Renderer.SetRendererInfo(rendererInfo); } + + /// + /// Adds an asset to the that components + /// rendered with this can access through their property. + /// + /// + /// Pass a to map a stable asset key to its (fingerprinted) , + /// i.e. AddAsset("img.abc123.png", label: "img.png") makes Assets["img.png"] return img.abc123.png. + /// Adding multiple assets with the same label results in an + /// when the property is first accessed. + /// + /// The url of the asset. + /// The label of the asset, used as the lookup key by the indexer. Pass to add the asset without a label. + /// Additional properties to associate with the asset. + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Using string to align with ResourceAsset")] + public void AddAsset(string url, string? label = null, params ResourceAssetProperty[] properties) + => Renderer.AddAsset(url, label, properties); #endif /// diff --git a/src/bunit/Rendering/BunitRenderer.cs b/src/bunit/Rendering/BunitRenderer.cs index 5a9d47731..88740fcbc 100644 --- a/src/bunit/Rendering/BunitRenderer.cs +++ b/src/bunit/Rendering/BunitRenderer.cs @@ -77,6 +77,54 @@ public void SetRendererInfo(RendererInfo? rendererInfo) { this.rendererInfo = rendererInfo; } + + private readonly List resourceAssets = []; + private ResourceAssetCollection? resourceAssetCollection; + + /// + protected override ResourceAssetCollection Assets + => resourceAssets.Count == 0 + ? ResourceAssetCollection.Empty + : resourceAssetCollection ??= new ResourceAssetCollection(resourceAssets); + + /// + /// Adds an asset to the returned by the renderers property, + /// which components rendered by this renderer can access through their property. + /// + /// + /// Pass a to map a stable asset key to its (fingerprinted) , + /// i.e. AddAsset("img.abc123.png", label: "img.png") makes Assets["img.png"] return img.abc123.png. + /// Adding multiple assets with the same label results in an + /// when the property is first accessed. + /// + /// The url of the asset. + /// The label of the asset, used as the lookup key by the indexer. Pass to add the asset without a label. + /// Additional properties to associate with the asset. + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Using string to align with ResourceAsset")] + public void AddAsset(string url, string? label = null, params ResourceAssetProperty[] properties) + { + ArgumentException.ThrowIfNullOrEmpty(url); + ArgumentNullException.ThrowIfNull(properties); + + var props = new List(properties.Length + 1); + if (label is not null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(label); + const string labelMarker = "label"; + + if (properties.Any(static property => string.Equals(property.Name, labelMarker, StringComparison.Ordinal))) + { + throw new ArgumentException("The label property is reserved when a label is provided.", nameof(properties)); + } + + props.Add(new ResourceAssetProperty(labelMarker, label)); + } + + props.AddRange(properties); + + resourceAssets.Add(new ResourceAsset(url, props.Count > 0 ? props : null)); + resourceAssetCollection = null; + } #endif /// diff --git a/tests/bunit.testassets/Assets/AssetsIndexerComponent.razor b/tests/bunit.testassets/Assets/AssetsIndexerComponent.razor new file mode 100644 index 000000000..3b3a7cb3d --- /dev/null +++ b/tests/bunit.testassets/Assets/AssetsIndexerComponent.razor @@ -0,0 +1,7 @@ +@{ +#if NET9_0_OR_GREATER +} + +@{ +#endif +} diff --git a/tests/bunit.testassets/Assets/AssetsIterationComponent.razor b/tests/bunit.testassets/Assets/AssetsIterationComponent.razor new file mode 100644 index 000000000..f6dfbe340 --- /dev/null +++ b/tests/bunit.testassets/Assets/AssetsIterationComponent.razor @@ -0,0 +1,21 @@ +@{ +#if NET9_0_OR_GREATER +} +@foreach (var name in SubresourceNames) +{ + @name +} +@{ +#endif +} +@code { +#if NET9_0_OR_GREATER + private IReadOnlyList SubresourceNames { get; set; } = Array.Empty(); + + protected override void OnInitialized() + => SubresourceNames = Assets + .Select(asset => asset.Properties?.SingleOrDefault(property => property.Name == "label")?.Value) + .OfType() + .ToList(); +#endif +} diff --git a/tests/bunit.testassets/Assets/AssetsPropertiesComponent.razor b/tests/bunit.testassets/Assets/AssetsPropertiesComponent.razor new file mode 100644 index 000000000..bb4d4ffab --- /dev/null +++ b/tests/bunit.testassets/Assets/AssetsPropertiesComponent.razor @@ -0,0 +1,15 @@ +@{ +#if NET9_0_OR_GREATER +} +@foreach (var asset in Assets) +{ +
    + @foreach (var property in asset.Properties ?? Array.Empty()) + { +
  • @property.Name=@property.Value
  • + } +
+} +@{ +#endif +} diff --git a/tests/bunit.tests/Rendering/AssetsTest.razor b/tests/bunit.tests/Rendering/AssetsTest.razor new file mode 100644 index 000000000..3f2dd16aa --- /dev/null +++ b/tests/bunit.tests/Rendering/AssetsTest.razor @@ -0,0 +1,106 @@ +@code{ + #if NET9_0_OR_GREATER +} +@using Bunit.TestAssets.Assets; +@inherits BunitContext +@code { + [Fact(DisplayName = "Assets defaults to an empty collection where the indexer returns the key unchanged")] + public void Test001() + { + var cut = Render(); + + cut.MarkupMatches(@); + } + + [Fact(DisplayName = "Iterating Assets without any added assets renders nothing")] + public void Test002() + { + var cut = Render(); + + cut.MarkupMatches(string.Empty); + } + + [Fact(DisplayName = "AddAsset with label maps the asset key to the fingerprinted url")] + public void Test003() + { + AddAsset("img.abc123.png", label: "img.png"); + + var cut = Render(); + + cut.MarkupMatches(@); + } + + [Fact(DisplayName = "Components can iterate added assets and read their label property")] + public void Test004() + { + AddAsset("css/app.abc123.css", label: "css/app.css"); + AddAsset("js/app.def456.js", label: "js/app.js"); + + var cut = Render(); + + cut.MarkupMatches( + @ + css/app.css + js/app.js + ); + } + + [Fact(DisplayName = "AddAsset without label does not map the asset key")] + public void Test005() + { + AddAsset("img.png"); + + var cut = Render(); + + cut.MarkupMatches(@); + } + + [Fact(DisplayName = "AddAsset exposes additional properties to components")] + public void Test006() + { + AddAsset("js/app.js", label: "app.js", new ResourceAssetProperty("integrity", "sha256-abc")); + + var cut = Render(); + + cut.MarkupMatches( + @
    +
  • label=app.js
  • +
  • integrity=sha256-abc
  • +
); + } + + [Fact(DisplayName = "Assets added after the initial render are available to components rendered afterwards")] + public void Test007() + { + var first = Render(); + first.MarkupMatches(string.Empty); + + AddAsset("img.abc123.png", label: "img.png"); + + var second = Render(); + second.MarkupMatches(@img.png); + } + + [Theory(DisplayName = "AddAsset rejects empty or whitespace-only labels")] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public void Test008(string label) + { + var ex = Assert.Throws(() => AddAsset("app.js", label: label)); + + ex.ParamName.ShouldBe("label"); + } + + [Fact(DisplayName = "AddAsset rejects explicit label properties when label is provided")] + public void Test009() + { + var ex = Assert.Throws( + () => AddAsset("app.js", label: "app.js", new ResourceAssetProperty("label", "other"))); + + ex.ParamName.ShouldBe("properties"); + } +} +@code{ + #endif +} From 035a5268da6c65dc2918dbd831a63dd0e1af1955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:15:05 +0000 Subject: [PATCH 12/26] Bump AngleSharp from 1.4.0 to 1.5.0 --- updated-dependencies: - dependency-name: AngleSharp dependency-version: 1.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a22935522..dd37bacc0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + From 94f239990ba354aec3e7bad7b6ba6e897105f31e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:15:23 +0000 Subject: [PATCH 13/26] Bump Meziantou.Polyfill from 1.0.140 to 1.0.146 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.146 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd37bacc0..b8ad748c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From ccf165390e296d0fec14fae8aa1909966a2365cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:15:06 +0000 Subject: [PATCH 14/26] Bump Meziantou.Polyfill from 1.0.146 to 1.0.148 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.148 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b8ad748c7..c99e6027c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 98d37f14d9bd2f13fca00f96883796c26826dca2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:27:08 +0000 Subject: [PATCH 15/26] Bump Verify.XunitV3 from 31.19.0 to 31.19.1 --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 31.19.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c99e6027c..a2cfa9d94 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -99,7 +99,7 @@ - + From d803aa364740cf3a96af0837c442a021ad6f87f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:15:08 +0000 Subject: [PATCH 16/26] Bump Meziantou.Polyfill from 1.0.148 to 1.0.150 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.150 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a2cfa9d94..26867da76 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 988c2dbda02eddadc31c78d01ef097ac1675f28e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:15:36 +0000 Subject: [PATCH 17/26] Bump Meziantou.Polyfill from 1.0.150 to 1.0.151 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.151 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 26867da76..dbb0529d1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 78a4db7ca9a6170e849555aef75adc696ef022f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:15:20 +0000 Subject: [PATCH 18/26] Bump AngleSharp from 1.5.0 to 1.5.1 --- updated-dependencies: - dependency-name: AngleSharp dependency-version: 1.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dbb0529d1..dd4840013 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + From 6ebf4289a99945c79d8988254754c5bead35e6d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:12:30 +0000 Subject: [PATCH 19/26] chore(deps): Bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/docs-deploy.yml | 2 +- .github/workflows/prepare-release.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96d6cbafb..aa579716e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: create-nuget: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Get all history to allow automatic versioning using MinVer @@ -118,7 +118,7 @@ jobs: steps: - name: 🛒 Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -153,7 +153,7 @@ jobs: runs-on: ubuntu-latest needs: [ create-nuget ] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Get all history to allow automatic versioning using MinVer @@ -213,7 +213,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 🛒 Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -267,7 +267,7 @@ jobs: if: github.event_name == 'pull_request' && github.repository_owner == 'bunit-dev' steps: - name: 'Checkout Repository' - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 'Dependency Review' uses: actions/dependency-review-action@v5 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 6379d8e49..45e50a22c 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -30,7 +30,7 @@ jobs: steps: - name: 🛒 Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 token: ${{ secrets.BUNIT_BOT_TOKEN }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index a96183015..490fa4934 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -21,7 +21,7 @@ jobs: if: github.ref == 'refs/heads/main' && contains(fromJson('["major","minor","build"]'), github.event.inputs.versionIncrement) steps: - name: 🛒 Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 token: ${{ secrets.BUNIT_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5441e7b85..6c5ece3d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: steps: - name: 🛒 Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 token: ${{ secrets.BUNIT_BOT_TOKEN }} From 332a6222d66ffb5c80fb8df08d8e92162ea2eca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:28:20 +0000 Subject: [PATCH 20/26] Bump Verify.XunitV3 from 31.19.1 to 31.20.0 --- updated-dependencies: - dependency-name: Verify.XunitV3 dependency-version: 31.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd4840013..c962b5bed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -99,7 +99,7 @@ - + From 0c6a3f9bcefb96cde30fe441c41762381474ffae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:27:14 +0000 Subject: [PATCH 21/26] Bump Nerdbank.GitVersioning from 3.9.50 to 3.10.85 --- updated-dependencies: - dependency-name: Nerdbank.GitVersioning dependency-version: 3.10.85 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c962b5bed..33565a613 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + From e54382238e1a0d0f6c2900fb331f51241b88def5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:14:49 +0000 Subject: [PATCH 22/26] Bump Meziantou.Polyfill from 1.0.151 to 1.0.152 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.152 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 33565a613..eaf6b9eca 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 025a68eaadf83520d6a9bf55243543d586988a88 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 20 Jun 2026 14:55:02 +0200 Subject: [PATCH 23/26] chore: Update preview packages --- Directory.Packages.props | 24 +++++++++---------- .../Web.AngleSharp/WrapperElementGenerator.cs | 6 ++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eaf6b9eca..dbbf6e7b8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -72,18 +72,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs index 81d0039e7..4fc6eeb64 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs @@ -185,18 +185,16 @@ private static StringBuilder AppendCallParameters(this StringBuilder source, Imm return source; } - private static StringBuilder AppendInheritDoc(this StringBuilder source) + private static void AppendInheritDoc(this StringBuilder source) { source.AppendLine("\t/// "); - return source; } - private static StringBuilder AppendDefaultAttributes(this StringBuilder source, string tabs = "\t\t") + private static void AppendDefaultAttributes(this StringBuilder source, string tabs = "\t\t") { source.Append(tabs).AppendLine("[System.Diagnostics.DebuggerHidden]"); source.Append(tabs).AppendLine("[System.Diagnostics.DebuggerStepThrough]"); source.Append(tabs).AppendLine("[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]"); - return source; } private static bool IsSpecialMethod(string methodName) From b30437e6865a4ab3a0cf582db5c9a7e1da8b6f0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:14:42 +0000 Subject: [PATCH 24/26] Bump Meziantou.Polyfill from 1.0.152 to 1.0.154 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.154 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dbbf6e7b8..9a14d40cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From cac64eada115f047460041d69338f0b6c28555ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:27:07 +0000 Subject: [PATCH 25/26] Bump Microsoft.NET.Test.Sdk from 18.6.0 to 18.7.0 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a14d40cb..447dd84cc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -91,7 +91,7 @@ - + From 10eaa1941ff2ef025299e7f7ae3a66fc4f78f30f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:14:41 +0000 Subject: [PATCH 26/26] Bump Meziantou.Polyfill from 1.0.154 to 1.0.155 --- updated-dependencies: - dependency-name: Meziantou.Polyfill dependency-version: 1.0.155 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 447dd84cc..9576fa66d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - +