diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 7f33490caf..b17a1150fc 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -142,20 +142,25 @@ public static class HeadlessWindowExtensions action(GetImpl(topLevel)); RunJobsAndRender(); - void RunJobsAndRender() + static void RunJobsAndRender() { - var count = 0; var dispatcher = Dispatcher.UIThread; - while (dispatcher.HasJobsWithPriority(DispatcherPriority.MinimumActiveValue)) + // Run jobs and render frames until everything is stable. + // We use a simple approach: run jobs, render, and repeat until + // there are no more pending jobs. The render timer tick can schedule + // new jobs, so we loop until stable. + for (var i = 0; i < 10; i++) { - if (count >= 10) - throw new InvalidOperationException("Dispatcher job loop detected"); - dispatcher.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - ++count; + + if (!dispatcher.HasJobsWithPriority(DispatcherPriority.MinimumActiveValue)) + return; } + + // Final attempt: run remaining jobs without rendering + dispatcher.RunJobs(); } } diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index 841201e51e..c643c4b96d 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -72,6 +72,37 @@ public class InputTests Assert.True(_window.Position == newWindowPosition); } +#if NUNIT + [AvaloniaTest, Timeout(10000)] +#elif XUNIT + [AvaloniaFact] +#endif + public void Should_Click_Button_After_Explicit_RunJobs() + { + // Regression test for https://github.com/AvaloniaUI/Avalonia/issues/20309 + // Ensure that calling Dispatcher.UIThread.RunJobs() before MouseDown does not throw + var button = new Button { Content = "Test content" }; + _window.Content = button; + _window.Show(); + + Dispatcher.UIThread.RunJobs(); + + var clickCount = 0; + button.Click += (_, _) => clickCount++; + + var point = new Point(button.Bounds.Width / 2, button.Bounds.Height / 2); + var translatePoint = button.TranslatePoint(point, _window); + + // Move + _window.MouseMove(translatePoint!.Value, RawInputModifiers.None); + + // Click + _window.MouseDown(translatePoint.Value, MouseButton.Left, RawInputModifiers.None); + _window.MouseUp(translatePoint.Value, MouseButton.Left, RawInputModifiers.None); + + Assert.True(clickCount == 1); + } + #if NUNIT [TearDown] public void TearDown()