ae2e1efb1c
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.
105 lines
4.7 KiB
C#
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));
|
|
}
|
|
}
|