Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ModbusOptionsViewModelTests.cs
Joseph Doherty 0b7653d3b2 Task #147 — wire ModbusOptionsEditor into DriversTab
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.
2026-04-25 00:58:03 -04:00

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);
}
}