using Bunit; using Bunit.TestDoubles; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using ScadaLink.CentralUI.Components.Health; using ScadaLink.Commons.Types; namespace ScadaLink.CentralUI.Tests.Components.Health; /// /// bUnit tests for (#23 M7 Bundle E / M7-T13). The /// component renders three Bootstrap-card tiles — Volume, Error Rate, Backlog — /// from a single . The tests pin: /// /// /// Three-tile render contract (data-test attributes for stable selectors). /// Error-rate maths: ErrorEventsLastHour / TotalEventsLastHour with /// safe zero-events handling (no DivideByZero, displays "0.0%"). /// Unavailable snapshot renders em dashes plus the error message. /// Tile clicks navigate to the correct pre-filtered Audit Log URL. /// /// public class AuditKpiTilesTests : BunitContext { private static AuditLogKpiSnapshot MakeSnapshot(long total, long errors, long backlog) => new(total, errors, backlog, new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc)); [Fact] public void Renders_ThreeTiles_FromSnapshot() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 120, errors: 3, backlog: 7)) .Add(c => c.IsAvailable, true)); // Three stable data-test selectors — these are the contract for both // tests and any future Playwright sweep. Assert.Contains("data-test=\"audit-kpi-volume\"", cut.Markup); Assert.Contains("data-test=\"audit-kpi-error-rate\"", cut.Markup); Assert.Contains("data-test=\"audit-kpi-backlog\"", cut.Markup); // Tile values render the snapshot's counters. Assert.Contains("120", cut.Markup); // volume Assert.Contains("7", cut.Markup); // backlog } [Fact] public void ErrorRate_Computed_From_Total_AndErrors() { // 5 errors out of 100 → 5.0%. var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 100, errors: 5, backlog: 0)) .Add(c => c.IsAvailable, true)); Assert.Contains("5.0%", cut.Markup); } [Fact] public void ZeroEvents_DoesNotDivideByZero_RendersZeroPercent() { // Total = 0 → naïve division would throw or yield NaN. The tile must // render "0.0%" instead (zero events means zero errors too — a real // signal, not an unavailability marker). var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 0, errors: 0, backlog: 0)) .Add(c => c.IsAvailable, true)); Assert.Contains("0.0%", cut.Markup); // And the volume tile shows "0", not an em dash — the snapshot itself // is available; the system was just quiet for the hour. Assert.Contains("data-test=\"audit-kpi-volume\"", cut.Markup); } [Fact] public void UnavailableSnapshot_RendersEmDashes_AndErrorMessage() { var cut = Render(p => p .Add(c => c.Snapshot, (AuditLogKpiSnapshot?)null) .Add(c => c.IsAvailable, false) .Add(c => c.ErrorMessage, "DB connection refused")); // All three tiles show em dashes — em dash (U+2014) "—" must appear. Assert.Contains("—", cut.Markup); // Inline error message renders below. Assert.Contains("Audit KPIs unavailable", cut.Markup); Assert.Contains("DB connection refused", cut.Markup); } [Fact] public void ErrorRateTile_Click_NavigatesToAuditLog_WithFailedStatusFilter() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 50, errors: 3, backlog: 0)) .Add(c => c.IsAvailable, true)); // bUnit's BunitNavigationManager records the last URI a Navigation.NavigateTo call hit. var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"audit-kpi-error-rate\"]"); tile.Click(); // Spec: error-rate tile drills into ?status=Failed. Assert.Contains("/audit/log?status=Failed", nav.Uri); } [Fact] public void VolumeTile_Click_NavigatesToUnfilteredAuditLog() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 50, errors: 3, backlog: 0)) .Add(c => c.IsAvailable, true)); var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"audit-kpi-volume\"]"); tile.Click(); // Unfiltered /audit/log — no query string. Assert.EndsWith("/audit/log", nav.Uri); } [Fact] public void BacklogTile_Click_NavigatesToAuditLog() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 50, errors: 0, backlog: 12)) .Add(c => c.IsAvailable, true)); var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"audit-kpi-backlog\"]"); tile.Click(); Assert.EndsWith("/audit/log", nav.Uri); } [Fact] public void NonzeroErrorRate_GetsWarningBorder_NotDangerBelowTenPercent() { // 5% is < 10% → warning border, not danger. var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 100, errors: 5, backlog: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"audit-kpi-error-rate\"]"); Assert.Contains("border-warning", tile.GetAttribute("class") ?? string.Empty); Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty); } [Fact] public void HighErrorRate_GetsDangerBorder() { // 25% is > 10% → danger border. var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(total: 100, errors: 25, backlog: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"audit-kpi-error-rate\"]"); Assert.Contains("border-danger", tile.GetAttribute("class") ?? string.Empty); } }