From 7f59ae12cb63c34d6ee9d0d61a609540fbaefcf2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 15:33:28 -0400 Subject: [PATCH] =?UTF-8?q?test(playwright):=20DebugView=20tabs+trees=20?= =?UTF-8?q?=E2=80=94=20assert=20tabbed=20layout,=20tree=20panes,=20Alarms-?= =?UTF-8?q?tab=20switch=20on=20connected=20instance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Deployment/DebugViewTreeTests.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs new file mode 100644 index 00000000..13c5c134 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs @@ -0,0 +1,134 @@ +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 tabbed, tree-rendered Debug View +/// (/deployment/debug-view) shipped by the debugview-tabs-trees feature. +/// Where proves the controls render and Connect +/// resolves to a terminal state, this class proves the connected layout: a tab +/// strip (Attributes / Alarms), each tab a composition . +/// +/// +/// Seeding mirrors exactly — the Enabled-only +/// instance <select> (verified against +/// DebugView.razor::LoadInstancesForSite, i.State == InstanceState.Enabled) +/// lists only Enabled instances. A freshly-minted instance is NotDeployed +/// and would never appear, so the test mints an instance via the shared +/// and drives it to Enabled over the CLI +/// (instance deploy) before selecting it. The fixture provisions onto the +/// real, running site-a so the ClusterClient snapshot round-trip resolves +/// fast (an unknown site has no registered ClusterClient and only times out). +/// +/// +/// +/// Tolerant terminal-state wait, then the tabs/trees assertions — Connect +/// awaits a ClusterClient snapshot round-trip; on success the page flips to the +/// Live badge and renders the tabbed card, on failure it surfaces a +/// Connect failed: … .toast (see DebugView.razor::Connect). +/// The test first waits on a TERMINAL state (Live badge OR error toast) within a +/// generous window so it never hangs, then asserts the Live badge directly +/// (OBSERVED reliable on this live cluster in ). Only +/// once connected does the card with the tab strip render +/// (@if (_connected && _snapshot != null)), so the tabs/trees +/// assertions are gated behind the Live badge. +/// +/// +/// +/// Why the tree assertion is robust regardless of seeded alarms — the +/// always renders its <ul role="tree"> root +/// (with an EmptyContent "No alarms." / "No attributes." slot when the +/// forest is empty), so asserting a [role="tree"] inside the active pane +/// holds whether or not the fixture instance happens to carry a configured alarm +/// or composition members. The test therefore does NOT depend on heavy alarm +/// seeding: it proves the Alarms tab switches the visible pane and that pane hosts +/// a tree, which is the structural contract of the tabs+trees rework. +/// +/// +[Collection("Playwright")] +public class DebugViewTreeTests : IClassFixture +{ + private readonly PlaywrightFixture _pw; + private readonly DeploymentFixture _cluster; + + public DebugViewTreeTests(PlaywrightFixture pw, DeploymentFixture cluster) + { + _pw = pw; + _cluster = cluster; + } + + [SkippableFact] + public async Task DebugView_Connected_RendersTabbedTrees_AndAlarmsTabSwitches() + { + 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); + + // Select the seeded site + Enabled instance (same hooks as DebugViewTests). + 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 floor: Live badge OR an error toast within a + // generous window so the click never hangs (mirrors DebugViewTests). Then tighten + // to the observed-reliable Live outcome — the tabbed card only renders when + // connected, so the tabs/trees assertions below depend on reaching Live. + var terminal = page.Locator("span.badge[aria-label='Connection state: Live'], .toast"); + await Assertions.Expect(terminal.First).ToBeVisibleAsync(new() { Timeout = 25_000 }); + await Assertions.Expect(page.Locator("span.badge[aria-label='Connection state: Live']")) + .ToBeVisibleAsync(new() { Timeout = 2_000 }); + + // ── Tab strip renders with both tabs (the tabs+trees rework's structural hooks). ── + var attrTab = page.Locator("[data-test='debug-tab-attributes']"); + var alarmTab = page.Locator("[data-test='debug-tab-alarms']"); + await Assertions.Expect(attrTab).ToBeVisibleAsync(new() { Timeout = 10_000 }); + await Assertions.Expect(alarmTab).ToBeVisibleAsync(); + + var attrPane = page.Locator("[data-test='debug-pane-attributes']"); + var alarmPane = page.Locator("[data-test='debug-pane-alarms']"); + + // Attributes is the default tab: its pane is visible and hosts a tree; + // the Alarms pane is present in the DOM but hidden (d-none) until selected. + await Assertions.Expect(attrTab).ToHaveAttributeAsync("aria-selected", "true"); + await Assertions.Expect(attrPane).ToBeVisibleAsync(); + await Assertions.Expect(attrPane.Locator("[role='tree']")).ToHaveCountAsync(1); + await Assertions.Expect(alarmPane).ToBeHiddenAsync(); + + // ── Switch to the Alarms tab and assert the pane swaps + hosts its own tree. ── + await alarmTab.ClickAsync(); + await Assertions.Expect(alarmTab).ToHaveAttributeAsync("aria-selected", "true"); + await Assertions.Expect(alarmPane).ToBeVisibleAsync(new() { Timeout = 10_000 }); + await Assertions.Expect(attrPane).ToBeHiddenAsync(); + // The TreeView always renders its