diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/KpiTrendChartTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/KpiTrendChartTests.cs new file mode 100644 index 00000000..a639ff51 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/KpiTrendChartTests.cs @@ -0,0 +1,106 @@ +using Microsoft.Playwright; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring; + +/// +/// E2E coverage for the M6 "KPI History & Trends" feature (K17's spec slice): +/// proves a KpiTrendChart trend section renders in the live Central UI. +/// +/// +/// Page targeted — the Notification Outbox KPIs page +/// (NotificationKpis.razor, @page "/notifications/kpis"). Its trend +/// section is wrapped in a container marked data-test="notification-trends" +/// and renders three cards (Queue Depth, Parked, +/// Delivered / interval). The NotificationOutbox KPI source samples on every +/// recorder tick even at zero values, so this page is the most reliably-populated +/// trend surface on a fresh cluster — preferred over the Health dashboard fallback +/// (/monitoring/health, container data-test="site-health-trends"). +/// The page requires the Deployment policy, which the multi-role test user +/// holds (Admin + Design + Deployment). +/// +/// +/// +/// Fixture / auth / skip plumbing mirrors +/// — a navigate-and-read test (no instance seeding), so it takes the +/// directly (not DeploymentFixture), +/// authenticates with +/// (the multi-role / password default), and gates on +/// via Skip.IfNot so the +/// suite stays green when docker is down. +/// +/// +/// +/// Why the polyline assertion is tolerant (the DebugView lesson) — a fresh +/// cluster may have little or no KPI history sampled yet. +/// renders its data-test="kpi-trend-*" CARD in +/// ALL THREE states: the ≥2-point <polyline> chart, the single-sample +/// note, and the unavailable/empty em-dash placeholder. The <polyline> +/// only exists in the first (≥2 samples) state. So the hard gate is card presence +/// (renders regardless of data); the polyline is asserted TOLERANTLY — present is +/// logged, absent is fine (recorder hasn't sampled twice yet). This mirrors exactly +/// how tolerated the empty alarm tree +/// ([role='tree'] present vs. EmptyContent hint). +/// +/// +[Collection("Playwright")] +public class KpiTrendChartTests +{ + /// Notification Outbox KPIs page route (from NotificationKpis.razor's @page). + private const string KpisUrl = "/notifications/kpis"; + + private readonly PlaywrightFixture _fixture; + + public KpiTrendChartTests(PlaywrightFixture fixture) + { + _fixture = fixture; + } + + /// + /// Navigates to the Notification Outbox KPIs page and asserts the KPI trend + /// section renders: the trends container is visible, at least one + /// kpi-trend-* card is present, and (tolerantly) reports whether any + /// polyline has been plotted yet. + /// + [SkippableFact] + public async Task NotificationKpis_RendersTrendSection_WithKpiTrendCards() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{KpisUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // ── Hard gate 1: the trend section container renders. ── + // NotificationKpis.razor wraps the Trends heading + window toggle in a flex + // container marked data-test="notification-trends"; the three trend cards sit + // in the div.row immediately below it. + var trends = page.Locator("[data-test='notification-trends']"); + await Assertions.Expect(trends).ToBeVisibleAsync(new() { Timeout = 20_000 }); + + // ── Hard gate 2: at least one kpi-trend-* card is present. ── + // KpiTrendChart renders its data-test="kpi-trend-" card in EVERY state + // (polyline chart, single-sample note, OR em-dash placeholder), so this holds + // regardless of whether any history has been sampled yet on a fresh cluster. + var trendCards = page.Locator("[data-test^='kpi-trend-']"); + await Assertions.Expect(trendCards.First).ToBeVisibleAsync(new() { Timeout = 20_000 }); + var cardCount = await trendCards.CountAsync(); + Assert.True(cardCount >= 1, $"Expected at least one kpi-trend-* card; found {cardCount}."); + + // ── Tolerant data assertion: the polyline (actual plotted series). ── + // The renders ONLY in KpiTrendChart's ≥2-point chart state. On a + // fresh cluster the recorder may not have sampled twice yet (single-sample or + // empty), so its absence is NOT a failure — card presence above is the hard + // gate. Mirrors DebugViewTreeTests tolerating the empty alarm tree. + var polylineCount = await trendCards.Locator("polyline").CountAsync(); + if (polylineCount > 0) + { + // History has been sampled — at least one card plotted a series. + Assert.True( + polylineCount > 0, + $"Observed {polylineCount} plotted trend polyline(s) across the kpi-trend-* cards."); + } + // else: no polyline yet (recorder hasn't accumulated ≥2 samples / empty + // history) — tolerated by design; the card-presence gate above is authoritative. + } +}