test(playwright): add Configuration Audit modal/copy/bundle-chip coverage (Wave 3)

This commit is contained in:
Joseph Doherty
2026-06-06 15:37:57 -04:00
parent 0b71712ee1
commit 037184f213
@@ -138,4 +138,130 @@ public class AuditConfigurationTests
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
[SkippableFact]
public async Task LargeState_OpensAndClosesModal()
{
Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var marker = NewMarker();
try
{
// 1 bulk row, no bundle rows. The seeder gives bulk row index 0 a
// > 1024-char AfterStateJson, so its State cell renders the large-state
// "View in modal" button (small-state rows show an inline View/Hide
// toggle instead, which we are not exercising here).
await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 1, bundleRows: 0);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Filter by the unique marker and Search so only this run's single
// large-state row populates the grid.
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Open the modal from the seeded large-state row. The marker is unique,
// so a page-wide "View in modal" locator resolves to exactly this row.
await page.Locator("button.btn-outline-info.btn-sm:has-text('View in modal')").ClickAsync();
// The modal renders with a title like "Audit entry {id} — {EntityType} state".
await Assertions.Expect(page.Locator("div.modal.show .modal-title")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("div.modal.show .modal-title")).ToContainTextAsync("state");
// Closing via the footer Close button removes the modal from the DOM.
await page.Locator(".modal.show button.btn-outline-secondary.btn-sm:has-text('Close')").ClickAsync();
await Assertions.Expect(page.Locator("div.modal.show")).ToHaveCountAsync(0, new() { Timeout = 10_000 });
}
finally
{
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
[SkippableFact]
public async Task CopyEntityId_ShowsToast()
{
Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var marker = NewMarker();
try
{
// 1 bulk row (non-empty EntityId) so the row renders the 📋 copy button.
await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 1, bundleRows: 0);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Click the copy-entity-ID button on the (only) seeded row.
await page.Locator("button[aria-label^='Copy entity ID']").First.ClickAsync();
// Assert a toast appears, TOLERANTLY of which one. The cluster is served
// over a non-secure, non-localhost http origin (http://scadabridge-traefik),
// so navigator.clipboard is unavailable: CopyAsync's writeText throws and
// the handler shows the ERROR toast "Copy failed." instead of the success
// toast "Copied to clipboard.". Either path produces exactly one .toast, so
// we assert the count only and deliberately do NOT assert the message text.
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
}
finally
{
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
[SkippableFact]
public async Task BundleImportId_DrillIn_ChipFiltersAndClears()
{
Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var marker = NewMarker();
var bundleId = Guid.NewGuid();
try
{
// 1 bulk row (EntityName "{marker}-0", BundleImportId NULL) + 2 bundle
// rows (EntityName "{marker}-bundle-0/1", BundleImportId = bundleId), all
// EntityType = marker so the finally cleanup removes every row.
await ConfigAuditDataSeeder.SeedAsync(marker, bundleId, bulkCount: 1, bundleRows: 2);
var page = await _fixture.NewAuthenticatedPageAsync();
// Arriving WITH a ?bundleImportId= query param auto-loads the grid
// pre-filtered (OnParametersSetAsync fires FetchPage because the param
// differs from the initial null) — no Search click needed.
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/configuration?bundleImportId={bundleId}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// The filter chip is shown for the active bundle import.
await Assertions.Expect(page.Locator("span.badge.bg-primary:has-text('Filtered by Bundle Import:')")).ToBeVisibleAsync();
// Exactly the 2 bundle rows show (EntityName "{marker}-bundle-0/1"). The
// bulk row (EntityName "{marker}-0", BundleImportId NULL) is filtered out.
// The "{marker}-0" token is NOT a substring of "{marker}-bundle-0", so the
// two locators distinguish the bundle rows from the bulk row unambiguously.
await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = $"{marker}-bundle-" }))
.ToHaveCountAsync(2, new() { Timeout = 10_000 });
await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = $"{marker}-0" }))
.ToHaveCountAsync(0);
// Clearing the filter removes the chip and navigates to the bare route;
// because BundleImportId goes from the guid back to null, the page
// re-fetches and reloads the grid with all entries (chip gone, rows ≥ 1).
await page.Locator("button[aria-label='Clear Bundle Import filter']").ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Assertions.Expect(page.Locator("span.badge.bg-primary:has-text('Filtered by Bundle Import:')")).ToHaveCountAsync(0);
await Assertions.Expect(page.Locator("tbody tr").First).ToBeVisibleAsync();
}
finally
{
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
}