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 ExternalSystemCrudTests { private readonly PlaywrightFixture _pw; public ExternalSystemCrudTests(PlaywrightFixture pw) { _pw = pw; } /// /// Full create → card → delete round-trip for an external system via the Central UI. /// Uses CliRunner for best-effort teardown so no zztest-extsys-* definition leaks on /// failure (the happy path already deletes via the UI). /// [SkippableFact] public async Task Create_Delete_RoundTrips() { Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); var name = CliRunner.UniqueName("extsys"); var page = await _pw.NewAuthenticatedPageAsync(); try { // ── CREATE ──────────────────────────────────────────────────────────────── await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems/create"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Assertions.Expect(page.Locator("h4:has-text('Add External System')")).ToBeVisibleAsync(); // ExternalSystemForm.razor has THREE input[type=text].form-control fields // (Name, Endpoint URL, and Auth Config JSON), so index-based selection is // fragile. Anchor each fill to its own div.mb-3 wrapper via the field label // so the selectors survive field reordering and uniquely match one element. await page.Locator("div.mb-3:has(label:has-text('Name')) input[type=text].form-control") .FillAsync(name); await page.Locator("div.mb-3:has(label:has-text('Endpoint URL')) input[type=text].form-control") .FillAsync("https://example.invalid/api"); await page.Locator("button.btn-success:has-text('Save')").ClickAsync(); // Save redirects back to the list (no toast). await PlaywrightFixture.WaitForPathAsync(page, "/design/external-systems", excludePath: "/create"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); // The new card must be present (and unique) before we act on it. var card = page.Locator("div.card").Filter(new() { HasText = name }); await Assertions.Expect(card).ToHaveCountAsync(1, new() { Timeout = 10_000 }); // ── DELETE ──────────────────────────────────────────────────────────────── // Scope the kebab + delete item to the card's .dropdown for strict-mode safety. var cardDropdown = card.Locator(".dropdown"); await cardDropdown.Locator("button[aria-label^='More actions']").ClickAsync(); await cardDropdown.Locator(".dropdown-menu button.dropdown-item.text-danger").ClickAsync(); // Confirm the delete dialog. await Assertions.Expect(page.Locator(".modal-title:has-text('Delete External System')")).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 .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(card).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. foreach (var id in await CliRunner.ListExternalSystemIdsByNamePrefixAsync(name)) await CliRunner.DeleteExternalSystemAsync(id); } } /// /// Saving the create form with both fields blank surfaces the inline validation /// error and keeps the user on the create page. Mutates nothing. /// [SkippableFact] public async Task CreateForm_EmptyFields_ShowsInlineError() { Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); var page = await _pw.NewAuthenticatedPageAsync(); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems/create"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await page.Locator("button.btn-success:has-text('Save')").ClickAsync(); await Assertions.Expect(page.Locator("div.text-danger.small:has-text('Name and URL required.')")) .ToBeVisibleAsync(); await Assertions.Expect(page) .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/create")); } }