test(central-ui): fix test-host hang in CentralUI.Tests

DiffDialogTests.SetupBodyLockInterop registered bUnit SetupVoid planned
invocations that were never completed; DisposeAsync_WhileOpen awaited
DiffDialog.DisposeAsync -> TryUnlockBodyAsync -> InvokeVoidAsync on one of
them, suspending the test forever so the test host never exited (regression
from the CentralUI-023 catch-narrowing). SetupBodyLockInterop now uses Loose
JSInterop mode. Also dispose the leaked WebApplication instances in the Auth
tests (FileSystemWatcher + ConsoleLoggerProcessor threads) and the extra
ServiceProvider in the DebugView tests. Suite now runs 281 tests in ~7s and
exits cleanly.
This commit is contained in:
Joseph Doherty
2026-05-17 05:43:05 -04:00
parent e55bd46ca1
commit cfa8667c78
5 changed files with 25 additions and 11 deletions

View File

@@ -22,7 +22,11 @@ public class AuthEndpointsCsrfTests
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
builder.Services.AddRouting(); builder.Services.AddRouting();
builder.Services.AddAntiforgery(); builder.Services.AddAntiforgery();
var app = builder.Build(); // Dispose the host: an undisposed WebApplication leaks its config
// PhysicalFileProvider (appsettings reload-watch FileSystemWatcher — a
// process-wide macOS run-loop thread) and a ConsoleLoggerProcessor
// thread, which keep the test host process alive after the run.
using var app = builder.Build();
app.MapAuthEndpoints(); app.MapAuthEndpoints();
return ((IEndpointRouteBuilder)app).DataSources return ((IEndpointRouteBuilder)app).DataSources

View File

@@ -23,7 +23,11 @@ public class AuthPingEndpointTests
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
builder.Services.AddRouting(); builder.Services.AddRouting();
builder.Services.AddAntiforgery(); builder.Services.AddAntiforgery();
var app = builder.Build(); // Dispose the host: an undisposed WebApplication leaks its config
// PhysicalFileProvider (appsettings reload-watch FileSystemWatcher — a
// process-wide macOS run-loop thread) and a ConsoleLoggerProcessor
// thread, which keep the test host process alive after the run.
using var app = builder.Build();
app.MapAuthEndpoints(); app.MapAuthEndpoints();
return ((IEndpointRouteBuilder)app).DataSources return ((IEndpointRouteBuilder)app).DataSources

View File

@@ -50,8 +50,11 @@ public class DebugViewDisposalTests : BunitContext
Services.AddSingleton(comms); Services.AddSingleton(comms);
var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance); var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
// An empty throwaway provider — these tests never call StartStreamAsync,
// so the provider is unused. (Services.BuildServiceProvider() would leak
// an undisposed provider.)
var debugStream = new DebugStreamService( var debugStream = new DebugStreamService(
comms, Services.BuildServiceProvider(), grpcFactory, comms, new ServiceCollection().BuildServiceProvider(), grpcFactory,
NullLogger<DebugStreamService>.Instance); NullLogger<DebugStreamService>.Instance);
Services.AddSingleton(debugStream); Services.AddSingleton(debugStream);

View File

@@ -46,8 +46,11 @@ public class DebugViewStreamRaceTests : BunitContext
Services.AddSingleton(comms); Services.AddSingleton(comms);
var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance); var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
// An empty throwaway provider — these tests drive HandleStreamEvent
// directly and never call StartStreamAsync, so the provider is unused.
// (Services.BuildServiceProvider() would leak an undisposed provider.)
var debugStream = new DebugStreamService( var debugStream = new DebugStreamService(
comms, Services.BuildServiceProvider(), grpcFactory, comms, new ServiceCollection().BuildServiceProvider(), grpcFactory,
NullLogger<DebugStreamService>.Instance); NullLogger<DebugStreamService>.Instance);
Services.AddSingleton(debugStream); Services.AddSingleton(debugStream);

View File

@@ -14,16 +14,16 @@ namespace ScadaLink.CentralUI.Tests.Shared;
public class DiffDialogTests : BunitContext public class DiffDialogTests : BunitContext
{ {
/// <summary> /// <summary>
/// DiffDialog applies/removes a body scroll-lock class via JS interop on /// DiffDialog applies/removes a body scroll-lock class and focuses the modal
/// open/close. CentralUI-023 narrowed those catch blocks so they no longer /// via JS interop on open/close. Loose mode auto-completes those void calls
/// swallow every exception — including bUnit's strict-mode unplanned-call /// so a path that <c>await</c>s them (e.g. <c>DisposeAsync</c> →
/// exception. Tests that exercise open/close must therefore register the /// <c>TryUnlockBodyAsync</c>) resumes instead of hanging on a never-completed
/// body-class calls so they do not surface as harness exceptions. /// planned invocation, and no strict-mode unplanned-invocation exception
/// surfaces through the narrowed CentralUI-023 catch blocks.
/// </summary> /// </summary>
private void SetupBodyLockInterop() private void SetupBodyLockInterop()
{ {
JSInterop.SetupVoid("document.body.classList.add", "modal-open"); JSInterop.Mode = JSRuntimeMode.Loose;
JSInterop.SetupVoid("document.body.classList.remove", "modal-open");
} }
[Fact] [Fact]