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;
+ }
+}