docs(openspec): confirm android-driver UiAutomator2 commands via research
Resolve the previously-open design questions in the android-driver change by researching the appium-uiautomator2-driver docs and Appium 3 release notes: - tap -> mobile: clickGesture - swipe -> mobile: dragGesture (duration_ms converted to speed px/s) - home -> mobile: pressKey (KEYCODE_HOME) - port isolation -> appium:systemPort capability - Appium 3 breaking changes confirmed to not affect this design Updates design.md (Decisions/Risks/Open Questions/Migration Plan) and tasks.md (section 1 and tasks 2.1/2.4/4.1) accordingly.
This commit is contained in:
@@ -24,20 +24,27 @@ This is exactly the case the project's Hexagonal + DDD governance decision calls
|
||||
- **Registry key is `"uiautomator2"`, not `"android"`.** The existing key `"wda"` names the automation *backend* (WebDriverAgent), not the platform (`"ios"`). For the two supported driver types to stay consistent, Android's key should likewise name its backend — Appium's UiAutomator2 driver — not its platform. Rejected alternative: `"android"`, which would break that symmetry and would also be ambiguous if Espresso support is ever added later (both would be "android").
|
||||
- **`AndroidDriverConfig` mirrors `WDADriverConfig` field-for-field where an Android equivalent exists**: `server_url` (same default `http://127.0.0.1:4723` — one Appium server can host sessions for both platforms), `platform_name="Android"`, `automation_name="UiAutomator2"`, `device_name`, `udid` (adb serial, selects among multiple connected devices), `no_reset`, `extra_capabilities`. `wda_local_port` (WDA's per-session port-isolation capability for parallel devices) has a direct UiAutomator2 analog — a system-port capability serving the same purpose — carried over as `system_port`. Rejected alternative: a from-scratch config shape — rejected because the parity makes both drivers predictable to configure from the same `connection_info` dict shape the registry already handles generically.
|
||||
- **Same error-wrapping pattern as `WDADriver`**: every method's Appium/network exception is caught and re-raised as the existing `DriverError`, with `DeviceOfflineError` reserved for connect failures and pre-connect calls (via the same `_require_client()` guard pattern). Rejected alternative: introducing Android-specific error types — rejected because `core/errors.py`'s existing hierarchy is already driver-agnostic and callers above the driver layer must not need to know which concrete driver raised.
|
||||
- **Exact Appium UiAutomator2 mobile-command names/parameters for gesture-based methods (`tap`, `swipe`, `home`) are confirmed against the installed `Appium-Python-Client` source/docs at implementation time, not guessed here.** WDA's `tap`/`swipe`/`home` use WDA-specific `mobile:` command names (`mobile: tap`, `mobile: dragFromToForDuration`, `mobile: pressButton`) that do not carry over verbatim to UiAutomator2, which exposes its own gesture command set (e.g. click/drag/swipe gesture commands) with different parameter shapes. Implementation must read the installed driver's actual capability before writing each method body — this is called out as its own task rather than assumed. `screenshot`/`tree`/`input`/`launch`/`terminate`/`lock`/`unlock` map to the same cross-platform Selenium/Appium client methods WDA already uses (`get_screenshot_as_png`, `page_source`, `switch_to.active_element.send_keys`, `activate_app`, `terminate_app`, `lock`, `unlock`) and need no research.
|
||||
- **Appium UiAutomator2 mobile-command names/parameters for gesture-based methods (`tap`, `swipe`, `home`), confirmed via research against the official `appium-uiautomator2-driver` docs (2026-07)**: WDA's `tap`/`swipe`/`home` use WDA-specific `mobile:` command names (`mobile: tap`, `mobile: dragFromToForDuration`, `mobile: pressButton`) that do not carry over verbatim to UiAutomator2. The confirmed Android equivalents:
|
||||
- `tap(x, y)` → `execute_script("mobile: clickGesture", {"x": x, "y": y})`. The driver's own docs recommend this over any legacy tap call as a workaround for native-tap failures, so it is also the more robust choice, not just the closest analog.
|
||||
- `swipe(start_x, start_y, end_x, end_y, duration_ms)` → `execute_script("mobile: dragGesture", {"startX": start_x, "startY": start_y, "endX": end_x, "endY": end_y, "speed": speed})`. Unlike WDA's `dragFromToForDuration`, `dragGesture` takes a `speed` in pixels/second instead of a duration, so the implementation must convert: `speed = distance / (duration_ms / 1000)`, guarding against a zero/near-zero distance (fall back to the driver's default speed rather than dividing by zero). `mobile: swipeGesture` was considered and rejected — it takes a bounding-area + direction + percent shape, not a coordinate pair, so it does not match `Driver.swipe`'s signature.
|
||||
- `home()` → `execute_script("mobile: pressKey", {"keycode": 3})` (Android `KeyEvent.KEYCODE_HOME`).
|
||||
- The parallel-session port-isolation capability is confirmed as `appium:systemPort` (maps directly to `AndroidDriverConfig.system_port`), documented by the driver as "recommended for parallel tests" — the direct analog of WDA's `wdaLocalPort`.
|
||||
- `screenshot`/`tree`/`input`/`launch`/`terminate`/`lock`/`unlock` map to the same cross-platform Selenium/Appium client methods WDA already uses (`get_screenshot_as_png`, `page_source`, `switch_to.active_element.send_keys`, `activate_app`, `terminate_app`, `lock`, `unlock`) and needed no research.
|
||||
- Implementation should still sanity-check these against whatever `appium-uiautomator2-driver` version is actually resolved in `.venv` at coding time (docs reflect the driver's current released behavior as of this research, not a pinned version in this repo).
|
||||
- **Appium 3 compatibility, confirmed via research (released 2025-08-07, latest 3.5.2 as of this research)**: Appium 3 is a deliberately small breaking-change release (Node.js/npm minimum version bump, mandatory feature-flag scope prefixes for `--allow-insecure`, `GET /sessions` moved to `GET /appium/sessions` behind a feature flag, full JSONWP removal in favor of W3C-only parameters, driver-owned file upload handling). None of these affect this change: `AndroidDriver` (like `WDADriver`) builds sessions purely through W3C `Options` classes, never relies on session discovery, requests no insecure feature flags, and does no file upload. `Appium-Python-Client>=5.1.1` (already pinned in root `pyproject.toml`) has no reported incompatibility with Appium 3 servers. No version pin changes are needed.
|
||||
- **`driver-registry` spec gets a new scenario, not a new capability file.** No `wda-driver` capability spec exists today — the registry mechanism is spec'd once, generically, and individual driver behavior is not separately spec'd. Adding an `android-driver` capability would break that precedent for no benefit; instead, `driver-registry`'s existing "Building a factory for a known driver type" requirement gets a second scenario for `driver_type="uiautomator2"`, mirroring the existing `"wda"` scenario, so the spec's example coverage stays symmetric across both real driver types.
|
||||
- **Test coverage exceeds current WDA parity on purpose.** `WDADriver` today has zero mocked unit tests — only `tests/test_wda_integration.py`, hardware-gated and skipped without `APEX_WDA_*` env vars. For `AndroidDriver`, add mocked unit tests (mock `appium.webdriver.Remote`) covering connect failure → `DeviceOfflineError`, pre-connect calls → `DeviceOfflineError`, and operation exceptions → `DriverError`, in addition to an equivalent hardware-gated `tests/test_android_integration.py`. This is a deliberate quality bar increase, not scope creep — it costs nothing extra in production code and closes a gap the WDA driver has always had.
|
||||
- **No new setup documentation in this change.** Writing an accurate, detailed Android SDK/adb/real-device guide (parallel to `docs/MACOS_IPHONE_SETUP.md`'s 12 sections) without access to real hardware to validate each step would mean inventing untested instructions — the existing iOS doc reads as having been validated against a real device. `docs/MACOS_IPHONE_SETUP.md` §1 gets only a factual correction (Android driver is now registered); a full setup guide is explicit follow-up work once real-device validation is possible.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Appium UiAutomator2 mobile-command names for tap/swipe/home are not yet confirmed] → Mitigation: dedicated implementation task to read the installed `Appium-Python-Client` UiAutomator2 driver source/docs before writing these three method bodies; not treated as settled by this design doc.
|
||||
- [`dragGesture`'s `speed` (px/s) is a different shape from `Driver.swipe`'s `duration_ms`] → Mitigation: convert explicitly (`speed = distance / (duration_ms / 1000)`) with a guard for zero/near-zero distance; cover this conversion with a unit test (start==end and a normal case) rather than trusting it silently.
|
||||
- [No real Android device or emulator available during this change to exercise `connect()`/`screenshot()` end-to-end] → Mitigation: mocked unit tests cover the error-handling contract; the integration test is env-var gated and simply skips until real hardware is available, exactly mirroring `WDADriver`'s current state — no regression versus today's validation depth.
|
||||
- [`driver-registry` spec now carries two platform-specific example scenarios under one requirement] → Mitigation: accepted; this is the intended pattern for a third driver type in the future, not a maintenance burden.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Add `driver/android_driver.py` (`AndroidDriverConfig`, `AndroidDriver(Driver)`), verifying exact UiAutomator2 mobile-command names/parameters against the installed Appium client before implementing `tap`/`swipe`/`home`.
|
||||
1. Add `driver/android_driver.py` (`AndroidDriverConfig`, `AndroidDriver(Driver)`), using the confirmed UiAutomator2 mobile commands for `tap`/`swipe`/`home` (see Decisions), with a quick sanity check against whatever driver version is actually resolved in `.venv`.
|
||||
2. Add `build_android_driver_factory` and register `SUPPORTED_DRIVER_TYPES["uiautomator2"]` in `driver/registry.py`.
|
||||
3. Add mocked unit tests for `AndroidDriver` (connect failure, pre-connect calls, per-method exception wrapping).
|
||||
4. Add `tests/test_android_integration.py`, mirroring `tests/test_wda_integration.py`'s env-var-gated structure.
|
||||
@@ -49,5 +56,4 @@ Rollback: everything is additive (one new driver module, one new registry entry,
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Exact UiAutomator2 mobile-command names/parameters for `tap`/`swipe`/`home` — resolved during implementation (task-level, not a proposal-level blocker).
|
||||
- Exact capability key name for the system-port equivalent (`systemPort` or similar) — confirmed against `UiAutomator2Options` during implementation.
|
||||
None outstanding. Both items originally listed here (exact UiAutomator2 mobile-command names/parameters for `tap`/`swipe`/`home`, and the system-port capability's exact key name) were resolved via research against the official `appium-uiautomator2-driver` docs — see the Decisions section.
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
## 1. Verify Appium UiAutomator2 command surface
|
||||
## 1. Appium UiAutomator2 command surface (confirmed via research, see design.md Decisions)
|
||||
|
||||
- [ ] 1.1 Read the installed `Appium-Python-Client` UiAutomator2 driver source/docs (`appium.options.android.uiautomator2`, `appium.webdriver` extensions) to confirm the exact mobile-command names and parameters for: a tap/click gesture at a coordinate, a coordinate-to-coordinate swipe/drag with duration, and a home-button press. Confirm the exact capability key name for per-session port isolation (UiAutomator2's analog to WDA's `wdaLocalPort`).
|
||||
- [ ] 1.1 Sanity-check the confirmed mobile commands against whatever `appium-uiautomator2-driver` version is actually resolved in `.venv` before coding: `mobile: clickGesture` (tap), `mobile: dragGesture` (swipe/drag), `mobile: pressKey` (home), `appium:systemPort` (port isolation capability). Docs referenced (2026-07): `github.com/appium/appium-uiautomator2-driver` README and `docs/android-mobile-gestures.md`.
|
||||
|
||||
## 2. Driver implementation
|
||||
|
||||
- [ ] 2.1 Add `driver/android_driver.py` with `AndroidDriverConfig` (frozen dataclass): `server_url` (default `http://127.0.0.1:4723`), `platform_name` (default `"Android"`), `automation_name` (default `"UiAutomator2"`), `device_name`, `udid`, `system_port`, `no_reset` (default `True`), `extra_capabilities`.
|
||||
- [ ] 2.1 Add `driver/android_driver.py` with `AndroidDriverConfig` (frozen dataclass): `server_url` (default `http://127.0.0.1:4723`), `platform_name` (default `"Android"`), `automation_name` (default `"UiAutomator2"`), `device_name`, `udid`, `system_port` (maps to the `appium:systemPort` capability), `no_reset` (default `True`), `extra_capabilities`.
|
||||
- [ ] 2.2 Implement `AndroidDriver(Driver).connect()`/`disconnect()` using `appium.webdriver` + `UiAutomator2Options`, building capabilities the same way `WDADriver.connect()` does, with the same `_require_client()` guard and `DeviceOfflineError` on connect failure.
|
||||
- [ ] 2.3 Implement `screenshot()`, `tree()`, `input()`, `launch()`, `terminate()`, `lock()`, `unlock()` using the same cross-platform Appium client methods `WDADriver` already uses (`get_screenshot_as_png`, `page_source`, `switch_to.active_element.send_keys`, `activate_app`, `terminate_app`, `lock`, `unlock`).
|
||||
- [ ] 2.4 Implement `tap()`, `swipe()`, `home()` using the mobile-command names/parameters confirmed in task 1.1.
|
||||
- [ ] 2.4 Implement `tap()`, `swipe()`, `home()`:
|
||||
- `tap(x, y)` → `execute_script("mobile: clickGesture", {"x": x, "y": y})`
|
||||
- `swipe(start_x, start_y, end_x, end_y, duration_ms)` → `execute_script("mobile: dragGesture", {"startX": start_x, "startY": start_y, "endX": end_x, "endY": end_y, "speed": speed})` where `speed = distance / (duration_ms / 1000)`, guarded against zero/near-zero distance
|
||||
- `home()` → `execute_script("mobile: pressKey", {"keycode": 3})` (`KeyEvent.KEYCODE_HOME`)
|
||||
- [ ] 2.5 Wrap every method's underlying exception into `DriverError` (`DeviceOfflineError` for connect failure and for calls made before a client exists), matching `WDADriver`'s try/except-per-method pattern exactly.
|
||||
|
||||
## 3. Registry wiring
|
||||
@@ -17,7 +20,7 @@
|
||||
|
||||
## 4. Unit tests
|
||||
|
||||
- [ ] 4.1 Add mocked unit tests for `AndroidDriver` (mock `appium.webdriver.Remote`, no real device/emulator) covering: connect builds a client with the expected capabilities from a given `AndroidDriverConfig`; connect failure raises `DeviceOfflineError`; calling any operation before `connect()` raises `DeviceOfflineError`; each operation's underlying exception is wrapped into `DriverError`.
|
||||
- [ ] 4.1 Add mocked unit tests for `AndroidDriver` (mock `appium.webdriver.Remote`, no real device/emulator) covering: connect builds a client with the expected capabilities from a given `AndroidDriverConfig`; connect failure raises `DeviceOfflineError`; calling any operation before `connect()` raises `DeviceOfflineError`; each operation's underlying exception is wrapped into `DriverError`; `swipe()`'s `duration_ms` → `speed` conversion for both a normal case and a zero/near-zero-distance case (must not divide by zero).
|
||||
- [ ] 4.2 Add a unit test for `build_android_driver_factory` covering `connection_info` field extraction and `extra_capabilities` merging (mirror `build_wda_driver_factory`'s existing test coverage if any exists; if none exists today, note that in the test file rather than silently skipping equivalent WDA coverage).
|
||||
|
||||
## 5. Integration test
|
||||
|
||||
Reference in New Issue
Block a user