test(playwright): add Configuration Audit render/search/pagination coverage (Wave 3)

This commit is contained in:
Joseph Doherty
2026-06-06 15:29:39 -04:00
parent 6523499ddb
commit 6975988ab4
@@ -0,0 +1,141 @@
using Microsoft.Playwright;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
using Xunit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
/// <summary>
/// End-to-end coverage for the Configuration Audit Log page
/// (<c>/audit/configuration</c>, <c>OperationalAudit</c> policy — the
/// <c>multi-role</c> test user carries Admin + Viewer so it can reach the page).
/// The page reads the central <c>AuditLogEntries</c> table; each fact seeds its
/// own rows via <see cref="ConfigAuditDataSeeder"/> under a unique per-test
/// <c>marker</c> (stamped as <c>EntityType</c>), filters the grid by that marker
/// so the <c>(N total)</c> pagination footer is deterministic regardless of the
/// other rows the live cluster produces, then best-effort deletes the marker's
/// rows in <c>finally</c>.
///
/// <para>
/// This part (Wave 3, Task 10) covers render / search-narrows / pagination. The
/// class is intentionally structured so Task 11 can APPEND modal/copy/chip facts
/// to it without restructuring: shared constants (the DB-unavailable skip reason,
/// the page route) and the seeded-test gate idiom live here once.
/// </para>
///
/// <para>
/// The seeded tests are <see cref="SkippableFactAttribute"/> + <c>Skip.IfNot</c>:
/// when the cluster / MSSQL is unreachable they report as Skipped (not Failed),
/// matching the established <see cref="AuditGridColumnTests"/> idiom.
/// </para>
/// </summary>
[Collection("Playwright")]
public class AuditConfigurationTests
{
/// <summary>
/// Route to the Configuration Audit Log page. The results grid only renders
/// after a Search (a bare visit, without a <c>?bundleImportId=</c> query param,
/// does not auto-fetch), so each fact issues a marker-scoped Search.
/// </summary>
private const string ConfigAuditUrl = "/audit/configuration";
/// <summary>Skip reason shared by the DB-seeding tests when MSSQL is down.</summary>
private const string DbUnavailableSkipReason =
"AuditDataSeeder cannot reach MSSQL at localhost:1433 — bring up infra/docker-compose and docker/deploy.sh, " +
"or set SCADABRIDGE_PLAYWRIGHT_DB to a reachable connection string.";
private readonly PlaywrightFixture _fixture;
public AuditConfigurationTests(PlaywrightFixture fixture)
{
_fixture = fixture;
}
/// <summary>
/// Generates a fresh per-test marker: a <c>zzCfgAudit-</c> prefix plus a short
/// GUID slice. Stamped as <c>EntityType</c> on every seeded row so a UI filter
/// on Entity Type isolates exactly this run's rows (deterministic totals) and
/// the <c>finally</c> cleanup never touches cluster-produced rows.
/// </summary>
private static string NewMarker() => "zzCfgAudit-" + Guid.NewGuid().ToString("N")[..8];
[SkippableFact]
public async Task Grid_RendersAndSearchNarrows()
{
Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var marker = NewMarker();
try
{
// 3 rows, all EntityType = marker, no bundle rows → exactly 3 match.
await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 3, bundleRows: 0);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// The page renders its heading and filter controls. The results table
// (the whole @if (_entries != null) block) does NOT render until a
// Search is issued — the page only auto-fetches when arriving with a
// ?bundleImportId= query param, which a bare visit lacks — so the
// Entity Type filter is present and ready to drive the first query.
await Assertions.Expect(page.Locator("h4:has-text('Configuration Audit Log')")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("#audit-filter-entity-type")).ToBeVisibleAsync();
// Filter by the unique marker so only this run's 3 rows remain, then
// Search to load 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);
// Seeded rows are isolated: a known row is visible AND the footer total
// is exactly 3 (filtering by the unique marker makes this deterministic
// regardless of the other rows the live cluster holds).
await Assertions.Expect(page.Locator($"tr:has-text('{marker}-0')")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("span:has-text('(3 total)')")).ToBeVisibleAsync();
}
finally
{
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
[SkippableFact]
public async Task Pagination_PrevDisabledOnPage1_NextPaginates()
{
Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var marker = NewMarker();
try
{
// 55 rows → 2 pages at 50/page.
await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 55, bundleRows: 0);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Filter by the marker so the totals/page-count reflect only this run.
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);
// Page 1 of 2: Previous disabled, Next enabled.
await Assertions.Expect(page.Locator("span:has-text('Page 1 of 2')")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("span:has-text('(55 total)')")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("button:has-text('Previous')")).ToBeDisabledAsync();
await Assertions.Expect(page.Locator("button:has-text('Next')")).ToBeEnabledAsync();
// Advance to the last page: Previous enabled, Next disabled.
await page.Locator("button:has-text('Next')").ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Assertions.Expect(page.Locator("span:has-text('Page 2 of 2')")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("button:has-text('Previous')")).ToBeEnabledAsync();
await Assertions.Expect(page.Locator("button:has-text('Next')")).ToBeDisabledAsync();
}
finally
{
await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker);
}
}
}