using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.DebugView; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment; /// /// bUnit tests for the Debug View's instance selector after it moved from a flat /// <select> to the shared InstancePicker area-tree combobox /// (design 2026-09-08). Pins that the page hands the picker the site's areas and /// its Enabled-only instance list, that drilling down the tree selects an instance, /// and that the selection still enables Connect. /// public class DebugViewInstancePickerTests : BunitContext { private const int SiteId = 1; private void RegisterServices() { // DebugView touches localStorage on render; let bUnit answer loosely. JSInterop.Mode = JSRuntimeMode.Loose; var repo = Substitute.For(); repo.GetInstancesBySiteIdAsync(SiteId).Returns(new List { new("Sim01") { Id = 7, SiteId = SiteId, AreaId = 2, State = InstanceState.Enabled }, new("Off01") { Id = 8, SiteId = SiteId, State = InstanceState.Disabled }, }); repo.GetAreasBySiteIdAsync(SiteId).Returns(new List { new("Line1") { Id = 1, SiteId = SiteId }, new("Cell A") { Id = 2, SiteId = SiteId, ParentAreaId = 1 }, }); Services.AddSingleton(repo); var siteRepo = Substitute.For(); siteRepo.GetAllSitesAsync().Returns(new List { new("Site One", "site-a") { Id = SiteId }, }); Services.AddSingleton(siteRepo); var comms = new CommunicationService( Options.Create(new CommunicationOptions()), NullLogger.Instance); Services.AddSingleton(comms); var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance); // An empty throwaway provider — these tests never call StartStreamAsync, // so the provider is unused. (Services.BuildServiceProvider() would leak // an undisposed provider.) var debugStream = new DebugStreamService( comms, new ServiceCollection().BuildServiceProvider(), grpcFactory, NullLogger.Instance); Services.AddSingleton(debugStream); // No SiteId claim => system-wide, so site 1 passes the scope re-check. var identity = new ClaimsIdentity( new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie"); var authState = new AuthenticationState(new ClaimsPrincipal(identity)); var stubAuth = new StubAuthStateProvider(authState); Services.AddSingleton(stubAuth); Services.AddScoped(_ => new SiteScopeService(stubAuth)); } private sealed class StubAuthStateProvider : AuthenticationStateProvider { private readonly AuthenticationState _state; public StubAuthStateProvider(AuthenticationState state) => _state = state; public override Task GetAuthenticationStateAsync() => Task.FromResult(_state); } /// Renders the page, selects the seeded site and opens the picker panel. private IRenderedComponent RenderWithOpenPicker() { RegisterServices(); var cut = Render(); cut.Find("[data-test='debug-site-select']").Change(SiteId.ToString()); cut.Find("[data-test='debug-instance-toggle']").Click(); return cut; } private static void ClickRow(IRenderedComponent cut, string text) => cut.FindAll("[data-test='debug-instance-row']") .First(r => r.TextContent.Trim() == text) .Click(); [Fact] public void InstanceSelector_IsThePicker_NotAFlatSelect() { RegisterServices(); var cut = Render(); Assert.Single(cut.FindAll("[data-test='debug-instance-picker']")); Assert.Empty(cut.FindAll("select[data-test='debug-instance-select']")); } [Fact] public void OpeningThePicker_ShowsOnlyTheRootArea() { var cut = RenderWithOpenPicker(); var rows = cut.FindAll("[data-test='debug-instance-row']"); Assert.Single(rows); Assert.Equal("Line1", rows[0].TextContent.Trim()); Assert.False(rows[0].HasAttribute("data-instance-id")); } [Fact] public void DrillingDown_RevealsTheEnabledInstanceOnly() { var cut = RenderWithOpenPicker(); ClickRow(cut, "Line1"); ClickRow(cut, "Cell A"); var instanceRow = cut.Find("[data-instance-id='7']"); Assert.Equal("Sim01 (Enabled)", instanceRow.TextContent.Trim()); // Off01 is Disabled, so the page filters it out before the picker sees it. Assert.DoesNotContain( cut.FindAll("[data-test='debug-instance-row']"), r => r.TextContent.Contains("Off01")); } [Fact] public void PickingAnInstance_FillsTheField_AndEnablesConnect() { var cut = RenderWithOpenPicker(); ClickRow(cut, "Line1"); ClickRow(cut, "Cell A"); cut.Find("[data-instance-id='7']").Click(); Assert.Equal( "Sim01 (Enabled)", cut.Find("[data-test='debug-instance-input']").GetAttribute("value")); var connect = cut.FindAll("button.btn.btn-primary.btn-sm") .First(b => b.TextContent.Contains("Connect")); Assert.False(connect.HasAttribute("disabled")); } }