Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/InstancePickerTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

591 lines
22 KiB
C#

using Bunit;
using Microsoft.AspNetCore.Components.Web;
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, the <c>ItemLabel</c> hook, and the search filter
/// with its keyboard surface (Enter / Escape / ArrowDown).
/// </summary>
public class InstancePickerTests : BunitContext
{
/// <summary>Initializes the test context with loose JS interop.</summary>
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;
private static void Type(IRenderedComponent<InstancePicker> cut, string text) =>
cut.Find("[data-test='t-input']").Input(text);
private static void Press(IRenderedComponent<InstancePicker> cut, string key) =>
cut.Find("[data-test='t-input']").KeyDown(new KeyboardEventArgs { Key = key });
private static string[] Rows(IRenderedComponent<InstancePicker> cut) =>
cut.FindAll("[data-test='t-row']").Select(r => r.TextContent.Trim()).ToArray();
/// <summary>The nth treeitem in document order - i.e. the nth row the operator can see.</summary>
private static AngleSharp.Dom.IElement TreeItem(IRenderedComponent<InstancePicker> cut, int index) =>
cut.FindAll("[role='treeitem']")[index];
/// <summary>Verifies that a closed picker renders only the input field.</summary>
[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"));
}
/// <summary>Verifies that toggling opens the panel with all areas collapsed.</summary>
[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"));
}
/// <summary>Verifies that clicking an area label expands that area.</summary>
[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']"));
}
/// <summary>Verifies that clicking an instance selects it, closes the panel and raises the selection callback.</summary>
[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));
}
/// <summary>Verifies that a disabled picker never opens.</summary>
[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']"));
}
/// <summary>Verifies that clicking the backdrop closes the panel without changing the selection.</summary>
[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));
}
/// <summary>Verifies that a selection set by the parent updates the field text.</summary>
[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));
}
/// <summary>Verifies that supplying a new instances list after a site change clears the stale selection text.</summary>
[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']"));
}
/// <summary>Verifies that the item label function is used for both the rows and the field text.</summary>
[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));
}
/// <summary>Verifies that typing filters the tree and expands it to the matches.</summary>
[Fact]
public void Typing_Filters_AndExpandsToTheMatches()
{
var cut = RenderPicker();
Open(cut);
Type(cut, "p1");
// The match's ancestor areas are force-expanded, so its row renders without a click.
Assert.Single(cut.FindAll("[data-instance-id='7']"));
var rows = Rows(cut);
Assert.Contains("Line1", rows);
Assert.Contains("Cell A", rows);
Assert.DoesNotContain("Zulu", rows);
}
/// <summary>Verifies that clearing the filter restores the full tree in a collapsed state.</summary>
[Fact]
public void Clearing_RestoresTheFullTree_Collapsed()
{
var cut = RenderPicker();
Open(cut);
Type(cut, "p1");
Assert.Single(cut.FindAll("[data-instance-id='7']"));
Type(cut, string.Empty);
Assert.Equal(new[] { "Line1", "Zulu" }, Rows(cut));
Assert.Empty(cut.FindAll("[data-instance-id='7']"));
}
/// <summary>Verifies that typing opens a closed panel.</summary>
[Fact]
public void Typing_OpensTheClosedPanel()
{
var cut = RenderPicker();
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Type(cut, "z");
Assert.Single(cut.FindAll("[data-test='t-panel']"));
Assert.Equal(new[] { "Zulu" }, Rows(cut));
}
/// <summary>Verifies that pressing Enter with no matching instance does nothing.</summary>
[Fact]
public void Enter_WithNoMatch_DoesNothing()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
Type(cut, "zzz");
Press(cut, "Enter");
Assert.Null(picked);
Assert.Single(cut.FindAll("[data-test='t-panel']"));
}
/// <summary>Verifies that pressing Enter picks the first visible match.</summary>
[Fact]
public void Enter_PicksTheFirstVisibleMatch()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
// "u" matches Zulu only — P1 drops out along with its now-empty ancestor areas.
Type(cut, "u");
Press(cut, "Enter");
Assert.Equal(8, picked);
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("Zulu", InputValue(cut));
}
/// <summary>Verifies that pressing Enter with no filter picks the first instance in document order.</summary>
[Fact]
public void Enter_WithNoFilter_PicksTheFirstInstanceInDocumentOrder()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
Press(cut, "Enter");
// Areas render before root instances, but every area is collapsed — so the first
// instance the operator can actually see is the root-level Zulu, not P1.
Assert.Equal(8, picked);
Assert.Equal("Zulu", InputValue(cut));
}
/// <summary>Verifies that pressing Escape closes the panel and restores the selected text.</summary>
[Fact]
public void Escape_ClosesAndRestoresTheSelectedText()
{
var picked = 0;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
ClickRow(cut, "Line1");
ClickRow(cut, "Cell A");
cut.Find("[data-instance-id='7']").Click();
cut.Render(ps => ps.Add(p => p.SelectedInstanceId, picked));
Assert.Equal("P1", InputValue(cut));
Open(cut);
Type(cut, "zzz");
Assert.Equal("zzz", InputValue(cut));
Press(cut, "Escape");
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("P1", InputValue(cut));
}
/// <summary>Verifies that pressing ArrowDown moves focus into the tree.</summary>
[Fact]
public void ArrowDown_MovesFocusIntoTheTree()
{
var cut = RenderPicker();
Open(cut);
// The first row owns the tab stop by default, so move it off with the tree's own
// arrow navigation first — otherwise the assertion below could pass by accident.
ClickRow(cut, "Line1");
cut.FindAll("[role='treeitem']")[0].KeyDown(new KeyboardEventArgs { Key = "ArrowDown" });
Assert.Equal("-1", cut.FindAll("[role='treeitem']")[0].GetAttribute("tabindex"));
Press(cut, "ArrowDown");
var items = cut.FindAll("[role='treeitem']");
Assert.Equal("0", items[0].GetAttribute("tabindex"));
Assert.All(items.Skip(1), i => Assert.Equal("-1", i.GetAttribute("tabindex")));
// The tab stop above is the behavioural assertion; this is the belt-and-braces check
// that real browser focus was requested too. ElementReference.FocusAsync surfaces as a
// JS invocation under Loose mode — drop this line if a bUnit upgrade stops recording it.
Assert.Contains(
JSInterop.Invocations,
i => i.Identifier.Contains("focus", StringComparison.OrdinalIgnoreCase));
}
/// <summary>Verifies that pressing ArrowDown on a closed picker opens the panel and focuses the first row.</summary>
[Fact]
public void ArrowDown_OnAClosedPicker_OpensThePanel_AndFocusesTheFirstRow()
{
// The tree does not exist yet when the keydown is handled, so this pins the deferred
// path: the panel opens in this render pass and the focus lands in the next one.
var cut = RenderPicker();
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Press(cut, "ArrowDown");
Assert.Single(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("0", cut.FindAll("[role='treeitem']")[0].GetAttribute("tabindex"));
}
/// <summary>Verifies that pressing Escape on a closed picker has no effect.</summary>
[Fact]
public void Escape_WhenClosed_IsANoOp()
{
var cut = RenderPicker(selectedInstanceId: 8);
Assert.Equal("Zulu", InputValue(cut));
Press(cut, "Escape");
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("Zulu", InputValue(cut));
}
/// <summary>Verifies that the filter text is not carried into the selection.</summary>
[Fact]
public void FilterIsNotCarriedIntoSelection()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
Type(cut, "p1");
cut.Find("[data-instance-id='7']").Click();
Assert.Equal(7, picked);
Assert.Equal("P1", InputValue(cut));
Open(cut);
Assert.Equal(new[] { "Line1", "Zulu" }, Rows(cut));
Assert.Empty(cut.FindAll("[data-instance-id='7']"));
}
/// <summary>Verifies that toggling while the panel is open closes it.</summary>
[Fact]
public void Toggle_WhileOpen_ClosesThePanel()
{
var cut = RenderPicker();
Open(cut);
Assert.Single(cut.FindAll("[data-test='t-panel']"));
Open(cut);
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("false", cut.Find("[data-test='t-input']").GetAttribute("aria-expanded"));
}
/// <summary>Verifies that the field sits above the backdrop so it stays clickable while the panel is open.</summary>
[Fact]
public void TheFieldSitsAboveTheBackdrop_SoItStaysClickableWhileOpen()
{
// The backdrop is a fixed full-viewport layer; the stylesheet lifts the field above
// it through this class. Without it a click on the input or the chevron would land on
// the backdrop instead and close the panel. Only the class can be pinned here — the
// z-index itself is scoped CSS, verified visually.
var cut = RenderPicker();
Open(cut);
var field = cut.Find("[data-test='t-picker'] .input-group");
Assert.Contains("ip-field", field.ClassList);
Assert.Single(cut.FindAll("[data-test='t-backdrop']"));
}
/// <summary>Verifies that two pickers on the same page get distinct panel ids.</summary>
[Fact]
public void TwoPickers_GetDistinctPanelIds()
{
// The panel id (and the field's aria-controls pointing at it) is minted per component
// instance, so two pickers on one page cannot collide on a DOM id.
var first = RenderPicker();
var second = RenderPicker();
Open(first);
Open(second);
var firstPanel = first.Find("[data-test='t-panel']").Id;
var secondPanel = second.Find("[data-test='t-panel']").Id;
Assert.False(string.IsNullOrEmpty(firstPanel));
Assert.NotEqual(firstPanel, secondPanel);
Assert.Equal(firstPanel, first.Find("[data-test='t-input']").GetAttribute("aria-controls"));
Assert.Equal(secondPanel, second.Find("[data-test='t-input']").GetAttribute("aria-controls"));
}
/// <summary>Verifies that re-rendering with the same instances reference leaves the typed text and open panel unchanged.</summary>
[Fact]
public void SameInstancesReference_LeavesTheTypedTextAndOpenPanelAlone()
{
// Complement of SiteChange_NewInstancesList_ClearsAStaleSelectionText: an unrelated
// parameter change is NOT a site change, so a half-typed filter must survive it.
var cut = RenderPicker();
Open(cut);
Type(cut, "zu");
cut.Render(ps => ps.Add(p => p.Placeholder, "Pick an instance"));
Assert.Equal("zu", InputValue(cut));
Assert.Single(cut.FindAll("[data-test='t-panel']"));
Assert.Equal(new[] { "Zulu" }, Rows(cut));
}
// -- Keyboard activation on the tree itself --------------------------------
/// <summary>Verifies that keyboard activation on a tree row selects the instance.</summary>
[Fact]
public void KeyboardActivation_OnATreeRow_SelectsTheInstance()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
Press(cut, "ArrowDown");
// Areas render before root instances, so Zulu is the SECOND treeitem - the first
// one is the Line1 area (see KeyboardActivation_OnAnAreaRow_TogglesIt).
TreeItem(cut, 1).KeyDown(new KeyboardEventArgs { Key = "Enter" });
Assert.Equal(8, picked);
Assert.Empty(cut.FindAll("[data-test='t-panel']"));
Assert.Equal("Zulu", InputValue(cut));
}
/// <summary>Verifies that keyboard activation on an area row toggles it.</summary>
[Fact]
public void KeyboardActivation_OnAnAreaRow_TogglesIt()
{
int? picked = null;
var cut = RenderPicker(onChanged: id => picked = id);
Open(cut);
Press(cut, "ArrowDown");
TreeItem(cut, 0).KeyDown(new KeyboardEventArgs { Key = "Enter" });
// An area activates as an expand, not as a pick: its children appear and the
// panel stays open with no callback raised.
Assert.Contains("Cell A", Rows(cut));
Assert.Single(cut.FindAll("[data-test='t-panel']"));
Assert.Null(picked);
// The same key on the same row closes it again - activation is a toggle.
TreeItem(cut, 0).KeyDown(new KeyboardEventArgs { Key = "Enter" });
Assert.DoesNotContain("Cell A", Rows(cut));
}
/// <summary>Verifies that keyboard activation followed by a mouse click raises exactly one selection.</summary>
[Fact]
public void KeyboardActivation_AndMouseClick_RaiseExactlyOneSelection()
{
// The label click and Enter both run through TreeView's single selection path,
// so neither input mode may double-fire the callback.
var picks = new List<int>();
var cut = RenderPicker(onChanged: picks.Add);
Open(cut);
ClickRow(cut, "Zulu");
Assert.Equal(new[] { 8 }, picks);
Open(cut);
TreeItem(cut, 1).KeyDown(new KeyboardEventArgs { Key = "Enter" });
Assert.Equal(new[] { 8, 8 }, picks);
}
// -- Accessible name / labelling -------------------------------------------
/// <summary>Verifies that the input carries an accessible name by default and from the parameter.</summary>
[Fact]
public void Input_CarriesAnAccessibleName_ByDefaultAndFromTheParameter()
{
var cut = RenderPicker();
Assert.Equal("Instance", cut.Find("[data-test='t-input']").GetAttribute("aria-label"));
cut.Render(ps => ps.Add(p => p.AriaLabel, "Target instance"));
Assert.Equal("Target instance", cut.Find("[data-test='t-input']").GetAttribute("aria-label"));
}
/// <summary>Verifies that the input id defaults to a generated id and honours the parameter.</summary>
[Fact]
public void InputId_DefaultsToAGeneratedId_AndHonoursTheParameter()
{
// A page that wants its own <label for> supplies the id; two pickers left to the
// generated one must still not collide.
var first = RenderPicker();
var second = RenderPicker();
var generated = first.Find("[data-test='t-input']").Id;
Assert.False(string.IsNullOrEmpty(generated));
Assert.NotEqual(generated, second.Find("[data-test='t-input']").Id);
first.Render(ps => ps.Add(p => p.InputId, "flow-instance"));
Assert.Equal("flow-instance", first.Find("[data-test='t-input']").Id);
}
/// <summary>Verifies that aria-controls is rendered only while the panel is open.</summary>
[Fact]
public void AriaControls_IsRenderedOnlyWhileThePanelIsOpen()
{
var cut = RenderPicker();
// Closed: the panel element does not exist, so a dangling aria-controls would
// point screen readers at nothing.
Assert.False(cut.Find("[data-test='t-input']").HasAttribute("aria-controls"));
Open(cut);
Assert.Equal(
cut.Find("[data-test='t-panel']").Id,
cut.Find("[data-test='t-input']").GetAttribute("aria-controls"));
}
}