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 }); } }