0bd5e0986f
Three dead-code bugs: --trigger-kind was registered but never read or forwarded on the
alarm-update, script-add, and script-update paths. Introduced TriggerConfigJson.InjectAnalysisKind
helper that rewrites any raw --trigger-config JSON blob, writing "analysisKind":"Strict" when
the flag is strict (case-insensitive) and stripping the key for any other value. Wired the
helper into all three handlers alongside the existing alarm-add path (which already used
AlarmTriggerConfigJson.Build). Added 6 unit tests for the new helper in TemplateTriggerKindTests.
Also fixed a false-positive bUnit test (AlarmTriggerEditor_Expression_NoAnalysisKindInConfig_
SelectorDefaultsAdvisory) that passed because "Advisory" appeared anywhere in the HTML; now
asserts select.GetAttribute("value") == "Advisory". Added the missing equivalent test for
ScriptTriggerEditor (ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory).
386 lines
15 KiB
C#
386 lines
15 KiB
C#
using System.Text.Json;
|
|
using Bunit;
|
|
using Bunit.JSInterop;
|
|
using Microsoft.AspNetCore.Components;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
|
|
|
/// <summary>
|
|
/// M9-T28b: the <c>AlarmTriggerEditor</c> and <c>ScriptTriggerEditor</c>
|
|
/// components must surface an Advisory|Strict selector for Expression triggers
|
|
/// and round-trip the <c>"analysisKind"</c> key into the trigger config JSON
|
|
/// that the T28a ValidationService backend reads.
|
|
/// </summary>
|
|
|
|
// ── AlarmTriggerConfigCodec — analysisKind parse / serialize ────────────────
|
|
|
|
public class AlarmTriggerAnalysisKindCodecTests
|
|
{
|
|
// Codec: parse — absent key defaults to Advisory (false)
|
|
[Fact]
|
|
public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
|
|
{
|
|
var json = @"{""expression"":""Attributes[""Temp""] > 50""}";
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
Assert.False(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: parse — "Advisory" explicit → false
|
|
[Fact]
|
|
public void Parse_Expression_AdvisoryKey_ReturnsFalse()
|
|
{
|
|
var json = @"{""expression"":""x > 0"",""analysisKind"":""Advisory""}";
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
Assert.False(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: parse — "Strict" → true
|
|
[Fact]
|
|
public void Parse_Expression_StrictKey_ReturnsTrue()
|
|
{
|
|
var json = @"{""expression"":""x > 0"",""analysisKind"":""Strict""}";
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
Assert.True(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: parse — case-insensitive
|
|
[Fact]
|
|
public void Parse_Expression_StrictKeyCaseInsensitive()
|
|
{
|
|
var json = @"{""expression"":""x > 0"",""analysisKind"":""strict""}";
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
Assert.True(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: serialize — Advisory (false) → "analysisKind" key omitted
|
|
[Fact]
|
|
public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
|
|
{
|
|
var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
|
|
// Codec: serialize — Strict (true) → "analysisKind":"Strict"
|
|
[Fact]
|
|
public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
|
|
{
|
|
var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
|
|
}
|
|
|
|
// Codec: serialize — Strict not emitted for non-Expression trigger types
|
|
[Fact]
|
|
public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Min = 0,
|
|
Max = 100,
|
|
IsStrictAnalysisKind = true // should be ignored for non-Expression
|
|
};
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
|
|
// Codec: round-trip Strict
|
|
[Fact]
|
|
public void RoundTrip_Expression_Strict_Preserved()
|
|
{
|
|
var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = true };
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
|
|
Assert.True(round.IsStrictAnalysisKind);
|
|
Assert.Equal("Temp > 80", round.Expression);
|
|
}
|
|
|
|
// Codec: round-trip Advisory (omit + re-parse → still false)
|
|
[Fact]
|
|
public void RoundTrip_Expression_Advisory_Preserved()
|
|
{
|
|
var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = false };
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
|
|
|
|
Assert.False(round.IsStrictAnalysisKind);
|
|
}
|
|
}
|
|
|
|
// ── ScriptTriggerConfigCodec — analysisKind parse / serialize ───────────────
|
|
|
|
public class ScriptTriggerAnalysisKindCodecTests
|
|
{
|
|
// Codec: parse — absent key defaults to Advisory (false)
|
|
[Fact]
|
|
public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
|
|
{
|
|
var json = @"{""expression"":""x > 0"",""mode"":""OnTrue""}";
|
|
var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
|
|
Assert.False(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: parse — "Strict" → true
|
|
[Fact]
|
|
public void Parse_Expression_StrictKey_ReturnsTrue()
|
|
{
|
|
var json = @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}";
|
|
var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
|
|
Assert.True(model.IsStrictAnalysisKind);
|
|
}
|
|
|
|
// Codec: serialize — Advisory → key omitted
|
|
[Fact]
|
|
public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
|
|
{
|
|
var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
|
|
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
|
|
// Codec: serialize — Strict → "analysisKind":"Strict"
|
|
[Fact]
|
|
public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
|
|
{
|
|
var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
|
|
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
|
|
}
|
|
|
|
// Codec: serialize — Strict not emitted for non-Expression triggers
|
|
[Fact]
|
|
public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
|
|
{
|
|
var model = new ScriptTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Operator = ">",
|
|
Threshold = 50,
|
|
IsStrictAnalysisKind = true // must be ignored for Conditional
|
|
};
|
|
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Conditional)!;
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
|
|
// Codec: round-trip Strict
|
|
[Fact]
|
|
public void RoundTrip_Expression_Strict_Preserved()
|
|
{
|
|
var original = new ScriptTriggerModel
|
|
{
|
|
Expression = "Temp > 80",
|
|
Mode = ScriptTriggerMode.WhileTrue,
|
|
IsStrictAnalysisKind = true
|
|
};
|
|
var json = ScriptTriggerConfigCodec.Serialize(original, ScriptTriggerKind.Expression)!;
|
|
var round = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
|
|
|
|
Assert.True(round.IsStrictAnalysisKind);
|
|
Assert.Equal(ScriptTriggerMode.WhileTrue, round.Mode);
|
|
}
|
|
}
|
|
|
|
// ── AlarmTriggerEditor — UI selector (bUnit component tests) ─────────────────
|
|
|
|
public class AlarmTriggerEditorAnalysisKindTests : BunitContext
|
|
{
|
|
public AlarmTriggerEditorAnalysisKindTests()
|
|
{
|
|
// MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
|
|
// mode so the editor renders without requiring a full setup for each call.
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
}
|
|
|
|
// Selector is visible for Expression trigger type
|
|
[Fact]
|
|
public void AlarmTriggerEditor_Expression_ShowsAnalysisKindSelector()
|
|
{
|
|
var cut = Render<AlarmTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
|
|
.Add(p => p.Value, @"{""expression"":""""}"));
|
|
|
|
// The selector must exist and have id="alarm-trigger-kind"
|
|
var select = cut.Find("#alarm-trigger-kind");
|
|
Assert.NotNull(select);
|
|
}
|
|
|
|
// Selector is NOT visible for non-Expression trigger types
|
|
[Fact]
|
|
public void AlarmTriggerEditor_NonExpression_DoesNotShowAnalysisKindSelector()
|
|
{
|
|
var cut = Render<AlarmTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, AlarmTriggerType.RangeViolation)
|
|
.Add(p => p.Value, @"{""attributeName"":""Temp"",""min"":0,""max"":100}"));
|
|
|
|
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#alarm-trigger-kind"));
|
|
}
|
|
|
|
// Selector defaults to Advisory
|
|
[Fact]
|
|
public void AlarmTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
|
|
{
|
|
var cut = Render<AlarmTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
|
|
.Add(p => p.Value, @"{""expression"":""x > 0""}"));
|
|
|
|
var select = cut.Find("#alarm-trigger-kind");
|
|
// The bound value must be "Advisory" — not just present in the HTML
|
|
Assert.Equal("Advisory", select.GetAttribute("value"));
|
|
}
|
|
|
|
// Selector defaults to Advisory when config has no analysisKind (ScriptTriggerEditor)
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
|
|
{
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Expression")
|
|
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}"));
|
|
|
|
var select = cut.Find("#script-trigger-kind");
|
|
// The bound value must be "Advisory" — not just present in the HTML
|
|
Assert.Equal("Advisory", select.GetAttribute("value"));
|
|
}
|
|
|
|
// Choosing Strict writes analysisKind:"Strict" into emitted config
|
|
[Fact]
|
|
public void AlarmTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
|
|
{
|
|
string? emitted = null;
|
|
var cut = Render<AlarmTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
|
|
.Add(p => p.Value, @"{""expression"":""x > 0""}")
|
|
.Add(p => p.ValueChanged,
|
|
EventCallback.Factory.Create<string?>(this, v => emitted = v)));
|
|
|
|
cut.Find("#alarm-trigger-kind").Change("Strict");
|
|
|
|
Assert.NotNull(emitted);
|
|
using var doc = JsonDocument.Parse(emitted!);
|
|
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
|
|
}
|
|
|
|
// Loaded Strict config reflects in selector, emits Strict on next change
|
|
[Fact]
|
|
public void AlarmTriggerEditor_Expression_LoadedStrictConfig_RetainedOnEdit()
|
|
{
|
|
string? emitted = null;
|
|
var cut = Render<AlarmTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
|
|
.Add(p => p.Value, @"{""expression"":""x > 0"",""analysisKind"":""Strict""}")
|
|
.Add(p => p.ValueChanged,
|
|
EventCallback.Factory.Create<string?>(this, v => emitted = v)));
|
|
|
|
// Switch to Advisory — config must no longer carry analysisKind
|
|
cut.Find("#alarm-trigger-kind").Change("Advisory");
|
|
|
|
Assert.NotNull(emitted);
|
|
using var doc = JsonDocument.Parse(emitted!);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
}
|
|
|
|
// ── ScriptTriggerEditor — UI selector (bUnit component tests) ────────────────
|
|
|
|
public class ScriptTriggerEditorAnalysisKindTests : BunitContext
|
|
{
|
|
public ScriptTriggerEditorAnalysisKindTests()
|
|
{
|
|
// MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
|
|
// mode so the editor renders without requiring a full setup for each call.
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
}
|
|
|
|
// Selector is visible for Expression trigger type
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Expression_ShowsAnalysisKindSelector()
|
|
{
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Expression")
|
|
.Add(p => p.TriggerConfig, @"{""expression"":"""",""mode"":""OnTrue""}"));
|
|
|
|
var select = cut.Find("#script-trigger-kind");
|
|
Assert.NotNull(select);
|
|
}
|
|
|
|
// Selector is NOT visible for non-Expression trigger types
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Conditional_DoesNotShowAnalysisKindSelector()
|
|
{
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Conditional")
|
|
.Add(p => p.TriggerConfig, @"{""attributeName"":""Temp"",""operator"":"">"",""threshold"":50,""mode"":""OnTrue""}"));
|
|
|
|
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#script-trigger-kind"));
|
|
}
|
|
|
|
// Choosing Strict writes analysisKind:"Strict" into emitted config
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
|
|
{
|
|
ScriptTriggerValue? captured = null;
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Expression")
|
|
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}")
|
|
.Add(p => p.Changed,
|
|
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
|
|
|
|
cut.Find("#script-trigger-kind").Change("Strict");
|
|
|
|
Assert.NotNull(captured);
|
|
using var doc = JsonDocument.Parse(captured!.Config!);
|
|
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
|
|
}
|
|
|
|
// Choosing Advisory emits config without analysisKind key
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Expression_ChoosingAdvisory_OmitsAnalysisKindKey()
|
|
{
|
|
ScriptTriggerValue? captured = null;
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Expression")
|
|
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
|
|
.Add(p => p.Changed,
|
|
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
|
|
|
|
cut.Find("#script-trigger-kind").Change("Advisory");
|
|
|
|
Assert.NotNull(captured);
|
|
using var doc = JsonDocument.Parse(captured!.Config!);
|
|
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
|
|
}
|
|
|
|
// Loaded Strict is retained on unrelated edit (fire mode change)
|
|
[Fact]
|
|
public void ScriptTriggerEditor_Expression_LoadedStrict_RetainedOnModeChange()
|
|
{
|
|
ScriptTriggerValue? captured = null;
|
|
var cut = Render<ScriptTriggerEditor>(ps => ps
|
|
.Add(p => p.TriggerType, "Expression")
|
|
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
|
|
.Add(p => p.Changed,
|
|
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
|
|
|
|
// Change fire mode — Strict kind must survive
|
|
cut.Find("#script-trigger-mode").Change("WhileTrue");
|
|
|
|
Assert.NotNull(captured);
|
|
Assert.Contains("\"analysisKind\":\"Strict\"", captured!.Config);
|
|
}
|
|
}
|