From 1b78c762150474939d87d48813eef35afd018195 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 30 Aug 2026 09:20:59 +0200 Subject: [PATCH] Synchronize headless UI tests on idle and hit-tested clicks The headless UI tests synchronized with the application by pumping a fixed number of frames (39 loops of RunJobs/Delay across 19 files) and by pressing at a point computed once from a control's bounds. Both encode how fast the machine that wrote the test was: on the loaded Windows Debug CI agent the frame count comes up short and the point goes stale, which is the recurring timeout in the tree context-menu tests and the reason each such failure was repaired one test at a time. Waiters.WaitForIdleAsync replaces the frame loops. It observes the actual precondition - no dispatcher job queued at Background priority or above, no assembly still loading in the background sweep, a frame rendered - and requires it on two consecutive polls so a thread-pool continuation about to post back is caught as well. Window.ClickAsync replaces element-targeted MouseDown/MouseUp pairs. It re-resolves the target on every poll and presses only once the window's hit test at the click point answers with that target, reporting the point and what was hit instead on timeout. That diagnostic exposed one vacuous test: User_Click_On_Visible_Row_Does_Not_Recentre_Viewport clicked the centre of a row wider than the tree viewport, which lies under the decompiler text view, so its assertion held without the row ever being clicked. It now clamps the point to the viewport like the other tree-row clicks. Clicks at text positions and press-only gutter clicks stay raw; they do not target an element. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Analyzers/AnalyzerTreeKeyboardTests.cs | 7 +- .../AssemblyTreeExpanderHitboxTests.cs | 5 +- ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs | 63 ++------- .../Bookmarks/BookmarkContextMenuTests.cs | 12 +- ILSpy.Tests/Bookmarks/BookmarkGutterTests.cs | 24 +--- .../Bookmarks/BookmarkNavigationViewTests.cs | 12 +- .../ContextMenus/DecompileInNewViewTests.cs | 125 +++--------------- .../KeyboardContextMenuFocusTests.cs | 19 +-- .../ReferenceScopeAndNewTabTests.cs | 6 +- ILSpy.Tests/Controls/OmnibarSettingTests.cs | 12 +- .../Docking/DocumentTabStripModeTests.cs | 20 +-- ILSpy.Tests/Docking/MultiRowTabStripTests.cs | 7 +- ILSpy.Tests/Docking/RunInNewTabTests.cs | 6 +- ILSpy.Tests/Editor/DecompilerViewTests.cs | 6 +- ILSpy.Tests/Editor/DocumentationLinkTests.cs | 5 +- ILSpy.Tests/Editor/FoldingContextMenuTests.cs | 12 +- .../MetadataFilterRowEndToEndTests.cs | 7 +- ILSpy.Tests/Options/OptionsTabTests.cs | 3 +- ILSpy.Tests/Search/SearchPaneNicetiesTests.cs | 12 +- ILSpy.Tests/Waiters.cs | 37 ++++++ ILSpy.Tests/WindowExtensions.cs | 53 ++++++++ 21 files changed, 158 insertions(+), 295 deletions(-) diff --git a/ILSpy.Tests/Analyzers/AnalyzerTreeKeyboardTests.cs b/ILSpy.Tests/Analyzers/AnalyzerTreeKeyboardTests.cs index 7b34d4ed4..7cc914db1 100644 --- a/ILSpy.Tests/Analyzers/AnalyzerTreeKeyboardTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzerTreeKeyboardTests.cs @@ -154,12 +154,7 @@ public class AnalyzerTreeKeyboardTests var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); vm.AssemblyTreeModel.SelectNode(typeNode); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - tree.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); tree.Focus(); Dispatcher.UIThread.RunJobs(); diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs index 90df93d69..fa3f855e4 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs @@ -81,10 +81,7 @@ public class AssemblyTreeExpanderHitboxTests // outside the centred glyph at ~y=3.5..12.5) collapses the node. This proves the grown // area is genuinely hittable, not just larger in layout. assemblyNode.IsExpanded.Should().BeTrue("precondition: node is expanded before the click"); - var hitPoint = expander.TranslatePoint(new Point(expander.Bounds.Width / 2, 14), window); - hitPoint.Should().NotBeNull(); - HeadlessWindowExtensions.MouseDown(window, hitPoint!.Value, MouseButton.Left); - HeadlessWindowExtensions.MouseUp(window, hitPoint.Value, MouseButton.Left); + await window.ClickAsync(() => expander, pointInTarget: e => new Point(e.Bounds.Width / 2, 14)); TestCapture.Step("clicked-enlarged-expander-area"); await Waiters.WaitForAsync(() => !assemblyNode.IsExpanded, diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 7c8daeb90..0dac02119 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -634,11 +634,7 @@ public class AssemblyTreeTests vm.AssemblyTreeModel.SelectNode(enumerable); await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, enumerable)); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); grid.UpdateLayout(); var scrollViewer = await grid.WaitForComponent(); @@ -665,19 +661,13 @@ public class AssemblyTreeTests var offsetBefore = scrollViewer.Offset.Y; // Act — real pointer click. Setting SelectedItem programmatically would fire DataGrid's - // internal ScrollIntoView too, which a real user click does not. - var rowCentre = candidateRow!.TranslatePoint( - new Point(candidateRow.Bounds.Width / 2, candidateRow.Bounds.Height / 2), - window)!.Value; - global::Avalonia.Headless.HeadlessWindowExtensions.MouseDown(window, rowCentre, global::Avalonia.Input.MouseButton.Left); - global::Avalonia.Headless.HeadlessWindowExtensions.MouseUp(window, rowCentre, global::Avalonia.Input.MouseButton.Left); + // internal ScrollIntoView too, which a real user click does not. Tree rows stretch to + // content width, so clamp X to the visible grid viewport. + await window.ClickAsync(() => candidateRow, + pointInTarget: r => new Point(System.Math.Min(r.Bounds.Width, grid.Bounds.Width) / 2, r.Bounds.Height / 2)); TestCapture.Step("visible-row-clicked"); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); // Assert — viewport offset is unchanged (within 1px tolerance for layout jitter). scrollViewer.Offset.Y.Should().BeApproximately(offsetBefore, 1.0, @@ -711,11 +701,7 @@ public class AssemblyTreeTests vm.AssemblyTreeModel.SelectNode(enumerable); await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, enumerable)); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); grid.UpdateLayout(); var scrollViewer = await grid.WaitForComponent(); @@ -742,11 +728,7 @@ public class AssemblyTreeTests // Act — model-driven selection (the open-in-new-tab path), NOT a mouse click. vm.AssemblyTreeModel.SelectNode(candidateNode); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); scrollViewer.Offset.Y.Should().BeApproximately(offsetBefore, 1.0, "selecting an already-visible row via the model (e.g. Decompile to new tab) must not move the viewport"); @@ -780,11 +762,7 @@ public class AssemblyTreeTests // Select + reveal the type, then let it settle on screen. vm.AssemblyTreeModel.SelectNode(enumerable); await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, enumerable)); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); grid.UpdateLayout(); (scrollViewer.Extent.Height - scrollViewer.Viewport.Height).Should().BeGreaterThan(50, @@ -795,11 +773,7 @@ public class AssemblyTreeTests // rows above the selection and pushing it off-screen. var coreLib = vm.AssemblyTreeModel.FindNode(typeof(object).Assembly.GetName().Name!); coreLib.IsExpanded = true; - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); grid.UpdateLayout(); // Assert: the app did not chase the selection. The expand reveals the opened node's children; @@ -1774,14 +1748,9 @@ public class AssemblyTreeTests Assert.That(targetNode, Is.Not.Null, "the clicked row must wrap a tree node"); // Tree rows stretch to content width (with horizontal scroll), so a row can be wider than // the grid viewport. Click within the visible viewport, not at the (off-screen) row centre. - var clickX = System.Math.Min(targetRow.Bounds.Width, grid.Bounds.Width) / 2; - var rowCentre = targetRow.TranslatePoint( - new Point(clickX, targetRow.Bounds.Height / 2), window)!.Value; - HeadlessWindowExtensions.MouseDown(window, rowCentre, MouseButton.Left); - HeadlessWindowExtensions.MouseUp(window, rowCentre, MouseButton.Left); - Dispatcher.UIThread.RunJobs(); - await Task.Delay(50); - Dispatcher.UIThread.RunJobs(); + await window.ClickAsync(() => targetRow, + pointInTarget: r => new Point(System.Math.Min(r.Bounds.Width, grid.Bounds.Width) / 2, r.Bounds.Height / 2)); + await Waiters.WaitForIdleAsync(); TestCapture.Step("plain-click-collapsed-selection"); // Assert — selection collapsed to exactly the clicked row, in both grid and model. @@ -1822,11 +1791,7 @@ public class AssemblyTreeTests // Act -- run Load Dependencies on the System.Net.Http node. await vm.AssemblyTreeModel.LoadDependenciesAsync(new SharpTreeNode[] { httpNode }); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); // Assert -- references were resolved AND survive in the list as auto-loaded entries. var added = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() diff --git a/ILSpy.Tests/Bookmarks/BookmarkContextMenuTests.cs b/ILSpy.Tests/Bookmarks/BookmarkContextMenuTests.cs index 1acc4430f..23429a3fb 100644 --- a/ILSpy.Tests/Bookmarks/BookmarkContextMenuTests.cs +++ b/ILSpy.Tests/Bookmarks/BookmarkContextMenuTests.cs @@ -54,11 +54,7 @@ public class BookmarkContextMenuTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); var bookmarkableLines = Enumerable.Range(1, view.Editor.Document.LineCount) .Where(view.CanToggleBookmarkAtLine) @@ -94,11 +90,7 @@ public class BookmarkContextMenuTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); int line = Enumerable.Range(1, view.Editor.Document.LineCount) .First(view.CanToggleBookmarkAtLine); diff --git a/ILSpy.Tests/Bookmarks/BookmarkGutterTests.cs b/ILSpy.Tests/Bookmarks/BookmarkGutterTests.cs index d73f845bc..64f2dedb6 100644 --- a/ILSpy.Tests/Bookmarks/BookmarkGutterTests.cs +++ b/ILSpy.Tests/Bookmarks/BookmarkGutterTests.cs @@ -57,11 +57,7 @@ public class BookmarkGutterTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); window.UpdateLayout(); var margin = view.Editor.TextArea.LeftMargins.OfType().Single(); @@ -107,11 +103,7 @@ public class BookmarkGutterTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); window.UpdateLayout(); var margin = view.Editor.TextArea.LeftMargins.OfType().Single(); @@ -150,11 +142,7 @@ public class BookmarkGutterTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); window.UpdateLayout(); var margin = view.Editor.TextArea.LeftMargins.OfType().Single(); @@ -231,11 +219,7 @@ public class BookmarkGutterTests vm.AssemblyTreeModel.SelectNode(node); await vm.DockWorkspace.WaitForDecompiledTextAsync(); var shown = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); return shown; } diff --git a/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs b/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs index 52e147f18..74dc304ad 100644 --- a/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs +++ b/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs @@ -59,11 +59,7 @@ public class BookmarkNavigationViewTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); var view = await window.WaitForComponent(); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); int bookmarkLine = Enumerable.Range(1, view.Editor.Document.LineCount) .Where(view.CanToggleBookmarkAtLine) @@ -232,10 +228,6 @@ public class BookmarkNavigationViewTests static async Task PumpLayoutAsync() { - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); } } diff --git a/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs b/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs index 1072bb5a9..dd2cbaa50 100644 --- a/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs +++ b/ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs @@ -181,12 +181,7 @@ public class DecompileInNewViewTests vm.AssemblyTreeModel.SelectNode(nodeA); // Let the top-level rows realise and layout settle. - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); var rowB = grid.GetVisualDescendants().OfType() .FirstOrDefault(r => RowNodeEquals(r, nodeB)); @@ -200,16 +195,9 @@ public class DecompileInNewViewTests // Right-click the centre of B's row (clear of the far-left expander glyph). Tree rows // stretch to content width, so clamp X to the visible grid viewport. - var clickX = System.Math.Min(rowB!.Bounds.Width, grid.Bounds.Width) / 2; - var point = rowB.TranslatePoint(new Point(clickX, rowB.Bounds.Height / 2), window); - point.Should().NotBeNull(); - HeadlessWindowExtensions.MouseDown(window, point!.Value, MouseButton.Right); - HeadlessWindowExtensions.MouseUp(window, point.Value, MouseButton.Right); - for (int i = 0; i < 4; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(20); - } + await window.ClickAsync(() => rowB, MouseButton.Right, + pointInTarget: r => new Point(System.Math.Min(r.Bounds.Width, grid.Bounds.Width) / 2, r.Bounds.Height / 2)); + await Waiters.WaitForIdleAsync(); ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, nodeA).Should().BeTrue( "right-clicking an unselected row must not change the selection (Thunderbird-style context target)"); @@ -239,12 +227,7 @@ public class DecompileInNewViewTests var nodeC = assemblies[2]; vm.AssemblyTreeModel.SelectNode(nodeA); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); ICSharpCode.ILSpy.Controls.TreeView.SharpTreeViewItem Row(SharpTreeNode node) => grid.GetVisualDescendants() .OfType() @@ -252,43 +235,16 @@ public class DecompileInNewViewTests var menu = grid.ContextMenu!; - Point? ClickPoint(SharpTreeNode 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; - 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); + // Tree rows stretch to content width, so clamp X to the visible grid viewport. + await window.ClickAsync( + () => grid.GetVisualDescendants() + .OfType() + .FirstOrDefault(r => RowNodeEquals(r, node)), + MouseButton.Right, + pointInTarget: row => new Point(System.Math.Min(row.Bounds.Width, grid.Bounds.Width) / 2, row.Bounds.Height / 2), + description: $"the row for {node}"); // The highlight is scoped to the popup - set while the menu is being requested, dropped // again when it closes - so the popup is the point at which the gesture is finished and // the row's classes are worth reading. @@ -328,27 +284,15 @@ public class DecompileInNewViewTests var nodeA = vm.AssemblyTreeModel.FindNode("System.Linq"); var nodeB = vm.AssemblyTreeModel.FindNode(TreeNavigation.CoreLibName); vm.AssemblyTreeModel.SelectNode(nodeA); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); var rowB = grid.GetVisualDescendants().OfType() .First(r => RowNodeEquals(r, nodeB)); int tabsBefore = vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count(); - var clickX = System.Math.Min(rowB.Bounds.Width, grid.Bounds.Width) / 2; - var point = rowB.TranslatePoint(new Point(clickX, rowB.Bounds.Height / 2), window)!.Value; - HeadlessWindowExtensions.MouseDown(window, point, MouseButton.Middle); - HeadlessWindowExtensions.MouseUp(window, point, MouseButton.Middle); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await window.ClickAsync(() => rowB, MouseButton.Middle, + pointInTarget: r => new Point(System.Math.Min(r.Bounds.Width, grid.Bounds.Width) / 2, r.Bounds.Height / 2)); + await Waiters.WaitForIdleAsync(); vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count() .Should().BeGreaterThan(tabsBefore, "middle-clicking a row must open it in a new document tab"); @@ -376,23 +320,13 @@ public class DecompileInNewViewTests vm.AssemblyTreeModel.SelectNode(nodeA); await vm.DockWorkspace.WaitForDecompiledTextAsync(); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); var registry = AppComposition.Current.GetExport(); var menu = pane.BuildContextMenuForCurrentState(registry.Entries, rightClickedNode: nodeB); menu!.ClickItem(Resources.DecompileToNewPanel); await vm.DockWorkspace.WaitForDecompiledTextAsync(); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); // The model selection follows the new active tab... ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, nodeB).Should().BeTrue( @@ -428,12 +362,7 @@ public class DecompileInNewViewTests vm.AssemblyTreeModel.SelectedItems.Add(nodeA); vm.AssemblyTreeModel.SelectedItems.Add(nodeB); await vm.DockWorkspace.WaitForDecompiledTextAsync(); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); vm.AssemblyTreeModel.SelectedItems.Count.Should().Be(2, "precondition: a multi-selection is held"); // Open C in a new tab -> activates it -> the tree must follow to C. @@ -441,12 +370,7 @@ public class DecompileInNewViewTests var menu = pane.BuildContextMenuForCurrentState(registry.Entries, rightClickedNode: nodeC); menu!.ClickItem(Resources.DecompileToNewPanel); await vm.DockWorkspace.WaitForDecompiledTextAsync(); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, nodeC).Should().BeTrue( "the tree model selection must follow the newly-activated single-node tab"); @@ -494,12 +418,7 @@ public class DecompileInNewViewTests // 3) Re-activate the multi-node tab. vm.DockWorkspace.Factory.SetActiveDockable(multiTab); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); // 4) The tree selection must contain BOTH original nodes again -- in the model... vm.AssemblyTreeModel.SelectedItems.Should().Contain(nodeA) diff --git a/ILSpy.Tests/ContextMenus/KeyboardContextMenuFocusTests.cs b/ILSpy.Tests/ContextMenus/KeyboardContextMenuFocusTests.cs index 87e735994..78f2cd82c 100644 --- a/ILSpy.Tests/ContextMenus/KeyboardContextMenuFocusTests.cs +++ b/ILSpy.Tests/ContextMenus/KeyboardContextMenuFocusTests.cs @@ -60,12 +60,7 @@ public class KeyboardContextMenuFocusTests var node = vm.AssemblyTreeModel.Root!.Children.OfType().First(); vm.AssemblyTreeModel.SelectNode(node); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(25); - } + await Waiters.WaitForIdleAsync(); var row = grid.GetVisualDescendants() .OfType().First(); @@ -77,21 +72,13 @@ public class KeyboardContextMenuFocusTests // Keyboard invocation raises ContextRequested with no pointer position (the Shift+F10 / Apps path). row.RaiseEvent(new ContextRequestedEventArgs()); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); grid.ContextMenu!.IsOpen.Should().BeTrue("the keyboard gesture must open the tree context menu"); row.Classes.Should().Contain("contextTarget", "a keyboard-invoked menu must show the transient target highlight on the selected row, like the mouse path"); window.KeyPress(Key.Escape, RawInputModifiers.None, PhysicalKey.Escape, keySymbol: null); - for (int i = 0; i < 6; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); (focusManager.GetFocusedElement() == row).Should().BeTrue( "closing a keyboard-invoked context menu must return focus to the row, not strand it"); diff --git a/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs b/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs index cc571ad23..51eb162b3 100644 --- a/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs +++ b/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs @@ -64,11 +64,7 @@ public class ReferenceScopeAndNewTabTests int before = vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count(); entry.Execute(RefContext(entity)); - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - await Task.Delay(20); - } + await Waiters.WaitForIdleAsync(); vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count() .Should().BeGreaterThan(before, "Decompile to new tab on a code reference must open a new document tab"); diff --git a/ILSpy.Tests/Controls/OmnibarSettingTests.cs b/ILSpy.Tests/Controls/OmnibarSettingTests.cs index 61c8d9d67..1c7601393 100644 --- a/ILSpy.Tests/Controls/OmnibarSettingTests.cs +++ b/ILSpy.Tests/Controls/OmnibarSettingTests.cs @@ -51,17 +51,11 @@ public class OmnibarSettingTests "System.Linq", "System.Linq", "System.Linq.Enumerable"); vm.AssemblyTreeModel.SelectedItem = typeNode; Omnibar? omnibar = null; - for (int i = 0; i < 200; i++) - { - Dispatcher.UIThread.RunJobs(); - omnibar = window.GetVisualDescendants().OfType() + await Waiters.WaitForAsync(() => (omnibar = window.GetVisualDescendants().OfType() .Where(v => v.IsEffectivelyVisible) .SelectMany(v => v.GetVisualDescendants().OfType()) - .FirstOrDefault(); - if (omnibar != null) - break; - await Task.Delay(20); - } + .FirstOrDefault()) != null, + description: "a visible decompiler text view hosting the omnibar"); Assert.That(omnibar, Is.Not.Null, "selecting a node realizes a decompiler text view hosting the omnibar"); Assert.That(omnibar!.IsVisible, Is.False, diff --git a/ILSpy.Tests/Docking/DocumentTabStripModeTests.cs b/ILSpy.Tests/Docking/DocumentTabStripModeTests.cs index cf2b52792..72db830cb 100644 --- a/ILSpy.Tests/Docking/DocumentTabStripModeTests.cs +++ b/ILSpy.Tests/Docking/DocumentTabStripModeTests.cs @@ -65,15 +65,7 @@ public class DocumentTabStripModeTests return (window, strip); } - static async Task Pump(MainWindow window) - { - for (int i = 0; i < 12; i++) - { - Dispatcher.UIThread.RunJobs(); - window.UpdateLayout(); - await Task.Delay(20); - } - } + static Task Pump(MainWindow window) => Waiters.WaitForIdleAsync(); static Button? Dropdown(DocumentTabStrip strip) => strip.GetVisualDescendants().OfType