Browse Source

Aim the tree-gesture pointer tests at a hit-testable row

Middle_Click_On_An_Assembly_Tree_Row_Opens_A_New_Decompiler_Tab timed out
after its full 60s window on the macOS CI runner. The tests picked the first
SharpTreeViewItem present in the visual tree and clicked its centre, but a
container realised by the virtualizing panel is not necessarily arranged yet,
and a row can sit outside the grid's viewport - on a loaded runner, where
assemblies are still streaming into the tree, the click landed on nothing and
the gesture never happened. The two negative tests shared the same click-point
computation and would have passed vacuously in that state, so a missed click
was only ever visible on the positive one.

Hit-testing the candidate point back to its own row before clicking rules both
out, and fails with a description instead of an unexplained timeout if no row
is ever reachable.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4010/head
Christoph Wille 1 month ago
parent
commit
a134c8b28d
  1. 89
      ILSpy.Tests/Input/HeadlessMmbPointerTests.cs

89
ILSpy.Tests/Input/HeadlessMmbPointerTests.cs

@ -60,27 +60,12 @@ public class HeadlessMmbPointerTests
var pane = await window.WaitForComponent<AssemblyListPane>(); var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>(); var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>();
var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid);
// 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<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.Any(r => r.DataContext is ILSpyTreeNode));
var row = grid.GetVisualDescendants().OfType<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.First(r => r.DataContext is ILSpyTreeNode);
// Snapshot the tab count BEFORE the gesture so we can assert a strict +1 after. // Snapshot the tab count BEFORE the gesture so we can assert a strict +1 after.
var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!;
int before = documents.VisibleDockables?.Count ?? 0; 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. // Real pointer event — exercises bubble + handledEventsToo routing through SharpTreeView.
topLevel.MouseDown(centreInTopLevel, MouseButton.Middle); topLevel.MouseDown(centreInTopLevel, MouseButton.Middle);
topLevel.MouseUp(centreInTopLevel, MouseButton.Middle); topLevel.MouseUp(centreInTopLevel, MouseButton.Middle);
@ -109,23 +94,11 @@ public class HeadlessMmbPointerTests
var pane = await window.WaitForComponent<AssemblyListPane>(); var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>(); var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>();
var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid);
await Waiters.WaitForAsync(() => grid.GetVisualDescendants().OfType<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.Any(r => r.DataContext is ILSpyTreeNode));
var row = grid.GetVisualDescendants().OfType<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.First(r => r.DataContext is ILSpyTreeNode);
var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!;
int before = documents.VisibleDockables?.Count ?? 0; 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.MouseDown(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control);
topLevel.MouseUp(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control); topLevel.MouseUp(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control);
@ -154,25 +127,11 @@ public class HeadlessMmbPointerTests
var pane = await window.WaitForComponent<AssemblyListPane>(); var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>(); var grid = await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>();
// Any realised SharpTreeViewItem whose data is an ILSpyTreeNode is a fine target; the var (topLevel, centreInTopLevel) = await WaitForClickableRowAsync(grid);
// 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<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.Any(r => r.DataContext is ILSpyTreeNode));
var row = grid.GetVisualDescendants().OfType<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>()
.First(r => r.DataContext is ILSpyTreeNode);
var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!; var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!;
int before = documents.VisibleDockables?.Count ?? 0; 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 // 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. // second click within the double-click threshold as a real double-click event.
topLevel.MouseDown(centreInTopLevel, MouseButton.Left); topLevel.MouseDown(centreInTopLevel, MouseButton.Left);
@ -188,4 +147,46 @@ public class HeadlessMmbPointerTests
(documents.VisibleDockables?.Count ?? 0).Should().Be(before, (documents.VisibleDockables?.Count ?? 0).Should().Be(before,
"LMB double-click must not open a new tab — only MMB does"); "LMB double-click must not open a new tab — only MMB does");
} }
/// <summary>
/// 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 <see cref="ILSpyTreeNode"/>.
/// </summary>
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<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>())
{
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<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem>(includeSelf: true), row))
{
point = candidate;
return true;
}
}
return false;
}
} }

Loading…
Cancel
Save