From 8e65fc51e596f16d74387fddd507b87e277bd5d1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 15:13:47 -0400 Subject: [PATCH] test(playwright): node-scope DataConnection kebab + target site node (review fix) --- .../Design/DataConnectionCrudTests.cs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/DataConnectionCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/DataConnectionCrudTests.cs index 5a03a475..c2d5cb38 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/DataConnectionCrudTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/DataConnectionCrudTests.cs @@ -39,23 +39,30 @@ public class DataConnectionCrudTests 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. - await page.Locator("button.btn-outline-secondary.btn-sm.dropdown-toggle:has-text('Bulk actions')") - .ClickAsync(); - await page.Locator(".dropdown-item:has-text('Expand all')").ClickAsync(); + // 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 page.Locator($"button.dc-kebab[aria-label='More actions for {name}']") - .ClickAsync(new() { Force = true }); + await nodeActions.Locator("button.dc-kebab").ClickAsync(new() { Force = true }); - // Every connection node renders its own .text-danger "Delete" item, so scope the - // click to the one open menu. Bootstrap puts .show on the active node's - // ul.dropdown-menu (and auto-closes the others), making this match uniquely. - await page.Locator(".dropdown-menu.show .dropdown-item.text-danger:has-text('Delete')") + 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(); @@ -95,8 +102,10 @@ public class DataConnectionCrudTests var addButton = page.Locator("button.btn.btn-primary.btn-sm:has-text('+ Connection')"); await Assertions.Expect(addButton).ToBeDisabledAsync(); - // Selecting any site node (site-a exists on the live cluster) satisfies the gate. - await page.Locator("span.tv-label").First.ClickAsync(); + // 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(); }