From 1ecce5843703261670b9d0a7861eff2f6daae421 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 15:16:39 -0400 Subject: [PATCH] test(playwright): add ApiMethod validation + visibility + delete coverage (Wave 3) --- .../Design/ApiMethodFormTests.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ApiMethodFormTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ApiMethodFormTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ApiMethodFormTests.cs new file mode 100644 index 00000000..9c20142c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ApiMethodFormTests.cs @@ -0,0 +1,104 @@ +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 ApiMethodFormTests +{ + private readonly PlaywrightFixture _pw; + + public ApiMethodFormTests(PlaywrightFixture pw) + { + _pw = pw; + } + + /// + /// The Add-API-Method form's required-field gate fires when Name is present but the + /// script is empty. This deliberately exercises the validation WITHOUT touching the + /// Monaco editor (brittle to type into): fill only the Name, click Save, and expect the + /// inline "Name and script required." error. The form does not navigate, so nothing is + /// saved — this fixture name never reaches the database. + /// + [SkippableFact] + public async Task CreateForm_NameWithoutScript_ShowsInlineError() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _pw.NewAuthenticatedPageAsync(); + + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/api-methods/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Assertions.Expect(page.Locator("h4:has-text('Add API Method')")).ToBeVisibleAsync(); + + // The form has several text inputs (Name + SchemaBuilder/parameter rows), so anchor + // the Name input to its label wrapper to stay unambiguous. + var nameInput = page.Locator("div.mb-3:has(label:has-text('Name')) input[type=text]"); + await Assertions.Expect(nameInput).ToBeVisibleAsync(); + await nameInput.FillAsync("zzapimethod"); + + // Leave the Monaco script empty and submit — the gate should reject it. + await page.Locator("button.btn-success:has-text('Save')").ClickAsync(); + + await Assertions.Expect(page.Locator("div.text-danger.small:has-text('Name and script required.')")) + .ToBeVisibleAsync(); + + // The form must not have navigated away. + await Assertions.Expect(page).ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/design/api-methods/create$")); + } + + /// + /// CLI-create an inbound API method, confirm it renders on the External Systems page's + /// "Inbound API Methods" tab, then delete it via the card kebab and confirm dialog. The + /// create/edit form is Monaco-driven (brittle), so authoring happens via the CLI; the UI + /// delete round-trip is the behavior under test. A best-effort CLI delete in finally + /// guarantees no zztest-method-* row leaks if the UI path fails mid-way. + /// + [SkippableFact] + public async Task CliCreated_Method_VisibleAndDeletes() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var name = CliRunner.UniqueName("method"); + int id = await CliRunner.CreateApiMethodAsync(name); + + try + { + var page = await _pw.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Switch to the Inbound API Methods tab; only its panel renders, so the card + // locator below stays unambiguous against External System / DB Connection cards. + await page.Locator("button.nav-link:has-text('Inbound API Methods')").ClickAsync(); + + var card = page.Locator("div.card").Filter(new() { HasText = name }); + await Assertions.Expect(card).ToHaveCountAsync(1, new() { Timeout = 10_000 }); + await Assertions.Expect(card.Locator($"code:has-text('POST /api/{name}')")).ToBeVisibleAsync(); + + // Scope the kebab + delete item to the card's .dropdown for strict-mode safety. + var dropdown = card.Locator(".dropdown"); + await dropdown.Locator("button[aria-label^='More actions']").ClickAsync(); + await dropdown.Locator(".dropdown-menu button.dropdown-item.text-danger").ClickAsync(); + + await Assertions.Expect(page.Locator(".modal-footer .btn-danger")).ToBeVisibleAsync(); + await page.Locator(".modal-footer .btn-danger").ClickAsync(); + + // Single web-first assertion on the success toast — toasts auto-dismiss, 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 card must be gone. + await Assertions.Expect(page.Locator("div.card").Filter(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.DeleteApiMethodAsync(id); + } + } +}