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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user