From 67808082090fedcde3e2952ab71f504a80ad5a0c Mon Sep 17 00:00:00 2001 From: tobitege <10787084+tobitege@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:09:23 +0100 Subject: [PATCH] Wait for the hit test in the touch drag tests, and close system dialogs in the Android check The touch tests waited a fixed number of frames before pressing the moved grip, which a slow compositor on Xvfb did not always draw in time; they now wait until the pointer hits the grip. The Android check closes an emulator's "isn't responding" dialog of a system app with Wait instead of failing on it. --- .../Controls/DaisyTourTestHelpers.cs | 10 ++++++++++ .../Controls/Given_DaisyTourAvalonia.cs | 2 +- .../Controls/Given_DaisyTourEditorAvalonia.cs | 4 ++-- scripts/check_android_gallery.py | 9 ++++++++- scripts/test_check_android_gallery.py | 8 +++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Flowery.NET.RuntimeTests/Controls/DaisyTourTestHelpers.cs b/Flowery.NET.RuntimeTests/Controls/DaisyTourTestHelpers.cs index 5cb8ca7..c7680ea 100644 --- a/Flowery.NET.RuntimeTests/Controls/DaisyTourTestHelpers.cs +++ b/Flowery.NET.RuntimeTests/Controls/DaisyTourTestHelpers.cs @@ -62,6 +62,16 @@ internal static async Task CleanupAsync(Control root, DaisyTour tour) await SettleAsync(root); } + // Waits until the pointer in the middle of a control hits it. Hit testing reads the rendered scene, and after a + // control moved, a slow compositor (Xvfb) can show the old place for more than the frames WaitForRenderAsync waits. + internal static Task WaitUntilHitAsync(Control control) => RuntimeTestHelpers.WaitUntilAsync(() => + { + var root = TopLevel.GetTopLevel(control)!; + var center = control.TranslatePoint(new Point(control.Bounds.Width / 2, control.Bounds.Height / 2), root); + return center is { } point && root.InputHitTest(point) is Visual hit + && (ReferenceEquals(hit, control) || control.IsVisualAncestorOf(hit)); + }, $"the pointer hits the moved {control.GetType().Name}"); + // Presses the left button in the middle of a control like the mouse does: the element under the pointer takes // the capture and receives the press, which bubbles to the control. The control must be what the pointer hits. internal static (TopLevel Root, Point Start, Pointer Pointer) PressAtCenter(Control control, PointerType type = PointerType.Mouse) diff --git a/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourAvalonia.cs b/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourAvalonia.cs index 3d582f4..f835a57 100644 --- a/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourAvalonia.cs +++ b/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourAvalonia.cs @@ -549,7 +549,7 @@ await InEditorAsync(async (tour, root) => MoveCaptured(first, top, new Point(start.X - 60, start.Y - 40)); await SettleAsync(root); // Hit testing sees the moved grip once it has been rendered. - await RuntimeTestHelpers.WaitForRenderAsync(); + await WaitUntilHitAsync(grip); // The second finger presses, moves and lifts while the first one still holds the grip. It moves sideways, // where the card text cannot scroll, so the scroll gesture does not take it before its release. diff --git a/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourEditorAvalonia.cs b/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourEditorAvalonia.cs index a716e03..daef4d8 100644 --- a/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourEditorAvalonia.cs +++ b/Flowery.NET.RuntimeTests/Controls/Given_DaisyTourEditorAvalonia.cs @@ -140,7 +140,7 @@ await InEditorAsync(async (tour, editor) => // A second finger on the grip neither moves the origin of the drag nor resizes, and its release does not // end the resize of the first finger. Hit testing sees the moved grip once it has been rendered. - await RuntimeTestHelpers.WaitForRenderAsync(); + await WaitUntilHitAsync(thumb); var (_, otherStart, second) = PressAtCenter(thumb, PointerType.Touch); var otherEnd = new Point(otherStart.X - 90, otherStart.Y); MoveCaptured(second, root, otherEnd); @@ -155,7 +155,7 @@ await InEditorAsync(async (tour, editor) => // Once the first finger has finished, a third one resizes on its own, and its release away from its last // move sets the width. - await RuntimeTestHelpers.WaitForRenderAsync(); + await WaitUntilHitAsync(thumb); var before = editor.InspectorBounds.Width; var (_, thirdStart, third) = PressAtCenter(thumb, PointerType.Touch); await ChangeGeometryAsync(tour, () => MoveCaptured(third, root, new Point(thirdStart.X - 20, thirdStart.Y))); diff --git a/scripts/check_android_gallery.py b/scripts/check_android_gallery.py index 2047f0e..77f6281 100644 --- a/scripts/check_android_gallery.py +++ b/scripts/check_android_gallery.py @@ -98,7 +98,14 @@ def dump(self, name): root = ElementTree.fromstring(data).find("node") if root is None: raise RuntimeError("uiautomator dumped an empty hierarchy") - return Node(root) + node = Node(root) + # A slow emulator can show " isn't responding" over the Gallery. Wait closes the dialog, and the + # dump counts as failed, so the next one sees the Gallery. + wait = node.find(name="android:id/aerr_wait") + if wait is not None: + self.tap(wait) + raise RuntimeError("a not-responding dialog of the system covered the screen") + return node def shot(self, name): data = adb("exec-out", "screencap", "-p", binary=True) diff --git a/scripts/test_check_android_gallery.py b/scripts/test_check_android_gallery.py index 56517cf..51922fe 100644 --- a/scripts/test_check_android_gallery.py +++ b/scripts/test_check_android_gallery.py @@ -1,4 +1,4 @@ -"""Tests how check_android_gallery.py takes failed and empty UI dumps. +"""Tests how check_android_gallery.py takes failed and empty UI dumps and dialogs of the system. python3 -B -m unittest discover -s scripts -p "test_*.py" """ @@ -65,6 +65,12 @@ def test_an_empty_hierarchy_is_a_failed_dump(self): self.use(FakeDevice(EMPTY)) self.assertIsNone(self.check.wait_for("1-home", lambda root: root.find(kind="HomePage"), 0)) + def test_a_not_responding_dialog_of_the_system_is_closed_with_wait(self): + device = self.use(FakeDevice(hierarchy(("Button", "android:id/aerr_wait")), hierarchy(("HomePage", "")))) + home = self.check.wait_for("1-home", lambda root: root.find(kind="HomePage"), 30) + self.assertEqual("HomePage", home.kind) + self.assertEqual(1, device.taps) + def test_opening_the_sidebar_waits_out_failed_dumps(self): device = self.use(FakeDevice(RuntimeError("uiautomator failed"), EMPTY, b"