feat(driver): add W3C actions helper and Driver.swipe_path
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
POINTER_ID = "finger1"
|
||||
|
||||
|
||||
def _pointer(*actions: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "pointer",
|
||||
"id": POINTER_ID,
|
||||
"parameters": {"pointerType": "touch"},
|
||||
"actions": list(actions),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def build_swipe_actions(
|
||||
waypoints: list[tuple[float, float]], total_duration_ms: int
|
||||
) -> list[dict[str, Any]]:
|
||||
"""W3C actions moving through ``waypoints`` in one continuous touch."""
|
||||
if len(waypoints) < 2:
|
||||
raise ValueError("swipe path requires at least two waypoints")
|
||||
segments = len(waypoints) - 1
|
||||
per_segment = max(1, total_duration_ms // segments)
|
||||
sx, sy = waypoints[0]
|
||||
actions: list[dict[str, Any]] = [
|
||||
{"type": "pointerMove", "duration": 0, "x": int(sx), "y": int(sy)},
|
||||
{"type": "pointerDown", "button": 0},
|
||||
]
|
||||
for point in waypoints[1:]:
|
||||
actions.append(
|
||||
{
|
||||
"type": "pointerMove",
|
||||
"duration": per_segment,
|
||||
"x": int(point[0]),
|
||||
"y": int(point[1]),
|
||||
}
|
||||
)
|
||||
ex, ey = waypoints[-1]
|
||||
actions.append(
|
||||
{"type": "pointerUp", "button": 0, "x": int(ex), "y": int(ey)}
|
||||
)
|
||||
return _pointer(*actions)
|
||||
|
||||
|
||||
def build_double_tap_actions(
|
||||
x: float, y: float, interval_ms: int
|
||||
) -> list[dict[str, Any]]:
|
||||
actions = [
|
||||
{"type": "pointerMove", "duration": 0, "x": int(x), "y": int(y)},
|
||||
{"type": "pointerDown", "button": 0},
|
||||
{"type": "pointerUp", "button": 0},
|
||||
{"type": "pause", "duration": max(1, interval_ms)},
|
||||
{"type": "pointerDown", "button": 0},
|
||||
{"type": "pointerUp", "button": 0},
|
||||
]
|
||||
return _pointer(*actions)
|
||||
|
||||
|
||||
def perform_actions(client: Any, actions: list[dict[str, Any]]) -> None:
|
||||
"""Send a W3C actions payload via the Appium/Selenium command seam.
|
||||
|
||||
Import is lazy so the pure payload builders stay importable without
|
||||
selenium on the path (used by unit tests).
|
||||
"""
|
||||
from selenium.webdriver.remote.command import Command
|
||||
|
||||
client.execute(Command.W3C_ACTIONS, {"actions": actions})
|
||||
@@ -129,6 +129,17 @@ class AndroidDriver(Driver):
|
||||
except Exception as exc:
|
||||
raise DriverError("text input failed") from exc
|
||||
|
||||
def swipe_path(
|
||||
self, waypoints: list[tuple[float, float]], duration_ms: int
|
||||
) -> None:
|
||||
from driver._w3c_actions import build_swipe_actions, perform_actions
|
||||
|
||||
client = self._require_client()
|
||||
try:
|
||||
perform_actions(client, build_swipe_actions(waypoints, duration_ms))
|
||||
except Exception as exc:
|
||||
raise DriverError("swipe_path failed") from exc
|
||||
|
||||
def launch(self, app_id: str) -> None:
|
||||
client = self._require_client()
|
||||
try:
|
||||
|
||||
@@ -45,6 +45,12 @@ class Driver(ABC):
|
||||
) -> None:
|
||||
"""Swipe between two screen coordinates."""
|
||||
|
||||
@abstractmethod
|
||||
def swipe_path(
|
||||
self, waypoints: list[tuple[float, float]], duration_ms: int
|
||||
) -> None:
|
||||
"""Swipe through a sequence of waypoints in one continuous touch."""
|
||||
|
||||
@abstractmethod
|
||||
def input(self, text: str) -> None:
|
||||
"""Input text into the current focused field."""
|
||||
|
||||
@@ -120,6 +120,17 @@ class WDADriver(Driver):
|
||||
except Exception as exc:
|
||||
raise DriverError("text input failed") from exc
|
||||
|
||||
def swipe_path(
|
||||
self, waypoints: list[tuple[float, float]], duration_ms: int
|
||||
) -> None:
|
||||
from driver._w3c_actions import build_swipe_actions, perform_actions
|
||||
|
||||
client = self._require_client()
|
||||
try:
|
||||
perform_actions(client, build_swipe_actions(waypoints, duration_ms))
|
||||
except Exception as exc:
|
||||
raise DriverError("swipe_path failed") from exc
|
||||
|
||||
def launch(self, app_id: str) -> None:
|
||||
client = self._require_client()
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user