From 3998a6126f7322a8e43bbd3f204a21cc45a52225 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 5 Jun 2026 10:28:40 -0400 Subject: [PATCH] test(e2e): cover Site create/edit/delete round-trip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../SiteCrudTests.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCrudTests.cs index a89fb392..4c8cbc28 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCrudTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCrudTests.cs @@ -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(); } + /// + /// 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. + /// + [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); }