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. Short timeout: the OR-floor // above already resolved, so the Live badge is either present now or a toast // resolved instead (the error path) — in which case this fails fast rather than // burning the full window waiting for a badge that will never appear. await Assertions.Expect(page.Locator("span.badge[aria-label='Connection state: Live']")) .ToBeVisibleAsync(new() { Timeout = 2_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); } } }