feat(adminui): driver-agnostic isArray/arrayLength Tag-modal control
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the driver-agnostic <see cref="TagArrayConfig"/> merge helper — the pure seam the
|
||||
/// TagModal uses to read/write the root <c>isArray</c> / <c>arrayLength</c> array keys on the canonical
|
||||
/// TagConfig JSON WITHOUT disturbing the driver's own (typed-editor) fields. Both this helper and every
|
||||
/// typed editor preserve unknown keys, so they compose over the same JSON blob. Mirrors
|
||||
/// <see cref="TagHistorizeConfigTests"/>.
|
||||
/// </summary>
|
||||
public sealed class TagArrayConfigTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("{}")]
|
||||
[InlineData("not json")]
|
||||
public void Read_returns_defaults_for_empty_or_malformed(string? json)
|
||||
{
|
||||
var a = TagArrayConfig.Read(json);
|
||||
|
||||
a.IsArray.ShouldBeFalse();
|
||||
a.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_reads_both_keys()
|
||||
{
|
||||
var a = TagArrayConfig.Read("""{"FullName":"ns=2;s=X","isArray":true,"arrayLength":8}""");
|
||||
|
||||
a.IsArray.ShouldBeTrue();
|
||||
a.ArrayLength.ShouldBe(8u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_true_with_length_writes_both_camelCase_keys()
|
||||
{
|
||||
var json = TagArrayConfig.Set("""{"FullName":"ns=2;s=X"}""", isArray: true, arrayLength: 8);
|
||||
|
||||
json.ShouldContain("\"isArray\":true");
|
||||
json.ShouldContain("\"arrayLength\":8");
|
||||
// The driver field is untouched.
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_true_round_trips_through_Read()
|
||||
{
|
||||
var json = TagArrayConfig.Set("{}", isArray: true, arrayLength: 16);
|
||||
|
||||
var a = TagArrayConfig.Read(json);
|
||||
a.IsArray.ShouldBeTrue();
|
||||
a.ArrayLength.ShouldBe(16u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_false_removes_both_keys()
|
||||
{
|
||||
var json = TagArrayConfig.Set(
|
||||
"""{"FullName":"ns=2;s=X","isArray":true,"arrayLength":8}""",
|
||||
isArray: false, arrayLength: 8);
|
||||
|
||||
json.ShouldNotContain("isArray");
|
||||
json.ShouldNotContain("arrayLength");
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_false_removes_orphan_arrayLength()
|
||||
{
|
||||
// Clearing isArray must not leave an orphan arrayLength behind.
|
||||
var json = TagArrayConfig.Set(
|
||||
"""{"isArray":true,"arrayLength":4}""",
|
||||
isArray: false, arrayLength: 4);
|
||||
|
||||
json.ShouldNotContain("arrayLength");
|
||||
json.ShouldNotContain("isArray");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_true_null_length_drops_arrayLength_key()
|
||||
{
|
||||
// isArray on but no length yet (pre-validation authoring state): emit isArray, no arrayLength.
|
||||
var json = TagArrayConfig.Set("{}", isArray: true, arrayLength: null);
|
||||
|
||||
json.ShouldContain("\"isArray\":true");
|
||||
json.ShouldNotContain("arrayLength");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_preserves_driver_specific_unknown_keys()
|
||||
{
|
||||
var json = TagArrayConfig.Set(
|
||||
"""{"register":40001,"scale":0.1,"nested":{"a":1}}""",
|
||||
isArray: true, arrayLength: 32);
|
||||
|
||||
json.ShouldContain("register");
|
||||
json.ShouldContain("40001");
|
||||
json.ShouldContain("scale");
|
||||
json.ShouldContain("0.1");
|
||||
json.ShouldContain("nested");
|
||||
json.ShouldContain("\"isArray\":true");
|
||||
json.ShouldContain("\"arrayLength\":32");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_on_malformed_input_starts_from_empty_object()
|
||||
{
|
||||
var json = TagArrayConfig.Set("not json", isArray: true, arrayLength: 2);
|
||||
|
||||
json.ShouldContain("\"isArray\":true");
|
||||
json.ShouldContain("\"arrayLength\":2");
|
||||
json.ShouldStartWith("{");
|
||||
json.ShouldEndWith("}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_array_with_null_length()
|
||||
{
|
||||
TagArrayConfig.Validate(isArray: true, arrayLength: null).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_rejects_array_with_zero_length()
|
||||
{
|
||||
TagArrayConfig.Validate(isArray: true, arrayLength: 0).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_accepts_array_with_positive_length()
|
||||
{
|
||||
TagArrayConfig.Validate(isArray: true, arrayLength: 8).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_accepts_non_array_regardless_of_length()
|
||||
{
|
||||
TagArrayConfig.Validate(isArray: false, arrayLength: null).ShouldBeNull();
|
||||
TagArrayConfig.Validate(isArray: false, arrayLength: 0).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full seam: the array helper and a driver-typed editor compose over the same canonical TagConfig
|
||||
/// blob without clobbering each other. Setting the array keys, round-tripping through the OpcUaClient
|
||||
/// typed editor (FromJson → ToJson), then reading the array keys back recovers them intact — and the
|
||||
/// editor's own FullName field survives the array merge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Array_keys_survive_a_typed_editor_round_trip()
|
||||
{
|
||||
var withArray = TagArrayConfig.Set("""{"FullName":"ns=2;s=X"}""", isArray: true, arrayLength: 12);
|
||||
|
||||
var afterEditor = OpcUaClientTagConfigModel.FromJson(withArray).ToJson();
|
||||
|
||||
var a = TagArrayConfig.Read(afterEditor);
|
||||
a.IsArray.ShouldBeTrue();
|
||||
a.ArrayLength.ShouldBe(12u);
|
||||
OpcUaClientTagConfigModel.FromJson(afterEditor).FullName.ShouldBe("ns=2;s=X");
|
||||
}
|
||||
|
||||
/// <summary>The array helper and the historize helper compose over the same blob (both preserve
|
||||
/// unknown keys), so a tag can carry both array and history intent simultaneously.</summary>
|
||||
[Fact]
|
||||
public void Array_and_historize_keys_compose()
|
||||
{
|
||||
var withHistory = TagHistorizeConfig.Set("""{"FullName":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN");
|
||||
var withBoth = TagArrayConfig.Set(withHistory, isArray: true, arrayLength: 4);
|
||||
|
||||
TagArrayConfig.Read(withBoth).ArrayLength.ShouldBe(4u);
|
||||
TagHistorizeConfig.Read(withBoth).IsHistorized.ShouldBeTrue();
|
||||
TagHistorizeConfig.Read(withBoth).HistorianTagname.ShouldBe("TN");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user