feat(centralui): NodeBrowserDialog search + load-more + type column (T15/T16)
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the M7-B6 (T15/T16) additions to <c>NodeBrowserDialog</c>: the
|
||||
/// address-space search box that renders <see cref="IBrowseService.SearchAsync"/>
|
||||
/// matches as a flat selectable list, and that selecting a search result feeds
|
||||
/// the SAME selection mechanism the tree uses (so the dialog's existing
|
||||
/// <c>OnSelected</c> callback fires the chosen node id on confirm).
|
||||
/// </summary>
|
||||
public class NodeBrowserDialogSearchTests : BunitContext
|
||||
{
|
||||
private readonly IBrowseService _browse = Substitute.For<IBrowseService>();
|
||||
|
||||
public NodeBrowserDialogSearchTests()
|
||||
{
|
||||
Services.AddSingleton(_browse);
|
||||
// The root load fires on ShowAsync; give it an empty (successful) result
|
||||
// so the dialog renders without a failure banner and the tree is empty.
|
||||
_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));
|
||||
}
|
||||
|
||||
private static SearchAddressSpaceResult TwoMatches() => new(
|
||||
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.Flow", "Flow", BrowseNodeClass.Variable, HasChildren: false, DataType: "Float"),
|
||||
Path: "Devices/Pump1/Flow"),
|
||||
},
|
||||
CapReached: false,
|
||||
Failure: null);
|
||||
|
||||
private IRenderedComponent<NodeBrowserDialog> RenderShown(out string? selected)
|
||||
{
|
||||
string? captured = null;
|
||||
var cut = Render<NodeBrowserDialog>(p => p
|
||||
.Add(c => c.SiteId, "plant-a")
|
||||
.Add(c => c.ConnectionName, "PLC-OPC")
|
||||
.Add(c => c.OnSelected, EventCallback.Factory.Create<string>(this, id => captured = id)));
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-OPC", null));
|
||||
cut.Render();
|
||||
|
||||
// capture box is read back by the caller after assertions
|
||||
_capturedSelection = () => captured;
|
||||
selected = captured;
|
||||
return cut;
|
||||
}
|
||||
|
||||
private Func<string?> _capturedSelection = () => null;
|
||||
|
||||
[Fact]
|
||||
public void Search_RendersMatchRows_WithDataTestHooks()
|
||||
{
|
||||
_browse.SearchAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(TwoMatches());
|
||||
|
||||
var cut = RenderShown(out _);
|
||||
|
||||
var input = cut.Find("[data-test=node-search-input]");
|
||||
input.Input("Pump1");
|
||||
cut.Find("[data-test=node-search-button]").Click();
|
||||
|
||||
var rows = cut.FindAll("[data-test=node-search-result]");
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.Contains("Speed", cut.Markup);
|
||||
Assert.Contains("Devices/Pump1/Speed", cut.Markup);
|
||||
Assert.Contains("Double", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClickingSearchResult_RaisesSelectionCallback_WithNodeId()
|
||||
{
|
||||
_browse.SearchAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(TwoMatches());
|
||||
|
||||
var cut = RenderShown(out _);
|
||||
|
||||
cut.Find("[data-test=node-search-input]").Input("Pump1");
|
||||
cut.Find("[data-test=node-search-button]").Click();
|
||||
|
||||
// Click the first result's link — this drives the SAME selection
|
||||
// mechanism the tree uses, so the footer Select button confirms and
|
||||
// OnSelected fires.
|
||||
cut.FindAll("[data-test=node-search-result] a")[0].Click();
|
||||
cut.Find(".modal-footer .btn-primary").Click();
|
||||
|
||||
Assert.Equal("ns=2;s=Pump1.Speed", _capturedSelection());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlankQuery_ClearsResults()
|
||||
{
|
||||
_browse.SearchAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(TwoMatches());
|
||||
|
||||
var cut = RenderShown(out _);
|
||||
|
||||
cut.Find("[data-test=node-search-input]").Input("Pump1");
|
||||
cut.Find("[data-test=node-search-button]").Click();
|
||||
Assert.Equal(2, cut.FindAll("[data-test=node-search-result]").Count);
|
||||
|
||||
cut.Find("[data-test=node-search-input]").Input("");
|
||||
cut.Find("[data-test=node-search-button]").Click();
|
||||
Assert.Empty(cut.FindAll("[data-test=node-search-result]"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user