feat(kpi): K13 — Notification Outbox trend charts (T11 first consumer)

This commit is contained in:
Joseph Doherty
2026-06-17 20:29:30 -04:00
parent f0177d5073
commit 0dc819f191
2 changed files with 153 additions and 1 deletions
@@ -10,8 +10,10 @@ using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Security;
using NotificationKpisPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.NotificationKpis;
@@ -46,6 +48,18 @@ public class NotificationKpisPageTests : BunitContext
DeliveredLastInterval: 9, OldestPendingAge: TimeSpan.FromMinutes(7)),
});
// KPI-history trend service (K13 / T11). Default substitute returns a
// 3-point series for any (source, metric, scope, …) tuple so every chart
// renders a polyline; individual tests can re-stub it (e.g. to throw).
private readonly IKpiHistoryQueryService _kpiHistory = Substitute.For<IKpiHistoryQueryService>();
private static IReadOnlyList<KpiSeriesPoint> SampleSeries() => new List<KpiSeriesPoint>
{
new(DateTime.UtcNow.AddHours(-3), 3),
new(DateTime.UtcNow.AddHours(-2), 5),
new(DateTime.UtcNow.AddHours(-1), 4),
};
public NotificationKpisPageTests()
{
_comms = new CommunicationService(
@@ -66,6 +80,12 @@ public class NotificationKpisPageTests : BunitContext
}));
Services.AddSingleton(siteRepo);
_kpiHistory.GetSeriesAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
.Returns(_ => Task.FromResult(SampleSeries()));
Services.AddSingleton(_kpiHistory);
var claims = new[]
{
new Claim(JwtTokenService.UsernameClaimType, "tester"),
@@ -142,6 +162,47 @@ public class NotificationKpisPageTests : BunitContext
cut.WaitForAssertion(() => Assert.Contains("No per-site activity", cut.Markup));
}
[Fact]
public void RendersTrendCharts_WhenSeriesAvailable()
{
var cut = Render<NotificationKpisPage>();
cut.WaitForAssertion(() =>
{
// The Trends section container is present …
Assert.NotNull(cut.Find("[data-test=\"notification-trends\"]"));
// … and at least one trend chart renders an actual polyline (a chart
// needs IsAvailable + >= 2 points, which the substitute supplies).
Assert.NotEmpty(cut.FindAll("[data-test^=\"kpi-trend-\"]"));
Assert.Contains("<polyline", cut.Markup);
});
}
[Fact]
public void TrendQueryFailure_DoesNotBreakPage()
{
// Best-effort contract: a throwing trend query must not surface an
// unhandled exception — the KPI tiles still render and the charts fall
// back to their unavailable placeholders.
_kpiHistory.GetSeriesAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
.Returns<Task<IReadOnlyList<KpiSeriesPoint>>>(_ => throw new InvalidOperationException("kpi history down"));
var cut = Render<NotificationKpisPage>();
cut.WaitForAssertion(() =>
{
// Tiles still render.
Assert.Contains("Queue Depth", cut.Markup);
Assert.Contains("7", cut.Markup);
// Trends section + chart placeholders are present; no polyline drawn.
Assert.NotNull(cut.Find("[data-test=\"notification-trends\"]"));
Assert.NotEmpty(cut.FindAll("[data-test^=\"kpi-trend-\"]"));
Assert.DoesNotContain("<polyline", cut.Markup);
});
}
protected override void Dispose(bool disposing)
{
if (disposing)