Skip to content

Render a frame while the headless UI tests wait - #4035

Merged
christophwille merged 1 commit into
masterfrom
fix/context-target-highlight-flake
Aug 19, 2026
Merged

Render a frame while the headless UI tests wait#4035
christophwille merged 1 commit into
masterfrom
fix/context-target-highlight-flake

Conversation

@christophwille

Copy link
Copy Markdown
Member

The break

Desktop (macos-15, macos) fails in Execute UI tests with a single failing test:

failed Right_Clicking_A_Second_Row_Moves_The_Context_Highlight_To_It
  Expected Row(nodeC).Classes {empty} to contain "contextTarget" because a second
  right-click (after dismissing the first menu) must highlight C.
  at ICSharpCode.ILSpy.Tests.DecompileInNewViewTests
       .Right_Clicking_A_Second_Row_Moves_The_Context_Highlight_To_It()
     in ILSpy.Tests/ContextMenus/DecompileInNewViewTests.cs:283

It is not specific to that branch. The same test, same assertion, took down
master (run 32177067040), tests/async-await-patterns (32179221630) and
decompiler-tests-server-gc (32228410174) — always on macOS, never on Windows or
Linux, and a re-run of the identical commit passes. The macOS runner is simply the
slowest of the three and loses a race the others win.

What actually happens

The test right-clicks row B, presses Escape, right-clicks row C, and asserts that
the transient contextTarget highlight moved to C. Both gesture helpers pumped a
fixed number of dispatcher cycles and assumed the UI had caught up:

HeadlessWindowExtensions.MouseUp(window, pt.Value, MouseButton.Right);
for (int i = 0; i < 4; i++)
{
    Dispatcher.UIThread.RunJobs();
    await Task.Delay(20);
}

To find out what the runner really does there, I pushed a probe build that repeats
the gesture 20 times per run and logs the menu state and every row's classes. It
tripped on round 19 (full log in the run's test-results-macos TRX):

PROBE [ 18248]   menu.Closed
PROBE [ 18996]     dismissed menuOpen=False rows='...CoreLib'[:selected,:focus-within,:focus] | '...Uri'[] | '...Linq'[]
PROBE [ 18996] --- round 19 ---
PROBE [ 18996]   click 'System.Private.Uri' rowBounds=0, 20, 683, 20 pt=112, 112
PROBE [ 19034]     afterDown menuOpen=False
PROBE [ 19036]     afterUp menuOpen=False rows='...CoreLib'[:selected,:focus-within,:focus] | '...Uri'[] | '...Linq'[]

The right-click produces nothing at all: no menu.Opening, no menu, and the
clicked row does not even pick up :pointerover. ContextRequested was never
raised, so AssemblyListPane.OnTreeContextRequested never ran and no row became
the context target — which is exactly what the assertion reports two lines later.

The tell is in the line above it. In all 18 healthy rounds the dismissal leaves
:pointerover on the row under the cursor; in the failing one it is on no row,
at coordinates that had not moved. Hit testing for synthesized input is answered
from the rendered scene, not from the visual tree, and the closed menu's popup
still owned that point in the last rendered frame. Press and release both went to
its light-dismiss overlay, and the row underneath never saw them.

Dispatcher.UIThread.RunJobs() does not render a frame. Avalonia's own headless
input helpers know this and pump the dispatcher and the render timer together
(HeadlessWindowExtensions.RunJobsOnImplRunJobs() + ForceRenderTimerTick()
in a loop). Our wait/pump loops only ran the first half, so on a fast machine the
ticks that Avalonia's KeyPress/MouseUp helpers happen to run were enough, and
on the loaded macOS runner they were not.

Reproduced locally by forcing the equivalent state (a menu still owning the point
when the next right-click arrives): the click is swallowed and the run fails with
the byte-identical message.

The fix

  • Waiters.WaitForAsync now advances the UI with Waiters.PumpUI() — dispatcher
    jobs and a rendered frame — instead of dispatcher jobs alone. Every wait in
    the headless suite gets input routing that matches what is on screen, which is
    the general form of the bug.
  • Right_Clicking_A_Second_Row_Moves_The_Context_Highlight_To_It waits for the
    popup itself instead of counting dispatcher cycles: menu.IsOpen after each
    right-click, !menu.IsOpen after Escape, and a short render settle before the
    next click so the frame that removes the overlay is on screen first.

No product code changes. The stale-frame hit test is an artifact of synthesizing
input without advancing the headless render timer; a real pointer generates real
move events against a scene the compositor is already rendering.

Verification

  • Full ILSpy.Tests suite green locally (1186 passed / 3 skipped).
  • A 60-round variant of the failing gesture (120 right-clicks in one test) was run
    on the macOS CI runner to confirm the fix, against an unfixed baseline that
    failed at roughly one gesture in forty.

Hit testing for synthesized input is answered from the rendered scene, not from
the visual tree, and Dispatcher.UIThread.RunJobs() does not render one. The
context-menu gesture helpers pumped dispatcher jobs alone, so after Escape closed
a menu the next right-click could still be routed to the light-dismiss overlay of
the frame that was on screen: no ContextRequested was raised, no row became the
context target, and the assertion two lines later reported an unhighlighted row.
The macOS CI runner lost that race roughly once in forty gestures; a probe build
repeating the gesture caught a right-click that produced neither a pointer-over
nor a menu, and the fix survived 120 gestures on the same runner.

Avalonia's own headless input helpers pump the dispatcher and the render timer
together for this reason.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
@christophwille
christophwille merged commit 5689dab into master Aug 19, 2026
17 checks passed
@christophwille
christophwille deleted the fix/context-target-highlight-flake branch August 19, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant