From d61c9212d6c129ca9e308d5cf8bae4e7f3e28795 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 14:36:54 -0400 Subject: [PATCH] test(playwright): add NotificationKpis render + refresh coverage (Wave 3) --- .../Notifications/NotificationKpisTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Notifications/NotificationKpisTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Notifications/NotificationKpisTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Notifications/NotificationKpisTests.cs new file mode 100644 index 00000000..b9ee0455 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Notifications/NotificationKpisTests.cs @@ -0,0 +1,79 @@ +using Microsoft.Playwright; +using Xunit; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications; + +/// +/// End-to-end coverage for the Notification KPIs page (/notifications/kpis). +/// +/// +/// This page is pure-read (no mutations, no fixture seeding, no teardown). It requires +/// the Deployment role; the test user multi-role has it. The KPI values themselves +/// are non-deterministic; these tests assert structural render only — either the 5 KPI +/// tiles render (happy path) or the cluster-unavailable alert renders (degraded path), +/// and the Refresh button completes a round-trip without hanging. +/// +/// +[Collection("Playwright")] +public class NotificationKpisTests +{ + private const string KpisUrl = "/notifications/kpis"; + + private readonly PlaywrightFixture _pw; + + public NotificationKpisTests(PlaywrightFixture pw) + { + _pw = pw; + } + + /// + /// Navigates to the Notification KPIs page and asserts that the page resolved to a + /// real state — EITHER all 5 KPI tile labels rendered OR the 'KPIs unavailable' + /// alert is shown. + /// + [SkippableFact] + public async Task KpisPage_RendersTilesOrError() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _pw.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{KpisUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await Assertions.Expect(page.Locator("h4:has-text('Notification KPIs')")).ToBeVisibleAsync(); + + var tilesOk = await page.Locator("small.text-muted:has-text('Queue Depth')").IsVisibleAsync() + && await page.Locator("small.text-muted:has-text('Oldest Pending Age')").IsVisibleAsync(); + var errShown = await page.Locator(".alert.alert-warning:has-text('KPIs unavailable')").IsVisibleAsync(); + Assert.True(tilesOk || errShown, + "Expected either the 5 KPI tiles or the 'KPIs unavailable' alert to render."); + + if (tilesOk) + { + await Assertions.Expect(page.Locator("small.text-muted:has-text('Stuck')")).ToBeVisibleAsync(new() { Timeout = 5_000 }); + await Assertions.Expect(page.Locator("small.text-muted:has-text('Parked')")).ToBeVisibleAsync(new() { Timeout = 5_000 }); + await Assertions.Expect(page.Locator("small.text-muted:has-text('Delivered (last interval)')")).ToBeVisibleAsync(new() { Timeout = 5_000 }); + } + } + + /// + /// Navigates to the Notification KPIs page, clicks the Refresh button, and asserts + /// that the button re-enables within 10 s — proving the refresh round-trip completed + /// without hanging. + /// + [SkippableFact] + public async Task KpisPage_RefreshReenables() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _pw.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{KpisUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var refresh = page.Locator("button.btn.btn-outline-secondary.btn-sm:has-text('Refresh')"); + await Assertions.Expect(refresh).ToBeEnabledAsync(new() { Timeout = 10_000 }); + await refresh.ClickAsync(); + await Assertions.Expect(refresh).ToBeEnabledAsync(new() { Timeout = 10_000 }); + } +}