test(e2e): API-key enable/disable badge transition + delete-with-confirm removes row

This commit is contained in:
Joseph Doherty
2026-06-06 12:08:53 -04:00
parent 9fe3ac30c9
commit 89231e3245
@@ -145,4 +145,96 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
// The name-only submit must not create a key — no token panel.
await Assertions.Expect(page.Locator("[data-test='created-token']")).ToHaveCountAsync(0);
}
/// <summary>
/// Enable/disable round-trip on the list page (<c>/admin/api-keys</c>, rendered by
/// <c>ApiKeys.razor</c>). MUTATES — creates a key via the CLI, drives the per-row kebab
/// (<c>button[aria-label="More actions for {name}"]</c>, a Bootstrap <c>data-bs-toggle="dropdown"</c>)
/// to Disable then Enable. The authoritative state indicator is the
/// <c>span.badge.bg-secondary</c> "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 <c>finally</c> (best-effort) so nothing leaks.
/// </summary>
[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);
}
}
/// <summary>
/// Delete-with-confirm removes the row (<c>/admin/api-keys</c>, rendered by <c>ApiKeys.razor</c>).
/// MUTATES — creates a key via the CLI, then deletes it through the UI: kebab →
/// <c>.dropdown-item.text-danger</c> "Delete" → the confirm dialog ("Delete API Key", rendered by
/// <c>DialogHost.razor</c>) → <c>.modal-footer .btn-danger</c>. The row for that name must then be
/// gone. The CLI delete in the <c>finally</c> is an idempotent safety net (no-op if the UI already
/// removed it).
/// </summary>
[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);
}
}
}