test(ui): add external filtering tests for TreeView (R8)

This commit is contained in:
Joseph Doherty
2026-03-23 02:35:39 -04:00
parent 4e5b5facec
commit 08d511f609
2 changed files with 133 additions and 0 deletions

View File

@@ -104,6 +104,12 @@ else
ApplyInitialExpansion(_items); ApplyInitialExpansion(_items);
} }
} }
// Clear selection if the selected key no longer exists in the current items tree
if (Selectable && SelectedKey != null && _items is not null && !KeyExistsInTree(_items, SelectedKey))
{
_ = SelectedKeyChanged.InvokeAsync(null);
}
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -133,6 +139,20 @@ else
} }
} }
private bool KeyExistsInTree(IReadOnlyList<TItem> items, object key)
{
foreach (var item in items)
{
if (key.Equals(KeySelector(item)))
return true;
var children = ChildrenSelector(item);
if (children is { Count: > 0 } && KeyExistsInTree(children, key))
return true;
}
return false;
}
private void ApplyInitialExpansion(IReadOnlyList<TItem> items) private void ApplyInitialExpansion(IReadOnlyList<TItem> items)
{ {
foreach (var item in items) foreach (var item in items)

View File

@@ -464,6 +464,119 @@ public class TreeViewTests : BunitContext
Assert.Equal(2, labels.Count); Assert.Equal(2, labels.Count);
} }
// ── External filtering tests (R8) ──────────────────────────────────
[Fact]
public void Filtering_ReducedItems_HidesRemovedRoots()
{
var fullItems = SimpleRoots();
var cut = RenderTreeView(items: fullItems);
// Both roots visible
var labels = cut.FindAll(".node-label");
Assert.Equal(2, labels.Count);
// Re-render with only Alpha (Beta removed)
var alphaOnly = new List<TestNode> { fullItems[0] };
cut.Render(parameters =>
{
parameters
.Add(p => p.Items, alphaOnly)
.Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
.Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
.Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
.Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
{
builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
}));
});
labels = cut.FindAll(".node-label");
Assert.Single(labels);
Assert.Equal("Alpha", labels[0].TextContent);
}
[Fact]
public void Filtering_ExpansionStatePreserved()
{
var fullItems = SimpleRoots();
var cut = RenderTreeView(items: fullItems);
// Expand Alpha
cut.Find(".tv-toggle").Click();
Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");
// Re-render with only Alpha
var alphaOnly = new List<TestNode> { fullItems[0] };
cut.Render(parameters =>
{
parameters
.Add(p => p.Items, alphaOnly)
.Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
.Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
.Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
.Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
{
builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
}));
});
// Alpha-1 still visible (expansion state preserved)
Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");
// Re-render with full list again
cut.Render(parameters =>
{
parameters
.Add(p => p.Items, fullItems)
.Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
.Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
.Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
.Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
{
builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
}));
});
// Alpha-1 still visible after restoration
Assert.Contains(cut.FindAll(".node-label"), l => l.TextContent == "Alpha-1");
}
[Fact]
public void Filtering_SelectionCleared_WhenNodeDisappears()
{
var fullItems = SimpleRoots();
object? lastSelected = "b"; // track the last value passed to callback
var cut = RenderTreeView(
items: fullItems,
selectable: true,
selectedKey: "b",
onSelectedKeyChanged: k => lastSelected = k);
// Re-render with only Alpha (Beta disappears)
var alphaOnly = new List<TestNode> { fullItems[0] };
cut.Render(parameters =>
{
parameters
.Add(p => p.Items, alphaOnly)
.Add(p => p.ChildrenSelector, (Func<TestNode, IReadOnlyList<TestNode>>)(n => n.Children))
.Add(p => p.HasChildrenSelector, (Func<TestNode, bool>)(n => n.Children.Count > 0))
.Add(p => p.KeySelector, (Func<TestNode, object>)(n => n.Key))
.Add(p => p.NodeContent, (RenderFragment<TestNode>)(node => builder =>
{
builder.AddMarkupContent(0, $"<span class=\"node-label\">{node.Label}</span>");
}))
.Add(p => p.Selectable, true)
.Add(p => p.SelectedKey, (object?)"b")
.Add(p => p.SelectedKeyChanged, (Action<object?>)(k => lastSelected = k));
});
// SelectedKeyChanged should have been called with null
Assert.Null(lastSelected);
}
// ── Context menu tests ──────────────────────────────────────────────
[Fact] [Fact]
public void ContextMenu_Null_NoMenuRendered() public void ContextMenu_Null_NoMenuRendered()
{ {