From c41831794af5ecf34716ebe0cb61545c94407688 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 21 Apr 2026 02:31:26 -0400 Subject: [PATCH] =?UTF-8?q?Task=20#242=20finish=20=E2=80=94=20UnsTab=20dra?= =?UTF-8?q?g-drop=20interactive=20Playwright=20E2E=20tests=20un-skip=20+?= =?UTF-8?q?=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the scope-out left by the #242 partial. Root cause of the blazor.web.js zero-byte response turned out to be two co-operating harness bugs: 1) The static-asset manifest was discoverable but the runtime needs UseStaticWebAssets to be called so the StaticWebAssetsLoader composes a PhysicalFileProvider per ContentRoot declared in staticwebassets.development.json (Admin source wwwroot + obj/compressed + the framework NuGet cache). Without that call MapStaticAssets resolves the route but has no ContentRoot map — so every asset serves zero bytes. 2) The EF InMemory DB name was being re-generated on every DbContext construction (the lambda body called Guid.NewGuid() inline), so the seed scope, Blazor circuit scope, and test-assertion scopes all got separate stores. Capturing the name as a stable string per fixture instance fixes the "cluster not found → page stays at Loading…" symptom. Fixes: - AdminWebAppFactory: * ApplicationName set on WebApplicationOptions so UseStaticWebAssets discovers the manifest. * builder.WebHost.UseStaticWebAssets() wired explicitly (matches what `dotnet run` does via MSBuild targets). * dbName captured once per fixture; the options lambda reads the captured string instead of re-rolling a Guid. - UnsTabDragDropE2ETests: the two [Fact(Skip=...)] tests un-skip. Suite state: 3 passed, 0 skipped, 0 failed. Task #242 closed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../AdminWebAppFactory.cs | 24 +++++++--- .../UnsTabDragDropE2ETests.cs | 44 +++++++------------ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs b/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs index 4c99048..6c11859 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs @@ -47,18 +47,25 @@ public sealed class AdminWebAppFactory : IAsyncDisposable var port = GetFreeTcpPort(); BaseUrl = $"http://127.0.0.1:{port}"; - // Point the content root at the Admin project's build output so wwwroot/ (app.css, - // site CSS, icons) + the Blazor framework assets served from the Admin assembly - // resolve. Without this the default content root is the test project's bin dir and - // blazor.web.js + app.css return 404, which keeps the interactive circuit from - // booting at all. + // Point the content root at the Admin project's build output so the Admin + // assembly + its sibling staticwebassets manifest are discoverable. The manifest + // maps /_framework/* to the framework NuGet cache + /app.css to the Admin source + // wwwroot; StaticWebAssetsLoader.UseStaticWebAssets reads it and wires a composite + // file provider automatically. var adminAssemblyDir = System.IO.Path.GetDirectoryName( typeof(Admin.Components.App).Assembly.Location)!; var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = adminAssemblyDir, + ApplicationName = typeof(Admin.Components.App).Assembly.GetName().Name, }); builder.WebHost.UseUrls(BaseUrl); + // UseStaticWebAssets reads {ApplicationName}.staticwebassets.runtime.json (or the + // development variant via the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES convention) and + // composes a PhysicalFileProvider per declared ContentRoot. This is what + // `dotnet run` does automatically via the MSBuild targets — we replicate it + // explicitly for the test-owned pipeline. + builder.WebHost.UseStaticWebAssets(); // E2E host runs in Development so unhandled exceptions during Blazor render surface // as visible 500s with stacks the test can capture — prod-style generic errors make // diagnosis of circuit / DI misconfig effectively impossible. @@ -78,8 +85,13 @@ public sealed class AdminWebAppFactory : IAsyncDisposable .AddPolicy("CanPublish", p => p.RequireRole(Admin.Services.AdminRoles.FleetAdmin)); builder.Services.AddCascadingAuthenticationState(); + // One InMemory database name per fixture — the lambda below runs on every DbContext + // construction, so capturing a stable string (not calling Guid.NewGuid() inline) is + // critical: every scope (seed, Blazor circuit, test assertions) must share the same + // backing store or rows written in one scope disappear in the next. + var dbName = $"e2e-{Guid.NewGuid():N}"; builder.Services.AddDbContext(opt => - opt.UseInMemoryDatabase($"e2e-{Guid.NewGuid():N}")); + opt.UseInMemoryDatabase(dbName)); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs b/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs index 6ad2c8a..40bc5a4 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs @@ -8,37 +8,27 @@ using ZB.MOM.WW.OtOpcUa.Configuration; namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests; /// -/// Phase 6.4 UnsTab drag-drop E2E. Task #199 landed the scaffolding; task #242 drives the -/// Blazor Server interactive circuit through a real drag-drop → confirm-modal → apply flow -/// and a 409 concurrent-edit flow. Both interactive tests are currently -/// -guarded — see below. +/// Phase 6.4 UnsTab drag-drop E2E. Task #199 landed the scaffolding; task #242 (this file) +/// drives the Blazor Server interactive circuit through a real drag-drop → confirm-modal +/// → apply flow and a 409 concurrent-edit flow, both via Chromium. /// /// /// /// Prerequisite. Chromium must be installed locally: /// pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium. +/// When the binary is missing the tests rather than fail hard, +/// so CI pipelines that don't run the install step still report green. /// /// -/// Current blocker (both interactive tests skipped). The Blazor Server circuit -/// never boots in the test-owned pipeline because _framework/blazor.web.js -/// returns HTTP 200 with a zero-byte body. The asset's route is declared in the Admin -/// project's OtOpcUa.Admin.staticwebassets.endpoints.json manifest, but the -/// underlying file is shipped via the framework NuGet -/// (Microsoft.AspNetCore.App.Internal.Assets/_framework/blazor.web.js) rather -/// than the Admin's wwwroot. points the content -/// root at the Admin assembly directory + maps hubs + runs in Development, so routing -/// / auth / DbContext / hub negotiation all succeed — the only gap is wiring the -/// framework-asset file provider into MapStaticAssets or UseStaticFiles. -/// The drag-drop + 409 scenarios are fully written; un-skipping them is a matter of -/// plumbing, not rewriting the test logic. -/// -/// -/// Options for closing the gap. (a) Layer a composite file provider that maps -/// /_framework/* into the NuGet cache at test-init time. (b) Launch the real -/// dotnet run --project Admin process as a subprocess with an InMemory DB -/// override — closest to the production composition. (c) Copy the framework asset -/// files into the test project's output via MSBuild so UseStaticFiles with -/// ContentRootPath=Admin bin finds them. +/// Harness notes. points the content root at +/// the Admin assembly directory + sets ApplicationName + calls +/// UseStaticWebAssets so /_framework/blazor.web.js + /app.css +/// resolve from the Admin's staticwebassets.development.json manifest (which +/// stitches together Admin wwwroot + the framework NuGet cache). Hubs +/// /hubs/fleet + /hubs/alerts are mapped so ClusterDetail's +/// HubConnection negotiation doesn't 500 at first render. The InMemory +/// database name is captured as a stable string per fixture instance so the seed +/// scope + Blazor circuit scope + test-assertion scope all share one backing store. /// /// [Trait("Category", "E2E")] @@ -73,9 +63,7 @@ public sealed class UnsTabDragDropE2ETests } } - [Fact(Skip = "Task #242 blocked on blazor.web.js asset resolution — see class docstring. " + - "Test body is complete + validated against the scaffolding; un-skip once the framework " + - "file provider is wired into AdminWebAppFactory.")] + [Fact] public async Task Dragging_line_onto_new_area_shows_preview_modal_then_confirms_the_move() { await using var app = new AdminWebAppFactory(); @@ -126,7 +114,7 @@ public sealed class UnsTabDragDropE2ETests } } - [Fact(Skip = "Task #242 blocked on blazor.web.js asset resolution — see class docstring.")] + [Fact] public async Task Preview_shown_then_peer_edit_applied_surfaces_409_conflict_modal() { await using var app = new AdminWebAppFactory();