using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components; public class NodeBrowserDialogSelectionTests : BunitContext { private readonly IBrowseService _browse = Substitute.For(); public NodeBrowserDialogSelectionTests() { Services.AddSingleton(_browse); _browse.BrowseChildrenAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new BrowseNodeResult(Array.Empty(), Truncated: false, Failure: null)); _browse.SearchAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new SearchAddressSpaceResult( Matches: new[] { new AddressSpaceMatch( new BrowseNode("ns=2;s=Pump1.Speed", "Speed", BrowseNodeClass.Variable, HasChildren: false, DataType: "Double"), Path: "Devices/Pump1/Speed"), }, CapReached: false, Failure: null)); } [Fact] public void Confirm_emits_full_node_with_datatype_on_OnNodeSelected() { BrowseNode? captured = null; var cut = Render(p => p .Add(c => c.SiteId, "plant-a") .Add(c => c.ConnectionName, "PLC-OPC") .Add(c => c.OnNodeSelected, EventCallback.Factory.Create(this, n => captured = n))); cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-OPC", null)); cut.Render(); cut.Find("[data-test=node-search-input]").Input("Pump1"); cut.Find("[data-test=node-search-button]").Click(); cut.FindAll("[data-test=node-search-result] a")[0].Click(); cut.Find(".modal-footer .btn-primary").Click(); Assert.NotNull(captured); Assert.Equal("ns=2;s=Pump1.Speed", captured!.NodeId); Assert.Equal("Double", captured.DataType); } [Fact] public void ShowSearch_false_hides_search_box() { var cut = Render(p => p .Add(c => c.SiteId, "plant-a") .Add(c => c.ConnectionName, "PLC-MX") .Add(c => c.ShowSearch, false)); cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-MX", null)); cut.Render(); Assert.Empty(cut.FindAll("[data-test=node-search-input]")); } // The Secured Writes page browses MxGateway with ShowSearch=false, so the // ONLY user-reachable selection path in production is the tree (Select(TreeNode) // → _selectedNode = node.Source). This guards that path end-to-end: a tree-node // click must carry the node's DataType through Confirm → OnNodeSelected. [Fact] public void TreeSelect_emits_full_node_with_datatype_on_OnNodeSelected() { _browse.BrowseChildrenAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new BrowseNodeResult( new[] { new BrowseNode("Area1.Pump.Speed", "Speed", BrowseNodeClass.Variable, HasChildren: false, DataType: "Float") }, Truncated: false, Failure: null)); BrowseNode? captured = null; var cut = Render(p => p .Add(c => c.SiteId, "plant-a") .Add(c => c.ConnectionName, "PLC-MX") .Add(c => c.ShowSearch, false) .Add(c => c.OnNodeSelected, EventCallback.Factory.Create(this, n => captured = n))); cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-MX", null)); cut.Render(); // Click the tree node's select link (tree path, not search). cut.Find(".node-browser-tree a").Click(); cut.Find(".modal-footer .btn-primary").Click(); Assert.NotNull(captured); Assert.Equal("Area1.Pump.Speed", captured!.NodeId); Assert.Equal("Float", captured.DataType); } }