Render a frame while the headless UI tests wait - #4035
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The break
Desktop (macos-15, macos)fails inExecute UI testswith a single failing test:It is not specific to that branch. The same test, same assertion, took down
master(run 32177067040),tests/async-await-patterns(32179221630) anddecompiler-tests-server-gc(32228410174) — always on macOS, never on Windows orLinux, 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
contextTargethighlight moved to C. Both gesture helpers pumped afixed number of dispatcher cycles and assumed the UI had caught up:
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-macosTRX):The right-click produces nothing at all: no
menu.Opening, no menu, and theclicked row does not even pick up
:pointerover.ContextRequestedwas neverraised, so
AssemblyListPane.OnTreeContextRequestednever ran and no row becamethe 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
:pointeroveron 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 headlessinput helpers know this and pump the dispatcher and the render timer together
(
HeadlessWindowExtensions.RunJobsOnImpl→RunJobs()+ForceRenderTimerTick()in a loop). Our wait/pump loops only ran the first half, so on a fast machine the
ticks that Avalonia's
KeyPress/MouseUphelpers happen to run were enough, andon 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.WaitForAsyncnow advances the UI withWaiters.PumpUI()— dispatcherjobs 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_Itwaits for thepopup itself instead of counting dispatcher cycles:
menu.IsOpenafter eachright-click,
!menu.IsOpenafter Escape, and a short render settle before thenext 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
ILSpy.Testssuite green locally (1186 passed / 3 skipped).on the macOS CI runner to confirm the fix, against an unfixed baseline that
failed at roughly one gesture in forty.