test(e2e): cover Site create/edit/delete round-trip
Adds CreateEditDelete_Site_RoundTrips [SkippableFact] to SiteCrudTests. Exercises the full create → edit → delete UI flow against the live cluster, with CliRunner best-effort teardown so no zztest-* sites leak on mid-test failure.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests;
|
||||
|
||||
@@ -91,6 +92,107 @@ public class SiteCrudTests
|
||||
await Expect(page.Locator(".text-danger")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full create → edit → delete round-trip for a site via the Central UI.
|
||||
/// Uses CliRunner for best-effort teardown so no zztest-* site leaks on failure.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task CreateEditDelete_Site_RoundTrips()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var hex = Guid.NewGuid().ToString("N")[..8];
|
||||
var ident = $"zztest-{hex}";
|
||||
var name = $"zztest-site-{hex}";
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// ── CREATE ────────────────────────────────────────────────────────────────
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/sites/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Fill Identifier — label-anchored input (Node A/B share the same Akka/gRPC
|
||||
// placeholder text so we select the Akka inputs by placeholder index below).
|
||||
await page.Locator("label:has-text('Identifier') + input.form-control.form-control-sm").FillAsync(ident);
|
||||
await page.Locator("label:has-text('Name') + input.form-control.form-control-sm").FillAsync(name);
|
||||
await page.Locator("label:has-text('Description') + input.form-control.form-control-sm").FillAsync("e2e");
|
||||
|
||||
// Node A and Node B share identical placeholder text; select by index.
|
||||
// Index 0 = Node A Akka, Index 1 = Node B Akka.
|
||||
var akkaInputs = page.Locator("input[placeholder='akka.tcp://scadabridge@host:port/user/site-communication']");
|
||||
await akkaInputs.Nth(0).FillAsync("akka.tcp://scadabridge@zz:5000/user/site-communication");
|
||||
await akkaInputs.Nth(1).FillAsync("akka.tcp://scadabridge@zz:5000/user/site-communication");
|
||||
|
||||
// Index 0 = Node A gRPC, Index 1 = Node B gRPC.
|
||||
var grpcInputs = page.Locator("input[placeholder='http://host:8083']");
|
||||
await grpcInputs.Nth(0).FillAsync("http://zz:8083");
|
||||
await grpcInputs.Nth(1).FillAsync("http://zz:8083");
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
|
||||
// Wait for Blazor enhanced navigation back to the list page.
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// The new site card must be visible.
|
||||
var card = page.Locator("div.card", new() { HasText = name });
|
||||
await Assertions.Expect(card).ToBeVisibleAsync();
|
||||
|
||||
// ── EDIT ──────────────────────────────────────────────────────────────────
|
||||
// Scope the Edit button to the card to avoid strict-mode violations.
|
||||
await card.Locator("button.btn.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync();
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites/", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Update the description and save.
|
||||
await page.Locator("label:has-text('Description') + input.form-control.form-control-sm").FillAsync("e2e-edited");
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/edit");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Card must still be present after the edit.
|
||||
await Assertions.Expect(page.Locator("div.card", new() { HasText = name })).ToBeVisibleAsync();
|
||||
|
||||
// ── DELETE ────────────────────────────────────────────────────────────────
|
||||
// Re-locate the card after navigation; scope the kebab to avoid strict mode.
|
||||
var cardAfterEdit = page.Locator("div.card", new() { HasText = name });
|
||||
var kebab = cardAfterEdit.Locator("button[aria-label^='More actions']");
|
||||
await kebab.ClickAsync();
|
||||
|
||||
// Click Delete in the now-open dropdown — scoped to the card.
|
||||
var deleteBtn = cardAfterEdit.Locator(".dropdown-menu button.dropdown-item.text-danger");
|
||||
await deleteBtn.ClickAsync();
|
||||
|
||||
// Confirm the global danger dialog.
|
||||
await Assertions.Expect(page.Locator(".modal-footer .btn-danger")).ToBeVisibleAsync();
|
||||
await page.ClickAsync(".modal-footer .btn-danger");
|
||||
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// The card must be gone.
|
||||
await Assertions.Expect(page.Locator("div.card", new() { HasText = name }))
|
||||
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort teardown: delete the site via CLI in case the UI path failed
|
||||
// mid-way. The happy path already deleted it via the UI, so ResolveSiteIdAsync
|
||||
// will throw (no matching site) and the inner catch swallows it.
|
||||
try
|
||||
{
|
||||
await CliRunner.DeleteSiteAsync(await CliRunner.ResolveSiteIdAsync(ident));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Site already deleted (happy path) or cluster unreachable — ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ILocatorAssertions Expect(ILocator locator) =>
|
||||
Assertions.Expect(locator);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user