Task #242 finish — UnsTab drag-drop interactive E2E tests un-skip + pass #201

Merged
dohertj2 merged 1 commits from task-242-finish-interactive-tests into v2 2026-04-21 02:33:28 -04:00
2 changed files with 34 additions and 34 deletions

View File

@@ -47,18 +47,25 @@ public sealed class AdminWebAppFactory : IAsyncDisposable
var port = GetFreeTcpPort(); var port = GetFreeTcpPort();
BaseUrl = $"http://127.0.0.1:{port}"; BaseUrl = $"http://127.0.0.1:{port}";
// Point the content root at the Admin project's build output so wwwroot/ (app.css, // Point the content root at the Admin project's build output so the Admin
// site CSS, icons) + the Blazor framework assets served from the Admin assembly // assembly + its sibling staticwebassets manifest are discoverable. The manifest
// resolve. Without this the default content root is the test project's bin dir and // maps /_framework/* to the framework NuGet cache + /app.css to the Admin source
// blazor.web.js + app.css return 404, which keeps the interactive circuit from // wwwroot; StaticWebAssetsLoader.UseStaticWebAssets reads it and wires a composite
// booting at all. // file provider automatically.
var adminAssemblyDir = System.IO.Path.GetDirectoryName( var adminAssemblyDir = System.IO.Path.GetDirectoryName(
typeof(Admin.Components.App).Assembly.Location)!; typeof(Admin.Components.App).Assembly.Location)!;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{ {
ContentRootPath = adminAssemblyDir, ContentRootPath = adminAssemblyDir,
ApplicationName = typeof(Admin.Components.App).Assembly.GetName().Name,
}); });
builder.WebHost.UseUrls(BaseUrl); 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 // 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 // as visible 500s with stacks the test can capture — prod-style generic errors make
// diagnosis of circuit / DI misconfig effectively impossible. // 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)); .AddPolicy("CanPublish", p => p.RequireRole(Admin.Services.AdminRoles.FleetAdmin));
builder.Services.AddCascadingAuthenticationState(); 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<OtOpcUaConfigDbContext>(opt => builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
opt.UseInMemoryDatabase($"e2e-{Guid.NewGuid():N}")); opt.UseInMemoryDatabase(dbName));
builder.Services.AddScoped<Admin.Services.ClusterService>(); builder.Services.AddScoped<Admin.Services.ClusterService>();
builder.Services.AddScoped<Admin.Services.GenerationService>(); builder.Services.AddScoped<Admin.Services.GenerationService>();

View File

@@ -8,37 +8,27 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests; namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
/// <summary> /// <summary>
/// Phase 6.4 UnsTab drag-drop E2E. Task #199 landed the scaffolding; task #242 drives the /// Phase 6.4 UnsTab drag-drop E2E. Task #199 landed the scaffolding; task #242 (this file)
/// Blazor Server interactive circuit through a real drag-drop → confirm-modal → apply flow /// drives the Blazor Server interactive circuit through a real drag-drop → confirm-modal
/// and a 409 concurrent-edit flow. Both interactive tests are currently /// → apply flow and a 409 concurrent-edit flow, both via Chromium.
/// <see cref="FactAttribute.Skip"/>-guarded — see below.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// <b>Prerequisite.</b> Chromium must be installed locally: /// <b>Prerequisite.</b> Chromium must be installed locally:
/// <c>pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium</c>. /// <c>pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium</c>.
/// When the binary is missing the tests <see cref="Assert.Skip"/> rather than fail hard,
/// so CI pipelines that don't run the install step still report green.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Current blocker (both interactive tests skipped).</b> The Blazor Server circuit /// <b>Harness notes.</b> <see cref="AdminWebAppFactory"/> points the content root at
/// never boots in the test-owned pipeline because <c>_framework/blazor.web.js</c> /// the Admin assembly directory + sets <c>ApplicationName</c> + calls
/// returns HTTP 200 with a zero-byte body. The asset's route is declared in the Admin /// <c>UseStaticWebAssets</c> so <c>/_framework/blazor.web.js</c> + <c>/app.css</c>
/// project's <c>OtOpcUa.Admin.staticwebassets.endpoints.json</c> manifest, but the /// resolve from the Admin's <c>staticwebassets.development.json</c> manifest (which
/// underlying file is shipped via the framework NuGet /// stitches together Admin <c>wwwroot</c> + the framework NuGet cache). Hubs
/// (<c>Microsoft.AspNetCore.App.Internal.Assets/_framework/blazor.web.js</c>) rather /// <c>/hubs/fleet</c> + <c>/hubs/alerts</c> are mapped so <c>ClusterDetail</c>'s
/// than the Admin's <c>wwwroot</c>. <see cref="AdminWebAppFactory"/> points the content /// <c>HubConnection</c> negotiation doesn't 500 at first render. The InMemory
/// root at the Admin assembly directory + maps hubs + runs in Development, so routing /// database name is captured as a stable string per fixture instance so the seed
/// / auth / DbContext / hub negotiation all succeed — the only gap is wiring the /// scope + Blazor circuit scope + test-assertion scope all share one backing store.
/// framework-asset file provider into <c>MapStaticAssets</c> or <c>UseStaticFiles</c>.
/// The drag-drop + 409 scenarios are fully written; un-skipping them is a matter of
/// plumbing, not rewriting the test logic.
/// </para>
/// <para>
/// <b>Options for closing the gap.</b> (a) Layer a composite file provider that maps
/// <c>/_framework/*</c> into the NuGet cache at test-init time. (b) Launch the real
/// <c>dotnet run --project Admin</c> 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 <c>UseStaticFiles</c> with
/// <c>ContentRootPath</c>=Admin bin finds them.
/// </para> /// </para>
/// </remarks> /// </remarks>
[Trait("Category", "E2E")] [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. " + [Fact]
"Test body is complete + validated against the scaffolding; un-skip once the framework " +
"file provider is wired into AdminWebAppFactory.")]
public async Task Dragging_line_onto_new_area_shows_preview_modal_then_confirms_the_move() public async Task Dragging_line_onto_new_area_shows_preview_modal_then_confirms_the_move()
{ {
await using var app = new AdminWebAppFactory(); 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() public async Task Preview_shown_then_peer_edit_applied_surfaces_409_conflict_modal()
{ {
await using var app = new AdminWebAppFactory(); await using var app = new AdminWebAppFactory();