Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureListOverrideTests.cs
T
Joseph Doherty ae2e1efb1c feat(ui): List attribute override editor in InstanceConfigure
When overriding a List attribute, render the shared AttributeListEditor
(whole-list replacement; element type fixed by the base, shown read-only via
ShowElementType=false) instead of the single-line input. Loading an existing
override decodes its JSON into rows (malformed -> empty); saving encodes rows to
canonical JSON with a pre-submit Decode round-trip guard surfacing element
errors inline. Clearing removes the InstanceAttributeOverride row
(repository-direct, mirroring native-alarm-source overrides). Non-List override
UX unchanged.
2026-06-16 16:25:58 -04:00

105 lines
4.7 KiB
C#

using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
/// <summary>
/// MV-14: the Instance Configure attribute-override panel uses the shared
/// <c>AttributeListEditor</c> for a List attribute (whole-list replacement; the
/// element type is fixed by the base attribute, so the type select is hidden via
/// <c>ShowElementType="false"</c>). Loading an existing override decodes its JSON
/// into rows; saving encodes the rows back to canonical JSON with a pre-submit
/// round-trip guard; clearing removes the override row. <c>InstanceConfigure</c>
/// is a heavyweight page (multiple injected services incl. <c>InstanceService</c>
/// and the flattening pipeline), so — consistent with the native-alarm and
/// template-editor coverage — these are structural assertions over the component
/// source that pin the wiring, plus a real codec round-trip mirroring what the
/// page does on load/save.
/// </summary>
public class InstanceConfigureListOverrideTests
{
private static string InstanceConfigureMarkup
{
get
{
var dir = AppContext.BaseDirectory;
for (var i = 0; i < 6 && dir is not null; i++)
dir = Directory.GetParent(dir)?.FullName;
return File.ReadAllText(Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI",
"Components", "Pages", "Deployment", "InstanceConfigure.razor"));
}
}
[Fact]
public void ListOverride_RevealsSharedEditor_WithElementTypeHidden()
{
var markup = InstanceConfigureMarkup;
// Conditional reveal on a List attribute.
Assert.Contains("attr.DataType == DataType.List", markup);
Assert.Contains("<AttributeListEditor", markup);
// Element type is fixed by the base attribute → type select hidden.
Assert.Contains("ShowElementType=\"false\"", markup);
Assert.Contains("ElementDataType=\"@(attr.ElementDataType ?? DataType.String)\"", markup);
// Bound to the per-attribute working rows.
Assert.Contains("Rows=\"@GetListRows(attr.Name)\"", markup);
Assert.Contains("RowsChanged=\"@(r => OnListRowsChanged(attr.Name, r))\"", markup);
}
[Fact]
public void ListOverride_DecodesOnLoad_AndEncodesOnSaveWithGuard()
{
var markup = InstanceConfigureMarkup;
// Load: effective value (existing override JSON or template default)
// decoded into rows via the shared codec, malformed → empty rows.
Assert.Contains("DecodeListRows(", markup);
Assert.Contains("catch (FormatException)", markup);
// Save: rows encoded to canonical JSON + round-trip Decode guard.
Assert.Contains("AttributeValueCodec.Encode(GetListRows(", markup);
Assert.Contains("AttributeValueCodec.Decode(json, DataType.List, elementType)", markup);
Assert.Contains("_overrideErrors", markup);
}
[Fact]
public void ListOverride_ClearRemovesTheOverrideRow()
{
var markup = InstanceConfigureMarkup;
Assert.Contains("ClearListOverride", markup);
// Repository-direct delete (the page only edits InstanceConfigure; no new
// server method) — same pattern as native-alarm-source overrides.
Assert.Contains("DeleteInstanceAttributeOverrideAsync", markup);
Assert.Contains("HasOverrideRow", markup);
}
[Fact]
public void NonListOverride_KeepsSingleInputUx()
{
var markup = InstanceConfigureMarkup;
// The scalar path still binds the single text input via the existing helpers.
Assert.Contains("GetOverrideValue(attr.Name)", markup);
Assert.Contains("OnOverrideChanged(attr.Name, e)", markup);
}
[Fact]
public void EncodedRows_RoundTripThroughCodec_AsThePageDoes()
{
// Mirrors the load (Decode → rows) / save (Encode → JSON) cycle the page runs.
var json = AttributeValueCodec.Encode(new List<string> { "10", "20", "30" });
var decoded = AttributeValueCodec.Decode(json, DataType.List, DataType.Int32);
var list = Assert.IsType<List<int>>(decoded);
Assert.Equal(new[] { 10, 20, 30 }, list);
// The re-encoded form is stable, so a clean override round-trips losslessly.
var roundTrip = AttributeValueCodec.Encode(decoded);
Assert.Equal(json, roundTrip);
}
[Fact]
public void MalformedListElement_SurfacesFormatException_ForInlineError()
{
// The pre-submit guard catches this and shows it inline rather than crashing.
var json = AttributeValueCodec.Encode(new List<string> { "1", "not-a-number" });
Assert.Throws<FormatException>(
() => AttributeValueCodec.Decode(json, DataType.List, DataType.Int32));
}
}