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 alarms-tab assertion is tolerant — the /// renders its <ul role="tree"> root ONLY /// when the forest is non-empty; when empty it renders the EmptyContent /// slot ("No alarms.") with no [role="tree"] element. The seeded /// instance has no configured alarms, so the /// alarms pane shows the empty hint. The Attributes pane always has data (one /// attribute is seeded), so its [role="tree"] assertion remains strict. /// The populated alarm tree is covered by bUnit tests (DebugViewAlarmTableTests). /// /// [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(); // TreeView renders