using System.Text.Json; using System.Text.Json.Serialization; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; public sealed class TwinCATDriverPageFormSerializationTests { private static readonly JsonSerializerOptions _opts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, }; [Fact] public void RoundTrip_PreservesKnownFields() { var original = new TwinCATDriverOptions { Timeout = TimeSpan.FromSeconds(4), UseNativeNotifications = false, EnableControllerBrowse = true, NotificationMaxDelayMs = 50, Probe = new TwinCATProbeOptions { Enabled = false, Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(3), }, ProbeTimeoutSeconds = 20, Devices = [], Tags = [], }; var json = JsonSerializer.Serialize(original, _opts); var back = JsonSerializer.Deserialize(json, _opts); back.ShouldNotBeNull(); back.Timeout.ShouldBe(TimeSpan.FromSeconds(4)); back.UseNativeNotifications.ShouldBeFalse(); back.EnableControllerBrowse.ShouldBeTrue(); back.NotificationMaxDelayMs.ShouldBe(50); back.Probe.ShouldNotBeNull(); back.Probe.Enabled.ShouldBeFalse(); back.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(10)); back.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); back.ProbeTimeoutSeconds.ShouldBe(20); back.Devices.ShouldBeEmpty(); back.Tags.ShouldBeEmpty(); } [Fact] public void Deserialize_DropsUnknownFields() { var jsonWithExtra = """{"unknownField":"old-value","probeTimeoutSeconds":25}"""; var optsSkip = new JsonSerializerOptions(_opts) { UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; var back = JsonSerializer.Deserialize(jsonWithExtra, optsSkip); back.ShouldNotBeNull(); back.ProbeTimeoutSeconds.ShouldBe(25); } [Fact] public void FormModel_RoundTrip_PreservesEditableFields() { var opts = new TwinCATDriverOptions { Timeout = TimeSpan.FromSeconds(3), UseNativeNotifications = true, EnableControllerBrowse = false, NotificationMaxDelayMs = 100, Probe = new TwinCATProbeOptions { Enabled = true, Interval = TimeSpan.FromSeconds(6), Timeout = TimeSpan.FromSeconds(2), }, ProbeTimeoutSeconds = 15, }; var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.FormModel.FromOptions(opts); var roundTripped = form.ToOptions([], []); roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); roundTripped.UseNativeNotifications.ShouldBeTrue(); roundTripped.EnableControllerBrowse.ShouldBeFalse(); roundTripped.NotificationMaxDelayMs.ShouldBe(100); roundTripped.Probe.Enabled.ShouldBeTrue(); roundTripped.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(6)); roundTripped.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(2)); roundTripped.ProbeTimeoutSeconds.ShouldBe(15); } [Fact] public void DeviceRow_RoundTrip_PreservesEditableFields() { var def = new TwinCATDeviceOptions("192.168.0.1.1.1:851", "PLC1"); var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATDeviceRow.FromDefinition(def); var back = row.ToDefinition(); back.HostAddress.ShouldBe("192.168.0.1.1.1:851"); back.DeviceName.ShouldBe("PLC1"); } [Fact] public void DeviceRow_CarriesThroughUneditedSourceFields() { // Edit only DeviceName; HostAddress on the source must survive the round-trip via _source. var def = new TwinCATDeviceOptions("10.0.0.5.1.1:851", "Original"); var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATDeviceRow.FromDefinition(def); row.DeviceName = "Renamed"; var back = row.ToDefinition(); back.HostAddress.ShouldBe("10.0.0.5.1.1:851"); back.DeviceName.ShouldBe("Renamed"); } [Fact] public void DeviceRow_ValidateRow_RejectsDuplicateHostAddress() { var existing = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATDeviceRow.FromDefinition(new TwinCATDeviceOptions("192.168.0.1.1.1:851")); var dup = new ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATDeviceRow { HostAddress = "192.168.0.1.1.1:851" }; var all = new[] { existing, dup }; var error = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATDeviceRow.ValidateRow(dup, all, editIndex: 1); error.ShouldNotBeNull(); error.ShouldContain("Duplicate"); } [Fact] public void TagRow_RoundTrip_PreservesEditableFields() { var def = new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real, Writable: false); var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATTagRow.FromDefinition(def); var back = row.ToDefinition(); back.Name.ShouldBe("Speed"); back.DeviceHostAddress.ShouldBe("192.168.0.1.1.1:851"); back.SymbolPath.ShouldBe("MAIN.rSpeed"); back.DataType.ShouldBe(TwinCATDataType.Real); back.Writable.ShouldBeFalse(); } [Fact] public void TagRow_CarriesThroughUneditedWriteIdempotent() { // WriteIdempotent is not exposed by the editor; it must survive a load→edit→save via _source. var def = new TwinCATTagDefinition("Cmd", "192.168.0.1.1.1:851", "GVL.Start", TwinCATDataType.Bool, Writable: true, WriteIdempotent: true); var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATTagRow.FromDefinition(def); row.Name = "CmdRenamed"; // touch an edited field var back = row.ToDefinition(); back.Name.ShouldBe("CmdRenamed"); back.WriteIdempotent.ShouldBeTrue(); } [Fact] public void TagRow_ValidateRow_RejectsDuplicateName() { var existing = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATTagRow.FromDefinition( new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real)); var dup = new ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATTagRow { Name = "SPEED" }; // case-insensitive collision var all = new[] { existing, dup }; var error = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.TwinCATTagRow.ValidateRow(dup, all, editIndex: 1); error.ShouldNotBeNull(); error.ShouldContain("Duplicate"); } [Fact] public void FormModel_ToOptions_SerializesDeviceAndTagLists() { var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.FormModel.FromOptions(new TwinCATDriverOptions()); var devices = new[] { new TwinCATDeviceOptions("192.168.0.1.1.1:851", "PLC1") }; var tags = new[] { new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real, Writable: true, WriteIdempotent: true), }; var opts = form.ToOptions(devices, tags); var json = JsonSerializer.Serialize(opts, _opts); var back = JsonSerializer.Deserialize(json, _opts); back.ShouldNotBeNull(); back.Devices.Count.ShouldBe(1); back.Devices[0].HostAddress.ShouldBe("192.168.0.1.1.1:851"); back.Devices[0].DeviceName.ShouldBe("PLC1"); back.Tags.Count.ShouldBe(1); back.Tags[0].Name.ShouldBe("Speed"); back.Tags[0].DeviceHostAddress.ShouldBe("192.168.0.1.1.1:851"); back.Tags[0].SymbolPath.ShouldBe("MAIN.rSpeed"); back.Tags[0].DataType.ShouldBe(TwinCATDataType.Real); back.Tags[0].Writable.ShouldBeTrue(); back.Tags[0].WriteIdempotent.ShouldBeTrue(); } }