feat(adminui): typed native-alarm section in the raw tag modal (#642)
NativeAlarmModel: IsAlarm=false now removes the alarm key; advisory Warnings, driver gating, order-safe Edit. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>Gitea #642 — the shared NativeAlarmEditor's pure-model behaviour: remove-on-off, composition
|
||||
/// with the per-driver editors' models, validation parity with the deploy gate, driver-type gating.</summary>
|
||||
public sealed class NativeAlarmEditorModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToJson_with_IsAlarm_false_removes_the_alarm_key_and_keeps_the_rest()
|
||||
{
|
||||
var m = NativeAlarmModel.FromJson("""{"FullName":"X.Y","alarm":{"alarmType":"LimitAlarm","severity":300,"extra":1}}""");
|
||||
m.IsAlarm = false;
|
||||
|
||||
var o = JsonNode.Parse(m.ToJson())!.AsObject();
|
||||
o.ContainsKey("alarm").ShouldBeFalse();
|
||||
o["FullName"]!.GetValue<string>().ShouldBe("X.Y");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Toggle_off_then_on_again_starts_from_defaults()
|
||||
{
|
||||
var off = NativeAlarmModel.Edit("""{"alarm":{"severity":300}}""", m => m.IsAlarm = false);
|
||||
var on = NativeAlarmModel.Edit(off, m => m.IsAlarm = true);
|
||||
var back = NativeAlarmModel.FromJson(on);
|
||||
back.IsAlarm.ShouldBeTrue();
|
||||
back.Severity.ShouldBe(500);
|
||||
back.AlarmType.ShouldBe("AlarmCondition");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Edit_preserves_unknown_keys_at_both_levels()
|
||||
{
|
||||
var json = NativeAlarmModel.Edit("""{"FullName":"A","zz":1,"alarm":{"alarmType":"OffNormalAlarm","custom":"k"}}""",
|
||||
m => m.Severity = 900);
|
||||
var o = JsonNode.Parse(json)!.AsObject();
|
||||
o["zz"]!.GetValue<int>().ShouldBe(1);
|
||||
o["alarm"]!["custom"]!.GetValue<string>().ShouldBe("k");
|
||||
o["alarm"]!["severity"]!.GetValue<int>().ShouldBe(900);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alarm_and_mtconnect_driver_editor_compose_in_either_order()
|
||||
{
|
||||
const string start = """{"fullName":"Xact"}""";
|
||||
|
||||
// alarm first, then driver editor (driver model re-parses the alarm-bearing JSON)
|
||||
var a1 = NativeAlarmModel.Edit(start, m => { m.IsAlarm = true; m.Severity = 800; });
|
||||
var d1 = MTConnectTagConfigModel.FromJson(a1);
|
||||
d1.FullName = "Yact";
|
||||
var r1 = d1.ToJson();
|
||||
|
||||
// driver editor first, then alarm
|
||||
var d2 = MTConnectTagConfigModel.FromJson(start);
|
||||
d2.FullName = "Yact";
|
||||
var r2 = NativeAlarmModel.Edit(d2.ToJson(), m => { m.IsAlarm = true; m.Severity = 800; });
|
||||
|
||||
foreach (var r in new[] { r1, r2 })
|
||||
{
|
||||
var o = JsonNode.Parse(r)!.AsObject();
|
||||
o["fullName"]!.GetValue<string>().ShouldBe("Yact");
|
||||
o["alarm"]!["severity"]!.GetValue<int>().ShouldBe(800);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Modbus")]
|
||||
[InlineData("S7")]
|
||||
[InlineData("AbCip")]
|
||||
[InlineData("AbLegacy")]
|
||||
[InlineData("TwinCAT")]
|
||||
[InlineData("FOCAS")]
|
||||
[InlineData("OpcUaClient")]
|
||||
[InlineData("Sql")]
|
||||
[InlineData("Mqtt")]
|
||||
[InlineData("MTConnect")]
|
||||
public void Every_typed_driver_model_preserves_an_alarm_object(string driverType)
|
||||
{
|
||||
const string json = """{"alarm":{"alarmType":"LimitAlarm","severity":123}}""";
|
||||
var round = TagConfigRoundTrip.Through(driverType, json);
|
||||
NativeAlarmModel.FromJson(round).Severity.ShouldBe(123);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_is_not_stricter_than_the_deploy_gate()
|
||||
{
|
||||
// The gate/parser accepts any severity (clamped 1..1000 at publish) and any alarmType string.
|
||||
var m = NativeAlarmModel.FromJson("""{"alarm":{"alarmType":"Whatever","severity":5000}}""");
|
||||
m.Validate().ShouldBeNull();
|
||||
m.Warnings().ShouldNotBeEmpty(); // advisory only
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsNativeAlarms_covers_exactly_the_IAlarmSource_drivers()
|
||||
{
|
||||
foreach (var t in new[] { DriverTypeNames.MTConnect, DriverTypeNames.AbCip, DriverTypeNames.FOCAS,
|
||||
DriverTypeNames.OpcUaClient, DriverTypeNames.Galaxy })
|
||||
NativeAlarmModel.SupportsNativeAlarms(t).ShouldBeTrue(t);
|
||||
foreach (var t in new[] { DriverTypeNames.Modbus, DriverTypeNames.S7, DriverTypeNames.Mqtt, null })
|
||||
NativeAlarmModel.SupportsNativeAlarms(t).ShouldBeFalse(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>Test helper: FromJson→ToJson through the driver's typed TagConfig model.</summary>
|
||||
internal static class TagConfigRoundTrip
|
||||
{
|
||||
public static string Through(string driverType, string json) => driverType switch
|
||||
{
|
||||
DriverTypeNames.Modbus => ModbusTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.S7 => S7TagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.AbCip => AbCipTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.AbLegacy => AbLegacyTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.TwinCAT => TwinCATTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.FOCAS => FocasTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.OpcUaClient => OpcUaClientTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.Sql => SqlTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.Mqtt => MqttTagConfigModel.FromJson(json).ToJson(),
|
||||
DriverTypeNames.MTConnect => MTConnectTagConfigModel.FromJson(json).ToJson(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(driverType)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user