From 6975988ab4d1eb9d6065ca7443f77e74bcafec64 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 15:29:39 -0400 Subject: [PATCH] test(playwright): add Configuration Audit render/search/pagination coverage (Wave 3) --- .../Audit/AuditConfigurationTests.cs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs new file mode 100644 index 00000000..d8530286 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs @@ -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; + +/// +/// End-to-end coverage for the Configuration Audit Log page +/// (/audit/configuration, OperationalAudit policy — the +/// multi-role test user carries Admin + Viewer so it can reach the page). +/// The page reads the central AuditLogEntries table; each fact seeds its +/// own rows via under a unique per-test +/// marker (stamped as EntityType), filters the grid by that marker +/// so the (N total) pagination footer is deterministic regardless of the +/// other rows the live cluster produces, then best-effort deletes the marker's +/// rows in finally. +/// +/// +/// 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. +/// +/// +/// +/// The seeded tests are + Skip.IfNot: +/// when the cluster / MSSQL is unreachable they report as Skipped (not Failed), +/// matching the established idiom. +/// +/// +[Collection("Playwright")] +public class AuditConfigurationTests +{ + /// + /// Route to the Configuration Audit Log page. The results grid only renders + /// after a Search (a bare visit, without a ?bundleImportId= query param, + /// does not auto-fetch), so each fact issues a marker-scoped Search. + /// + private const string ConfigAuditUrl = "/audit/configuration"; + + /// Skip reason shared by the DB-seeding tests when MSSQL is down. + 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; + } + + /// + /// Generates a fresh per-test marker: a zzCfgAudit- prefix plus a short + /// GUID slice. Stamped as EntityType on every seeded row so a UI filter + /// on Entity Type isolates exactly this run's rows (deterministic totals) and + /// the finally cleanup never touches cluster-produced rows. + /// + 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); + } + } +}