using System.Text.Json; using System.Text.Json.Serialization; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers; using ZB.MOM.WW.OtOpcUa.Driver.S7; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; public sealed class S7DriverPageFormSerializationTests { private static readonly JsonSerializerOptions _opts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, }; [Fact] public void RoundTrip_PreservesKnownFields() { var original = new S7DriverOptions { Host = "10.0.0.5", Port = 102, CpuType = S7CpuType.S71200, Rack = 0, Slot = 1, Timeout = TimeSpan.FromSeconds(10), Probe = new S7ProbeOptions { Enabled = false, Interval = TimeSpan.FromSeconds(15), Timeout = TimeSpan.FromSeconds(3), }, ProbeTimeoutSeconds = 30, Tags = [], }; var json = JsonSerializer.Serialize(original, _opts); var back = JsonSerializer.Deserialize(json, _opts); back.ShouldNotBeNull(); back.Host.ShouldBe("10.0.0.5"); back.Port.ShouldBe(102); back.CpuType.ShouldBe(S7CpuType.S71200); back.Rack.ShouldBe((short)0); back.Slot.ShouldBe((short)1); back.Timeout.ShouldBe(TimeSpan.FromSeconds(10)); back.Probe.ShouldNotBeNull(); back.Probe.Enabled.ShouldBeFalse(); back.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(15)); back.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); back.ProbeTimeoutSeconds.ShouldBe(30); back.Tags.ShouldBeEmpty(); } [Fact] public void Deserialize_DropsUnknownFields() { var jsonWithExtra = """{"unknownField":"old-value","probeTimeoutSeconds":12}"""; var optsSkip = new JsonSerializerOptions(_opts) { UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; var back = JsonSerializer.Deserialize(jsonWithExtra, optsSkip); back.ShouldNotBeNull(); back.ProbeTimeoutSeconds.ShouldBe(12); } [Fact] public void FormModel_RoundTrip_PreservesEditableFields() { var tags = new[] { new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true), new S7TagDefinition("Status", "DB1.DBW4", S7DataType.Int16, Writable: false), }; var opts = new S7DriverOptions { Host = "192.168.1.50", Port = 102, CpuType = S7CpuType.S7300, Rack = 0, Slot = 2, Timeout = TimeSpan.FromSeconds(7), Probe = new S7ProbeOptions { Enabled = true, Interval = TimeSpan.FromSeconds(8), Timeout = TimeSpan.FromSeconds(4), }, ProbeTimeoutSeconds = 20, Tags = tags, }; var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .S7DriverPage.FormModel.FromOptions(opts); var tagRows = opts.Tags .Select(ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers.S7DriverPage.S7TagRow.FromDefinition) .ToList(); var roundTripped = form.ToOptions(tagRows.Select(r => r.ToDefinition()).ToList()); roundTripped.Host.ShouldBe("192.168.1.50"); roundTripped.Port.ShouldBe(102); roundTripped.CpuType.ShouldBe(S7CpuType.S7300); roundTripped.Rack.ShouldBe((short)0); roundTripped.Slot.ShouldBe((short)2); roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(7)); roundTripped.Probe.Enabled.ShouldBeTrue(); roundTripped.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(8)); roundTripped.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(4)); roundTripped.ProbeTimeoutSeconds.ShouldBe(20); // Tags must survive the FormModel round-trip unchanged (regression guard for the // Tags = [] data-loss bug fixed in this PR). roundTripped.Tags.Count.ShouldBe(2); roundTripped.Tags[0].Name.ShouldBe("Speed"); roundTripped.Tags[0].Address.ShouldBe("DB1.DBD0"); roundTripped.Tags[0].DataType.ShouldBe(S7DataType.Float32); roundTripped.Tags[1].Name.ShouldBe("Status"); roundTripped.Tags[1].Writable.ShouldBeFalse(); } [Fact] public void S7TagRow_RoundTrip_PreservesEditableFields() { var def = new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true, StringLength: 80); var row = S7DriverPage.S7TagRow.FromDefinition(def); var back = row.ToDefinition(); back.Name.ShouldBe("Speed"); back.Address.ShouldBe("DB1.DBD0"); back.DataType.ShouldBe(S7DataType.Float32); back.Writable.ShouldBeTrue(); back.StringLength.ShouldBe(80); } [Fact] public void S7TagRow_CarriesThroughUneditedFields() { // WriteIdempotent is not exposed by the editor; it must survive FromDefinition→edit→ToDefinition. var def = new S7TagDefinition("Setpoint", "DB10.DBD0", S7DataType.Float32, Writable: true, WriteIdempotent: true); var row = S7DriverPage.S7TagRow.FromDefinition(def); row.Name = "SetpointRenamed"; row.Writable = false; var back = row.ToDefinition(); back.Name.ShouldBe("SetpointRenamed"); back.Writable.ShouldBeFalse(); // Un-edited field carried through via _source. back.WriteIdempotent.ShouldBeTrue(); } [Fact] public void S7TagRow_ValidateRow_RejectsDuplicateNames() { var all = new List { S7DriverPage.S7TagRow.FromDefinition(new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32)), S7DriverPage.S7TagRow.FromDefinition(new S7TagDefinition("Status", "DB1.DBW4", S7DataType.Int16)), }; // Editing index 1 to a name that case-insensitively collides with index 0. var edited = all[1].Clone(); edited.Name = "speed"; S7DriverPage.S7TagRow.ValidateRow(edited, all, editIndex: 1) .ShouldBe("Duplicate tag name 'speed'."); // Required-name guard. var blank = new S7DriverPage.S7TagRow(); S7DriverPage.S7TagRow.ValidateRow(blank, all, editIndex: null) .ShouldBe("Name is required."); // Unique name passes. var ok = all[1].Clone(); ok.Name = "Torque"; S7DriverPage.S7TagRow.ValidateRow(ok, all, editIndex: 1).ShouldBeNull(); } [Fact] public void TagList_SerializeRoundTrip_PreservesTags() { var opts = new S7DriverOptions { Host = "10.1.1.1", Tags = [ new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true), new S7TagDefinition("Name", "DB2.DBB0", S7DataType.String, Writable: false, StringLength: 32), ], }; var optsSkip = new JsonSerializerOptions(_opts) { UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; var json = JsonSerializer.Serialize(opts, optsSkip); var back = JsonSerializer.Deserialize(json, optsSkip); back.ShouldNotBeNull(); back.Tags.Count.ShouldBe(2); back.Tags[0].Name.ShouldBe("Speed"); back.Tags[0].Address.ShouldBe("DB1.DBD0"); back.Tags[0].DataType.ShouldBe(S7DataType.Float32); back.Tags[0].Writable.ShouldBeTrue(); back.Tags[1].Name.ShouldBe("Name"); back.Tags[1].DataType.ShouldBe(S7DataType.String); back.Tags[1].StringLength.ShouldBe(32); back.Tags[1].Writable.ShouldBeFalse(); } }