using Microsoft.Playwright; using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; using Xunit; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design; [Collection("Playwright")] public class DataConnectionCrudTests { private readonly PlaywrightFixture _pw; public DataConnectionCrudTests(PlaywrightFixture pw) { _pw = pw; } /// /// CLI-create a data connection on site-a, then exercise the high-value UI delete /// path: the connection renders as a child node in the TreeView, the per-node kebab /// → Delete → confirm dialog removes it, a success toast fires, and the node is gone. /// The create form requires a brittle OPC UA endpoint sub-editor, so we seed via the /// CLI (a minimal create needs no primary config) and only drive the delete via the UI. /// The best-effort finally is a safety net: the happy path deletes via the UI. /// [SkippableFact] public async Task CliCreated_Connection_DeletesViaTree() { Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); var siteId = await CliRunner.ResolveSiteIdAsync("site-a"); var name = CliRunner.UniqueName("dconn"); var id = await CliRunner.CreateDataConnectionAsync(siteId, name); try { var page = await _pw.NewAuthenticatedPageAsync(); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/connections"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Assertions.Expect(page.Locator("h4:has-text('Connections')")).ToBeVisibleAsync(); // Reveal all nodes so the new connection (a child of its site) is in the tree. // Scope the "Expand all" click to the Bulk-actions dropdown we just opened, so it // can't multi-match any other "Expand all" on the page. var bulkDropdown = page.Locator(".dropdown") .Filter(new() { Has = page.Locator("button.dropdown-toggle:has-text('Bulk actions')") }); await bulkDropdown.Locator("button.dropdown-toggle:has-text('Bulk actions')").ClickAsync(); await bulkDropdown.Locator(".dropdown-menu .dropdown-item:has-text('Expand all')").ClickAsync(); var node = page.Locator("span.tv-label", new() { HasText = name }); await Assertions.Expect(node).ToBeVisibleAsync(new() { Timeout = 10_000 }); // Scope ALL dropdown interaction to THIS node's own .dc-node-actions wrapper (the // per-node div.dropdown in DataConnections.razor that holds the kebab + its menu), // keyed by the unique aria-label. This avoids any reliance on Bootstrap's .show — // multiple connection-node menus (or the TreeView's separate ContextMenu) can carry // .show at once, which would multi-match under Playwright strict mode. var nodeActions = page.Locator(".dc-node-actions") .Filter(new() { Has = page.Locator($"button[aria-label='More actions for {name}']") }); // The kebab is opacity:0 until its row is hovered; hovering first makes the // reveal deterministic, then a forced click sidesteps any residual flake. await node.HoverAsync(); await nodeActions.Locator("button.dc-kebab").ClickAsync(new() { Force = true }); await nodeActions.Locator(".dropdown-menu .dropdown-item.text-danger:has-text('Delete')") .ClickAsync(); await Assertions.Expect(page.Locator(".modal-title:has-text('Delete Connection')")).ToBeVisibleAsync(); await page.Locator(".modal-footer .btn-danger").ClickAsync(); // Single web-first assertion on the success toast — toasts auto-dismiss at 5s, // so we do NOT chase the toast body in a second sequential check. await Assertions.Expect(page.Locator(".toast", new() { HasText = "deleted" })) .ToHaveCountAsync(1, new() { Timeout = 15_000 }); // The node must be gone from the tree. await Assertions.Expect(page.Locator("span.tv-label", new() { HasText = name })) .ToHaveCountAsync(0, new() { Timeout = 10_000 }); } finally { // Best-effort teardown: the happy path deletes via the UI, so this finds // nothing; it only fires if the UI path failed mid-way. await CliRunner.DeleteDataConnectionAsync(id); } } /// /// The "+ Connection" button is gated on a tree-node selection: it starts disabled /// and only enables once a site (or connection) node is selected. Mutates nothing. /// [SkippableFact] public async Task CreateButton_GatedOnNodeSelection() { Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); var page = await _pw.NewAuthenticatedPageAsync(); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/connections"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Assertions.Expect(page.Locator("h4:has-text('Connections')")).ToBeVisibleAsync(); var addButton = page.Locator("button.btn.btn-primary.btn-sm:has-text('+ Connection')"); await Assertions.Expect(addButton).ToBeDisabledAsync(); // Selecting a site node (site-a exists on the live cluster) satisfies the gate. // Site-level labels carry the extra `fw-semibold` class (connection labels don't), // so this targets a SITE node specifically rather than any tree label. await page.Locator("span.tv-label.fw-semibold").First.ClickAsync(); await Assertions.Expect(addButton).ToBeEnabledAsync(); } }