diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentActionTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentActionTests.cs new file mode 100644 index 00000000..502fe3de --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentActionTests.cs @@ -0,0 +1,109 @@ +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 per-instance deploy-family actions exposed from the +/// Topology tree's right-click context menu. This is the first of the +/// deploy-action tests; the helper +/// (navigate → expand → locate the instance row → right-click) is shared so the +/// Enable/Disable/Delete tests can extend this file without re-deriving the tree +/// navigation. +/// +/// +/// Each fact mints a fresh ephemeral instance on the real site-a (via +/// ) so it carries the +/// expected Deploy (not Redeploy) menu state, exercises the action +/// against the live cluster, then deletes the instance in a finally. +/// Outcomes are tolerant: the relay to site-a may return a deployed +/// confirmation or a fast error, but either way a single outcome toast appears. +/// +/// +[Collection("Playwright")] +public class DeploymentActionTests : IClassFixture +{ + private readonly PlaywrightFixture _pw; + private readonly DeploymentFixture _cluster; + + public DeploymentActionTests(PlaywrightFixture pw, DeploymentFixture cluster) + { + _pw = pw; + _cluster = cluster; + } + + [SkippableFact] + public async Task Deploy_Instance_ShowsOutcomeToast() + { + Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason); + + var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync(); + try + { + var page = await _pw.NewAuthenticatedPageAsync(); + await OpenInstanceContextMenuAsync(page, uniqueName); + + // A fresh instance is NotDeployed, so the action reads "Deploy" (it would + // read "Redeploy" only for a stale, already-deployed node). Deploy has no + // confirm dialog — clicking relays to site-a immediately. + await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Deploy" }) + .ClickAsync(); + + // The relay outcome surfaces on a toast — deployed confirmation or a fast + // error. We assert exactly one toast (the single-toast contract), not which + // outcome, since the live cluster may answer either way. The toast + // auto-dismisses ~5s after it appears, so assert promptly with a generous + // wait for the relay round-trip. + var toast = page.Locator(".toast"); + await Assertions.Expect(toast).ToBeVisibleAsync(new() { Timeout = 15_000 }); + Assert.Equal(1, await toast.CountAsync()); + } + finally + { + await CliRunner.DeleteInstanceAsync(instanceId); + } + } + + /// + /// Navigates to the Topology page, expands the tree so the instance row under + /// site-a → zztest area is rendered, locates the row by its + /// label, and opens its right-click context menu. + /// Returns the located row so callers can re-target it if needed. + /// + /// + /// Live updates are switched off first: the page reloads the tree on a 15s timer, + /// which would collapse a freshly-expanded node and tear down an open context + /// menu mid-interaction. With it off the tree stays stable through the action. + /// + /// + private static async Task 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(); + + var row = page.Locator("div.tv-row", new() { HasText = uniqueName }); + await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 }); + + // The tree scrolls inside a fixed-height container; bring the row into view + // so the right-click lands on it and the menu renders at a usable position. + await row.ScrollIntoViewIfNeededAsync(); + await row.ClickAsync(new() { Button = MouseButton.Right }); + + await Assertions.Expect(page.Locator(".dropdown-menu.show")) + .ToBeVisibleAsync(new() { Timeout = 5_000 }); + + return row; + } +}