using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Web; 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.Security; using AlarmSummaryPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.AlarmSummary; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring; /// /// bUnit rendering tests for the Operator Alarm Summary page — arch-review UA6 /// (sortable-header accessibility). The sortable column headers must be keyboard /// operable: they carry tabindex="0", expose their sort direction via /// aria-sort (ascending/descending on the active column, /// none otherwise), and toggle the sort when Enter is pressed on them — /// mirroring the existing pointer-click sort. /// public class AlarmSummaryRenderTests : BunitContext { private readonly IAlarmSummaryService _summary = Substitute.For(); private readonly ISiteRepository _siteRepo = Substitute.For(); public AlarmSummaryRenderTests() { // Two rows whose default sort (severity DESC) orders Zeta (900) before // Alpha (500); an ascending sort by instance name flips them. var rows = new List { new("Zeta", new AlarmStateChanged("Zeta", "Z-alarm", AlarmState.Active, 900, DateTimeOffset.UtcNow)), new("Alpha", new AlarmStateChanged("Alpha", "A-alarm", AlarmState.Active, 500, DateTimeOffset.UtcNow)), }; _summary.GetSiteAlarmsAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new AlarmSummaryResult(rows, Array.Empty()))); _summary.ComputeRollup(Arg.Any>()) .Returns(new AlarmRollup(2, 900, 0, new Dictionary())); Services.AddSingleton(_summary); _siteRepo.GetAllSitesAsync(Arg.Any()) .Returns(Task.FromResult>(new List { new("Site 1", "site1") { Id = 1 } })); Services.AddSingleton(_siteRepo); var claims = new[] { new Claim(JwtTokenService.UsernameClaimType, "tester"), new Claim(JwtTokenService.RoleClaimType, "Administrator"), }; var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); } // Renders the page and drives the site picker so the alarm table appears. private IRenderedComponent RenderWithSiteSelected() { var cut = Render(); cut.Find("[data-test='alarm-summary-site']").Change("1"); cut.WaitForAssertion(() => Assert.NotEmpty(cut.FindAll("tr[data-test='alarm-summary-row']"))); return cut; } [Fact] public void SortableHeaders_CarryTabindexAndAriaSort() { var cut = RenderWithSiteSelected(); var headers = cut.FindAll("thead th"); // 0=Instance (sortable), 1=Alarm (sortable), 2=State/Kind (not sortable), 3=Severity (sortable, active). var instance = headers[0]; var stateKind = headers[2]; var severity = headers[3]; Assert.Equal("0", instance.GetAttribute("tabindex")); Assert.Equal("0", severity.GetAttribute("tabindex")); // Default sort is severity descending. Assert.Equal("descending", severity.GetAttribute("aria-sort")); Assert.Equal("none", instance.GetAttribute("aria-sort")); // The non-sortable column is not keyboard-focusable and carries no aria-sort. Assert.Null(stateKind.GetAttribute("tabindex")); Assert.Null(stateKind.GetAttribute("aria-sort")); } [Fact] public void EnterOnInstanceHeader_FlipsRowOrder() { var cut = RenderWithSiteSelected(); // Default severity-descending order → Zeta (900) first. Assert.Equal("Zeta", FirstRowInstance(cut)); // Enter on the Instance header sorts ascending by instance → Alpha first. cut.FindAll("thead th")[0].TriggerEvent("onkeydown", new KeyboardEventArgs { Key = "Enter" }); cut.WaitForAssertion(() => Assert.Equal("Alpha", FirstRowInstance(cut))); // aria-sort now reflects the active ascending instance column. Assert.Equal("ascending", cut.FindAll("thead th")[0].GetAttribute("aria-sort")); } private static string FirstRowInstance(IRenderedComponent cut) => cut.FindAll("tr[data-test='alarm-summary-row']")[0] .QuerySelector("td")!.TextContent.Trim(); }