test(e2e): Topology create-area (toolbar) + inline rename (Enter commits, Escape reverts) with CLI read-back
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// E2E coverage for the Topology page's area authoring surface — creating an area
|
||||
/// from the toolbar "+ Area" dialog (the site-picker variant), and the inline
|
||||
/// double-click rename on an area node's label.
|
||||
///
|
||||
/// <para>
|
||||
/// Every fact opens the page through <see cref="OpenStableTopologyAsync"/>, which
|
||||
/// FIRST unchecks the <c>#live-updates</c> switch. The page reloads the whole tree
|
||||
/// on a 15s timer; left on, that reload collapses freshly-expanded nodes and tears
|
||||
/// down any open dialog or in-progress rename input mid-interaction. With it off
|
||||
/// the tree stays stable for the duration of the action. The helper then clicks
|
||||
/// <c>Expand all areas</c> so nested area nodes are rendered and locatable.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Toolbar create: "+ Area" opens <c>CreateAreaDialog</c> in its site-picker mode,
|
||||
/// where the FIRST <c><select></c> is the Site picker (option value = site id)
|
||||
/// and the second is the parent area. Success closes the dialog, raises an
|
||||
/// "Area '…' created." toast, and renders the new node.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Inline rename: double-clicking an area's <c>span.tv-label</c> swaps it for a
|
||||
/// rename input. <b>Enter commits, Escape reverts, and blur CANCELS</b> — so a
|
||||
/// committing fact fills the input then presses Enter on that SAME input, never
|
||||
/// clicking elsewhere first (a click would blur and silently discard the edit).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Areas these tests create/rename are ephemeral and cleaned up in a
|
||||
/// <c>finally</c>: the rename facts create their own area over the CLI (known id →
|
||||
/// delete by id), while the toolbar-create fact never learns the new id from the
|
||||
/// UI, so it sweeps by name prefix
|
||||
/// (<see cref="CliRunner.ListAreaIdsByNamePrefixAsync"/> →
|
||||
/// <see cref="CliRunner.DeleteAreaAsync"/>). A CLI read-back after each action
|
||||
/// confirms the change actually persisted, not just that the toast appeared.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
public TopologyAreaTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CreateArea_ViaToolbar_AppearsInTreeAndPersists()
|
||||
{
|
||||
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var areaName = CliRunner.UniqueName("topoarea");
|
||||
try
|
||||
{
|
||||
var page = await OpenStableTopologyAsync(_pw);
|
||||
|
||||
await page.Locator("button:has-text('+ Area')").ClickAsync();
|
||||
var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text('New Area'))");
|
||||
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
||||
|
||||
// First select = Site (option value = site.Id); then the name; then Create.
|
||||
await dialog.Locator("select").First.SelectOptionAsync(
|
||||
new SelectOptionValue { Value = _cluster.SiteAId.ToString() });
|
||||
await dialog.Locator("input[placeholder='Area name']").FillAsync(areaName);
|
||||
await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Create')").ClickAsync();
|
||||
|
||||
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(page.Locator($"span.tv-label:has-text('{areaName}')"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
|
||||
var ids = await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, areaName);
|
||||
Assert.Single(ids);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var id in await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, areaName))
|
||||
await CliRunner.DeleteAreaAsync(id);
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task RenameArea_EnterCommits_PersistsNewName()
|
||||
{
|
||||
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var original = CliRunner.UniqueName("rnarea");
|
||||
var renamed = CliRunner.UniqueName("rndone");
|
||||
var areaId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, original);
|
||||
try
|
||||
{
|
||||
var page = await OpenStableTopologyAsync(_pw);
|
||||
|
||||
var label = page.Locator($"span.tv-label:has-text('{original}')");
|
||||
await Assertions.Expect(label).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await label.DblClickAsync();
|
||||
|
||||
var input = page.Locator($"input[aria-label='Rename {original}']");
|
||||
await Assertions.Expect(input).ToBeVisibleAsync();
|
||||
await input.FillAsync(renamed);
|
||||
// The input is @bind="_renameBuffer" with the default onchange trigger, which a
|
||||
// raw Enter keystroke does NOT fire (onchange only fires on blur in the browser) —
|
||||
// and blur CANCELS the rename. So dispatch a `change` event to flush the bound
|
||||
// value to the server WITHOUT losing focus, then press Enter to run CommitRename
|
||||
// against the now-updated _renameBuffer.
|
||||
await input.DispatchEventAsync("change");
|
||||
await input.PressAsync("Enter"); // commit WITHOUT blurring (blur would cancel)
|
||||
|
||||
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(page.Locator($"span.tv-label:has-text('{renamed}')"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
|
||||
Assert.Single(await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, renamed));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CliRunner.DeleteAreaAsync(areaId);
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task RenameArea_Escape_RevertsToOriginalLabel()
|
||||
{
|
||||
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var original = CliRunner.UniqueName("esarea");
|
||||
var areaId = await CliRunner.CreateAreaAsync(_cluster.SiteAId, original);
|
||||
try
|
||||
{
|
||||
var page = await OpenStableTopologyAsync(_pw);
|
||||
|
||||
var label = page.Locator($"span.tv-label:has-text('{original}')");
|
||||
await Assertions.Expect(label).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await label.DblClickAsync();
|
||||
|
||||
var input = page.Locator($"input[aria-label='Rename {original}']");
|
||||
await Assertions.Expect(input).ToBeVisibleAsync();
|
||||
await input.FillAsync(CliRunner.UniqueName("discarded"));
|
||||
await input.PressAsync("Escape");
|
||||
|
||||
await Assertions.Expect(input).ToHaveCountAsync(0, new() { Timeout = 5_000 });
|
||||
await Assertions.Expect(page.Locator($"span.tv-label:has-text('{original}')")).ToBeVisibleAsync();
|
||||
Assert.Single(await CliRunner.ListAreaIdsByNamePrefixAsync(_cluster.SiteAId, original));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CliRunner.DeleteAreaAsync(areaId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
await page.Locator("button[aria-label='Expand all areas']").ClickAsync();
|
||||
return page;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user