feat(adminui): B2-WP3 driver/device config modals + page-to-form refactor

Extract the 8 typed driver pages into embeddable <Driver>DriverForm bodies
(channel/protocol config) and add per-driver <Driver>DeviceForm bodies
(connection endpoint -> Device.DeviceConfig, v3 endpoint split). New
DriverConfigModal + DeviceModal dispatch by DriverType (DriverTypeNames) for
the /raw tree; Test-connect inside DeviceModal builds the merged Driver+Device
config transiently via DriverDeviceConfigMerger. Routed pages now host the
form bodies and keep working. Round-trip + enum-serialization guard tests for
the Modbus driver form + Modbus device model; retargeted the existing
page-form serialization tests + the _jsonOpts converter guard to the forms.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
Joseph Doherty
2026-07-16 03:26:24 -04:00
parent 54ab413396
commit 844f93f64f
44 changed files with 3728 additions and 2474 deletions
@@ -0,0 +1,70 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.DeviceForms;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// Round-trip guard for the Modbus <b>device</b> endpoint model — the gate-critical DeviceConfig
/// surface. Endpoint keys MUST be PascalCase (Host/Port/UnitId) so the merged probe config binds via
/// the runtime's case-insensitive deserializer, matching the seed and Wave-B service tests.
/// </summary>
public sealed class ModbusDeviceModelTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("{}")]
public void FromJson_returns_defaults_for_empty_input(string? json)
{
var m = ModbusDeviceModel.FromJson(json);
m.Host.ShouldBe("127.0.0.1");
m.Port.ShouldBe(502);
m.UnitId.ShouldBe(1);
}
[Fact]
public void Round_trip_preserves_endpoint_fields()
{
var m = new ModbusDeviceModel { Host = "10.100.0.35", Port = 5020, UnitId = 7 };
var json = m.ToJson();
var m2 = ModbusDeviceModel.FromJson(json);
m2.Host.ShouldBe("10.100.0.35");
m2.Port.ShouldBe(5020);
m2.UnitId.ShouldBe(7);
}
[Fact]
public void ToJson_emits_PascalCase_endpoint_keys()
{
var json = new ModbusDeviceModel { Host = "10.1.2.3", Port = 5020, UnitId = 1 }.ToJson();
// Runtime binds ModbusDriverOptions.Host/Port/UnitId from these exact PascalCase keys.
json.ShouldContain("\"Host\":\"10.1.2.3\"");
json.ShouldContain("\"Port\":5020");
json.ShouldContain("\"UnitId\":1");
}
[Fact]
public void FromJson_then_ToJson_preserves_unknown_keys()
{
var json = ModbusDeviceModel
.FromJson("""{"Host":"10.1.2.3","Port":5020,"UnitId":1,"customKey":"keepme"}""")
.ToJson();
json.ShouldContain("customKey");
json.ShouldContain("keepme");
json.ShouldContain("\"Host\":\"10.1.2.3\"");
}
[Fact]
public void Validate_flags_blank_host()
{
new ModbusDeviceModel { Host = "" }.Validate().ShouldNotBeNull();
new ModbusDeviceModel { Host = "127.0.0.1" }.Validate().ShouldBeNull();
}
}
@@ -0,0 +1,64 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// Round-trip + enum-serialization guard for the Modbus <b>driver</b> (channel/protocol) form model.
/// Reproduces the historical driver-enum-serialization bug: enums MUST serialize as their name strings
/// (JsonStringEnumConverter) with camelCase keys, or the runtime factory faults on the numeric form.
/// </summary>
public sealed class ModbusDriverFormModelTests
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
WriteIndented = false,
Converters = { new JsonStringEnumConverter() },
};
[Fact]
public void Round_trip_preserves_channel_fields_and_enum()
{
var form = new ModbusDriverForm.FormModel
{
Family = ModbusFamily.MELSEC,
MelsecSubFamily = MelsecFamily.Q_L_iQR,
TimeoutSeconds = 4,
MaxRegistersPerRead = 100,
WriteOnChangeOnly = true,
KeepAliveRetryCount = 5,
ReconnectBackoffMultiplier = 3.0,
ProbeAddress = 40,
};
var json = JsonSerializer.Serialize(form.ToOptions(), JsonOpts);
var back = ModbusDriverForm.FormModel.FromOptions(
JsonSerializer.Deserialize<ModbusDriverOptions>(json, JsonOpts)!);
back.Family.ShouldBe(ModbusFamily.MELSEC);
back.MelsecSubFamily.ShouldBe(MelsecFamily.Q_L_iQR);
back.TimeoutSeconds.ShouldBe(4);
back.MaxRegistersPerRead.ShouldBe(100);
back.WriteOnChangeOnly.ShouldBeTrue();
back.KeepAliveRetryCount.ShouldBe(5);
back.ReconnectBackoffMultiplier.ShouldBe(3.0);
back.ProbeAddress.ShouldBe(40);
}
[Fact]
public void Serialized_config_uses_camelCase_keys_and_enum_names()
{
var form = new ModbusDriverForm.FormModel { Family = ModbusFamily.MELSEC };
var json = JsonSerializer.Serialize(form.ToOptions(), JsonOpts);
json.ShouldContain("\"family\":\"MELSEC\""); // enum-as-name (not a number), camelCase key
json.ShouldNotContain("\"family\":0", Case.Sensitive); // never the numeric enum form
}
}