From ff91bd4f704aa25b1b1fbd90f44b6a9e68cb3cee Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 15 Jul 2026 18:57:53 +0800 Subject: [PATCH] feat(driver): add Driver.long_press on WDA and Android --- apps/device-host-agent/tests/test_e2e.py | 3 +++ apps/device-host-agent/tests/test_execution.py | 3 +++ driver/android_driver.py | 11 +++++++++++ driver/base.py | 7 +++++++ driver/wda_driver.py | 13 +++++++++++++ tests/fakes.py | 3 +++ tests/test_android_driver.py | 16 ++++++++++++++++ tests/test_describe_screen.py | 3 +++ 8 files changed, 59 insertions(+) diff --git a/apps/device-host-agent/tests/test_e2e.py b/apps/device-host-agent/tests/test_e2e.py index 29d0b83..a475366 100644 --- a/apps/device-host-agent/tests/test_e2e.py +++ b/apps/device-host-agent/tests/test_e2e.py @@ -47,6 +47,9 @@ class FakeDriver(Driver): def tap(self, x: float, y: float) -> None: self.calls.append(("tap", (x, y))) + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + return None + def swipe( self, start_x: float, diff --git a/apps/device-host-agent/tests/test_execution.py b/apps/device-host-agent/tests/test_execution.py index 61ebdf1..3278e01 100644 --- a/apps/device-host-agent/tests/test_execution.py +++ b/apps/device-host-agent/tests/test_execution.py @@ -33,6 +33,9 @@ class FakeDriver(Driver): def tap(self, x: float, y: float) -> None: return None + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + return None + def swipe( self, start_x: float, diff --git a/driver/android_driver.py b/driver/android_driver.py index 35ccbf7..462403e 100644 --- a/driver/android_driver.py +++ b/driver/android_driver.py @@ -87,6 +87,17 @@ class AndroidDriver(Driver): except Exception as exc: raise DriverError("tap failed") from exc + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + # Android ``mobile: longClickGesture`` uses a system-fixed hold + # duration; the caller's ``duration_ms`` is intentionally ignored + # (see design R2). Verify on real Appium against the running device + # driver — see plan task 2. + client = self._require_client() + try: + client.execute_script("mobile: longClickGesture", {"x": x, "y": y}) + except Exception as exc: + raise DriverError("long press failed") from exc + def swipe( self, start_x: float, diff --git a/driver/base.py b/driver/base.py index a6ec952..07e59a1 100644 --- a/driver/base.py +++ b/driver/base.py @@ -27,6 +27,13 @@ class Driver(ABC): def tap(self, x: float, y: float) -> None: """Tap the screen at the given coordinates.""" + @abstractmethod + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + """Press and hold at the given coordinates for ``duration_ms``. + + Some platforms ignore ``duration_ms`` (fixed system hold duration). + """ + @abstractmethod def swipe( self, diff --git a/driver/wda_driver.py b/driver/wda_driver.py index 4ae366b..1db8f8f 100644 --- a/driver/wda_driver.py +++ b/driver/wda_driver.py @@ -77,6 +77,19 @@ class WDADriver(Driver): except Exception as exc: raise DriverError("tap failed") from exc + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + # ``mobile: touchAndHold`` is an XCUITest WDA endpoint; ``duration`` is + # in seconds (float). Verify on real Appium against the running device + # driver — see plan task 2. + client = self._require_client() + try: + client.execute_script( + "mobile: touchAndHold", + {"x": x, "y": y, "duration": duration_ms / 1000}, + ) + except Exception as exc: + raise DriverError("long press failed") from exc + def swipe( self, start_x: float, diff --git a/tests/fakes.py b/tests/fakes.py index a89836c..b3dab39 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -51,6 +51,9 @@ class FakeDriver(Driver): def tap(self, x: float, y: float) -> None: self.calls.append(("tap", (x, y))) + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + self.calls.append(("long_press", (x, y, duration_ms))) + def swipe( self, start_x: float, diff --git a/tests/test_android_driver.py b/tests/test_android_driver.py index 11be733..4574c23 100644 --- a/tests/test_android_driver.py +++ b/tests/test_android_driver.py @@ -103,6 +103,7 @@ def test_connect_failure_raises_device_offline_and_clears_client() -> None: ("lock", {}), ("unlock", {}), ("disconnect", {}), + ("long_press", {"x": 1, "y": 2}), ], ) def test_operation_before_connect_raises_device_offline( @@ -255,6 +256,21 @@ def test_swipe_near_zero_distance_uses_default_speed() -> None: assert speed > 0 +def test_long_press_uses_long_click_gesture() -> None: + driver = _connected_driver() + driver.long_press(30, 40, duration_ms=1500) + driver._client.execute_script.assert_called_once_with( + "mobile: longClickGesture", {"x": 30, "y": 40} + ) + + +def test_long_press_wraps_exception_into_driver_error() -> None: + driver = _connected_driver() + driver._client.execute_script.side_effect = RuntimeError("boom") + with pytest.raises(DriverError): + driver.long_press(1, 2) + + # --------------------------------------------------------------------------- # # build_android_driver_factory # --------------------------------------------------------------------------- # diff --git a/tests/test_describe_screen.py b/tests/test_describe_screen.py index 934932a..39e7b00 100644 --- a/tests/test_describe_screen.py +++ b/tests/test_describe_screen.py @@ -33,6 +33,9 @@ class TreeFailingDriver(Driver): def tap(self, x: float, y: float) -> None: return None + def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: + return None + def swipe( self, start_x: float,