From 828d035221968bf06e966c54ff24684e5615eec2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 13:38:22 -0400 Subject: [PATCH] test(e2e): DebugView controls/Connect gating + tolerant connect-resolves-without-hang --- .../Deployment/DebugViewTests.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs new file mode 100644 index 00000000..680849b0 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs @@ -0,0 +1,132 @@ +using Microsoft.Playwright; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; +using Xunit; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; + +/// +/// E2E coverage for the Debug View page (/deployment/debug-view), which +/// live-streams a single instance's attribute values and alarm states from its +/// owning site. Two facts: the controls always render with Connect correctly +/// gated, and connecting a freshly-deployed instance resolves to a terminal state +/// without hanging. +/// +/// +/// The Enabled-only dropdown + deploy precondition (verified against +/// DebugView.razor::LoadInstancesForSite): the instance <select> +/// lists ONLY instances whose config state is Enabled +/// (i.State == InstanceState.Enabled). A freshly-minted instance is +/// NotDeployed and would never appear, so Fact B first drives the instance +/// to Enabled over the CLI (instance deploy) before it can be picked +/// in the dropdown. Both <select>s carry the data-test hooks +/// (debug-site-select / debug-instance-select, option value = entity +/// id); Fact A asserts those hooks are served, which doubles as proof the cluster +/// rebuild that added them actually took. +/// +/// +/// +/// Tolerant terminal-state assertion (validation-behavior protocol): whether +/// Connect reaches a Live badge depends on the owning site returning a +/// snapshot for the instance. Reading DebugView.razor::Connect: it awaits +/// DebugStreamService.StartStreamAsync (a ClusterClient snapshot round-trip); +/// on success it flips _connected = true (Live badge + success toast), and on +/// any exception it surfaces a Connect failed: … error .toast. Either +/// way _connecting resets, so the click always resolves — it never hangs. The +/// protocol therefore first waits on a TERMINAL state (Live badge OR an error toast) +/// within a generous window. OBSERVED reality on this live cluster (4 runs): Connect +/// RELIABLY reaches Live in ~1s with a Success: Streaming … toast (never +/// an error), so — as the protocol permits — Fact B is tightened past the tolerant +/// floor to assert the Live badge, then Disconnect → Disconnected badge + selects +/// re-enabled. The tolerant OR-wait is kept as a belt-and-braces floor so a one-off +/// snapshot hiccup degrades to a clear missing-Live assertion failure, not a hang. +/// +/// +[Collection("Playwright")] +public class DebugViewTests : IClassFixture +{ + private readonly PlaywrightFixture _pw; + private readonly DeploymentFixture _cluster; + + public DebugViewTests(PlaywrightFixture pw, DeploymentFixture cluster) + { + _pw = pw; + _cluster = cluster; + } + + [SkippableFact] + public async Task DebugView_ControlsRender_ConnectGatedOnSelection() + { + Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason); + + var page = await _pw.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/debug-view"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await Assertions.Expect(page.Locator("h4:has-text('Debug View')")).ToBeVisibleAsync(); + // The data-test hooks from Task 4 must be served (proves the rebuild took). + await Assertions.Expect(page.Locator("[data-test='debug-site-select']")).ToHaveCountAsync(1); + await Assertions.Expect(page.Locator("[data-test='debug-instance-select']")).ToHaveCountAsync(1); + + // No site/instance selected -> Connect is disabled. + var connect = page.Locator("button.btn.btn-primary.btn-sm:has-text('Connect')"); + await Assertions.Expect(connect).ToBeDisabledAsync(); + await Assertions.Expect(page.Locator("span.badge[aria-label='Connection state: Disconnected']")) + .ToBeVisibleAsync(); + } + + [SkippableFact] + public async Task DebugView_ConnectEnabledInstance_ResolvesAndDisconnect() + { + Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason); + + var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync(); + try + { + await CliRunner.DeployInstanceAsync(instanceId); // -> Enabled, so it lists in the dropdown + + var page = await _pw.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/debug-view"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.Locator("[data-test='debug-site-select']") + .SelectOptionAsync(new SelectOptionValue { Value = _cluster.SiteAId.ToString() }); + var instanceOption = page.Locator("[data-test='debug-instance-select'] option", + new() { HasText = uniqueName }); + await Assertions.Expect(instanceOption).ToHaveCountAsync(1, new() { Timeout = 10_000 }); + await page.Locator("[data-test='debug-instance-select']") + .SelectOptionAsync(new SelectOptionValue { Value = instanceId.ToString() }); + + var connect = page.Locator("button.btn.btn-primary.btn-sm:has-text('Connect')"); + await Assertions.Expect(connect).ToBeEnabledAsync(new() { Timeout = 10_000 }); + await connect.ClickAsync(); + + // Outcome-tolerant terminal-state wait: Connect resolves to Live badge OR an error + // toast within a generous window rather than hanging. The OR-form is the tolerant + // floor required by the validation-behavior protocol. + // + // OBSERVED on this live cluster (4 runs): Connect RELIABLY reaches Live — the site's + // snapshot round-trip succeeds for a freshly-deployed zztest instance in ~1s and the + // page shows the Live badge plus a "Success: Streaming " toast every time + // (never an error toast). Because Live is reliable, the protocol permits tightening: + // after the tolerant wait below resolves, this fact asserts the Live badge directly, + // then Disconnect → Disconnected badge + selects re-enabled. The OR-locator stays as a + // belt-and-braces floor so a one-off snapshot hiccup degrades to a clear assertion + // failure on the missing Live badge rather than a 25s hang. + var terminal = page.Locator("span.badge[aria-label='Connection state: Live'], .toast"); + await Assertions.Expect(terminal.First).ToBeVisibleAsync(new() { Timeout = 25_000 }); + + // Tightened to the observed-reliable Live outcome. + await Assertions.Expect(page.Locator("span.badge[aria-label='Connection state: Live']")) + .ToBeVisibleAsync(new() { Timeout = 25_000 }); + + await page.Locator("button.btn-outline-danger.btn-sm:has-text('Disconnect')").ClickAsync(); + await Assertions.Expect(page.Locator("span.badge[aria-label='Connection state: Disconnected']")) + .ToBeVisibleAsync(new() { Timeout = 10_000 }); + await Assertions.Expect(page.Locator("[data-test='debug-site-select']")).ToBeEnabledAsync(); + } + finally + { + await CliRunner.DeleteInstanceAsync(instanceId); + } + } +}