fix(raw): manual tag entry feeds each row's picker the merged device config (#468 follow-up)
This commit is contained in:
+132
@@ -0,0 +1,132 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Pins that the bulk manual-entry grid hands each row's typed tag editor the <b>merged</b>
|
||||
/// Driver+Device config, not a stub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The AbCip / TwinCAT / FOCAS editors' address pickers dial the <c>GetDriverConfigJson</c> Func as
|
||||
/// their browse connect config. <see cref="RawManualTagEntryModal"/> passed a literal <c>"{}"</c>,
|
||||
/// so "Browse controller" always connected against an empty config and could never reach the
|
||||
/// device — the picker was authorable but unusable, and no offline test reached it (AdminUI has no
|
||||
/// bUnit).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Unlike the single-tag modal, the device here is a direct <c>[Parameter]</c> — no tag load
|
||||
/// precedes it — so the resolve keys off <c>DeviceId</c> on the open path. The accessor must stay
|
||||
/// <b>live</b> (reading the field, not a captured snapshot), because the resolve is awaited after
|
||||
/// the rows are seeded. Exercised as a plain class: AdminUI has no bUnit, so the injected service
|
||||
/// and the lifecycle/parameter-building members are reached by reflection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RawManualTagEntryModalMergedConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Each_rows_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
const string merged = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = deviceId =>
|
||||
deviceId == "dev-1" ? ("AbCip", merged) : null,
|
||||
};
|
||||
|
||||
var modal = new RawManualTagEntryModal();
|
||||
SetParameters(modal, visible: true, deviceId: "dev-1", driverType: "AbCip");
|
||||
InjectService(modal, svc);
|
||||
|
||||
await OpenAsync(modal);
|
||||
|
||||
GetDriverConfigJson(modal).ShouldBe(merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device unreachable"),
|
||||
};
|
||||
|
||||
var modal = new RawManualTagEntryModal();
|
||||
SetParameters(modal, visible: true, deviceId: "dev-1", driverType: "AbCip");
|
||||
InjectService(modal, svc);
|
||||
|
||||
await Should.NotThrowAsync(() => OpenAsync(modal));
|
||||
|
||||
GetDriverConfigJson(modal).ShouldBe("{}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the modal's <c>[Parameter]</c>s. By reflection because BL0005 forbids assigning component
|
||||
/// parameters from outside the component, and this project treats warnings as errors.
|
||||
/// </summary>
|
||||
private static void SetParameters(
|
||||
RawManualTagEntryModal modal, bool visible, string deviceId, string driverType)
|
||||
{
|
||||
Set("Visible", visible);
|
||||
Set("DeviceId", deviceId);
|
||||
Set("DriverType", driverType);
|
||||
|
||||
void Set(string name, object value)
|
||||
{
|
||||
var property = typeof(RawManualTagEntryModal).GetProperty(name);
|
||||
property.ShouldNotBeNull($"the [Parameter] {name} was renamed or removed.");
|
||||
property!.SetValue(modal, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets the <c>@inject</c>-generated private <c>Svc</c> property.</summary>
|
||||
private static void InjectService(RawManualTagEntryModal modal, FakeRawTreeService svc)
|
||||
{
|
||||
var property = typeof(RawManualTagEntryModal).GetProperty(
|
||||
"Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
property.ShouldNotBeNull("the injected IRawTreeService property is no longer named Svc.");
|
||||
|
||||
property!.SetValue(modal, svc);
|
||||
}
|
||||
|
||||
/// <summary>Drives the open path — the protected lifecycle method the resolve hangs off.</summary>
|
||||
private static async Task OpenAsync(RawManualTagEntryModal modal)
|
||||
{
|
||||
var method = typeof(RawManualTagEntryModal).GetMethod(
|
||||
"OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
method.ShouldNotBeNull("the open path moved off OnParametersSetAsync — retarget this driver.");
|
||||
|
||||
await (Task)method!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
/// <summary>Invokes the <c>GetDriverConfigJson</c> Func the first seeded row's editor is handed.</summary>
|
||||
private static string GetDriverConfigJson(RawManualTagEntryModal modal)
|
||||
{
|
||||
var rowsField = typeof(RawManualTagEntryModal).GetField(
|
||||
"_rows",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
rowsField.ShouldNotBeNull("the manual-entry grid's row list is no longer named _rows.");
|
||||
|
||||
var rows = ((System.Collections.IEnumerable)rowsField!.GetValue(modal)!).Cast<object>().ToList();
|
||||
rows.ShouldNotBeEmpty("the open path no longer seeds a row, so no editor parameters exist to check.");
|
||||
|
||||
var build = typeof(RawManualTagEntryModal).GetMethod(
|
||||
"BuildEditorParameters",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
build.ShouldNotBeNull("BuildEditorParameters was renamed — this test reads it by name.");
|
||||
|
||||
var parameters = (IDictionary<string, object>)build!.Invoke(modal, [rows[0]])!;
|
||||
|
||||
return ((Func<string>)parameters["GetDriverConfigJson"])();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user