Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/NodeBrowserDialogSelectionTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

154 lines
7.3 KiB
C#

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<IBrowseService>();
/// <summary>Initializes the test context with a substituted browse service.</summary>
public NodeBrowserDialogSelectionTests()
{
Services.AddSingleton(_browse);
_browse.BrowseChildrenAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
Arg.Any<string?>(), Arg.Any<CancellationToken>())
.Returns(new BrowseNodeResult(Array.Empty<BrowseNode>(), Truncated: false, Failure: null));
_browse.SearchAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.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"),
new AddressSpaceMatch(
new BrowseNode("ns=2;s=Pump1", "Pump1", BrowseNodeClass.Object, HasChildren: true),
Path: "Devices/Pump1"),
},
CapReached: false, Failure: null));
}
/// <summary>Verifies that confirming a selection emits the full node including its data type through the node-selected callback.</summary>
[Fact]
public void Confirm_emits_full_node_with_datatype_on_OnNodeSelected()
{
BrowseNode? captured = null;
var cut = Render<NodeBrowserDialog>(p => p
.Add(c => c.SiteId, "plant-a")
.Add(c => c.ConnectionName, "PLC-OPC")
.Add(c => c.OnNodeSelected, EventCallback.Factory.Create<BrowseNode>(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] button.btn-link")[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);
}
/// <summary>Verifies that setting ShowSearch to false hides the search box.</summary>
[Fact]
public void ShowSearch_false_hides_search_box()
{
var cut = Render<NodeBrowserDialog>(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.
/// <summary>Verifies that selecting a node in the tree emits the full node including its data type through the node-selected callback.</summary>
[Fact]
public void TreeSelect_emits_full_node_with_datatype_on_OnNodeSelected()
{
_browse.BrowseChildrenAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
Arg.Any<string?>(), Arg.Any<CancellationToken>())
.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<NodeBrowserDialog>(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<BrowseNode>(this, n => captured = n)));
cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-MX", null));
cut.Render();
// Click the tree node's select control (tree path, not search). It is a
// <button class="btn btn-link"> since the 2026-08-11 UI sweep replaced the
// href="javascript:void(0)" anchor; both click handlers were preserved.
cut.Find(".node-browser-tree button.btn-link").Click();
cut.Find(".modal-footer .btn-primary").Click();
Assert.NotNull(captured);
Assert.Equal("Area1.Pump.Speed", captured!.NodeId);
Assert.Equal("Float", captured.DataType);
}
// SelectableNodeClasses (design 2026-08-21): the create page needs to pick a
// module's CONTAINER object, not just a leaf tag. Default stays Variable-only
// so every existing caller is unaffected.
/// <summary>Verifies that an Object node is not selectable by default.</summary>
[Fact]
public void Object_node_is_not_selectable_by_default()
{
var cut = Render<NodeBrowserDialog>(p => p
.Add(c => c.SiteId, "plant-a")
.Add(c => c.ConnectionName, "PLC-OPC"));
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();
// Only the Variable renders a select button; the Object is plain text.
Assert.Single(cut.FindAll("[data-test=node-search-result] button.btn-link"));
}
/// <summary>Verifies that an Object node is selectable when its class is listed as selectable.</summary>
[Fact]
public void Object_node_is_selectable_when_listed()
{
BrowseNode? captured = null;
var cut = Render<NodeBrowserDialog>(p => p
.Add(c => c.SiteId, "plant-a")
.Add(c => c.ConnectionName, "PLC-OPC")
.Add(c => c.SelectableNodeClasses, (IReadOnlyCollection<BrowseNodeClass>)new[] { BrowseNodeClass.Object, BrowseNodeClass.Variable })
.Add(c => c.OnNodeSelected, EventCallback.Factory.Create<BrowseNode>(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();
var buttons = cut.FindAll("[data-test=node-search-result] button.btn-link");
Assert.Equal(2, buttons.Count);
buttons.First(b => b.TextContent.Trim() == "Pump1").Click();
cut.Find(".modal-footer .btn-primary").Click();
Assert.Equal("ns=2;s=Pump1", captured!.NodeId);
Assert.Equal(BrowseNodeClass.Object, captured.NodeClass);
}
}