using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Security; using AlarmSummaryPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.AlarmSummary; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring; /// /// Covers the Alarm Summary flat table's two rendering paths (arch-review WP2.4). A short /// list renders every row with a plain foreach; past the threshold the same row template is /// handed to Virtualize so a site with thousands of alarms stops materialising the /// whole table into the circuit's render tree on every delta. Row markup — including the /// data-test hook the operator tests key off — is identical either way. /// public class AlarmSummaryVirtualizeTests : BunitContext { private readonly IAlarmSummaryService _summary = Substitute.For(); private readonly ISiteRepository _siteRepo = Substitute.For(); private void Arrange(int rowCount) { JSInterop.Mode = JSRuntimeMode.Loose; var rows = Enumerable.Range(0, rowCount) .Select(i => new AlarmSummaryRow( $"inst-{i:D4}", new AlarmStateChanged($"inst-{i:D4}", $"alarm-{i:D4}", AlarmState.Active, i, DateTimeOffset.UtcNow))) .ToList(); _summary.GetSiteAlarmsAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new AlarmSummaryResult(rows, Array.Empty()))); _summary.ComputeRollup(Arg.Any>()) .Returns(new AlarmRollup(rowCount, rowCount, 0, new Dictionary())); Services.AddSingleton(_summary); _siteRepo.GetAllSitesAsync(Arg.Any()) .Returns(Task.FromResult>(new List { new("Site 1", "site1") { Id = 1 }, })); Services.AddSingleton(_siteRepo); Services.AddSingleton(new InertLiveCache()); var claims = new[] { new Claim(JwtTokenService.UsernameClaimType, "tester"), new Claim(JwtTokenService.RoleClaimType, "Administrator"), }; Services.AddSingleton( new TestAuthStateProvider(new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")))); Services.AddAuthorizationCore(); } private IRenderedComponent RenderWithSiteSelected() { var cut = Render(); cut.Find("[data-test='alarm-summary-site']").Change("1"); return cut; } [Fact] public void ShortList_RendersEveryRowInline() { Arrange(rowCount: 20); var cut = RenderWithSiteSelected(); cut.WaitForAssertion(() => Assert.Equal(20, cut.FindAll("tr[data-test='alarm-summary-row']").Count)); // The row-count line is unchanged by either path. Assert.Contains("Showing 20 of 20", cut.Markup); } [Fact] public void LongList_VirtualizesWithoutRenderingEveryRow() { Arrange(rowCount: 2000); var cut = RenderWithSiteSelected(); cut.WaitForAssertion(() => Assert.Contains("Showing 2000 of 2000", cut.Markup)); var rendered = cut.FindAll("tr[data-test='alarm-summary-row']").Count; Assert.InRange(rendered, 1, 1999); // Rows still carry the same shape — instance name, alarm name, severity. Assert.Contains("alarm-", cut.Markup); } /// Never goes live, so the page keeps its poll snapshot for these tests. private sealed class InertLiveCache : ISiteAlarmLiveCache { public IDisposable Subscribe(int siteId, Action onChanged) => new NoOp(); public IReadOnlyList GetCurrentAlarms(int siteId) => Array.Empty(); public bool IsLive(int siteId) => false; public IReadOnlyList GetNotReportingInstances(int siteId) => Array.Empty(); private sealed class NoOp : IDisposable { public void Dispose() { } } } }