Branches the DriversTab driver-add form on driver type: - For DriverType=Modbus, render the typed <ModbusOptionsEditor> component shipped in #145 instead of the generic JSON textarea. - For other driver types, the existing textarea stays (other drivers ship their own typed editors per decision #94). On Save, when type is Modbus, the form serialises ModbusOptionsViewModel into the JSON DTO shape ModbusDriverFactoryExtensions consumes (host / port / unitId / family / keepAlive / reconnect / max*** / writeOnChangeOnly / etc.). Other types still pass the textarea contents verbatim. Drive-by fix: the DriverType dropdown listed "ModbusTcp" but the actual factory-registered name is "Modbus" — DriverInstanceBootstrapper would silently skip a row created with the old label because the factory lookup would miss. Renamed to match. Tests (2 new in ModbusOptionsViewModelTests): - DriversTab_Serialized_Defaults_RoundTrip_Through_Factory — unedited view-model serializes to a JSON the factory accepts; resulting ModbusDriverOptions matches the form defaults bit-for-bit. - DriversTab_Serializes_Edited_Values_Correctly — flipping Host / Port / UnitId / Family / MaxReadGap / WriteOnChangeOnly in the view model surfaces in the constructed driver's options. The serializer in the test mirrors DriversTab.razor's SerializeModbusOptions helper. If the form's serialization shape drifts, both must be updated together; that's the cost of testing through the JSON DTO without bUnit. Follow-up still open: the per-tag editor (ModbusAddressEditor wiring into EquipmentTab.razor + the bulk-import help-text update) — that's a separate surface that touches the equipment-row CRUD flow; covered as a follow-up when the equipment tag editor surface is next touched.
137 lines
6.1 KiB
C#
137 lines
6.1 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Modbus;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;
|
|
|
|
/// <summary>
|
|
/// #145 Admin UI: smoke coverage for the ModbusOptionsEditor view model. The Blazor
|
|
/// component itself is exercised in browser-runtime tests; this fixture pins the default
|
|
/// values the form initialises to so a regression that flips an unedited row to a
|
|
/// non-default value gets caught.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ModbusOptionsViewModelTests
|
|
{
|
|
[Fact]
|
|
public void DriversTab_Serialized_Defaults_RoundTrip_Through_Factory()
|
|
{
|
|
// #147 — the form's SaveAsync serialises ModbusOptionsViewModel to the JSON DTO
|
|
// shape ModbusDriverFactoryExtensions consumes. This test pins the round-trip:
|
|
// unedited form → JSON → driver instance → options match defaults.
|
|
var vm = new ModbusOptionsEditor.ModbusOptionsViewModel();
|
|
var json = SerializeForRoundTrip(vm);
|
|
|
|
var driver = ModbusDriverFactoryExtensions.CreateInstance("modbus-roundtrip", json);
|
|
var opts = (ModbusDriverOptions)typeof(ModbusDriver)
|
|
.GetField("_options", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
|
|
.GetValue(driver)!;
|
|
|
|
opts.Host.ShouldBe(vm.Host);
|
|
opts.Port.ShouldBe(vm.Port);
|
|
opts.UnitId.ShouldBe(vm.UnitId);
|
|
opts.Family.ShouldBe(vm.Family);
|
|
opts.MelsecSubFamily.ShouldBe(vm.MelsecSubFamily);
|
|
opts.KeepAlive.Enabled.ShouldBe(vm.KeepAliveEnabled);
|
|
opts.MaxRegistersPerRead.ShouldBe((ushort)vm.MaxRegistersPerRead);
|
|
opts.MaxCoilsPerRead.ShouldBe((ushort)vm.MaxCoilsPerRead);
|
|
opts.MaxReadGap.ShouldBe((ushort)vm.MaxReadGap);
|
|
opts.WriteOnChangeOnly.ShouldBe(vm.WriteOnChangeOnly);
|
|
}
|
|
|
|
[Fact]
|
|
public void DriversTab_Serializes_Edited_Values_Correctly()
|
|
{
|
|
// Sanity: changing a few fields in the view model produces a JSON the factory
|
|
// accepts and the resulting driver carries the edited values.
|
|
var vm = new ModbusOptionsEditor.ModbusOptionsViewModel
|
|
{
|
|
Host = "10.5.5.5",
|
|
Port = 1502,
|
|
UnitId = 7,
|
|
Family = ModbusFamily.DL205,
|
|
MaxReadGap = 12,
|
|
WriteOnChangeOnly = true,
|
|
};
|
|
var json = SerializeForRoundTrip(vm);
|
|
var driver = ModbusDriverFactoryExtensions.CreateInstance("modbus-edited", json);
|
|
var opts = (ModbusDriverOptions)typeof(ModbusDriver)
|
|
.GetField("_options", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
|
|
.GetValue(driver)!;
|
|
|
|
opts.Host.ShouldBe("10.5.5.5");
|
|
opts.Port.ShouldBe(1502);
|
|
opts.UnitId.ShouldBe((byte)7);
|
|
opts.Family.ShouldBe(ModbusFamily.DL205);
|
|
opts.MaxReadGap.ShouldBe((ushort)12);
|
|
opts.WriteOnChangeOnly.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirror of DriversTab.razor's SerializeModbusOptions — kept here so the test
|
|
/// doesn't have to reach through Blazor component plumbing to invoke it. If the
|
|
/// component method signature drifts, update both.
|
|
/// </summary>
|
|
private static string SerializeForRoundTrip(ModbusOptionsEditor.ModbusOptionsViewModel m) =>
|
|
System.Text.Json.JsonSerializer.Serialize(new
|
|
{
|
|
host = m.Host,
|
|
port = m.Port,
|
|
unitId = m.UnitId,
|
|
family = m.Family.ToString(),
|
|
melsecSubFamily = m.MelsecSubFamily.ToString(),
|
|
keepAlive = new
|
|
{
|
|
enabled = m.KeepAliveEnabled,
|
|
timeMs = m.KeepAliveTimeSec * 1000,
|
|
intervalMs = m.KeepAliveIntervalSec * 1000,
|
|
retryCount = m.KeepAliveRetryCount,
|
|
},
|
|
reconnect = new
|
|
{
|
|
initialDelayMs = m.ReconnectInitialDelayMs,
|
|
maxDelayMs = m.ReconnectMaxDelayMs,
|
|
backoffMultiplier = m.ReconnectBackoffMultiplier,
|
|
},
|
|
maxRegistersPerRead = m.MaxRegistersPerRead,
|
|
maxRegistersPerWrite = m.MaxRegistersPerWrite,
|
|
maxCoilsPerRead = m.MaxCoilsPerRead,
|
|
maxReadGap = m.MaxReadGap,
|
|
useFC15ForSingleCoilWrites = m.UseFC15ForSingleCoilWrites,
|
|
useFC16ForSingleRegisterWrites = m.UseFC16ForSingleRegisterWrites,
|
|
writeOnChangeOnly = m.WriteOnChangeOnly,
|
|
tags = Array.Empty<object>(),
|
|
});
|
|
|
|
[Fact]
|
|
public void Defaults_Match_DriverOption_Defaults()
|
|
{
|
|
var vm = new ModbusOptionsEditor.ModbusOptionsViewModel();
|
|
var driverDefault = new ModbusDriverOptions();
|
|
|
|
vm.Host.ShouldBe(driverDefault.Host);
|
|
vm.Port.ShouldBe(driverDefault.Port);
|
|
vm.UnitId.ShouldBe(driverDefault.UnitId);
|
|
vm.Family.ShouldBe(driverDefault.Family);
|
|
vm.MelsecSubFamily.ShouldBe(driverDefault.MelsecSubFamily);
|
|
|
|
vm.KeepAliveEnabled.ShouldBe(driverDefault.KeepAlive.Enabled);
|
|
vm.KeepAliveTimeSec.ShouldBe((int)driverDefault.KeepAlive.Time.TotalSeconds);
|
|
vm.KeepAliveIntervalSec.ShouldBe((int)driverDefault.KeepAlive.Interval.TotalSeconds);
|
|
vm.KeepAliveRetryCount.ShouldBe(driverDefault.KeepAlive.RetryCount);
|
|
|
|
vm.ReconnectInitialDelayMs.ShouldBe((int)driverDefault.Reconnect.InitialDelay.TotalMilliseconds);
|
|
vm.ReconnectMaxDelayMs.ShouldBe((int)driverDefault.Reconnect.MaxDelay.TotalMilliseconds);
|
|
vm.ReconnectBackoffMultiplier.ShouldBe(driverDefault.Reconnect.BackoffMultiplier);
|
|
|
|
vm.MaxRegistersPerRead.ShouldBe(driverDefault.MaxRegistersPerRead);
|
|
vm.MaxRegistersPerWrite.ShouldBe(driverDefault.MaxRegistersPerWrite);
|
|
vm.MaxCoilsPerRead.ShouldBe(driverDefault.MaxCoilsPerRead);
|
|
vm.MaxReadGap.ShouldBe(driverDefault.MaxReadGap);
|
|
vm.UseFC15ForSingleCoilWrites.ShouldBe(driverDefault.UseFC15ForSingleCoilWrites);
|
|
vm.UseFC16ForSingleRegisterWrites.ShouldBe(driverDefault.UseFC16ForSingleRegisterWrites);
|
|
vm.WriteOnChangeOnly.ShouldBe(driverDefault.WriteOnChangeOnly);
|
|
}
|
|
}
|