feat(adminui): editable OpcUaClient endpoint URL list via CollectionEditor

This commit is contained in:
Joseph Doherty
2026-05-29 09:41:09 -04:00
parent 244949caa3
commit 7570df76d3
2 changed files with 149 additions and 19 deletions
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Shouldly;
@@ -85,9 +86,10 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
[Fact]
public void FormModel_RoundTrip_PreservesAllFields()
{
// Construct options with non-default values for every editable property plus
// non-empty EndpointUrls and UnsMappingTable — both are "read-only" in the form
// but must survive the FormModel translation unchanged.
// Construct options with non-default values for every editable property plus a
// non-empty UnsMappingTable (read-only in the form, round-tripped via the original
// record). EndpointUrls is now edited via the CollectionEditor on the page and is
// threaded into ToRecord explicitly; see EndpointUrls_ListRoundTrip_PreservesOrder.
var endpointUrls = new List<string> { "opc.tcp://primary:4840", "opc.tcp://backup:4840" };
var unsMappingTable = new Dictionary<string, string>
{
@@ -123,7 +125,7 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
};
var form = OpcUaClientDriverPage.OpcUaClientFormModel.FromRecord(original);
var result = form.ToRecord();
var result = form.ToRecord(endpointUrls);
result.EndpointUrl.ShouldBe("opc.tcp://fallback:4840");
result.EndpointUrls.Count.ShouldBe(2);
@@ -153,4 +155,92 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
result.UnsMappingTable["Line2/"].ShouldBe("Site/Area1/Line2");
result.ProbeTimeoutSeconds.ShouldBe(25);
}
[Fact]
public void EndpointUrlRow_FromUrl_ToUrl_Trims()
{
var row = OpcUaClientDriverPage.EndpointUrlRow.FromUrl(" opc.tcp://plc:4840 ");
row.Url.ShouldBe(" opc.tcp://plc:4840 ");
row.ToUrl().ShouldBe("opc.tcp://plc:4840");
}
[Fact]
public void EndpointUrlRow_ValidateRow_RejectsBlank()
{
var all = new List<OpcUaClientDriverPage.EndpointUrlRow>();
var row = new OpcUaClientDriverPage.EndpointUrlRow { Url = " " };
var error = OpcUaClientDriverPage.EndpointUrlRow.ValidateRow(row, all, null);
error.ShouldBe("URL is required.");
}
[Fact]
public void EndpointUrlRow_ValidateRow_RejectsNonOpcTcpScheme()
{
var all = new List<OpcUaClientDriverPage.EndpointUrlRow>();
var row = new OpcUaClientDriverPage.EndpointUrlRow { Url = "http://plc:4840" };
var error = OpcUaClientDriverPage.EndpointUrlRow.ValidateRow(row, all, null);
error.ShouldBe("Endpoint URL must start with opc.tcp://");
}
[Fact]
public void EndpointUrlRow_ValidateRow_RejectsDuplicate()
{
var all = new List<OpcUaClientDriverPage.EndpointUrlRow>
{
new() { Url = "opc.tcp://primary:4840" },
new() { Url = "opc.tcp://backup:4840" },
};
// Adding a new row (editIndex null) duplicating the first — case-insensitive, whitespace-insensitive.
var row = new OpcUaClientDriverPage.EndpointUrlRow { Url = " OPC.TCP://primary:4840 " };
var error = OpcUaClientDriverPage.EndpointUrlRow.ValidateRow(row, all, null);
error.ShouldNotBeNull();
error.ShouldContain("Duplicate endpoint");
}
[Fact]
public void EndpointUrlRow_ValidateRow_AllowsEditingRowInPlace()
{
var all = new List<OpcUaClientDriverPage.EndpointUrlRow>
{
new() { Url = "opc.tcp://primary:4840" },
new() { Url = "opc.tcp://backup:4840" },
};
// Editing index 0 and keeping the same URL must not flag itself as a duplicate.
var row = new OpcUaClientDriverPage.EndpointUrlRow { Url = "opc.tcp://primary:4840" };
var error = OpcUaClientDriverPage.EndpointUrlRow.ValidateRow(row, all, 0);
error.ShouldBeNull();
}
[Fact]
public void EndpointUrls_ListRoundTrip_PreservesOrder()
{
// The page holds endpoints as a List<EndpointUrlRow>; loading from EndpointUrls and
// converting back must preserve order (the failover list is ordered, primary first).
var endpointUrls = new List<string> { "opc.tcp://primary:4840", "opc.tcp://secondary:4840", "opc.tcp://tertiary:4840" };
var rows = endpointUrls
.Select(OpcUaClientDriverPage.EndpointUrlRow.FromUrl)
.ToList();
var roundTripped = rows.Select(r => r.ToUrl()).ToList();
roundTripped.Count.ShouldBe(3);
roundTripped[0].ShouldBe("opc.tcp://primary:4840");
roundTripped[1].ShouldBe("opc.tcp://secondary:4840");
roundTripped[2].ShouldBe("opc.tcp://tertiary:4840");
var form = OpcUaClientDriverPage.OpcUaClientFormModel.FromRecord(new OpcUaClientDriverOptions());
var result = form.ToRecord(roundTripped);
result.EndpointUrls.Count.ShouldBe(3);
result.EndpointUrls[0].ShouldBe("opc.tcp://primary:4840");
result.EndpointUrls[2].ShouldBe("opc.tcp://tertiary:4840");
}
}