diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/ApiKeyCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/ApiKeyCrudTests.cs index 0f1d4f0d..1e681dc5 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/ApiKeyCrudTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/ApiKeyCrudTests.cs @@ -145,4 +145,96 @@ public sealed class ApiKeyCrudTests : IClassFixture // The name-only submit must not create a key — no token panel. await Assertions.Expect(page.Locator("[data-test='created-token']")).ToHaveCountAsync(0); } + + /// + /// Enable/disable round-trip on the list page (/admin/api-keys, rendered by + /// ApiKeys.razor). MUTATES — creates a key via the CLI, drives the per-row kebab + /// (button[aria-label="More actions for {name}"], a Bootstrap data-bs-toggle="dropdown") + /// to Disable then Enable. The authoritative state indicator is the + /// span.badge.bg-secondary "Disabled" badge the razor renders on a disabled row's name + /// cell: it must appear after Disable and be gone after Enable. The CLI-created key is deleted + /// in a finally (best-effort) so nothing leaks. + /// + [SkippableFact] + public async Task ToggleEnabled_TransitionsDisabledBadge() + { + Skip.IfNot(_api.Available, ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); + var keyName = CliRunner.UniqueName("apikey"); + var keyId = await CliRunner.CreateApiKeyAsync(keyName, _api.MethodName); + + try + { + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/api-keys"); + + // Web-first: the row for our key (by name) must render before we drive it. + var row = page.Locator("tr").Filter(new() { HasText = keyName }); + await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 }); + + var kebab = page.Locator($"button[aria-label=\"More actions for {keyName}\"]"); + var disabledBadge = row.Locator("span.badge.bg-secondary"); + + // Open the kebab (Bootstrap dropdown) and click Disable. The dropdown item is only + // visible/clickable once the menu is .show; Playwright auto-waits for it. + await kebab.ClickAsync(); + await page.Locator("button.dropdown-item").Filter(new() { HasText = "Disable" }).ClickAsync(); + + // Authoritative: the "Disabled" badge appears on the row's name cell. + await Assertions.Expect(disabledBadge).ToBeVisibleAsync(new() { Timeout = 10_000 }); + + // Re-open the kebab and click Enable; the badge must disappear from this row. + await kebab.ClickAsync(); + await page.Locator("button.dropdown-item").Filter(new() { HasText = "Enable" }).ClickAsync(); + + await Assertions.Expect(disabledBadge).ToHaveCountAsync(0, new() { Timeout = 10_000 }); + } + finally + { + await CliRunner.DeleteApiKeyAsync(keyId); + } + } + + /// + /// Delete-with-confirm removes the row (/admin/api-keys, rendered by ApiKeys.razor). + /// MUTATES — creates a key via the CLI, then deletes it through the UI: kebab → + /// .dropdown-item.text-danger "Delete" → the confirm dialog ("Delete API Key", rendered by + /// DialogHost.razor) → .modal-footer .btn-danger. The row for that name must then be + /// gone. The CLI delete in the finally is an idempotent safety net (no-op if the UI already + /// removed it). + /// + [SkippableFact] + public async Task Delete_WithConfirm_RemovesRow() + { + Skip.IfNot(_api.Available, ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); + var keyName = CliRunner.UniqueName("apikey"); + var keyId = await CliRunner.CreateApiKeyAsync(keyName, _api.MethodName); + + try + { + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/api-keys"); + + var row = page.Locator("tr").Filter(new() { HasText = keyName }); + await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 }); + + // Open the kebab and click the danger "Delete" item. + await page.Locator($"button[aria-label=\"More actions for {keyName}\"]").ClickAsync(); + await page.Locator(".dropdown-item.text-danger").Filter(new() { HasText = "Delete" }).ClickAsync(); + + // The confirm dialog must appear before we confirm. + await Assertions.Expect(page.Locator(".modal-title").Filter(new() { HasText = "Delete API Key" })) + .ToBeVisibleAsync(new() { Timeout = 10_000 }); + await page.Locator(".modal-footer .btn-danger").ClickAsync(); + + // The row for that name must be gone after the delete round-trips. + await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = keyName })) + .ToHaveCountAsync(0, new() { Timeout = 10_000 }); + } + finally + { + await CliRunner.DeleteApiKeyAsync(keyId); + } + } }