Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MTConnectBrowseCommitRoundTripTests.cs
T
Joseph Doherty 115a43dd47 feat(mtconnect): typed AdminUI tag-config editor + browse round-trip (Task 18 Part A)
Registers the MTConnect typed tag editor in TagConfigEditorMap /
TagConfigValidator (keyed off DriverTypeNames.MTConnect, never a literal) and
adds the razor shell over Task 17's MTConnectTagConfigModel.

The shell stays trivial: the /probe-sourced mtCategory/mtType/mtSubType/units
row is rendered disabled + readonly with NO binding of any kind, and no
inferred-type hint is shown — MTConnectDataTypeInference.Infer() also keys on
the DataItem `representation`, which the tag-config model does not carry, so
calling it here would agree with the browse picker for ordinary items and
silently disagree for exactly the DATA_SET/TABLE/TIME_SERIES ones.

Also fixes the picker -> editor round trip, which was broken in both
directions:

- RawBrowseCommitMapper.BuildTagConfig had no MTConnect branch, so a committed
  tag landed as the generic {"address": "<dataItemId>"} while the model 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.
- MTConnectTagConfigModel.FromJson now accepts dataItemId/address as fallbacks
  (fixing tags committed before this change) and ToJson normalises onto
  `fullName`, dropping the aliases so the two spellings cannot drift.
2026-07-27 13:58:19 -04:00

103 lines
4.1 KiB
C#

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;
/// <summary>
/// Pins the browse-picker → typed-editor round trip for MTConnect, the driver's <b>primary</b>
/// authoring path (a tag is committed from the universal browser, then confirmed/overridden in the
/// typed editor).
/// </summary>
/// <remarks>
/// <para>
/// The defect this guards: <see cref="RawBrowseCommitMapper.BuildTagConfig"/> had no MTConnect
/// branch, so a committed tag landed as the generic <c>{"address": "&lt;dataItemId&gt;"}</c>
/// while <see cref="MTConnectTagConfigModel"/> read only <c>fullName</c>. 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 <c>fullName: ""</c>
/// over the tag. Razor is exactly what this repo cannot unit-test, so both halves of the fix
/// are pinned here in plain C#.
/// </para>
/// <para>
/// Both halves are asserted: the mapper now writes the canonical <c>fullName</c> (fixes new
/// commits), and the model accepts <c>dataItemId</c>/<c>address</c> as fallbacks (fixes tags
/// already committed before the fix).
/// </para>
/// </remarks>
[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<string>().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<string>().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<string>().ShouldBe("MILLIMETER");
}
}