diff --git a/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs b/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs index 562c3f1f8..1072bb5a9 100644 --- a/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs +++ b/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs @@ -252,20 +252,41 @@ public class DecompileInNewViewTests var menu = grid.ContextMenu!; - async Task RightClick(SharpTreeNode node) + Point? ClickPoint(SharpTreeNode node) { - var row = Row(node); + var row = grid.GetVisualDescendants() + .OfType() + .FirstOrDefault(r => RowNodeEquals(r, node)); + if (row == null) + return null; var clickX = System.Math.Min(row.Bounds.Width, grid.Bounds.Width) / 2; - var pt = row.TranslatePoint(new Point(clickX, row.Bounds.Height / 2), window); - // A popup's light-dismiss overlay keeps answering hit tests for as long as the frame - // that still shows it, and it swallows a press without raising ContextRequested - so - // nothing becomes the context target. Hit testing the point is the same question the - // context-request handler asks, so waiting for it to reach the row is exactly the - // precondition for this click, however many frames the overlay takes to disappear. - await Waiters.WaitForAsync( - () => window.InputHitTest(pt!.Value) is Visual hit - && ReferenceEquals(hit.FindAncestorOfType(includeSelf: true), row), - description: "the row to answer hit tests at the point about to be right-clicked"); + return row.TranslatePoint(new Point(clickX, row.Bounds.Height / 2), window); + } + + async Task RightClick(SharpTreeNode node) + { + // A press only becomes a context request for the row when the hit test at the press + // point answers with that row - the same question OnTreeContextRequested asks - and it + // takes an unknown number of frames for that to hold: a closed popup's light-dismiss + // overlay keeps answering hit tests until the scene is rendered again, and assemblies + // still loading in the background reshuffle the rows, which re-realises containers and + // shifts them. So the row container and the point are resolved afresh on every poll and + // the hit is matched by node, not by container identity. + Point? pt = null; + Visual? lastHit = null; + try + { + await Waiters.WaitForAsync( + () => (pt = ClickPoint(node)) is { } p + && (lastHit = window.InputHitTest(p) as Visual) is { } hit + && hit.FindAncestorOfType(includeSelf: true) is { } hitRow + && RowNodeEquals(hitRow, node), + description: "the row to answer hit tests at the point about to be right-clicked"); + } + catch (System.TimeoutException ex) + { + throw new System.TimeoutException($"{ex.Message} (point: {pt?.ToString() ?? "row not realised"}, hit: {lastHit?.GetType().Name ?? "nothing"})", ex); + } HeadlessWindowExtensions.MouseDown(window, pt!.Value, MouseButton.Right); HeadlessWindowExtensions.MouseUp(window, pt.Value, MouseButton.Right); // The highlight is scoped to the popup - set while the menu is being requested, dropped