Files
q792602257 78ce788e2f
Tests / Test passed: 624
chore(openspec): archive android-driver
Archives the completed android-driver change (16/16 tasks). Promotes
the driver_type="uiautomator2" scenario into the canonical
driver-registry spec and moves the change artifacts to
openspec/changes/archive/2026-07-13-android-driver/.
2026-07-13 20:39:27 +08:00

11 KiB

Context

driver/base.py::Driver is the driver-independent ABC (connect/disconnect/screenshot/tap/swipe/input/launch/terminate/tree/home/lock/unlock). driver/wda_driver.py::WDADriver is the only implementation today: it wraps the Appium Python Client's XCUITestOptions, builds a webdriver.Remote session against an Appium server, and wraps every operation's exceptions into core.errors.DriverError/DeviceOfflineError. driver/registry.py::SUPPORTED_DRIVER_TYPES maps a driver_type string (today only "wda") to a builder that strips out the driver's own config fields from a connection_info dict and routes the rest into extra_capabilities.

openspec/specs/driver-registry/spec.md already documents this mechanism as driver-type-agnostic: "Adding a driver type requires no changes outside the driver layer." Grepping api/console.py and api/rest.py confirms neither contains a "wda" literal — the API layer resolves driver_type purely through the registry, so this change is expected to be additive to driver/ alone.

This is exactly the case the project's Hexagonal + DDD governance decision calls out: core/driver/device/tools must stay free of LLM/HTTP-framework/MCP dependencies, and every new driver's design must state how it preserves that boundary. docs/MACOS_IPHONE_SETUP.md §1 already documents Android as the known, not-yet-registered gap this change closes.

Goals / Non-Goals

Goals:

  • Add a second real Driver implementation (AndroidDriver, Appium UiAutomator2) proving the registry extension point generalizes beyond WDA.
  • Keep the change additive: no edits to api/, device/, tools/, or runtime/.
  • Bring the new driver's test coverage above the existing WDA bar (mocked unit tests, not just a hardware-gated integration test).

Non-Goals:

  • A full Android SDK/adb/real-device setup guide (docs/ANDROID_SETUP.md or equivalent) — deferred to a follow-up change once the driver can be validated against real hardware (user-confirmed scope decision).
  • Espresso driver support — UiAutomator2 only, mirroring WDA's XCUITest-only scope.
  • Renaming APEX_WDA_* env vars — an unrelated, already-documented follow-up item in docs/MACOS_IPHONE_SETUP.md §12.
  • Any change to Driver's abstract interface — the existing method set is sufficient; Android does not need new capabilities the ABC doesn't already express.

Decisions

  • 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.
  • 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

  • [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)), 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.
  5. Add the driver-registry spec delta scenario for driver_type="uiautomator2".
  6. Correct docs/MACOS_IPHONE_SETUP.md §1's now-outdated "Android not registered" statement.
  7. Run the full non-integration test suite (uv run --all-packages pytest -m "not integration") and confirm no regressions.

Rollback: everything is additive (one new driver module, one new registry entry, new test files, a one-paragraph doc correction). No data/schema migration. Rollback is deleting the new files and reverting the doc line.

Open Questions

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.