diff --git a/ILSpy.Tests/Input/HeadlessMmbPointerTests.cs b/ILSpy.Tests/Input/HeadlessMmbPointerTests.cs index d4d1a7d4c..d2c904f08 100644 --- a/ILSpy.Tests/Input/HeadlessMmbPointerTests.cs +++ b/ILSpy.Tests/Input/HeadlessMmbPointerTests.cs @@ -60,27 +60,12 @@ public class HeadlessMmbPointerTests var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); - - // Any realised SharpTreeViewItem whose DataContext is an ILSpyTreeNode is a valid click - // target — the exact node doesn't matter, only that the gesture pipeline fires. - await Waiters.WaitForAsync(() => grid.GetVisualDescendants().OfType() - .Any(r => r.DataContext is ILSpyTreeNode)); - var row = grid.GetVisualDescendants().OfType() - .First(r => r.DataContext is ILSpyTreeNode); + var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid); // Snapshot the tab count BEFORE the gesture so we can assert a strict +1 after. var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; int before = documents.VisibleDockables?.Count ?? 0; - // Translate the row's centre into TopLevel coordinates so MouseDown lands inside it. - var topLevel = TopLevel.GetTopLevel(row)!; - var rowBounds = row.Bounds; - // Tree rows stretch to content width (with horizontal scroll), so clamp the click X to the - // visible grid viewport — the row centre can sit off-screen past the grid's right edge. - var centreInRow = new Point(System.Math.Min(rowBounds.Width, grid.Bounds.Width) / 2, rowBounds.Height / 2); - var centreInTopLevel = row.TranslatePoint(centreInRow, topLevel) - ?? throw new System.InvalidOperationException("Row not in TopLevel visual tree"); - // Real pointer event — exercises bubble + handledEventsToo routing through SharpTreeView. topLevel.MouseDown(centreInTopLevel, MouseButton.Middle); topLevel.MouseUp(centreInTopLevel, MouseButton.Middle); @@ -109,23 +94,11 @@ public class HeadlessMmbPointerTests var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); - - await Waiters.WaitForAsync(() => grid.GetVisualDescendants().OfType() - .Any(r => r.DataContext is ILSpyTreeNode)); - var row = grid.GetVisualDescendants().OfType() - .First(r => r.DataContext is ILSpyTreeNode); + var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid); var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; int before = documents.VisibleDockables?.Count ?? 0; - var topLevel = TopLevel.GetTopLevel(row)!; - var rowBounds = row.Bounds; - // Tree rows stretch to content width (with horizontal scroll), so clamp the click X to the - // visible grid viewport — the row centre can sit off-screen past the grid's right edge. - var centreInRow = new Point(System.Math.Min(rowBounds.Width, grid.Bounds.Width) / 2, rowBounds.Height / 2); - var centreInTopLevel = row.TranslatePoint(centreInRow, topLevel) - ?? throw new System.InvalidOperationException("Row not in TopLevel visual tree"); - topLevel.MouseDown(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control); topLevel.MouseUp(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control); @@ -154,25 +127,11 @@ public class HeadlessMmbPointerTests var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); - // Any realised SharpTreeViewItem whose data is an ILSpyTreeNode is a fine target; the - // non-leaf assembly node row (depth 0) is always present and never confused with a - // method row whose tree-toggle area is null. - await Waiters.WaitForAsync(() => grid.GetVisualDescendants().OfType() - .Any(r => r.DataContext is ILSpyTreeNode)); - var row = grid.GetVisualDescendants().OfType() - .First(r => r.DataContext is ILSpyTreeNode); + var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid); var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; int before = documents.VisibleDockables?.Count ?? 0; - var topLevel = TopLevel.GetTopLevel(row)!; - var rowBounds = row.Bounds; - // Tree rows stretch to content width (with horizontal scroll), so clamp the click X to the - // visible grid viewport — the row centre can sit off-screen past the grid's right edge. - var centreInRow = new Point(System.Math.Min(rowBounds.Width, grid.Bounds.Width) / 2, rowBounds.Height / 2); - var centreInTopLevel = row.TranslatePoint(centreInRow, topLevel) - ?? throw new System.InvalidOperationException("Row not in TopLevel visual tree"); - // Drive two LMB clicks at the same coordinate — Avalonia.Headless interprets the // second click within the double-click threshold as a real double-click event. topLevel.MouseDown(centreInTopLevel, MouseButton.Left); @@ -188,4 +147,46 @@ public class HeadlessMmbPointerTests (documents.VisibleDockables?.Count ?? 0).Should().Be(before, "LMB double-click must not open a new tab — only MMB does"); } + + /// + /// Waits until an assembly-tree row is actually clickable and returns the TopLevel point that + /// lands on it. "Realised in the visual tree" is not enough: under layout churn (assemblies + /// still streaming into the tree) the virtualizing panel can hand out a container whose Bounds + /// are still empty, and a row can sit outside the grid's viewport - a pointer event aimed at + /// either reaches nothing, so the gesture silently does not happen. Hit-testing the candidate + /// point back to its own row before clicking rules both out. Which row is used does not matter, + /// only that the click lands on one whose node is an . + /// + static async Task<(TopLevel TopLevel, Point Point)> WaitForClickableRowAsync( + ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView grid) + { + Point point = default; + await Waiters.WaitForAsync(() => TryGetRowClickPoint(grid, out point), + description: "an assembly-tree row whose centre hit-tests back to that row"); + return (TopLevel.GetTopLevel(grid)!, point); + } + + static bool TryGetRowClickPoint(ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView grid, out Point point) + { + point = default; + if (TopLevel.GetTopLevel(grid) is not { } topLevel) + return false; + foreach (var row in grid.GetVisualDescendants().OfType()) + { + if (row.DataContext is not ILSpyTreeNode || row.Bounds.Width <= 0 || row.Bounds.Height <= 0) + continue; + // Tree rows stretch to content width (with horizontal scroll), so clamp the click X to the + // visible grid viewport - the row centre can sit off-screen past the grid's right edge. + var centreInRow = new Point(System.Math.Min(row.Bounds.Width, grid.Bounds.Width) / 2, row.Bounds.Height / 2); + if (row.TranslatePoint(centreInRow, topLevel) is not { } candidate) + continue; + if (topLevel.InputHitTest(candidate) is Visual hit + && ReferenceEquals(hit.FindAncestorOfType(includeSelf: true), row)) + { + point = candidate; + return true; + } + } + return false; + } }