feat(ui): add TreeView<TItem> component with core rendering, expand/collapse, ARIA (R1-R4, R14)
This commit is contained in:
241
tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs
Normal file
241
tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using ScadaLink.CentralUI.Components.Shared;
|
||||
|
||||
namespace ScadaLink.CentralUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// bUnit tests for the TreeView component covering core rendering,
|
||||
/// expand/collapse behavior, ARIA attributes, and indentation.
|
||||
/// </summary>
|
||||
public class TreeViewTests : BunitContext
|
||||
{
|
||||
private record TestNode(string Key, string Label, List<TestNode> Children);
|
||||
|
||||
private static List<TestNode> SimpleRoots() => new()
|
||||
{
|
||||
new("a", "Alpha", new()
|
||||
{
|
||||
new("a1", "Alpha-1", new()),
|
||||
new("a2", "Alpha-2", new()
|
||||
{
|
||||
new("a2x", "Alpha-2-X", new())
|
||||
})
|
||||
}),
|
||||
new("b", "Beta", new()),
|
||||
};
|
||||
|
||||
private IRenderedComponent<TreeView<TestNode>> RenderTreeView(
|
||||
List<TestNode>? items = null,
|
||||
RenderFragment? emptyContent = null,
|
||||
int indentPx = 24,
|
||||
Func<TestNode, bool>? initiallyExpanded = null)
|
||||
{
|
||||
return Render<TreeView<TestNode>>(parameters => parameters
|
||||
.Add(p => p.Items, items ?? SimpleRoots())
|
||||
.Add(p => p.ChildrenSelector, n => n.Children)
|
||||
.Add(p => p.HasChildrenSelector, n => n.Children.Count > 0)
|
||||
.Add(p => p.KeySelector, n => n.Key)
|
||||
.Add(p => p.NodeContent, node => builder =>
|
||||
{
|
||||
builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
|
||||
})
|
||||
.Add(p => p.IndentPx, indentPx)
|
||||
.Add(p => p.EmptyContent, emptyContent)
|
||||
.Add(p => p.InitiallyExpanded, initiallyExpanded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersRootLevelItems_WithCorrectLabels()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
var labels = cut.FindAll(".node-label");
|
||||
// Only root-level items visible (children collapsed)
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.Equal("Alpha", labels[0].TextContent);
|
||||
Assert.Equal("Beta", labels[1].TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersEmptyContent_WhenItemsEmpty()
|
||||
{
|
||||
var cut = RenderTreeView(
|
||||
items: new List<TestNode>(),
|
||||
emptyContent: builder =>
|
||||
{
|
||||
builder.AddMarkupContent(0, "<p class=\"empty-msg\">Nothing here</p>");
|
||||
});
|
||||
|
||||
var msg = cut.Find(".empty-msg");
|
||||
Assert.Equal("Nothing here", msg.TextContent);
|
||||
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("ul[role='tree']"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeafNodes_HaveNoToggle()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Beta is a leaf (index 1 in the li list)
|
||||
var treeItems = cut.FindAll("li[role='treeitem']");
|
||||
var betaLi = treeItems[1]; // Beta is second root
|
||||
Assert.Throws<Bunit.ElementNotFoundException>(() => betaLi.QuerySelector(".tv-toggle")
|
||||
?? throw new Bunit.ElementNotFoundException(".tv-toggle"));
|
||||
// Should have spacer instead
|
||||
Assert.NotNull(betaLi.QuerySelector(".tv-spacer"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BranchNodes_ShowCollapsedToggle()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
var alphaLi = cut.FindAll("li[role='treeitem']")[0];
|
||||
Assert.Equal("false", alphaLi.GetAttribute("aria-expanded"));
|
||||
var toggle = alphaLi.QuerySelector(".tv-toggle");
|
||||
Assert.NotNull(toggle);
|
||||
Assert.Equal("+", toggle!.TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollapsedBranch_ChildrenNotInDom()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Alpha is collapsed by default, children should not be in DOM
|
||||
var groups = cut.FindAll("ul[role='group']");
|
||||
Assert.Empty(groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClickToggle_ExpandsNode_ShowsChildren()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Click Alpha's toggle
|
||||
var toggle = cut.Find(".tv-toggle");
|
||||
toggle.Click();
|
||||
|
||||
// Alpha should now be expanded
|
||||
var alphaLi = cut.FindAll("li[role='treeitem']")[0];
|
||||
Assert.Equal("true", alphaLi.GetAttribute("aria-expanded"));
|
||||
|
||||
// Children should appear
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-1");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClickExpandedToggle_Collapses_HidesChildren()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Expand Alpha
|
||||
var toggle = cut.Find(".tv-toggle");
|
||||
toggle.Click();
|
||||
|
||||
// Verify children visible
|
||||
Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");
|
||||
|
||||
// Collapse Alpha - find the toggle again (DOM changed)
|
||||
var toggleAgain = cut.Find(".tv-toggle");
|
||||
toggleAgain.Click();
|
||||
|
||||
// Children gone
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
|
||||
Assert.Empty(cut.FindAll("ul[role='group']"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeepNesting_ExpandParentThenChild_ShowsGrandchildren()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Expand Alpha
|
||||
cut.Find(".tv-toggle").Click();
|
||||
|
||||
// Now find Alpha-2's toggle (Alpha-2 is a branch)
|
||||
var toggles = cut.FindAll(".tv-toggle");
|
||||
// toggles[0] = Alpha (now expanded, shows minus), toggles[1] = Alpha-2
|
||||
Assert.True(toggles.Count >= 2);
|
||||
toggles[1].Click();
|
||||
|
||||
// Alpha-2-X should be visible
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitiallyExpanded_ExpandsMatchingNodes()
|
||||
{
|
||||
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a" || n.Key == "a2");
|
||||
|
||||
// Alpha and Alpha-2 should be expanded, so Alpha-2-X should be visible
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-1");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootUl_HasRoleTree()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
var rootUl = cut.Find("ul[role='tree']");
|
||||
Assert.NotNull(rootUl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NodeLi_HasRoleTreeitem()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
var items = cut.FindAll("li[role='treeitem']");
|
||||
Assert.Equal(2, items.Count); // Two root nodes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandedBranch_HasAriaExpandedTrue()
|
||||
{
|
||||
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
||||
|
||||
var alphaLi = cut.FindAll("li[role='treeitem']")[0];
|
||||
Assert.Equal("true", alphaLi.GetAttribute("aria-expanded"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChildGroup_HasRoleGroup()
|
||||
{
|
||||
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
||||
|
||||
var groups = cut.FindAll("ul[role='group']");
|
||||
Assert.Single(groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Children_IndentedByIndentPxPerDepth()
|
||||
{
|
||||
var cut = RenderTreeView(indentPx: 30, initiallyExpanded: n => n.Key == "a" || n.Key == "a2");
|
||||
|
||||
var rows = cut.FindAll(".tv-row");
|
||||
// Root nodes at depth 0: padding-left: 0px
|
||||
// Children at depth 1: padding-left: 30px
|
||||
// Grandchildren at depth 2: padding-left: 60px
|
||||
|
||||
// Find Alpha row (depth 0)
|
||||
var alphaRow = rows[0];
|
||||
Assert.Contains("padding-left: 0px", alphaRow.GetAttribute("style"));
|
||||
|
||||
// Find Alpha-1 row (depth 1)
|
||||
var alpha1Row = rows[1];
|
||||
Assert.Contains("padding-left: 30px", alpha1Row.GetAttribute("style"));
|
||||
|
||||
// Find Alpha-2-X row (depth 2) - it's after Alpha-2 at index 3
|
||||
var alpha2xRow = rows[3];
|
||||
Assert.Contains("padding-left: 60px", alpha2xRow.GetAttribute("style"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user