a33a2f4456
ArrowDown moved focus into the tree, but the picker passed no Selectable /
SelectedKeyChanged, so Enter/Space on a treeitem only moved the roving tabindex —
the pick lived on the label span's @onclick, i.e. mouse only. The tree now runs in
Selectable mode and BOTH gestures arrive at one handler (OnTreeKeyActivatedAsync)
through TreeView's SelectedKeyChanged: an `i:{id}` key selects that instance, an
`a:{id}` key toggles the branch. The label's own @onclick is gone — the click
already bubbles into TreeView's content span, so a second handler there would fire
the callback twice. SelectedKey stays unbound (a combobox shows its pick in the
field, not as a row highlight, and binding it would arm TreeView's
selected-key-not-in-tree reset on every render while the pick is filtered out);
SelectedCssClass is emptied to match.
a11y: the field gains an accessible name via a new AriaLabel parameter (default
"Instance") and an optional InputId so a page can point a <label for> at it;
aria-controls is rendered only while the panel exists, instead of always pointing
at an id that is absent when closed.
Also drops the unused @using of Commons.Entities.Instances from the .razor.
Tests: keyboard activation on an instance row and on an area row, a
one-selection-per-gesture guard across mouse and keyboard, and the labelling
assertions. CentralUI suite 1331 passed / 0 failed; build 0 warnings.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
560 lines
19 KiB
C#
560 lines
19 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
|
|
{
|
|
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];
|
|
|
|
[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));
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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']"));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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']"));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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"));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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']"));
|
|
}
|
|
|
|
[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"));
|
|
}
|
|
|
|
[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']"));
|
|
}
|
|
|
|
[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"));
|
|
}
|
|
|
|
[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 --------------------------------
|
|
|
|
[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));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
|
|
[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 -------------------------------------------
|
|
|
|
[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"));
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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"));
|
|
}
|
|
}
|