From 7bc40b96dbc9b375c9933e539fb3da3892bb9931 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 14:39:06 -0400 Subject: [PATCH] test(playwright): add EventLogs render + search-gating coverage (Wave 3) --- .../Monitoring/EventLogsTests.cs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/EventLogsTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/EventLogsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/EventLogsTests.cs new file mode 100644 index 00000000..c1df6f76 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/EventLogsTests.cs @@ -0,0 +1,110 @@ +using Microsoft.Playwright; +using Xunit; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring; + +/// +/// End-to-end render + control-gating tests for the Site Event Logs page +/// (/monitoring/event-logs). +/// +/// +/// Why this is a render + controls + tolerant-query suite (no row assertions): +/// Site event logs live in the SITE's local SQLite store and are fetched on demand via +/// an Akka Ask round-trip to a live site — there is no central table, and no CLI +/// or DB seed path. A clean site may legitimately have zero logged events, so these +/// facts must never assert on specific event rows. Instead they verify the two things +/// that are deterministic regardless of site state: +/// +/// The Search button is gated on site selection (disabled until a +/// site is chosen, then enabled). +/// Clicking Search resolves the query — the results table renders +/// either a data row or the empty-state row. +/// +/// The row-expand affordance is exercised only when real data rows happen to exist. +/// +/// +/// +/// Gated on via Skip.IfNot: when the cluster is +/// unreachable the facts report as Skipped (not Failed), matching the suite idiom. +/// +/// +[Collection("Playwright")] +public class EventLogsTests +{ + private const string EventLogsUrl = "/monitoring/event-logs"; + + private readonly PlaywrightFixture _fixture; + + public EventLogsTests(PlaywrightFixture fixture) + { + _fixture = fixture; + } + + /// + /// The Search button must be disabled on page load (no site selected) and become + /// enabled once a site is selected from the site dropdown. Deterministic regardless + /// of whether the site has any logged events. + /// + [SkippableFact] + public async Task EventLogs_SearchGatedOnSiteSelection() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{EventLogsUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await Assertions.Expect(page.Locator("h4:has-text('Site Event Logs')")).ToBeVisibleAsync(); + + // The site selector is populated from the cluster's known sites; the live cluster + // exposes site-a. Each option's value is the site's SiteIdentifier. + await Assertions.Expect(page.Locator("#filter-site option[value='site-a']")).ToHaveCountAsync(1); + + // Search is gated: disabled until a site is selected. + var search = page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')"); + await Assertions.Expect(search).ToBeDisabledAsync(); + + await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" }); + await Assertions.Expect(search).ToBeEnabledAsync(new() { Timeout = 5_000 }); + } + + /// + /// After selecting a site and clicking Search, the query must resolve: the results + /// table renders either a data row or the empty-state row (both match + /// table tbody tr). The row-expand affordance is exercised only when real data + /// rows exist — a site may have zero logged events, and event logs are not seedable + /// from central, so the empty-state render is itself the assertion in that case. + /// + [SkippableFact] + public async Task EventLogs_Search_RendersTableOrEmptyState() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{EventLogsUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" }); + await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync(); + + // The query resolved once the results table renders. The empty-state is itself a + // No events found., so a data row OR the empty-state + // row both surface under table tbody tr. Generous timeout: the fetch is an Akka + // Ask round-trip to a live site. + var settled = page.Locator("table tbody tr, td:has-text('No events found.')"); + await Assertions.Expect(settled.First).ToBeVisibleAsync(new() { Timeout = 15_000 }); + + // Exercise the row-expand ONLY IF real data rows exist. A clean site may have zero + // logged events, and event logs are not seedable from central — so when no expand + // toggle is present, the empty-state render above is the assertion (tolerated). + var expandBtns = page.Locator("button[aria-label='View full message']"); + if (await expandBtns.CountAsync() > 0) + { + await expandBtns.First.ClickAsync(); + await Assertions.Expect(page.Locator("button[aria-label='Hide full message']").First) + .ToBeVisibleAsync(new() { Timeout = 5_000 }); + } + // else: no events logged on this site — the empty-state render is the assertion. + } +}