test(playwright): DeploymentActionTests load the lazy Topology tree via a shared helper

The Topology page has loaded lazily (bounded instance pages behind 'Load
instances' loaders) since 9ee402bb (2026-09-09); TopologyAreaTests was
updated then but DeploymentActionTests still relied on one Expand-all click
revealing instance rows, so all four facts timed out locating the row. Move
OpenStableTopologyAsync into TopologyPageHelper and use it from both.
This commit is contained in:
Joseph Doherty
2026-10-22 13:48:52 -04:00
parent 560adefb0c
commit 4ae3ed3fdd
3 changed files with 59 additions and 47 deletions
@@ -199,19 +199,9 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
/// </summary>
private static async Task<ILocator> OpenInstanceContextMenuAsync(IPage page, string uniqueName)
{
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/topology");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Stop the 15s live-updates timer from rebuilding the tree under us.
var liveToggle = page.Locator("#live-updates");
if (await liveToggle.IsCheckedAsync())
{
await liveToggle.UncheckAsync();
}
// ExpandAll() recurses every branch (site → area → …), so one click reveals
// the nested instance row under site-a's zztest area.
await page.Locator("button[aria-label='Expand all areas']").ClickAsync();
// The tree loads lazily (since the 2026-09-09 low-findings wave): one Expand-all
// click no longer reveals instance rows, which sit behind "Load instances" loaders.
await TopologyPageHelper.OpenStableTopologyAsync(page);
var row = page.Locator("div.tv-row", new() { HasText = uniqueName });
await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 });
@@ -302,38 +302,6 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
}
}
/// <summary>
/// Navigates to the Topology page and returns a page whose tree is stable and
/// fully expanded: it unchecks the <c>#live-updates</c> switch (stopping the 15s
/// timer that would otherwise reload the tree and collapse expansions / tear down
/// open dialogs and rename inputs), then clicks <c>Expand all areas</c> so nested
/// area nodes are rendered and locatable.
/// </summary>
private static async Task<IPage> OpenStableTopologyAsync(PlaywrightFixture pw)
{
var page = await pw.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/topology");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var live = page.Locator("#live-updates");
if (await live.IsCheckedAsync()) await live.UncheckAsync(); // stop the 15s tree reload
// Lazy topology loads a bounded first page per branch. These small authoring
// fixtures need their sites/areas loaded before addressing labels or instances.
// "Expand all" re-renders over the SignalR circuit, so the click returns before the
// newly-expanded branches (and their "Load instances" loaders) exist. Wait until no
// collapsed branch remains before counting loaders — counting immediately raced the
// render and returned an unexpanded tree whenever the round-trip lost.
var collapsed = page.Locator("li[role='treeitem'][aria-expanded='false']");
for (var branch = 0; branch < 300; branch++)
{
await page.Locator("button[aria-label='Expand all areas']").ClickAsync();
await Assertions.Expect(collapsed).ToHaveCountAsync(0);
var load = page.GetByRole(AriaRole.Button, new() { Name = "Load instances", Exact = true });
if (await load.CountAsync() == 0) return page;
var pages = page.GetByText("Page 1", new() { Exact = true });
var count = await pages.CountAsync();
await load.First.ClickAsync();
await Assertions.Expect(pages).ToHaveCountAsync(count + 1);
}
throw new InvalidOperationException("Topology fixture exceeded 300 branches.");
}
private static Task<IPage> OpenStableTopologyAsync(PlaywrightFixture pw) =>
TopologyPageHelper.OpenStableTopologyAsync(pw);
}
@@ -0,0 +1,54 @@
using Microsoft.Playwright;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
/// <summary>
/// Shared Topology-page navigation for the Playwright suites. The page loads lazily
/// (bounded instance pages per branch, behind explicit "Load instances" loaders), so
/// any test that addresses an area or instance row must first expand and load every
/// branch.
/// </summary>
internal static class TopologyPageHelper
{
/// <summary>
/// Navigates to the Topology page and returns a page whose tree is stable and
/// fully expanded: it unchecks the <c>#live-updates</c> switch (stopping the 15s
/// timer that would otherwise reload the tree and collapse expansions / tear down
/// open dialogs and rename inputs), then clicks <c>Expand all areas</c> so nested
/// area nodes are rendered and locatable.
/// </summary>
/// <param name="pw">The Playwright fixture used to open an authenticated page.</param>
/// <returns>The authenticated page showing the fully-loaded Topology tree.</returns>
public static async Task<IPage> OpenStableTopologyAsync(PlaywrightFixture pw) =>
await OpenStableTopologyAsync(await pw.NewAuthenticatedPageAsync());
/// <summary>Navigates an existing authenticated <paramref name="page"/> to the fully-loaded Topology tree.</summary>
/// <param name="page">An authenticated page.</param>
/// <returns>The same page, showing the fully-loaded Topology tree.</returns>
public static async Task<IPage> OpenStableTopologyAsync(IPage page)
{
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/topology");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var live = page.Locator("#live-updates");
if (await live.IsCheckedAsync()) await live.UncheckAsync(); // stop the 15s tree reload
// Lazy topology loads a bounded first page per branch. These small authoring
// fixtures need their sites/areas loaded before addressing labels or instances.
// "Expand all" re-renders over the SignalR circuit, so the click returns before the
// newly-expanded branches (and their "Load instances" loaders) exist. Wait until no
// collapsed branch remains before counting loaders — counting immediately raced the
// render and returned an unexpanded tree whenever the round-trip lost.
var collapsed = page.Locator("li[role='treeitem'][aria-expanded='false']");
for (var branch = 0; branch < 300; branch++)
{
await page.Locator("button[aria-label='Expand all areas']").ClickAsync();
await Assertions.Expect(collapsed).ToHaveCountAsync(0);
var load = page.GetByRole(AriaRole.Button, new() { Name = "Load instances", Exact = true });
if (await load.CountAsync() == 0) return page;
var pages = page.GetByText("Page 1", new() { Exact = true });
var count = await pages.CountAsync();
await load.First.ClickAsync();
await Assertions.Expect(pages).ToHaveCountAsync(count + 1);
}
throw new InvalidOperationException("Topology fixture exceeded 300 branches.");
}
}