113 lines
5.7 KiB
C#
113 lines
5.7 KiB
C#
using Microsoft.Playwright;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
|
|
|
[Collection("Playwright")]
|
|
public class DataConnectionCrudTests
|
|
{
|
|
private readonly PlaywrightFixture _pw;
|
|
|
|
public DataConnectionCrudTests(PlaywrightFixture pw)
|
|
{
|
|
_pw = pw;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CLI-create a data connection on site-a, then exercise the high-value UI delete
|
|
/// path: the connection renders as a child node in the TreeView, the per-node kebab
|
|
/// → Delete → confirm dialog removes it, a success toast fires, and the node is gone.
|
|
/// The create form requires a brittle OPC UA endpoint sub-editor, so we seed via the
|
|
/// CLI (a minimal create needs no primary config) and only drive the delete via the UI.
|
|
/// The best-effort <c>finally</c> is a safety net: the happy path deletes via the UI.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task CliCreated_Connection_DeletesViaTree()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var siteId = await CliRunner.ResolveSiteIdAsync("site-a");
|
|
var name = CliRunner.UniqueName("dconn");
|
|
var id = await CliRunner.CreateDataConnectionAsync(siteId, name);
|
|
|
|
try
|
|
{
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/connections");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
await Assertions.Expect(page.Locator("h4:has-text('Connections')")).ToBeVisibleAsync();
|
|
|
|
// Reveal all nodes so the new connection (a child of its site) is in the tree.
|
|
// Scope the "Expand all" click to the Bulk-actions dropdown we just opened, so it
|
|
// can't multi-match any other "Expand all" on the page.
|
|
var bulkDropdown = page.Locator(".dropdown")
|
|
.Filter(new() { Has = page.Locator("button.dropdown-toggle:has-text('Bulk actions')") });
|
|
await bulkDropdown.Locator("button.dropdown-toggle:has-text('Bulk actions')").ClickAsync();
|
|
await bulkDropdown.Locator(".dropdown-menu .dropdown-item:has-text('Expand all')").ClickAsync();
|
|
|
|
var node = page.Locator("span.tv-label", new() { HasText = name });
|
|
await Assertions.Expect(node).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
|
|
|
// Scope ALL dropdown interaction to THIS node's own .dc-node-actions wrapper (the
|
|
// per-node div.dropdown in DataConnections.razor that holds the kebab + its menu),
|
|
// keyed by the unique aria-label. This avoids any reliance on Bootstrap's .show —
|
|
// multiple connection-node menus (or the TreeView's separate ContextMenu) can carry
|
|
// .show at once, which would multi-match under Playwright strict mode.
|
|
var nodeActions = page.Locator(".dc-node-actions")
|
|
.Filter(new() { Has = page.Locator($"button[aria-label='More actions for {name}']") });
|
|
|
|
// The kebab is opacity:0 until its row is hovered; hovering first makes the
|
|
// reveal deterministic, then a forced click sidesteps any residual flake.
|
|
await node.HoverAsync();
|
|
await nodeActions.Locator("button.dc-kebab").ClickAsync(new() { Force = true });
|
|
|
|
await nodeActions.Locator(".dropdown-menu .dropdown-item.text-danger:has-text('Delete')")
|
|
.ClickAsync();
|
|
|
|
await Assertions.Expect(page.Locator(".modal-title:has-text('Delete Connection')")).ToBeVisibleAsync();
|
|
await page.Locator(".modal-footer .btn-danger").ClickAsync();
|
|
|
|
// Single web-first assertion on the success toast — toasts auto-dismiss at 5s,
|
|
// so we do NOT chase the toast body in a second sequential check.
|
|
await Assertions.Expect(page.Locator(".toast", new() { HasText = "deleted" }))
|
|
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
|
|
|
// The node must be gone from the tree.
|
|
await Assertions.Expect(page.Locator("span.tv-label", new() { HasText = name }))
|
|
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
|
}
|
|
finally
|
|
{
|
|
// Best-effort teardown: the happy path deletes via the UI, so this finds
|
|
// nothing; it only fires if the UI path failed mid-way.
|
|
await CliRunner.DeleteDataConnectionAsync(id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The "+ Connection" button is gated on a tree-node selection: it starts disabled
|
|
/// and only enables once a site (or connection) node is selected. Mutates nothing.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task CreateButton_GatedOnNodeSelection()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/connections");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
await Assertions.Expect(page.Locator("h4:has-text('Connections')")).ToBeVisibleAsync();
|
|
|
|
var addButton = page.Locator("button.btn.btn-primary.btn-sm:has-text('+ Connection')");
|
|
await Assertions.Expect(addButton).ToBeDisabledAsync();
|
|
|
|
// Selecting a site node (site-a exists on the live cluster) satisfies the gate.
|
|
// Site-level labels carry the extra `fw-semibold` class (connection labels don't),
|
|
// so this targets a SITE node specifically rather than any tree label.
|
|
await page.Locator("span.tv-label.fw-semibold").First.ClickAsync();
|
|
|
|
await Assertions.Expect(addButton).ToBeEnabledAsync();
|
|
}
|
|
}
|