ba1ac38333
A shared Bootstrap-only combobox that replaces the flat instance <select> on Flow Debugger and Debug View (design docs/plans/2026-09-08-instance-picker-design.md, plan task 2): a text field over a dropdown panel hosting TreeView across the InstanceTreeBuilder forest, every area collapsed on open. Clicking an area row toggles it, clicking an instance row selects it, closes the panel and raises SelectedInstanceIdChanged. Outside clicks close through a transparent fixed backdrop — the same pattern TreeView uses for its context menu, so no JavaScript. A site change (new Areas/Instances references) closes the panel and clears the field when the selection no longer resolves; a parent-driven SelectedInstanceId change re-derives the field text while closed. ItemLabel customises row and field text (Debug View wants a "(State)" suffix) while search still matches UniqueName. TreeView gains one additive public ToggleExpanded(key) — the chevron's existing private toggle, exposed so a consumer whose own NodeContent owns the label click can toggle the branch. Search filtering and keyboard handling land in task 3; _filter/Rebuild() are in place for them. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
195 lines
6.3 KiB
C#
195 lines
6.3 KiB
C#
using Bunit;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
|
|
|
/// <summary>
|
|
/// bUnit tests for the <see cref="InstancePicker"/> combobox: the closed field, the
|
|
/// panel that opens with every area collapsed, area expansion, instance selection
|
|
/// (callback + close + field text), the disabled guard, the outside-click backdrop,
|
|
/// parent-driven selection changes and the <c>ItemLabel</c> hook.
|
|
/// </summary>
|
|
public class InstancePickerTests : BunitContext
|
|
{
|
|
public InstancePickerTests()
|
|
{
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
}
|
|
|
|
private static List<Area> Areas() =>
|
|
[
|
|
new("Line1") { Id = 1, SiteId = 1 },
|
|
new("Cell A") { Id = 2, SiteId = 1, ParentAreaId = 1 },
|
|
];
|
|
|
|
private static List<Instance> Instances() =>
|
|
[
|
|
new("P1") { Id = 7, SiteId = 1, AreaId = 2 },
|
|
new("Zulu") { Id = 8, SiteId = 1 },
|
|
];
|
|
|
|
private IRenderedComponent<InstancePicker> RenderPicker(
|
|
Action<int>? onChanged = null,
|
|
bool disabled = false,
|
|
Func<Instance, string>? itemLabel = null,
|
|
int selectedInstanceId = 0) =>
|
|
Render<InstancePicker>(ps =>
|
|
{
|
|
ps.Add(p => p.Areas, Areas());
|
|
ps.Add(p => p.Instances, Instances());
|
|
ps.Add(p => p.DataTestPrefix, "t");
|
|
ps.Add(p => p.Disabled, disabled);
|
|
ps.Add(p => p.SelectedInstanceId, selectedInstanceId);
|
|
if (itemLabel is not null) ps.Add(p => p.ItemLabel, itemLabel);
|
|
if (onChanged is not null) ps.Add(p => p.SelectedInstanceIdChanged, onChanged);
|
|
});
|
|
|
|
private static void Open(IRenderedComponent<InstancePicker> cut) =>
|
|
cut.Find("[data-test='t-toggle']").Click();
|
|
|
|
private static void ClickRow(IRenderedComponent<InstancePicker> cut, string text) =>
|
|
cut.FindAll("[data-test='t-row']").First(r => r.TextContent.Trim() == text).Click();
|
|
|
|
private static string InputValue(IRenderedComponent<InstancePicker> cut) =>
|
|
cut.Find("[data-test='t-input']").GetAttribute("value") ?? string.Empty;
|
|
|
|
[Fact]
|
|
public void Closed_ShowsNothingButTheField()
|
|
{
|
|
var cut = RenderPicker();
|
|
|
|
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
|
|
|
|
var input = cut.Find("[data-test='t-input']");
|
|
Assert.Equal("combobox", input.GetAttribute("role"));
|
|
Assert.Equal("false", input.GetAttribute("aria-expanded"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Toggle_OpensPanel_WithAreasCollapsed()
|
|
{
|
|
var cut = RenderPicker();
|
|
|
|
Open(cut);
|
|
|
|
Assert.Single(cut.FindAll("[data-test='t-panel']"));
|
|
Assert.Equal("true", cut.Find("[data-test='t-input']").GetAttribute("aria-expanded"));
|
|
|
|
var rows = cut.FindAll("[data-test='t-row']").Select(r => r.TextContent.Trim()).ToArray();
|
|
Assert.Equal(new[] { "Line1", "Zulu" }, rows);
|
|
|
|
// P1 sits under a collapsed area, so its row is not rendered at all.
|
|
Assert.Empty(cut.FindAll("[data-instance-id='7']"));
|
|
|
|
// Area rows must not carry a data-instance-id at all (Blazor drops a null attribute).
|
|
var areaRow = cut.FindAll("[data-test='t-row']").First(r => r.TextContent.Trim() == "Line1");
|
|
Assert.False(areaRow.HasAttribute("data-instance-id"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ClickingAnAreaLabel_ExpandsIt()
|
|
{
|
|
var cut = RenderPicker();
|
|
Open(cut);
|
|
|
|
ClickRow(cut, "Line1");
|
|
Assert.Contains(cut.FindAll("[data-test='t-row']"), r => r.TextContent.Trim() == "Cell A");
|
|
Assert.Empty(cut.FindAll("[data-instance-id='7']"));
|
|
|
|
ClickRow(cut, "Cell A");
|
|
Assert.Single(cut.FindAll("[data-instance-id='7']"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ClickingAnInstance_Selects_Closes_AndRaisesTheCallback()
|
|
{
|
|
int? picked = null;
|
|
var cut = RenderPicker(onChanged: id => picked = id);
|
|
|
|
Open(cut);
|
|
ClickRow(cut, "Line1");
|
|
ClickRow(cut, "Cell A");
|
|
cut.Find("[data-instance-id='7']").Click();
|
|
|
|
Assert.Equal(7, picked);
|
|
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
|
|
Assert.Equal("P1", InputValue(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void Disabled_NeverOpens()
|
|
{
|
|
var cut = RenderPicker(disabled: true);
|
|
|
|
Assert.True(cut.Find("[data-test='t-input']").HasAttribute("disabled"));
|
|
Assert.True(cut.Find("[data-test='t-toggle']").HasAttribute("disabled"));
|
|
|
|
Open(cut);
|
|
|
|
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Backdrop_ClickClosesThePanel_WithoutChangingSelection()
|
|
{
|
|
int? picked = null;
|
|
var cut = RenderPicker(onChanged: id => picked = id);
|
|
|
|
Open(cut);
|
|
cut.Find("[data-test='t-backdrop']").Click();
|
|
|
|
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
|
|
Assert.Null(picked);
|
|
Assert.Equal(string.Empty, InputValue(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParentDrivenSelection_UpdatesTheFieldText()
|
|
{
|
|
var cut = RenderPicker();
|
|
|
|
cut.Render(ps => ps.Add(p => p.SelectedInstanceId, 8));
|
|
Assert.Equal("Zulu", InputValue(cut));
|
|
|
|
cut.Render(ps => ps.Add(p => p.SelectedInstanceId, 0));
|
|
Assert.Equal(string.Empty, InputValue(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void SiteChange_NewInstancesList_ClearsAStaleSelectionText()
|
|
{
|
|
var cut = RenderPicker(selectedInstanceId: 8);
|
|
Assert.Equal("Zulu", InputValue(cut));
|
|
|
|
Open(cut);
|
|
|
|
// A different site: neither the areas nor the previously selected instance survive.
|
|
cut.Render(ps =>
|
|
{
|
|
ps.Add(p => p.Areas, new List<Area>());
|
|
ps.Add(p => p.Instances, new List<Instance> { new("Other") { Id = 99, SiteId = 2 } });
|
|
});
|
|
|
|
Assert.Equal(string.Empty, InputValue(cut));
|
|
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemLabel_IsUsedForRowsAndField()
|
|
{
|
|
var cut = RenderPicker(itemLabel: i => i.UniqueName + " (x)");
|
|
|
|
Open(cut);
|
|
|
|
var rows = cut.FindAll("[data-test='t-row']").Select(r => r.TextContent.Trim()).ToArray();
|
|
Assert.Equal(new[] { "Line1", "Zulu (x)" }, rows);
|
|
|
|
ClickRow(cut, "Line1");
|
|
ClickRow(cut, "Cell A");
|
|
cut.Find("[data-instance-id='7']").Click();
|
|
|
|
Assert.Equal("P1 (x)", InputValue(cut));
|
|
}
|
|
}
|