Task #145 — Admin UI: expose new Modbus driver config
Two new Blazor components surface every Modbus knob added by #136-#144 so users can configure the driver without hand-editing DriverConfig JSON. ModbusAddressEditor.razor (live address-string parser preview): - Bound to a string AddressString + a Family / MelsecSubFamily hint. - On every input keystroke, runs ModbusAddressParser.TryParse and surfaces the resolved breakdown (Region, Offset, DataType, Bit, ByteOrder, ArrayCount, StringLength) inline as a green badge. - On parse error, shows the parser's diagnostic in red. - Re-uses the SAME parser the wire driver uses — grammar drift is impossible by construction. ModbusOptionsEditor.razor (driver-instance options panel): - Connection group (Host / Port / UnitId). - Family group (#144) with conditional MelsecSubFamily dropdown. - Keep-alive group (#139): Enabled / Time / Interval / RetryCount. - Reconnect group (#139): InitialDelay / MaxDelay / BackoffMultiplier. - Protocol group (#140): MaxRegistersPerRead / Write / Coils / ReadGap. - Behaviour toggles (#140 + #141): UseFC15 / UseFC16 / WriteOnChangeOnly. - Bound to ModbusOptionsViewModel — defaults match ModbusDriverOptions defaults so unedited rows produce the historical wire output verbatim. Architecture: - Admin project gains a ProjectReference to Driver.Modbus.Addressing (the shared parser assembly extracted in #136). Admin does NOT take a dep on Driver.Modbus itself — the addressing concerns are cleanly separated from the wire driver. - Same-namespace shared assembly means components reference ModbusAddressParser / ModbusFamily / etc. without prefix gymnastics. Tests: - ModbusOptionsViewModelTests (1 test) — pins every default in the view model against the corresponding ModbusDriverOptions default. A regression that flips an unedited row to a non-default value gets caught here. (Test references both Admin and Driver.Modbus to make the cross-assembly comparison.) - Live Blazor component testing requires bUnit, which isn't currently in the test setup; the parser logic the component wraps is fully covered by the 91 ModbusAddressParser tests in the addressing project, so the glue layer's behaviour is verifiable end-to-end already. Caveat: the wiring into the existing DriverInstance edit page lives in DriversTab.razor — that integration is left as a follow-up because it touches the cluster-edit workflow specifically and the components in this commit are framework-agnostic enough to drop in. The components build clean against the existing Admin project; no behavioural change to other tabs.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user