fix(raw): tag edit modal feeds the picker the merged device config, not '{}' (#468 follow-up)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the #468 follow-up: the <c>/raw</c> tag edit modal must hand its typed TagConfig editor the
|
||||
/// merged Driver+Device config, not the historical <c>"{}"</c> stub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The AbCip / TwinCAT / FOCAS tag editors embed an address picker whose "Browse controller"
|
||||
/// affordance dials the <c>GetDriverConfigJson</c> accessor as its connect config. While that
|
||||
/// accessor returned a literal <c>"{}"</c> the browse captured against an empty config and could
|
||||
/// never succeed — a defect no offline test could see, because the accessor was well-formed JSON.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// AdminUI has no bUnit, so the component is driven directly as a class: the injected
|
||||
/// <c>Svc</c> property and the protected <c>OnParametersSetAsync</c> lifecycle hook are reached by
|
||||
/// reflection, and the editor parameters are read back through the internal
|
||||
/// <c>BuildEditorParameters</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
// BL0005 forbids setting a [Parameter] from outside the component, which is right for production code
|
||||
// and impossible to honour here: with no bUnit there is no renderer to supply parameters, so driving the
|
||||
// component as a class means assigning them directly.
|
||||
#pragma warning disable BL0005
|
||||
public sealed class RawTagModalMergedConfigTests
|
||||
{
|
||||
private const string MergedConfig = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
[Fact]
|
||||
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, "dev-1"),
|
||||
OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-1" ? ("AbCip", MergedConfig) : null,
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
DriverConfigJson(modal).ShouldBe(MergedConfig);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, "dev-1"),
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device is gone"),
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
|
||||
// The modal must still open and remain saveable — only the browse affordance degrades, and it
|
||||
// reports its own error when used.
|
||||
await Should.NotThrowAsync(() => InvokeParametersSetAsync(modal));
|
||||
|
||||
DriverConfigJson(modal).ShouldBe("{}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_accessor_is_live_not_a_captured_snapshot()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, tagId == "tag-1" ? "dev-1" : "dev-2"),
|
||||
OnLoadMergedProbeConfig = _ => null,
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
var firstAccessor = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
firstAccessor().ShouldBe("{}");
|
||||
|
||||
// Close, then reopen on a different tag whose device now resolves. The per-open cache must have
|
||||
// been reset and re-resolved, and the accessor must read the field at call time rather than
|
||||
// having closed over the value it saw when it was built.
|
||||
modal.Visible = false;
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
svc.OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-2" ? ("AbCip", MergedConfig) : null;
|
||||
modal.Visible = true;
|
||||
modal.TagId = "tag-2";
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
DriverConfigJson(modal).ShouldBe(MergedConfig);
|
||||
firstAccessor().ShouldBe(MergedConfig);
|
||||
}
|
||||
|
||||
private static RawTagEditDto TagOn(string tagId, string deviceId) => new(
|
||||
tagId,
|
||||
deviceId,
|
||||
TagGroupId: null,
|
||||
Name: "Speed",
|
||||
DataType: "Float",
|
||||
AccessLevel: TagAccessLevel.Read,
|
||||
WriteIdempotent: false,
|
||||
PollGroupId: null,
|
||||
TagConfig: "{}",
|
||||
RowVersion: []);
|
||||
|
||||
private static RawTagModal NewModal(IRawTreeService svc, string tagId)
|
||||
{
|
||||
var modal = new RawTagModal { Visible = true, TagId = tagId, DriverType = "AbCip" };
|
||||
|
||||
// @inject generates a private property, so the fake goes in by reflection.
|
||||
var injected = typeof(RawTagModal).GetProperty(
|
||||
"Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
injected.ShouldNotBeNull("RawTagModal no longer injects IRawTreeService as 'Svc'.");
|
||||
injected!.SetValue(modal, svc);
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
private static Task InvokeParametersSetAsync(RawTagModal modal)
|
||||
{
|
||||
var hook = typeof(RawTagModal).GetMethod(
|
||||
"OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
hook.ShouldNotBeNull("RawTagModal no longer overrides OnParametersSetAsync.");
|
||||
|
||||
return (Task)hook!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
private static string DriverConfigJson(RawTagModal modal)
|
||||
=> ((Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"])();
|
||||
}
|
||||
#pragma warning restore BL0005
|
||||
Reference in New Issue
Block a user