using System.Text.Json.Nodes; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Pins the browse-picker → typed-editor round trip for MTConnect, the driver's primary /// authoring path (a tag is committed from the universal browser, then confirmed/overridden in the /// typed editor). /// /// /// /// The defect this guards: had no MTConnect /// branch, so a committed tag landed as the generic {"address": "<dataItemId>"} /// while read only fullName. The data plane kept /// working (the driver accepts all three id spellings), so the ONLY symptom was an empty /// DataItem-id field in the editor — and saving from that state stamped fullName: "" /// over the tag. Razor is exactly what this repo cannot unit-test, so both halves of the fix /// are pinned here in plain C#. /// /// /// Both halves are asserted: the mapper now writes the canonical fullName (fixes new /// commits), and the model accepts dataItemId/address as fallbacks (fixes tags /// already committed before the fix). /// /// [Trait("Category", "Unit")] public sealed class MTConnectBrowseCommitRoundTripTests { [Fact] public void BuildTagConfig_writes_the_canonical_fullName_key_for_MTConnect() { var json = RawBrowseCommitMapper.BuildTagConfig(DriverTypeNames.MTConnect, "avail"); var o = JsonNode.Parse(json)!.AsObject(); o["fullName"]!.GetValue().ShouldBe("avail"); o.ContainsKey("address").ShouldBeFalse( "the generic fallback key must not be used now that MTConnect has a typed editor"); } [Fact] public void A_browse_committed_tag_round_trips_through_the_typed_editor_model() { var committed = RawBrowseCommitMapper.BuildTagConfig(DriverTypeNames.MTConnect, "Xact"); var model = MTConnectTagConfigModel.FromJson(committed); model.FullName.ShouldBe("Xact"); model.Validate().ShouldBeNull("a browse-committed tag must open in the editor already valid"); MTConnectTagConfigModel.FromJson(model.ToJson()).FullName.ShouldBe("Xact"); } // ---- legacy blobs committed before the mapper fix ------------------------------------------- [Theory] [InlineData("address")] [InlineData("dataItemId")] public void FromJson_accepts_the_drivers_alternate_id_spellings(string key) { var model = MTConnectTagConfigModel.FromJson($$"""{"{{key}}":"Ypos"}"""); model.FullName.ShouldBe("Ypos"); model.Validate().ShouldBeNull(); } [Fact] public void FullName_wins_over_dataItemId_which_wins_over_address() { MTConnectTagConfigModel .FromJson("""{"fullName":"a","dataItemId":"b","address":"c"}""") .FullName.ShouldBe("a"); MTConnectTagConfigModel .FromJson("""{"dataItemId":"b","address":"c"}""") .FullName.ShouldBe("b"); } [Fact] public void A_blank_alias_does_not_shadow_a_populated_one() => MTConnectTagConfigModel .FromJson("""{"fullName":" ","address":"c"}""") .FullName.ShouldBe("c"); [Fact] public void Saving_a_legacy_blob_normalises_it_onto_fullName_and_drops_the_aliases() { var model = MTConnectTagConfigModel.FromJson("""{"address":"Zpos","units":"MILLIMETER"}"""); var o = JsonNode.Parse(model.ToJson())!.AsObject(); o["fullName"]!.GetValue().ShouldBe("Zpos"); o.ContainsKey("address").ShouldBeFalse( "leaving a second id spelling behind lets the two drift apart on the next edit"); o.ContainsKey("dataItemId").ShouldBeFalse(); // Unrelated keys are still preserved — normalisation is not a purge. o["units"]!.GetValue().ShouldBe("MILLIMETER"); } }