feat(adminui): B2-WP3 driver/device config modals + page-to-form refactor
Extract the 8 typed driver pages into embeddable <Driver>DriverForm bodies (channel/protocol config) and add per-driver <Driver>DeviceForm bodies (connection endpoint -> Device.DeviceConfig, v3 endpoint split). New DriverConfigModal + DeviceModal dispatch by DriverType (DriverTypeNames) for the /raw tree; Test-connect inside DeviceModal builds the merged Driver+Device config transiently via DriverDeviceConfigMerger. Routed pages now host the form bodies and keep working. Round-trip + enum-serialization guard tests for the Modbus driver form + Modbus device model; retargeted the existing page-form serialization tests + the _jsonOpts converter guard to the forms. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -3,67 +3,69 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression guard for the systemic driver-config enum-serialization bug found 2026-06-19.
|
||||
/// Every <c>*DriverPage</c> serialised enum config fields (e.g. S7 <c>CpuType</c>, Modbus
|
||||
/// <c>DataType</c>/<c>Region</c>, AbCip <c>PlcFamily</c>) as JSON <em>numbers</em> because its
|
||||
/// private static <c>_jsonOpts</c> had no <see cref="JsonStringEnumConverter"/>. The driver
|
||||
/// factories, however, deserialise into string-typed DTOs (+ lenient <c>ParseEnum</c>) and
|
||||
/// <em>throw</em> when binding a JSON number to a <c>string?</c> — so an AdminUI-authored
|
||||
/// config that contained any enum field produced a blob the driver could not parse, faulting
|
||||
/// the driver on deploy. The fix makes every page serialise enums as strings (matching the
|
||||
/// factory + the long-correct OpcUaClient template). This test fails if any driver page loses
|
||||
/// its string-enum converter again.
|
||||
/// Every driver-config editor serialised enum config fields (e.g. S7 <c>CpuType</c>, Modbus
|
||||
/// <c>Family</c>, AbCip <c>PlcFamily</c>) as JSON <em>numbers</em> when its private static
|
||||
/// <c>_jsonOpts</c> had no <see cref="JsonStringEnumConverter"/>. The driver factories, however,
|
||||
/// deserialise into string-typed DTOs (+ lenient <c>ParseEnum</c>) and <em>throw</em> when binding a
|
||||
/// JSON number to a <c>string?</c> — so an AdminUI-authored config that contained any enum field
|
||||
/// produced a blob the driver could not parse, faulting the driver on deploy. The fix makes every
|
||||
/// editor serialise enums as strings (matching the factory + the long-correct OpcUaClient template).
|
||||
/// <para>v3 Batch-2 (WP3): the config serializer moved from the routed <c>*DriverPage</c> into the
|
||||
/// embeddable <c>*DriverForm</c> body (shared by the page + the raw-tree DriverConfigModal), so this
|
||||
/// guard now reflects over the <c>*DriverForm</c> fleet. It fails if any driver form loses its
|
||||
/// string-enum converter again.</para>
|
||||
/// </summary>
|
||||
public sealed class DriverPageJsonConverterTests
|
||||
{
|
||||
/// <summary>Every concrete <c>*DriverPage</c> in the AdminUI assembly that declares a
|
||||
/// <summary>Every concrete <c>*DriverForm</c> in the AdminUI assembly that declares a
|
||||
/// <c>_jsonOpts</c> config serializer.</summary>
|
||||
private static IReadOnlyList<Type> DriverPageTypes { get; } =
|
||||
typeof(S7DriverPage).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverPage", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
private static IReadOnlyList<Type> DriverFormTypes { get; } =
|
||||
typeof(ModbusDriverForm).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverForm", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
.Where(t => t.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static) is not null)
|
||||
.OrderBy(t => t.Name)
|
||||
.ToList();
|
||||
|
||||
/// <summary>xUnit theory source over the driver-page types discovered by reflection.</summary>
|
||||
public static TheoryData<Type> DriverPagesWithJsonOpts()
|
||||
/// <summary>xUnit theory source over the driver-form types discovered by reflection.</summary>
|
||||
public static TheoryData<Type> DriverFormsWithJsonOpts()
|
||||
{
|
||||
var data = new TheoryData<Type>();
|
||||
foreach (var t in DriverPageTypes)
|
||||
foreach (var t in DriverFormTypes)
|
||||
data.Add(t);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Verifies every driver page's config serializer registers a string-enum converter so
|
||||
/// <summary>Verifies every driver form's config serializer registers a string-enum converter so
|
||||
/// enum config fields round-trip as strings (and the driver factory can parse the result).</summary>
|
||||
/// <param name="pageType">A driver page component type discovered by reflection.</param>
|
||||
/// <param name="formType">A driver form component type discovered by reflection.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(DriverPagesWithJsonOpts))]
|
||||
public void Driver_page_json_options_register_string_enum_converter(Type pageType)
|
||||
[MemberData(nameof(DriverFormsWithJsonOpts))]
|
||||
public void Driver_form_json_options_register_string_enum_converter(Type formType)
|
||||
{
|
||||
var field = pageType.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var field = formType.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var opts = (JsonSerializerOptions)field!.GetValue(null)!;
|
||||
opts.Converters.OfType<JsonStringEnumConverter>().ShouldNotBeEmpty(
|
||||
$"{pageType.Name}._jsonOpts must register a JsonStringEnumConverter; otherwise AdminUI-authored " +
|
||||
$"{formType.Name}._jsonOpts must register a JsonStringEnumConverter; otherwise AdminUI-authored " +
|
||||
"enum config fields serialise as numbers and the string-typed driver factory throws on parse.");
|
||||
}
|
||||
|
||||
/// <summary>Enforces that EVERY concrete <c>*DriverPage</c> routes config serialization through a
|
||||
/// <c>_jsonOpts</c> field — otherwise a new page that serialised config a different way would slip
|
||||
/// <summary>Enforces that EVERY concrete <c>*DriverForm</c> routes config serialization through a
|
||||
/// <c>_jsonOpts</c> field — otherwise a new form that serialised config a different way would slip
|
||||
/// past the converter guard above. Also a floor check so a rename can't silently shrink the set.</summary>
|
||||
[Fact]
|
||||
public void Every_driver_page_uses_a_guarded_jsonOpts_serializer()
|
||||
public void Every_driver_form_uses_a_guarded_jsonOpts_serializer()
|
||||
{
|
||||
var allDriverPages = typeof(S7DriverPage).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverPage", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
var allDriverForms = typeof(ModbusDriverForm).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverForm", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
.ToList();
|
||||
allDriverPages.Count.ShouldBeGreaterThanOrEqualTo(8, "reflection should discover the full driver-page fleet");
|
||||
DriverPageTypes.Count.ShouldBe(allDriverPages.Count,
|
||||
"every *DriverPage must declare a _jsonOpts config serializer so the string-enum converter guard covers it");
|
||||
allDriverForms.Count.ShouldBeGreaterThanOrEqualTo(8, "reflection should discover the full driver-form fleet");
|
||||
DriverFormTypes.Count.ShouldBe(allDriverForms.Count,
|
||||
"every *DriverForm must declare a _jsonOpts config serializer so the string-enum converter guard covers it");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user