From be53d52785cd0ad2580516bc99054084f3af3e69 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 9 Sep 2026 17:50:51 +0200 Subject: [PATCH 1/4] Alter InvokeOnSpacerBeforeVisible for NET 11.0 so it uses 4 parameters instead of 3 for invocation --- .../Implementation/VirtualizeJSRuntimeInvocationHandler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs b/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs index 2fb2abd73..882456025 100644 --- a/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs +++ b/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Components.Web.Virtualization; using System.Diagnostics; using System.Reflection; +using Microsoft.AspNetCore.Components.Web.Virtualization; namespace Bunit.JSInterop.InvocationHandlers.Implementation; @@ -58,7 +58,12 @@ private static void InvokeOnSpacerBeforeVisible(object dotNetObjectReference) 0f, /* spacerSize */ 0f, /* spacerSeparation */ 1_000_000_000f, /* containerSize - very large number to ensure all items are loaded at once */ +#if NET11_0_OR_GREATER + 3, /* RenderedContentMeasurement */ +#endif + }; + onSpacerBeforeVisibleMethodInfo.Invoke(virtualizeJsInterop, parameters); } } From 742be7739fcdeb702dd3971ec18d29be3f48f7d7 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 9 Sep 2026 23:00:16 +0200 Subject: [PATCH 2/4] - Use xunit.v3.mtp-v2 - Upgrade to net11.0 RC1 - Use UserScroll as 4th parameter - Fix compiler warnings which poped up after upgrading to net11.0 RC1 --- Directory.Packages.props | 27 +++++----- .../VirtualizeJSRuntimeInvocationHandler.cs | 9 +++- tests/Directory.Build.props | 6 +-- .../BlazorE2E/ElementRefComponent.razor | 8 ++- ...ultipleStateHasChangedInOnParametersSet.cs | 2 + .../SimpleAuthViewWithClaims.razor | 50 ++++++++++++++----- .../SimpleWithHttpClient.razor | 3 +- .../SimpleWithJSRuntimeDep.razor | 14 ++++-- 8 files changed, 81 insertions(+), 38 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56664c00a..7ce5d751d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -72,18 +72,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -105,7 +105,8 @@ - + + diff --git a/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs b/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs index 882456025..a771cd259 100644 --- a/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs +++ b/src/bunit/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs @@ -35,10 +35,15 @@ internal VirtualizeJSRuntimeInvocationHandler() /// protected internal override Task HandleAsync(JSRuntimeInvocation invocation) { - if (!invocation.Identifier.Equals(JsFunctionsPrefix + "dispose", StringComparison.Ordinal)) + if (!invocation.Identifier.Equals(JsFunctionsPrefix + "dispose", StringComparison.Ordinal) && + !invocation.Identifier.Equals(JsFunctionsPrefix + "refreshObservers", StringComparison.Ordinal)) { Debug.Assert(invocation.Identifier.Equals(JsFunctionsPrefix + "init", StringComparison.Ordinal)); +#if NET11_0_OR_GREATER + Debug.Assert(invocation.Arguments.Count == 4); +#else Debug.Assert(invocation.Arguments.Count == 3); +#endif Debug.Assert(invocation.Arguments[0] is not null); InvokeOnSpacerBeforeVisible(invocation.Arguments[0]!); @@ -59,7 +64,7 @@ private static void InvokeOnSpacerBeforeVisible(object dotNetObjectReference) 0f, /* spacerSeparation */ 1_000_000_000f, /* containerSize - very large number to ensure all items are loaded at once */ #if NET11_0_OR_GREATER - 3, /* RenderedContentMeasurement */ + 0, /* UserScroll */ #endif }; diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index cf491ca78..35d4848d1 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -14,9 +14,9 @@ true Exe - + - @@ -31,7 +31,7 @@ - + diff --git a/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor b/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor index b3f52d69d..a3f07aa3f 100644 --- a/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor @@ -31,6 +31,12 @@ async Task MakeInteropCall() { - await JSRuntime.InvokeVoidAsync("setElementValue", _myInput, $"Clicks: {++_count}"); + try { + await JSRuntime.InvokeVoidAsync("setElementValue", _myInput, $"Clicks: {++_count}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error invoking JS: {ex.Message}"); + } } } diff --git a/tests/bunit.testassets/SampleComponents/MultipleStateHasChangedInOnParametersSet.cs b/tests/bunit.testassets/SampleComponents/MultipleStateHasChangedInOnParametersSet.cs index 5e64874da..29ef33356 100644 --- a/tests/bunit.testassets/SampleComponents/MultipleStateHasChangedInOnParametersSet.cs +++ b/tests/bunit.testassets/SampleComponents/MultipleStateHasChangedInOnParametersSet.cs @@ -5,6 +5,7 @@ public class MultipleStateHasChangedInOnParametersSet : ComponentBase [Parameter] public int Value { get; set; } +#pragma warning disable BL0012 protected override void OnParametersSet() { base.OnParametersSet(); @@ -12,5 +13,6 @@ protected override void OnParametersSet() StateHasChanged(); StateHasChanged(); } +#pragma warning restore BL0012 } diff --git a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor index d889342d0..7470f0c88 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor @@ -1,5 +1,6 @@ @using Microsoft.AspNetCore.Components.Authorization @using System.Security.Claims +@implements IDisposable @inject AuthenticationStateProvider AuthenticationStateProvider @@ -15,18 +16,41 @@ } -@code { - string userName = ""; - string? userEmail = ""; - string? userId = ""; - bool hasUserEmail => userEmail != null; - bool hasUserId => userId != null; + @code { + bool disposed; + string userName = ""; + string? userEmail = ""; + string? userId = ""; + bool hasUserEmail => userEmail != null; + bool hasUserId => userId != null; - protected override async Task OnParametersSetAsync() - { - var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - userName = state?.User?.Identity?.Name ?? string.Empty; - userEmail = state?.User?.FindFirst(ClaimTypes.Email)?.Value; - userId = state?.User?.FindFirst(ClaimTypes.Sid)?.Value; - } + public SimpleAuthViewWithClaims() + { + AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged; + } + + + protected override async Task OnParametersSetAsync() + { + var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + userName = state?.User?.Identity?.Name ?? string.Empty; + userEmail = state?.User?.FindFirst(ClaimTypes.Email)?.Value; + userId = state?.User?.FindFirst(ClaimTypes.Sid)?.Value; + } + + private async void OnAuthenticationStateChanged(Task task) + { + // Exists just to prevent BL0013 'SimpleAuthViewWithClaims' calls GetAuthenticationStateAsync on AuthenticationStateProvider + // without subscribing to the AuthenticationStateChanged event. This may result in using stale authentication state. + + } + + public void Dispose() + { + if (!disposed) + { + AuthenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged; + disposed = true; + } + } } diff --git a/tests/bunit.testassets/SampleComponents/SimpleWithHttpClient.razor b/tests/bunit.testassets/SampleComponents/SimpleWithHttpClient.razor index b3ec30bf0..97468e805 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleWithHttpClient.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleWithHttpClient.razor @@ -1,4 +1,4 @@ -@inject HttpClient HttpClient +@inject HttpClient HttpClient

SimpleWithHttpClient

@@ -7,6 +7,5 @@ protected override async Task OnInitializedAsync() { await HttpClient.GetAsync("/api/weather"); - StateHasChanged(); } } diff --git a/tests/bunit.testassets/SampleComponents/SimpleWithJSRuntimeDep.razor b/tests/bunit.testassets/SampleComponents/SimpleWithJSRuntimeDep.razor index 4c8f6d1d0..76949690b 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleWithJSRuntimeDep.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleWithJSRuntimeDep.razor @@ -1,4 +1,4 @@ -@inject IJSRuntime jsRuntime +@inject IJSRuntime jsRuntime

@name

@code{ string name = string.Empty; @@ -7,8 +7,14 @@ { if (firstRender) { - name = await jsRuntime.InvokeAsync("getdata"); - StateHasChanged(); + try { + name = await jsRuntime.InvokeAsync("getdata"); + StateHasChanged(); + } + catch (Exception ex) + { + Console.WriteLine($"Error invoking JS: {ex.Message}"); + } } } -} \ No newline at end of file +} From 5129e6360dbc172b4cdc207e69031832ecf1093c Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 9 Sep 2026 23:28:06 +0200 Subject: [PATCH 3/4] Fix BL0013 errors --- .../BunitAuthenticationStateProvider.cs | 43 ++++++++++++++++++- .../BunitAuthorizationContext.cs | 32 ++++++++++++-- .../SimpleAuthViewWithClaims.razor | 22 +++++++++- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs b/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs index 83afac68e..a7945a2a2 100644 --- a/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs +++ b/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs @@ -7,9 +7,10 @@ namespace Bunit.TestDoubles; /// Represents a implementation of AuthenticationStateProvider for testing purposes that allows /// user to test components that use authentication and authorization. /// -public class BunitAuthenticationStateProvider : AuthenticationStateProvider +public class BunitAuthenticationStateProvider : AuthenticationStateProvider, IDisposable { private TaskCompletionSource authState = new(); + private bool disposed; /// /// Initializes a new instance of the class @@ -24,7 +25,10 @@ public BunitAuthenticationStateProvider( IEnumerable? roles = null, IEnumerable? claims = null, string? authenticationType = null) - => SetAuthenticatedState(userName, roles, claims, authenticationType); + { + AuthenticationStateChanged += OnAuthenticationStateChanged; + SetAuthenticatedState(userName, roles, claims, authenticationType); + } /// /// Initializes a new instance of the class. @@ -135,4 +139,39 @@ private static AuthenticationState CreateUnauthenticationState() var principal = new ClaimsPrincipal(new ClaimsIdentity()); return new AuthenticationState(principal); } + +#pragma warning disable AsyncFixer03 // Fire-and-forget async-void methods or delegates + private async void OnAuthenticationStateChanged(Task task) +#pragma warning restore AsyncFixer03 // Fire-and-forget async-void methods or delegates + { + // Exists just to prevent BL0013 'BunitAuthenticationStateProvider' calls GetAuthenticationStateAsync on AuthenticationStateProvider + // without subscribing to the AuthenticationStateChanged event. This may result in using stale authentication state. + + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + AuthenticationStateChanged -= OnAuthenticationStateChanged; + } + + // no unmanaged resources to release + + disposed = true; + } } diff --git a/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs b/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs index 25942ac0a..fb0bf5861 100644 --- a/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs +++ b/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs @@ -5,14 +5,14 @@ namespace Bunit.TestDoubles; /// -/// Root authorization service that manages different authentication/authorization state -/// in the system. +/// Root authorization service that manages different authentication/authorization state in the system. /// -public class BunitAuthorizationContext +public class BunitAuthorizationContext : IDisposable { private readonly BunitAuthorizationService authService = new(); private readonly BunitAuthorizationPolicyProvider policyProvider = new(); private readonly BunitAuthenticationStateProvider authProvider = new(); + private bool disposed; /// /// Gets a value indicating whether user is authenticated. @@ -160,4 +160,30 @@ public BunitAuthorizationContext SetAuthenticationType(string authenticationType this.authProvider.TriggerAuthenticationStateChanged(this.UserName, this.Roles, this.Claims, authenticationType); return this; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + authProvider.Dispose(); + } + + // no unmanaged resources to release + + disposed = true; + } } diff --git a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor index 7470f0c88..057e7419f 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor @@ -45,12 +45,30 @@ } + /// public void Dispose() { - if (!disposed) + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposed) { + return; + } + + if (disposing) + { + // dispose managed resources AuthenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged; - disposed = true; } + + // no unmanaged resources to release + + disposed = true; } + } From f8c1ddfd7806aa61704e943c95977c0e2e1953cc Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 10 Sep 2026 09:53:59 +0200 Subject: [PATCH 4/4] - Surpress BL0013 and BL0016 errors for this PR. Can be solved in a later separate PR - Revert all other mtp-v2 and related changes --- Directory.Packages.props | 3 +- .../BunitAuthenticationStateProvider.cs | 29 +------------ .../BunitAuthorizationContext.cs | 29 +------------ tests/Directory.Build.props | 2 +- .../BlazorE2E/ElementRefComponent.razor | 6 --- .../SimpleAuthViewWithClaims.razor | 42 ------------------- .../bunit.testassets/bunit.testassets.csproj | 3 +- tests/bunit.tests/bunit.tests.csproj | 5 ++- 8 files changed, 9 insertions(+), 110 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7ce5d751d..e1a2009d4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -105,8 +105,7 @@ - - + diff --git a/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs b/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs index a7945a2a2..a29a89bf4 100644 --- a/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs +++ b/src/bunit/TestDoubles/Authorization/BunitAuthenticationStateProvider.cs @@ -7,10 +7,9 @@ namespace Bunit.TestDoubles; /// Represents a implementation of AuthenticationStateProvider for testing purposes that allows /// user to test components that use authentication and authorization. /// -public class BunitAuthenticationStateProvider : AuthenticationStateProvider, IDisposable +public class BunitAuthenticationStateProvider : AuthenticationStateProvider { private TaskCompletionSource authState = new(); - private bool disposed; /// /// Initializes a new instance of the class @@ -148,30 +147,4 @@ private async void OnAuthenticationStateChanged(Task task) // without subscribing to the AuthenticationStateChanged event. This may result in using stale authentication state. } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - if (disposing) - { - // dispose managed resources - AuthenticationStateChanged -= OnAuthenticationStateChanged; - } - - // no unmanaged resources to release - - disposed = true; - } } diff --git a/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs b/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs index fb0bf5861..90febd8a8 100644 --- a/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs +++ b/src/bunit/TestDoubles/Authorization/BunitAuthorizationContext.cs @@ -7,12 +7,11 @@ namespace Bunit.TestDoubles; /// /// Root authorization service that manages different authentication/authorization state in the system. /// -public class BunitAuthorizationContext : IDisposable +public class BunitAuthorizationContext { private readonly BunitAuthorizationService authService = new(); private readonly BunitAuthorizationPolicyProvider policyProvider = new(); private readonly BunitAuthenticationStateProvider authProvider = new(); - private bool disposed; /// /// Gets a value indicating whether user is authenticated. @@ -160,30 +159,4 @@ public BunitAuthorizationContext SetAuthenticationType(string authenticationType this.authProvider.TriggerAuthenticationStateChanged(this.UserName, this.Roles, this.Claims, authenticationType); return this; } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - if (disposing) - { - // dispose managed resources - authProvider.Dispose(); - } - - // no unmanaged resources to release - - disposed = true; - } } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 35d4848d1..0b5093695 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -31,7 +31,7 @@ - + diff --git a/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor b/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor index a3f07aa3f..bbaefb042 100644 --- a/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/ElementRefComponent.razor @@ -31,12 +31,6 @@ async Task MakeInteropCall() { - try { await JSRuntime.InvokeVoidAsync("setElementValue", _myInput, $"Clicks: {++_count}"); - } - catch (Exception ex) - { - Console.WriteLine($"Error invoking JS: {ex.Message}"); - } } } diff --git a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor index 057e7419f..9e821bc95 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleAuthViewWithClaims.razor @@ -1,6 +1,5 @@ @using Microsoft.AspNetCore.Components.Authorization @using System.Security.Claims -@implements IDisposable @inject AuthenticationStateProvider AuthenticationStateProvider @@ -17,19 +16,12 @@ @code { - bool disposed; string userName = ""; string? userEmail = ""; string? userId = ""; bool hasUserEmail => userEmail != null; bool hasUserId => userId != null; - public SimpleAuthViewWithClaims() - { - AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged; - } - - protected override async Task OnParametersSetAsync() { var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); @@ -37,38 +29,4 @@ userEmail = state?.User?.FindFirst(ClaimTypes.Email)?.Value; userId = state?.User?.FindFirst(ClaimTypes.Sid)?.Value; } - - private async void OnAuthenticationStateChanged(Task task) - { - // Exists just to prevent BL0013 'SimpleAuthViewWithClaims' calls GetAuthenticationStateAsync on AuthenticationStateProvider - // without subscribing to the AuthenticationStateChanged event. This may result in using stale authentication state. - - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - if (disposing) - { - // dispose managed resources - AuthenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged; - } - - // no unmanaged resources to release - - disposed = true; - } - } diff --git a/tests/bunit.testassets/bunit.testassets.csproj b/tests/bunit.testassets/bunit.testassets.csproj index 305d09964..46b7a8fc8 100644 --- a/tests/bunit.testassets/bunit.testassets.csproj +++ b/tests/bunit.testassets/bunit.testassets.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0;net10.0;net11.0 @@ -11,6 +11,7 @@ false false true + BL0013;BL0016 diff --git a/tests/bunit.tests/bunit.tests.csproj b/tests/bunit.tests/bunit.tests.csproj index 638453b1a..ada564e14 100644 --- a/tests/bunit.tests/bunit.tests.csproj +++ b/tests/bunit.tests/bunit.tests.csproj @@ -4,15 +4,16 @@ net8.0;net9.0;net10.0;net11.0 Bunit Bunit.Tests + BL0016 - + - + \ No newline at end of file