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; /// /// #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. /// [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(); } /// /// 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. /// 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(), }); [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); } }