diff --git a/ILSpy.Tests/Controls/SharpTreeViewTests.cs b/ILSpy.Tests/Controls/SharpTreeViewTests.cs index 80c860eb7..ba87155e1 100644 --- a/ILSpy.Tests/Controls/SharpTreeViewTests.cs +++ b/ILSpy.Tests/Controls/SharpTreeViewTests.cs @@ -151,4 +151,125 @@ public class SharpTreeViewTests c.IsSelected.Should().BeTrue(); a.IsSelected.Should().BeFalse("moving the selection clears the old node's flag"); } + + /// The number of rows the flattener should expose for a tree rooted in + /// when the root itself is not shown. + static int VisibleRowCount(SharpTreeNode root) + { + int count = 0; + void Walk(SharpTreeNode node) + { + foreach (SharpTreeNode child in node.Children) + { + if (child.IsHidden) + continue; + count++; + if (child.IsExpanded) + Walk(child); + } + } + Walk(root); + return count; + } + + static void AssertEveryRowResolves(SharpTreeView tree, SharpTreeNode root, string because) + { + tree.UpdateLayout(); + Dispatcher.UIThread.RunJobs(); + int expected = VisibleRowCount(root); + tree.ItemCount.Should().Be(expected, because); + for (int i = 0; i < expected; i++) + tree.ItemsView[i].Should().NotBeNull($"row {i} must resolve after {because}"); + } + + /// + /// Collapsing and removing nodes above a scrolled viewport shrinks the flattened list under + /// the virtualizing panel's realized index range. The panel must not be left indexing rows + /// that no longer exist. + /// + [AvaloniaTest] + public void Shrinking_The_Tree_Above_A_Scrolled_Viewport_Keeps_Every_Row_Resolvable() + { + var groups = Enumerable.Range(0, 200) + .Select(i => new TestNode($"g{i}", Enumerable.Range(0, 5) + .Select(j => new TestNode($"g{i}_{j}")).ToArray())) + .ToArray(); + var root = new TestNode("root", groups); + var tree = new SharpTreeView { ShowRoot = false, Root = root }; + var window = new Window { Content = tree, Width = 300, Height = 400 }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + + foreach (var group in groups) + group.IsExpanded = true; + AssertEveryRowResolves(tree, root, "expanding every group"); + tree.GetRealizedContainers().Count().Should() + .BeLessThan(tree.ItemCount, "the panel must be virtualizing, or this test proves nothing"); + + // Park the realized range far from index 0, with a live selection inside it. + tree.SelectedItem = groups[^1].Children[^1]; + tree.ScrollIntoView(tree.ItemCount - 1); + AssertEveryRowResolves(tree, root, "scrolling to the last row"); + + for (int i = 0; i < 190; i++) + groups[i].IsExpanded = false; + AssertEveryRowResolves(tree, root, "collapsing 190 groups above the viewport"); + + for (int i = 0; i < 150; i++) + root.Children.RemoveAt(0); + AssertEveryRowResolves(tree, root, "removing 150 groups above the viewport"); + + tree.ScrollIntoView(0); + AssertEveryRowResolves(tree, root, "scrolling back to the top"); + } + + /// + /// A lazily loaded node replaces its placeholder with real children while the tree is + /// scrolled: the row count grows and shrinks in the same gesture. + /// + [AvaloniaTest] + public void Lazy_Loading_Under_A_Scrolled_Viewport_Keeps_Every_Row_Resolvable() + { + var lazy = Enumerable.Range(0, 100).Select(i => new LazyNode($"l{i}", 7)).ToArray(); + var root = new TestNode("root"); + foreach (var node in lazy) + root.Children.Add(node); + var tree = new SharpTreeView { ShowRoot = false, Root = root }; + var window = new Window { Content = tree, Width = 300, Height = 400 }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + + AssertEveryRowResolves(tree, root, "the initial collapsed list"); + + tree.ScrollIntoView(tree.ItemCount - 1); + AssertEveryRowResolves(tree, root, "scrolling to the last row"); + + foreach (var node in lazy) + { + node.IsExpanded = true; + AssertEveryRowResolves(tree, root, $"lazily expanding {node.Text}"); + } + + foreach (var node in lazy) + node.ReloadChildren(); + AssertEveryRowResolves(tree, root, "reloading every lazy subtree in place"); + } + + sealed class LazyNode : SharpTreeNode + { + readonly string text; + readonly int childCount; + public LazyNode(string text, int childCount) + { + this.text = text; + this.childCount = childCount; + LazyLoading = true; + } + public override object Text => text; + protected override void LoadChildren() + { + for (int i = 0; i < childCount; i++) + Children.Add(new TestNode($"{text}_{i}")); + } + } }