test(e2e): cover Topology Enable/Disable/Delete + fix toast assertion
This commit is contained in:
+122
-13
@@ -6,19 +6,33 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// E2E coverage for the per-instance deploy-family actions exposed from the
|
/// 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
|
/// Topology tree's right-click context menu — Deploy, Disable, Enable, and
|
||||||
/// deploy-action tests; the <see cref="OpenInstanceContextMenuAsync"/> helper
|
/// Delete. The <see cref="OpenInstanceContextMenuAsync"/> helper (navigate →
|
||||||
/// (navigate → expand → locate the instance row → right-click) is shared so the
|
/// expand → locate the instance row → right-click) is shared across every fact so
|
||||||
/// Enable/Disable/Delete tests can extend this file without re-deriving the tree
|
/// none has to re-derive the tree navigation.
|
||||||
/// navigation.
|
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Each fact mints a fresh ephemeral instance on the real <c>site-a</c> (via
|
/// Each fact mints a fresh ephemeral instance on the real <c>site-a</c> (via
|
||||||
/// <see cref="DeploymentFixture.CreateInstanceAsync"/>) so it carries the
|
/// <see cref="DeploymentFixture.CreateInstanceAsync"/>), exercises the action
|
||||||
/// expected <c>Deploy</c> (not <c>Redeploy</c>) menu state, exercises the action
|
|
||||||
/// against the live cluster, then deletes the instance in a <c>finally</c>.
|
/// against the live cluster, then deletes the instance in a <c>finally</c>.
|
||||||
/// Outcomes are tolerant: the relay to <c>site-a</c> may return a deployed
|
/// Outcomes are tolerant: the relay to <c>site-a</c> may return a confirmation or
|
||||||
/// confirmation or a fast error, but either way a single outcome toast appears.
|
/// a fast error, but either way a single outcome toast appears, which every fact
|
||||||
|
/// asserts with a single web-first <c>ToHaveCountAsync(1)</c> (it retries
|
||||||
|
/// atomically, so it neither flakes on the relay round-trip nor races the toast's
|
||||||
|
/// ~5s auto-dismiss).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// The context menu offered per instance depends on the instance's config state
|
||||||
|
/// (verified against <c>Topology.razor</c>): <c>Deploy</c>/<c>Redeploy</c> and
|
||||||
|
/// <c>Delete</c> are always present; <c>Disable</c> is shown only when the state
|
||||||
|
/// is <c>Enabled</c>, and <c>Enable</c> only when <c>Disabled</c>. So the
|
||||||
|
/// Disable/Enable facts drive the precondition state deterministically over the
|
||||||
|
/// CLI (<c>deploy</c> → Enabled; <c>deploy</c> then <c>disable</c> → Disabled)
|
||||||
|
/// before opening the menu. <c>Deploy</c> and <c>Enable</c> fire with no confirm
|
||||||
|
/// dialog; <c>Disable</c> and <c>Delete</c> are danger actions that first raise a
|
||||||
|
/// confirm modal whose confirm button is <c>.modal-footer .btn-danger</c> (labelled
|
||||||
|
/// <c>Delete</c>).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Collection("Playwright")]
|
[Collection("Playwright")]
|
||||||
@@ -53,11 +67,12 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
|||||||
// The relay outcome surfaces on a toast — deployed confirmation or a fast
|
// The relay outcome surfaces on a toast — deployed confirmation or a fast
|
||||||
// error. We assert exactly one toast (the single-toast contract), not which
|
// error. We assert exactly one toast (the single-toast contract), not which
|
||||||
// outcome, since the live cluster may answer either way. The toast
|
// outcome, since the live cluster may answer either way. The toast
|
||||||
// auto-dismisses ~5s after it appears, so assert promptly with a generous
|
// auto-dismisses ~5s after it appears, so a separate ToBeVisible + CountAsync
|
||||||
// wait for the relay round-trip.
|
// would race that dismissal (a TOCTOU between the two reads); ToHaveCountAsync
|
||||||
|
// retries atomically against the live DOM, with a generous wait for the relay
|
||||||
|
// round-trip.
|
||||||
var toast = page.Locator(".toast");
|
var toast = page.Locator(".toast");
|
||||||
await Assertions.Expect(toast).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
await Assertions.Expect(toast).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
Assert.Equal(1, await toast.CountAsync());
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -65,6 +80,100 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task Disable_Instance_ShowsOutcomeToast()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||||
|
|
||||||
|
var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
|
||||||
|
// Deploy moves the instance config state to Enabled, so the context menu
|
||||||
|
// offers "Disable" (it would offer "Enable" only for a Disabled instance).
|
||||||
|
await CliRunner.DeployInstanceAsync(instanceId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var page = await _pw.NewAuthenticatedPageAsync();
|
||||||
|
await OpenInstanceContextMenuAsync(page, uniqueName);
|
||||||
|
|
||||||
|
await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Disable" })
|
||||||
|
.ClickAsync();
|
||||||
|
|
||||||
|
// Disable is a danger action: it raises a confirm modal first. Confirm via
|
||||||
|
// the danger button (labelled "Delete" for every danger confirm).
|
||||||
|
await page.Locator(".modal-footer .btn-danger", new() { HasText = "Delete" })
|
||||||
|
.ClickAsync();
|
||||||
|
|
||||||
|
// One outcome toast (confirmation or fast error). ToHaveCountAsync retries
|
||||||
|
// atomically, so it survives the relay round-trip and the toast's ~5s dismiss.
|
||||||
|
await Assertions.Expect(page.Locator(".toast"))
|
||||||
|
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await CliRunner.DeleteInstanceAsync(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task Enable_Instance_ShowsOutcomeToast()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||||
|
|
||||||
|
var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
|
||||||
|
// Deploy then Disable leaves the instance config state at Disabled, so the
|
||||||
|
// context menu offers "Enable".
|
||||||
|
await CliRunner.DeployInstanceAsync(instanceId);
|
||||||
|
await CliRunner.DisableInstanceAsync(instanceId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var page = await _pw.NewAuthenticatedPageAsync();
|
||||||
|
await OpenInstanceContextMenuAsync(page, uniqueName);
|
||||||
|
|
||||||
|
// Enable fires immediately — no confirm dialog.
|
||||||
|
await page.Locator(".dropdown-menu.show button.dropdown-item", new() { HasText = "Enable" })
|
||||||
|
.ClickAsync();
|
||||||
|
|
||||||
|
await Assertions.Expect(page.Locator(".toast"))
|
||||||
|
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await CliRunner.DeleteInstanceAsync(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task Delete_Instance_RemovesFromTree()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||||
|
|
||||||
|
var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var page = await _pw.NewAuthenticatedPageAsync();
|
||||||
|
await OpenInstanceContextMenuAsync(page, uniqueName);
|
||||||
|
|
||||||
|
// Delete is always present (independent of state) and is the danger item.
|
||||||
|
await page.Locator(".dropdown-menu.show button.dropdown-item.text-danger")
|
||||||
|
.ClickAsync();
|
||||||
|
|
||||||
|
// Danger action: confirm via the modal's danger button.
|
||||||
|
await page.Locator(".modal-footer .btn-danger", new() { HasText = "Delete" })
|
||||||
|
.ClickAsync();
|
||||||
|
|
||||||
|
// The delete succeeds locally (NotDeployed instance) and the tree reloads,
|
||||||
|
// so the row for this instance disappears. ToHaveCountAsync(0) retries until
|
||||||
|
// the reload settles.
|
||||||
|
await Assertions.Expect(page.Locator("div.tv-row", new() { HasText = uniqueName }))
|
||||||
|
.ToHaveCountAsync(0, new() { Timeout = 15_000 });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Best-effort net: if the UI delete failed, this removes the leftover
|
||||||
|
// instance; if it already deleted, this is a no-op (delete is best-effort).
|
||||||
|
await CliRunner.DeleteInstanceAsync(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Navigates to the Topology page, expands the tree so the instance row under
|
/// Navigates to the Topology page, expands the tree so the instance row under
|
||||||
/// <c>site-a → zztest area</c> is rendered, locates the row by its
|
/// <c>site-a → zztest area</c> is rendered, locates the row by its
|
||||||
|
|||||||
Reference in New Issue
Block a user