feat(ui): List attribute editor in TemplateEdit

This commit is contained in:
Joseph Doherty
2026-06-16 16:20:08 -04:00
parent 85db4571b2
commit ba7331e67c
3 changed files with 355 additions and 7 deletions
@@ -0,0 +1,132 @@
using Bunit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design;
/// <summary>
/// MV-13: the shared <c>AttributeListEditor</c> reveals an element-type select
/// plus a repeatable list-value editor for structured List attributes. These are
/// real bUnit rendering/interaction tests over the self-contained component, plus
/// structural assertions pinning the TemplateEdit attribute-form wiring (the page
/// itself is heavyweight to render — see <c>TemplateNativeAlarmSourceEditorTests</c>).
/// </summary>
public class AttributeListEditorTests : BunitContext
{
private static string TemplateEditMarkup
{
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", "Design", "TemplateEdit.razor"));
}
}
[Fact]
public void Editor_RendersElementTypeSelect_WithSixValidScalars()
{
var cut = Render<AttributeListEditor>(p => p
.Add(x => x.ElementDataType, DataType.String)
.Add(x => x.Rows, new List<string>()));
var select = cut.Find("select.form-select");
var options = select.QuerySelectorAll("option");
Assert.Equal(6, options.Length);
var texts = options.Select(o => o.TextContent).ToArray();
Assert.Contains("String", texts);
Assert.Contains("Int32", texts);
Assert.Contains("Float", texts);
Assert.Contains("Double", texts);
Assert.Contains("Boolean", texts);
Assert.Contains("DateTime", texts);
// List itself must never appear as an element type.
Assert.DoesNotContain("List", texts);
}
[Fact]
public void ShowElementType_False_HidesTheSelect()
{
var cut = Render<AttributeListEditor>(p => p
.Add(x => x.ShowElementType, false)
.Add(x => x.Rows, new List<string>()));
Assert.Throws<ElementNotFoundException>(() => cut.Find("select.form-select"));
}
[Fact]
public void Editor_RendersOneInputPerRow()
{
var cut = Render<AttributeListEditor>(p => p
.Add(x => x.ElementDataType, DataType.Int32)
.Add(x => x.Rows, new List<string> { "1", "2", "3" }));
var inputs = cut.FindAll("input.form-control");
Assert.Equal(3, inputs.Count);
Assert.Equal("1", inputs[0].GetAttribute("value"));
Assert.Equal("3", inputs[2].GetAttribute("value"));
}
[Fact]
public void AddElement_AppendsRow_AndRaisesRowsChanged()
{
var rows = new List<string> { "a" };
List<string>? changed = null;
var cut = Render<AttributeListEditor>(p => p
.Add(x => x.Rows, rows)
.Add(x => x.RowsChanged, r => changed = r));
cut.Find("button.btn-outline-secondary").Click();
Assert.NotNull(changed);
Assert.Equal(2, changed!.Count);
Assert.Equal("", changed[1]);
}
[Fact]
public void RemoveElement_DropsRow_AndRaisesRowsChanged()
{
var rows = new List<string> { "a", "b" };
List<string>? changed = null;
var cut = Render<AttributeListEditor>(p => p
.Add(x => x.Rows, rows)
.Add(x => x.RowsChanged, r => changed = r));
// First per-row Remove button.
cut.FindAll("button.btn-outline-danger")[0].Click();
Assert.NotNull(changed);
Assert.Single(changed!);
Assert.Equal("b", changed![0]);
}
[Fact]
public void EncodedRows_RoundTripThroughCodec()
{
// Mirrors what TemplateEdit.SaveAttribute does on submit.
var rows = new List<string> { "10", "20" };
var json = AttributeValueCodec.Encode(rows);
var decoded = AttributeValueCodec.Decode(json, DataType.List, DataType.Int32);
var list = Assert.IsType<List<int>>(decoded);
Assert.Equal(new[] { 10, 20 }, list);
}
[Fact]
public void TemplateEdit_RevealsListEditor_AndSendsElementType()
{
var markup = TemplateEditMarkup;
// Conditional reveal on DataType.List.
Assert.Contains("_attrDataType == DataType.List", markup);
Assert.Contains("<AttributeListEditor", markup);
Assert.Contains("@bind-ElementDataType=\"_attrElementDataType\"", markup);
Assert.Contains("@bind-Rows=\"_attrListRows\"", markup);
// Submit encodes rows to canonical JSON and passes the element type.
Assert.Contains("AttributeValueCodec.Encode(_attrListRows)", markup);
Assert.Contains("ElementDataType = elementType", markup);
// Edit decodes the stored JSON value into rows.
Assert.Contains("DecodeListRows(", markup);
}
}