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.
+ }
+}