Skip to content

Instantly share code, notes, and snippets.

@tai2
Created June 4, 2026 01:09
Show Gist options
  • Select an option

  • Save tai2/6ba9e520e3599788462db8ff103da9ea to your computer and use it in GitHub Desktop.

Select an option

Save tai2/6ba9e520e3599788462db8ff103da9ea to your computer and use it in GitHub Desktop.

Research: Appium's supported commands / messages for XCUITestDriver and UIAutomator2Driver, assuming WebdriverIO as the client interface

Context note. The project root /Users/tai2/aco is currently a TypeScript CLI scaffold (commander.js based) with no Appium‑related source yet. The file plan.md is empty. There is therefore no in‑repo implementation to trace; this report is a topical/architectural deep‑dive into the subject requested. It captures the full surface area of commands that flow from a WebdriverIO test through the Appium server into the XCUITest driver (iOS) and UIAutomator2 driver (Android), with attention to the wire format, the legacy/W3C/mobile: triplication, and the practical edge cases. No changes are proposed and nothing is implemented.


1. Purpose

The goal of this research is to understand, at the level of "what bytes go on the wire," the complete catalog of commands and protocol messages that an Appium‑backed mobile‑automation client (specifically WebdriverIO) can send to a running Appium 2.x/3.x server, when that server is brokering for the two officially‑supported, in‑tree drivers for the dominant mobile platforms:

  • XCUITest driver (appium-xcuitest-driver) — iOS / iPadOS / tvOS, backed by Apple's XCTest / XCUITest framework via the bundled WebDriverAgent runner (a sub‑process Appium spins up and proxies HTTP requests to).
  • UIAutomator2 driver (appium-uiautomator2-driver) — Android, backed by Google's UIAutomator2 framework via the bundled appium-uiautomator2-server APK that Appium installs on the device and proxies HTTP requests to.

Each driver also inherits from a base driver: XCUITest from @appium/base-driver directly, UIAutomator2 from appium-android-driver (which itself extends @appium/base-driver). The complete command surface is therefore the union of three method maps per driver:

  1. The W3C WebDriver protocol routes (and a small set of legacy JSON Wire Protocol routes still honoured for backward compatibility), defined in @appium/base-driver's routes.js / method-map.
  2. The driver's own custom REST routes (the newMethodMap export — used to add wholly‑new HTTP endpoints), generally a small handful.
  3. The driver's executeMethodMap — the set of "mobile: …" script names callable through the standard W3C POST /session/:sessionId/execute/sync endpoint. This is where almost all modern, driver‑specific functionality lives.

Understanding this triplication is the single most important takeaway from this research: the exact same logical action (for example, "lock the device") is reachable through three different wire encodings depending on the Appium and driver version, the client, and whether the client is up‑to‑date. WebdriverIO's high‑level mobile commands (e.g. browser.lock()) explicitly hide this by attempting the modern path first and falling back to the deprecated endpoint when the driver doesn't advertise the mobile: extension.


2. Architecture

2.1 The full request flow (WebdriverIO → device)

+--------------------------------------------------------------+
|  User test file                                              |
|    await driver.$('~Login').click()                          |
|    await driver.execute('mobile: swipe', { direction: 'up' })|
+----------------------------+---------------------------------+
                             |
                             v
+--------------------------------------------------------------+
|  WebdriverIO client (Node.js)                                |
|  - Builds the command prototype by merging protocols:        |
|      WebDriver + WebDriverBidi                               |
|      + (if mobile) AppiumProtocol + MJsonWP                  |
|      + (if vendor matches) Chromium/Gecko/Sauce/...          |
|  - Each protocol entry maps a JS method name to an HTTP      |
|    method + URL template + payload schema                    |
|  - HTTP request (typically POST, JSON body) is dispatched    |
|    via undici/fetch to the Appium server's base URL          |
+----------------------------+---------------------------------+
                             | HTTP/1.1 JSON
                             v
+--------------------------------------------------------------+
|  Appium server (Node.js, Express-based)                      |
|  - Matches request URL against routes from                   |
|    @appium/base-driver `METHOD_MAP` + driver's `newMethodMap`|
|  - Looks up driver method name, validates payload, calls it  |
|  - For /execute/sync with script "mobile: X":                |
|       1. base-driver dispatches to driver.execute()          |
|       2. driver.execute checks `executeMethodMap['mobile: X']|
|       3. driver invokes the resolved instance method         |
+----------------------------+---------------------------------+
                             |
              +--------------+--------------+
              |                             |
              v                             v
+----------------------+        +-------------------------+
| XCUITest driver      |        | UIAutomator2 driver     |
|  (Node.js)           |        |  (Node.js)              |
|                      |        |                         |
|  Spawns + proxies to |        |  Installs + proxies to  |
|  WebDriverAgent      |        |  appium-uiautomator2-   |
|  (Xcode test runner) |        |  server APK             |
|  on simulator/device |        |  via adb forward        |
+----------+-----------+        +-----------+-------------+
           | XCTest HTTP                    | UIAutomator2 HTTP
           v                                v
   iOS device / Simulator           Android device / Emulator

2.2 The three protocol layers exposed to a client

A WebdriverIO test using appium:platformName: 'iOS' or 'Android' capabilities receives a hybrid command surface composed of:

Layer Where it lives Encoded as Examples
W3C WebDriver protocol @appium/base-driver/lib/protocol/routes.js POST /session/.../actions, GET /session/.../element/:id/text, etc. findElement, elementClick, performActions, getPageSource, takeScreenshot, alerts, cookies, timeouts, frames, windows, executeScript
Legacy Mobile JSON Wire Protocol (MJSONWP) — deprecated but still routed Same routes.js, paths under /session/.../appium/* POST /session/.../appium/device/lock, POST /session/.../appium/device/install_app, etc. lock, unlock, installApp, pushFile, pullFile, hideKeyboard, pressKeyCode, getStrings, …
mobile: execute extensions — the modern surface Each driver's executeMethodMap POST /session/.../execute/sync with { "script": "mobile: <name>", "args": [<params>] } mobile: tap, mobile: scroll, mobile: alert, mobile: swipeGesture, mobile: installApp, mobile: shell (Android), mobile: runXCTest (iOS), mobile: siriCommand, …

WebdriverIO knows about all three layers and exposes them as JavaScript methods on the browser / driver object — see §4 for the WebdriverIO side in detail.

2.3 Driver-specific sub‑architectures

XCUITest driver (iOS). The driver process launches WebDriverAgent (WDA) — a small XCUITest‑based HTTP server compiled and installed on the simulator or real device via xcodebuild/xcrun. WDA exposes its own near‑W3C HTTP API; the XCUITest driver's strategy for most W3C commands is to proxy the request straight through to WDA after light translation. The bulk of the driver's own Node code is:

  • Bringing WDA up (installing, code‑signing, building, port‑forwarding via iproxy/tidevice).
  • Translating Appium's payloads to WDA's (similar but not identical).
  • Implementing things WDA cannot do alone: simulator control (simctl, applesimutils), permission/clipboard/biometric simulation, app management on real devices via appium-ios-device/go-ios, Siri, AppleScript, performance recording via instruments, screen recording, push notifications, certificates, audio (ffmpeg), and the entire mobile: extension surface.

UIAutomator2 driver (Android). The driver installs two APKs on the device — the UIAutomator2 server (a test runner that wraps Android's UIAutomator2 framework over HTTP) and a helper "Settings" APK that provides things the UIAutomator2 framework alone cannot do (toast inspection, clipboard write, notifications dump, Unicode IME, network/Wi‑Fi toggling on newer Android). A TCP port is then forwarded over adb forward. Most W3C commands are proxied through to the server APK; everything else is implemented driver‑side using adb shell … and appium-adb. Like XCUITest, the driver inherits a large pre‑existing pool of commands from the parent appium-android-driver module, so the full mobile: catalog spans both modules.

2.4 The capabilities contract that selects this surface

The choice of driver — and therefore the realized set of commands — is made at session‑creation time by the POST /session payload (W3C capabilities.alwaysMatch / firstMatch). The key vendor‑prefixed appium: capabilities are:

  • platformName — "iOS", "Android" (case‑sensitive in some clients), "tvOS", etc. Required.
  • appium:automationName — "XCUITest" or "UiAutomator2". Required to disambiguate from older drivers ("UiAutomator1", "Instruments", "Espresso", "Mac2", etc.).
  • appium:deviceName, appium:platformVersion, appium:udid — device targeting.
  • appium:app / appium:bundleId (iOS) / appium:appPackage + appium:appActivity (Android) — what to launch.
  • A long tail of driver‑specific tuning capabilities (e.g. appium:wdaLocalPort, appium:systemPort, appium:noReset, appium:fullReset, appium:autoGrantPermissions, appium:locale, …).

WebdriverIO accepts these as capabilities in its config; the W3C spec demands the appium: prefix on any non‑standard cap, and recent WebdriverIO versions enforce it.


3. Key Files (in the upstream Appium projects)

Repository Path Role
appium/appium-base-driver (legacy) — moved into the monorepo as appium/appium packages/base-driver lib/protocol/routes.js Single source of truth for the W3C + legacy MJSONWP wire routes (~250 endpoints). Defines METHOD_MAP, route matcher, payload validators.
appium/appium-base-driver lib/basedriver/driver.js The BaseDriver class implementing execute() (dispatches mobile: to executeMethodMap), session lifecycle, settings, event log, timeouts.
appium/appium-xcuitest-driver lib/driver.ts (and previously .js) XCUITest driver class. Imports its own executeMethodMap and newMethodMap, wires WDA proxying, defines per‑command instance methods like mobileTap, mobileSwipe.
appium/appium-xcuitest-driver lib/execute-method-map.ts The ~99‑entry registry of every mobile: … command the XCUITest driver supports. See §5 for the full extracted list.
appium/appium-xcuitest-driver lib/method-map.ts (or method-map.js) Custom HTTP routes added on top of base‑driver routes (e.g. deprecated POST /session/.../appium/device/get_clipboard, the log routes).
appium/appium-xcuitest-driver lib/commands/*.ts (alert, app-install, app-management, biometric, certificate, clipboard, context, device-info, element, file-movement, find, gesture, geolocation, hid-event, iohid, keyboard, keychains, localization, location, lock, log, memory, navigation, network-monitor, notifications, pasteboard, performance, permissions, recordscreen, screenshots, simctl, source, timeouts, web, xctest, xctest-record-screen, …) Implementation files for every command grouping. Each exports instance methods that get bound on the driver class.
appium/appium-xcuitest-driver lib/commands/wda/*, lib/commands/bidi/*, lib/commands/helpers/* WDA bootstrap, WebDriver BiDi events, shared utilities.
appium/appium-uiautomator2-driver lib/driver.ts UIAutomator2 driver class. Extends AndroidDriver (from appium-android-driver), so the visible execute map = parent map ∪ this map.
appium/appium-uiautomator2-driver lib/execute-method-map.ts The 32 UIAutomator2‑specific mobile: … entries (gestures, deepLink, alerts, clipboard, action scheduling, viewport, list windows/displays, etc.).
appium/appium-uiautomator2-driver lib/method-map.ts Custom routes: deprecated POST .../appium/device/get_clipboard, and POST /session/.../log + GET .../log/types.
appium/appium-uiautomator2-driver lib/commands/{actions,alert,app-management,aut,battery,clipboard,element,find,gestures,keyboard,misc,navigation,screenshot,types,viewport,windows}.ts Implementation files.
appium/appium-android-driver (parent) lib/execute-method-map.ts The much larger 60+‑entry catalog inherited by UIAutomator2: shell, telephony (gsmCall, gsmSignal, sendSms), power, sensors, performance data, locale, screen streaming, broadcasts, services, activities, notifications, fingerprint, bluetooth, NFC, UI mode, etc.
webdriverio/webdriverio packages/webdriver/src/protocols/{webdriver,appium,mjsonwp,webdriverBidi}.json The static JSON definitions that map JS method names to HTTP method + URL + parameter schema. Loaded at runtime; one branch per protocol layer.
webdriverio/webdriverio packages/webdriver/src/utils.ts → getPrototype() The function that decides, per session, which protocol JSONs to merge — and therefore which methods appear on the browser object. The mobile path merges webdriver + appium + mjsonwp.
webdriverio/webdriverio packages/webdriverio/src/commands/mobile/*.ts The high‑level cross‑platform mobile commands (the API listed at https://webdriver.io/docs/api/mobile/). Each command internally branches on isIOS/isAndroid and dispatches the appropriate underlying mobile: execute or W3C action.
webdriverio/webdriverio packages/wdio-appium-service/src/launcher.ts The Appium service that spawns the Appium server as a child process for the duration of a test run.

4. Data flow — the three command "shapes" on the WebdriverIO side

WebdriverIO exposes the Appium surface to user tests through three distinct API tiers. Knowing which tier any given command belongs to is essential for understanding what actually goes on the wire.

4.1 Tier A — generic W3C WebDriver commands (also work in browsers)

All standard W3C WebDriver commands are available unchanged in a mobile session because Appium implements the W3C spec. They are produced by the protocols/webdriver.json table in WebdriverIO. The complete extracted list (from appium.io/docs/en/3.0/reference/api/webdriver/):

JS method (WebdriverIO) HTTP URL template
createSession POST /session
deleteSession DELETE /session/:sessionId
getStatus GET /status
getTimeouts / setTimeouts GET / POST /session/:sessionId/timeouts
navigateTo / getCurrentUrl POST / GET /session/:sessionId/url
back, forward, refresh POST /session/:sessionId/{back,forward,refresh}
getTitle GET /session/:sessionId/title
getWindowHandle, closeWindow, switchToWindow, getWindowHandles, newWindow GET/DELETE/POST /session/:sessionId/window[/handle[s]|/new]
switchToFrame, switchToParentFrame POST /session/:sessionId/frame[/parent]
getWindowRect, setWindowRect, maximizeWindow, minimizeWindow, fullscreenWindow GET/POST /session/:sessionId/window/{rect,maximize,minimize,fullscreen}
findElement, findElements, findElementFromElement, findElementsFromElement, findElementFromShadowRoot, findElementsFromShadowRoot POST /session/:sessionId/[element|elements][/...][/element[s]]
getActiveElement, getShadowRoot GET /session/:sessionId/element/{active,:id/shadow}
isElementSelected, isElementDisplayed, isElementEnabled GET /session/:sessionId/element/:id/{selected,displayed,enabled}
getElementAttribute, getElementProperty, getElementCssValue GET /session/:sessionId/element/:id/{attribute,property,css}/:name
getElementText, getElementTagName, getElementRect GET /session/:sessionId/element/:id/{text,name,rect}
getComputedRole, getComputedLabel GET /session/:sessionId/element/:id/{computedrole,computedlabel}
elementClick, elementClear, elementSendKeys POST /session/:sessionId/element/:id/{click,clear,value}
getPageSource GET /session/:sessionId/source
executeScript, executeAsyncScript POST /session/:sessionId/execute/{sync,async}
getCookies, getNamedCookie, addCookie, deleteCookie, deleteAllCookies GET/POST/DELETE /session/:sessionId/cookie[/:name]
performActions, releaseActions POST/DELETE /session/:sessionId/actions
dismissAlert, acceptAlert, getAlertText, sendAlertText POST/GET /session/:sessionId/alert/{dismiss,accept,text}
takeScreenshot, takeElementScreenshot GET /session/:sessionId/[element/:id/]screenshot

In mobile context the semantics change for some of these (e.g. findElement accepts the special -ios predicate string, -ios class chain, -android uiautomator, accessibility id, -android datamatcher, -image locator strategies; performActions is the modern way to express touch gestures via pointerType: 'touch').

4.2 Tier B — Appium‑specific commands surfaced as first‑class WebdriverIO methods

These are the methods documented at webdriver.io/docs/api/appium/. They predate the modern mobile: extensions and target the legacy MJSONWP/Appium routes (/session/:sessionId/appium/...). They are still available because the routes themselves are still served by Appium 2.x — but the underlying handlers in modern drivers often just dispatch to the equivalent mobile: extension internally.

Catalog (extracted from the WebdriverIO Appium API page):

Session & introspection: getLogTypes, getLogs(type), getAppiumContext (alias of getCurrentContext), switchAppiumContext, getAppiumContexts, getAppiumSessionCapabilities (GET /session/:sessionId), getAppiumCommands (returns the route registry), getAppiumExtensions (returns the executeMethodMap entries advertised by the driver — the canonical way to discover what mobile: calls are available at runtime).

Device control: appiumShake, appiumLock(seconds) (iOS‑only seconds arg), appiumUnlock, appiumIsLocked, rotateDevice(x,y,z).

Recording: startRecordingScreen(options), stopRecordingScreen(remotePath, user, pass, method).

Performance (Android): appiumGetPerformanceDataTypes, appiumGetPerformanceData(packageName, dataType, dataReadTimeout), appiumGetDisplayDensity, getDeviceTime.

Keyboard/input: hideKeyboard(strategy, key, keyCode, keyName), isKeyboardShown, appiumPressKeyCode(keycode, metastate, flags), appiumLongPressKeyCode, appiumSendKeyEvent.

App lifecycle: installApp(appPath|appId, options), activateApp, removeApp, terminateApp, isAppInstalled, appiumQueryAppState (returns 0/1/2/3/4 — not installed / not running / background suspended / background / foreground), appiumLaunchApp, appiumCloseApp, appiumBackground(seconds), appiumGetStrings(language, stringFile).

Android‑specific: appiumGetCurrentActivity, appiumGetCurrentPackage, appiumStartActivity, appiumGetSystemBars, appiumOpenNotifications, appiumToggleAirplaneMode, appiumToggleData, appiumToggleWiFi, appiumToggleLocationServices, appiumToggleNetworkSpeed(netspeed).

File ops: pushFile(path, data), pullFile(path), pullFolder(path).

iOS‑specific (simulator): appiumTouchId(match), appiumToggleEnrollTouchId(enabled).

Settings: getSettings, updateSettings(settings) — read/write the per‑session driver settings (e.g. mjpegServerScreenshotQuality, ignoreUnimportantViews, snapshotMaxDepth).

4.3 Tier C — cross‑platform high‑level Mobile commands (webdriver.io/docs/api/mobile/)

These are WebdriverIO's opinionated abstraction layer. Each command is a small Node function that detects the platform from the session capabilities and dispatches to whichever underlying mechanism is best on that platform — most often a mobile: execute call.

Complete catalog (Android/iOS unless noted):

Method Platforms Underlying dispatch (when known from WDIO docs)
background A/i POST .../appium/app/background (legacy) or mobile: backgroundApp
closeApp A/i mobile: terminateApp (iOS) / mobile: terminateApp (A)
deepLink(link, appIdentifier, waitForLaunch) A/i mobile: deepLink on both drivers
dragAndDrop(target, {duration}) A/i W3C performActions with pointerType: 'touch'
fingerPrint(fingerprintId 1‑10) A only mobile: fingerprint (emulator only)
getClipboard({contentType}) A/i mobile: getClipboard (Android only plaintext; iOS supports plaintext|image|url)
setClipboard(content, contentType) A/i mobile: setClipboard
getContext({returnDetailedContext, androidWebviewConnectionRetryTime, androidWebviewConnectTimeout, waitForWebviewMs}) A/i GET /session/.../context; detailed mode adds metadata via mobile: getContexts
getContexts(...) A/i mobile: getContexts; on Android returns {packageName,title,url,webviewPageId}, on iOS {bundleId,title,url}
getCurrentActivity / getCurrentPackage A mobile: getCurrentActivity / mobile: getCurrentPackage
getDisplayDensity A mobile: getDisplayDensity
getPerformanceData(packageName, dataType, dataReadTimeout) A mobile: getPerformanceData
getPerformanceDataTypes A mobile: getPerformanceDataTypes
getStrings(language, stringFile) A/i mobile: getAppStrings
getSystemBars A mobile: getSystemBars
gsmCall(phoneNumber, action) A mobile: gsmCall (emulator)
gsmSignal(strength) A mobile: gsmSignal
gsmVoice(state) A mobile: gsmVoice
isLocked A/i mobile: isLocked
launchApp A/i mobile: activateApp (iOS implicitly) — the WebdriverIO doc warns this is being deprecated in favour of activateApp(appId)
lock(seconds?) A/i mobile: lock (seconds is iOS‑only auto‑unlock)
longPress({x, y, duration}) A/i W3C performActions with a pause; in iOS webview replaced by injected JS
longPressKeyCode A mobile: pressKey with isLongPress: true
openNotifications A mobile: openNotifications
pinch({duration, scale}) A/i mobile: pinchCloseGesture (A) / mobile: pinch (iOS)
powerAC(state) A mobile: powerAc (emulator)
powerCapacity(percent) A mobile: powerCapacity (emulator)
pressKeyCode A mobile: pressKey
queryAppState(appId|bundleId) A/i mobile: queryAppState
relaunchActiveApp A/i composite: mobile: terminateApp + mobile: activateApp
scrollIntoView({direction, maxScrolls, duration, scrollableElement, percent}) A/i builds on swipe repeatedly until the element is in viewport
sendKeyEvent A legacy POST .../appium/device/keyevent
sendSms(phoneNumber, message) A mobile: sendSms (emulator)
shake A/i mobile: shake (iOS) / W3C rotation‑based fallback on A
startActivity(appPackage, appActivity, ...) A mobile: startActivity
swipe({direction, duration, scrollableElement, percent, from, to}) A/i W3C performActions — explicitly NOT mobile: scrollGesture/mobile: scroll; the WDIO doc warns to avoid from/to as they are device‑specific
switchContext(name) A/i POST /session/.../context
tap({direction, maxScrolls, scrollableElement, x, y}) A/i mobile: tap (iOS), mobile: clickGesture (A) for native; W3C click/action for web/hybrid. Auto‑scrolls if element not found.
toggleAirplaneMode, toggleData, toggleLocationServices, toggleNetworkSpeed, toggleWiFi A legacy appium/device/toggle_* or mobile: setConnectivity / mobile: toggleGps
toggleEnrollTouchId, touchId i mobile: enrollBiometric / mobile: sendBiometricMatch (simulator)
unlock A/i mobile: unlock (Android has many strategies: PIN, pattern, password, fingerprint via appium:unlockKey/appium:unlockType)
zoom({duration, scale}) A/i mobile: pinchOpenGesture (A) / mobile: pinch with positive scale (iOS)

A defining trait of Tier C: every command's internal implementation in packages/webdriverio/src/commands/mobile/*.ts follows the pattern "try the mobile: extension; if the driver returns unknown command (Appium error code 405/status:9), fall back to the deprecated Tier B endpoint." This is explicit in the WDIO source for lock, getClipboard, setClipboard, fingerPrint, and others.

4.4 The escape hatch: browser.execute('mobile: …', {…})

For everything not surfaced as a method (the long tail of mobile: entries in §5), WebdriverIO supports calling browser.execute(script, args) — which goes to W3C POST /session/.../execute/sync. When the script string starts with "mobile: ", base‑driver's execute() consults the driver's executeMethodMap, validates the args, and invokes the bound instance method.

await browser.execute('mobile: source', { format: 'xml' })          // both
await browser.execute('mobile: shell', { command: 'getprop' })       // Android
await browser.execute('mobile: siriCommand', { text: 'Open Safari' })// iOS
await browser.execute('mobile: setPermission', {                     // iOS sim
  bundleId: 'com.example',
  access: { camera: 'yes', photos: 'yes' }
})

The args object's keys must match the required / optional arrays in the entry's params definition; passing an unknown key is silently dropped by the validator (base‑driver only checks for missing required keys, not extra ones, which has been a perennial source of "why isn't my param taking effect" bug reports).


5. The complete mobile: … command catalogs

5.1 XCUITest driver — full executeMethodMap (~99 entries, extracted from upstream lib/execute-method-map.ts)

Grouped by theme. Each entry shows 'mobile: name' and the bound implementation method name.

Gestures & input

  • mobile: tap → mobileTap (coords or element)
  • mobile: doubleTap → mobileDoubleTap
  • mobile: twoFingerTap → mobileTwoFingerTap
  • mobile: tapWithNumberOfTaps → mobileTapWithNumberOfTaps (params: numberOfTaps, numberOfTouches)
  • mobile: touchAndHold → mobileTouchAndHold (duration)
  • mobile: forcePress → mobileForcePress (pressure)
  • mobile: swipe → mobileSwipe (direction left/up/right/down on element or coords)
  • mobile: scroll → mobileScroll (direction, predicateString, name, toVisible)
  • mobile: scrollToElement → mobileScrollToElement
  • mobile: pinch → mobilePinch (scale + velocity)
  • mobile: rotateElement → mobileRotateElement
  • mobile: dragFromToForDuration → mobileDragFromToForDuration
  • mobile: dragFromToWithVelocity → mobileDragFromToWithVelocity
  • mobile: selectPickerWheelValue → mobileSelectPickerWheelValue (order next/previous, offset)
  • mobile: pressButton → mobilePressButton (home, volumeup/down, action, camera; tvOS: menu, playpause, select, pageup/down, guide, …)
  • mobile: performIoHidEvent → mobilePerformIoHidEvent (low‑level IOKit HID page/usage)
  • mobile: keys → mobileKeys (sequence of key codes / modifiers, also character strings)
  • mobile: hideKeyboard / mobile: isKeyboardShown
  • mobile: shake → mobileShake

Alerts

  • mobile: alert → mobileHandleAlert (action: accept / dismiss / getButtons; optional buttonLabel)

Pasteboard / clipboard

  • mobile: setPasteboard, mobile: getPasteboard (simulator pasteboard)
  • mobile: setClipboard, mobile: getClipboard (device — plaintext/image/url)

Source / contexts / coordinates

  • mobile: source → mobileGetSource (format: xml / json / description; excludedAttributes)
  • mobile: getContexts → mobileGetContexts (extended info incl URL, title, waitForWebviewMs)
  • mobile: viewportScreenshot / mobile: viewportRect
  • mobile: deviceScreenInfo → getScreenInfo
  • mobile: calibrateWebToRealCoordinatesTranslation
  • mobile: updateSafariPreferences

App lifecycle

  • mobile: installApp (app, timeoutMs, checkVersion)
  • mobile: isAppInstalled, mobile: removeApp
  • mobile: launchApp (bundleId, arguments, environment) — uses XCUIApplication
  • mobile: terminateApp, mobile: killApp (real‑device only, via instruments)
  • mobile: queryAppState (0=not installed, 1=not running, 2=background suspended, 3=background, 4=foreground)
  • mobile: activateApp, mobile: clearApp (simulator‑only)
  • mobile: listApps (real device; applicationType, returnAttributes)
  • mobile: backgroundApp → background
  • mobile: activeAppInfo → mobileGetActiveAppInfo (pid, bundleId, name, processArguments)
  • mobile: getAppStrings → getStrings (language)

Device info / time

  • mobile: deviceInfo (locale, timeZone, name, model, uuid, idiom, style, thermalState)
  • mobile: batteryInfo (level, state, optional advanced)
  • mobile: getDeviceTime (format)
  • mobile: lock, mobile: unlock, mobile: isLocked

Biometrics (simulator)

  • mobile: enrollBiometric (isEnabled)
  • mobile: sendBiometricMatch (type: touchId/faceId; match: bool)
  • mobile: isBiometricEnrolled

Permissions / keychains

  • mobile: getPermission (simulator only, requires applesimutils; result: yes/no/unset/limited)
  • mobile: setPermission (map of services to yes/no/unset)
  • mobile: resetPermission (simulator + real device)
  • mobile: clearKeychains (simulator)

Appearance / a11y

  • mobile: getAppearance / mobile: setAppearance (light/dark)
  • mobile: getIncreaseContrast / mobile: setIncreaseContrast
  • mobile: contentSize / mobile: setContentSize
  • mobile: performAccessibilityAudit

Localization

  • mobile: configureLocalization (keyboard, language, locale maps)

Location

  • mobile: setSimulatedLocation, mobile: getSimulatedLocation, mobile: resetSimulatedLocation
  • mobile: resetLocationService

Notifications / Siri / memory

  • mobile: pushNotification (simulator; bundleId + APNS‑like payload)
  • mobile: expectNotification (name, type: plain/darwin, timeoutSeconds)
  • mobile: siriCommand (text)
  • mobile: sendMemoryWarning (iOS 17+ real devices)

Performance / Instruments

  • mobile: startPerfRecord, mobile: stopPerfRecord (returns base64 .trace or remote upload)
  • mobile: listConditionInducers, mobile: enableConditionInducer, mobile: disableConditionInducer (network throttling, thermal pressure, etc.)
  • mobile: startNetworkMonitor, mobile: stopNetworkMonitor (iOS/tvOS 18+, real devices)
  • mobile: startLogsBroadcast, mobile: stopLogsBroadcast (syslog → WebSocket)

Audio / screen recording

  • mobile: startAudioRecording (requires ffmpeg)
  • mobile: stopAudioRecording
  • mobile: startScreenRecording, mobile: stopScreenRecording
  • mobile: startXCTestScreenRecording, mobile: getXCTestScreenRecordingInfo, mobile: stopXCTestScreenRecording

XCTest runner (real devices, iOS/tvOS 18+; needs appium-ios-remotexpc)

  • mobile: runXCTest, mobile: installXCTestBundle, mobile: listXCTestBundles

Certificates (real devices, iOS/tvOS 18+)

  • mobile: installCertificate, mobile: removeCertificate, mobile: listCertificates

Files

  • mobile: pushFile, mobile: pullFile, mobile: pullFolder
  • mobile: deleteFile, mobile: deleteFolder

Other

  • mobile: deepLink (open URL)
  • mobile: simctl (raw simctl subcommand passthrough — simulator only)

5.2 UIAutomator2 driver — full executeMethodMap (inherited from appium-android-driver ∪ own)

Direct entries defined in appium-uiautomator2-driver/lib/execute-method-map.ts (32, verified from source):

  • mobile: dragGesture, mobile: flingGesture, mobile: doubleClickGesture, mobile: clickGesture, mobile: longClickGesture, mobile: pinchCloseGesture, mobile: pinchOpenGesture, mobile: swipeGesture, mobile: scrollGesture
  • mobile: scrollBackTo, mobile: scroll (UiSelector strategy + selector)
  • mobile: viewportScreenshot, mobile: viewportRect
  • mobile: deepLink
  • mobile: acceptAlert, mobile: dismissAlert
  • mobile: batteryInfo, mobile: deviceInfo
  • mobile: openNotifications
  • mobile: type (Unicode IME, requires Settings APK), mobile: replaceElementValue
  • mobile: installMultipleApks
  • mobile: pressKey (keycode, metastate, flags, isLongPress, source)
  • mobile: screenshots (per displayId)
  • mobile: scheduleAction, mobile: getActionHistory, mobile: unscheduleAction (run background gestures periodically — Appium 2.5+)
  • mobile: setClipboard, mobile: getClipboard
  • mobile: resetAccessibilityCache
  • mobile: listWindows, mobile: listDisplays

Entries inherited from appium-android-driver/lib/execute-method-map.ts (60+, verified from source):

ADB / emulator console

  • mobile: shell (command, args, timeout, includeStderr)
  • mobile: execEmuConsoleCommand

Logs / WS

  • mobile: startLogsBroadcast, mobile: stopLogsBroadcast

Permissions / IME

  • mobile: changePermissions (permissions, appPackage, action grant/revoke, target pm/appops)
  • mobile: getPermissions (type: granted/denied/requested)
  • mobile: performEditorAction

Time

  • mobile: getDeviceTime (format)

Screen streaming (GStreamer MJPEG)

  • mobile: startScreenStreaming (width, height, bitRate, host, port, pathname, tcpPort, quality, considerRotation, logPipelineDetails)
  • mobile: stopScreenStreaming

Notifications / SMS

  • mobile: getNotifications
  • mobile: listSms (default max 100)

File ops

  • mobile: pushFile, mobile: pullFolder, mobile: pullFile, mobile: deleteFile

App lifecycle

  • mobile: isAppInstalled, mobile: listApps, mobile: queryAppState
  • mobile: activateApp, mobile: removeApp, mobile: terminateApp, mobile: installApp (allowTestPackages, useSdcard, grantPermissions, replace, noIncremental)
  • mobile: clearApp, mobile: backgroundApp
  • mobile: startService, mobile: stopService, mobile: startActivity, mobile: broadcast

Contexts

  • mobile: getContexts (waitForWebviewMs)
  • mobile: getChromeCapabilities

Lock / device

  • mobile: lock, mobile: unlock (key, type pin/pattern/password/fingerprint, strategy locksettings/uiautomator, timeoutMs)
  • mobile: isLocked

Location

  • mobile: refreshGpsCache
  • mobile: setGeolocation (lat, lon, altitude, satellites, speed, bearing, accuracy)
  • mobile: getGeolocation, mobile: resetGeolocation
  • mobile: toggleGps, mobile: isGpsEnabled

Recording (MediaProjection)

  • mobile: startMediaProjectionRecording, mobile: isMediaProjectionRecordingRunning, mobile: stopMediaProjectionRecording

Connectivity / radios

  • mobile: getConnectivity / mobile: setConnectivity (wifi, data, airplaneMode)
  • mobile: bluetooth, mobile: nfc

Keyboard

  • mobile: hideKeyboard, mobile: isKeyboardShown

Power / doze / memory / sensors

  • mobile: deviceidle (action whitelistAdd/Remove, packages)
  • mobile: sendTrimMemory (pkg, level)
  • mobile: powerAc, mobile: powerCapacity, mobile: networkSpeed, mobile: sensorSet (acceleration, light, proximity, magnetic-field, …)

UI mode

  • mobile: setUiMode, mobile: getUiMode (night, car)

Camera (emulator only)

  • mobile: injectEmulatorCameraImage

Performance

  • mobile: getPerformanceData, mobile: getPerformanceDataTypes

Display / a11y / status bar

  • mobile: getDisplayDensity, mobile: getSystemBars
  • mobile: statusBar (expandNotifications, expandSettings, collapse, …)

Biometric (emulator) / telephony

  • mobile: fingerprint (fingerprintId 1‑10)
  • mobile: sendSms, mobile: gsmCall, mobile: gsmSignal, mobile: gsmVoice

Misc

  • mobile: getCurrentActivity, mobile: getCurrentPackage
  • mobile: setStylusHandwriting
  • mobile: getAppStrings

5.3 Side‑by‑side: how the same logical action differs

Logical action XCUITest UIAutomator2
Tap at coords / element mobile: tap (XCUI gesture) mobile: clickGesture (UiAutomator2 gesture)
Swipe within element mobile: swipe {direction} mobile: swipeGesture {direction, percent}
Scroll to find element mobile: scroll {predicateString, toVisible: true} mobile: scroll {strategy: '-android uiautomator', selector}
Long press mobile: touchAndHold {duration} mobile: longClickGesture {duration}
Pinch in / zoom out mobile: pinch {scale<1} mobile: pinchCloseGesture {percent}
Pinch out / zoom in mobile: pinch {scale>1} mobile: pinchOpenGesture {percent}
Drag & drop mobile: dragFromToForDuration or mobile: dragFromToWithVelocity mobile: dragGesture
Source as JSON not XML mobile: source {format: 'json'} not directly supported (XML only)
Hardware button mobile: pressButton {name: 'home'} adb shell input keyevent via mobile: pressKey
Keyboard sequence mobile: keys mobile: type (Settings APK IME) or mobile: pressKey
Background app mobile: backgroundApp mobile: backgroundApp (legacy background)
Biometric match mobile: sendBiometricMatch (sim) mobile: fingerprint (emu)
Install certificate mobile: installCertificate (iOS 18+ real) adb shell to settings (no mobile: shortcut)
Shell access not available (sandbox) mobile: shell
Get clipboard image mobile: getClipboard {contentType: 'image'} not supported (plaintext only)

6. Data flow worked example — await browser.lock(5) on an iPhone simulator

  1. WebdriverIO command lock is invoked. The mobile command file branches to call:
    return await browser.execute('mobile: lock', { seconds: 5 })
  2. Internally execute builds the W3C payload:
    POST /session/<id>/execute/sync
    Content-Type: application/json
    { "script": "mobile: lock", "args": [{ "seconds": 5 }] }
  3. The Appium server routes /execute/sync to BaseDriver.execute(script, args).
  4. BaseDriver.execute sees the "mobile: " prefix and looks up XCUITestDriver.executeMethodMap['mobile: lock'] → { command: 'lock', params: { optional: ['seconds'] } }.
  5. It validates the args against params, then calls xcuitestDriver.lock(5).
  6. lock(seconds) in lib/commands/lock.ts proxies to WDA at POST /session/<wda-id>/wda/lock with { seconds: 5 }.
  7. WDA invokes XCTest's XCUIDevice.shared.perform(.lock) and schedules an unlock after 5 s.
  8. Result bubbles back: WDA → driver → base‑driver → Express response → WebdriverIO undici client → user awaits resolved Promise (undefined).

If the driver were too old and lock were not in the executeMethodMap, step 4 would return Appium's unknown command (script) error (code 405). WebdriverIO's lock implementation catches that exact error and falls back to the legacy:

POST /session/<id>/appium/device/lock
{ "seconds": 5 }

which base-driver's routes.js maps to command: 'lock' directly (see §3 file routes.js). Same endpoint name from the driver's standpoint — only the wire encoding differs.

This double‑path design is pervasive — see the WDIO source comment "Falls back to the deprecated Appium 2 protocol endpoint if the driver does not support the mobile: execute method."


7. Dependencies

A complete dependency tree for the system under research:

WebdriverIO side (client)

Package Role
webdriverio High‑level API, including Tier C mobile commands
@wdio/protocols Static JSON dictionaries: webdriver.json, appium.json, mjsonwp.json, webdriverBidi.json, chromium.json, …
webdriver (the npm package) Low‑level HTTP client, request/response codec, error mapping, prototype construction via getPrototype()
@wdio/appium-service Optional: spawns Appium as a child process, manages args (camelCase → --kebab-case)
Appium client capabilities — 'appium:platformName', 'appium:automationName', etc. Selects driver

Appium server side

Package Role
appium The CLI + server (Express). Owns plugin/driver lifecycle, port binding, session multiplexing.
@appium/base-driver W3C + legacy route map, BaseDriver class with execute, settings, event log, timeouts, image plugin glue.
@appium/types, @appium/support, @appium/docutils Shared types and helpers.
appium-xcuitest-driver The iOS driver. Depends on:
↳ appium-webdriveragent Builds & runs WDA.
↳ appium-ios-device, appium-ios-simulator, node-simctl, appium-ios-remotexpc, appium-idb Real device & simulator control.
appium-uiautomator2-driver The Android driver. Depends on:
↳ appium-android-driver (parent class with the 60+ mobile: extensions)
↳ appium-uiautomator2-server The APK installed on the device.
↳ appium-adb Wrapper around adb.
↳ io.appium.settings APK Helper for clipboard write, Wi‑Fi toggle on newer Android, IME, notifications dump.
Optional plugins (appium-images-plugin, appium-relaxed-caps-plugin, …) Add their own endpoints.

External toolchain

  • iOS: Xcode (xcodebuild, xcrun simctl), applesimutils (for permission/keychain), ffmpeg (audio recording), code‑signing identity.
  • Android: Android SDK platform‑tools (adb), an emulator AVD or USB‑connected device, GStreamer (only if using mobile: startScreenStreaming).

8. Edge cases & specificities

8.1 Three flavours of "the same" command

For dozens of operations, the wire surface is triplicated:

  1. Legacy MJSONWP endpoint (deprecated but still routed): POST /session/:id/appium/device/lock.
  2. Driver‑specific mobile: execute: POST /session/:id/execute/sync with { script: 'mobile: lock', args: [{seconds}] }.
  3. WebdriverIO high‑level wrapper: await browser.lock(seconds) — auto‑falls back from (2) to (1).

The implications:

  • Two different parameter conventions: legacy passes a flat JSON body matching payloadParams, modern passes positional args after the script name where args[0] is the named‑params object. Crossing them produces confusing 400 — payload validation errors.
  • Some legacy endpoints accept both appId and bundleId as alternatives (see [['appId'], ['bundleId']] payload constraint in routes.js), making cross‑platform tests less branchy on the legacy path.
  • The legacy POST /session/.../appium/app/background requires seconds; the modern mobile: backgroundApp makes it optional. Tests that worked before WDIO 8 sometimes break after upgrade because the modern path is now preferred and the missing seconds reaches a different default.

8.2 W3C touch actions vs mobile: gestures

Two completely different gesture systems coexist:

  • W3C Actions (POST /session/.../actions) — a generic sequence of pointer events with pointerType: 'touch' or 'mouse'. Used by WebdriverIO's swipe, longPress, dragAndDrop because they need to work cross‑platform and across web/native/hybrid. The doc explicitly notes: swipe NOT based on mobile: scrollGesture (Android) or mobile: scroll (iOS), but uses W3C Actions.
  • mobile: …Gesture family — driver‑native gestures (XCUITest's XCUIElement methods on iOS; UIAutomator2 UiObject.swipe on Android). More reliable because they speak the platform's native UI Automation layer rather than synthesizing pointer events the OS may interpret differently.

Choosing wrong matters: W3C Actions cannot scroll inside a non‑scrollable container the way mobile: scrollGesture can, and mobile: gestures often fail in webview contexts where W3C click works.

8.3 Element ID format

Appium element IDs are opaque strings that include a fixed prefix the W3C spec defines: element-6066-11e4-a52e-4f735466cecf. Inside the JSON of a findElement response:

{ "value": { "element-6066-11e4-a52e-4f735466cecf": "00000000-0000-0000-19E2-000000000000" } }

The string 00000000-… is what you put into /element/:elementId/click in subsequent requests. UIAutomator2 uses a UUID, XCUITest uses XCTest's hash. Older ELEMENT key still exists for compatibility but W3C clients must read the prefixed key.

8.4 Locator strategies

The W3C using field accepts only standard strategies (css selector, link text, etc.), but Appium drivers extend with:

  • accessibility id — the recommended locator on both platforms; maps to content-desc on Android and name/accessibilityIdentifier on iOS. Fastest, most portable.
  • xpath — works on both but slow and brittle (the page source has to be serialized first). Subject to driver settings enforceXPath1, limitXPathContextScope, snapshotMaxDepth.
  • iOS only: -ios predicate string (NSPredicate over XCUIElement attributes), -ios class chain (an Appium‑invented mini‑language similar to XPath but mapped directly to XCTest hierarchy traversal — much faster than xpath), class name (e.g. XCUIElementTypeButton).
  • Android only: -android uiautomator (UiSelector Java source as a string — the driver compiles it on the server APK), -android viewtag, -android datamatcher, -android viewmatcher (these last two are Espresso‑specific; UIAutomator2 throws).
  • Cross‑platform: -image (provided by the optional appium-images-plugin; matches by image template).

8.5 findElement settings affect response shape

UIAutomator2's setting shouldUseCompactResponses (default true) and elementResponseAttributes decide whether findElement returns just an ID or also text, bounds, className, etc. Changing it during a session changes the JSON shape of every subsequent find — tests that key on response shape can silently break.

8.6 Hybrid app contexts

In a webview context, the driver's HTTP server proxies most requests to an underlying Chromedriver (Android) or Safari remote inspector (iOS) instead of the UIAutomator2/XCUITest server. The exact same WebdriverIO test code now sends findElement requests that are handled by a wholly different stack. Consequences:

  • Some mobile: extensions return unknown command while in webview context (e.g. mobile: swipeGesture).
  • executeScript switches from "execute Node code over the driver" to "execute real JavaScript in the page".
  • Context switch is via POST /session/:id/context (legacy) or mobile: switchContext. iOS also supports getting context metadata via mobile: getContexts with waitForWebviewMs.

8.7 WDA / UIAutomator2 server failure modes

Both drivers are front‑ends; the actual work is done by an HTTP server running inside the device or simulator. If WDA crashes (common when wdaLaunchTimeout is hit) or the UIAutomator2 server is killed by Android's low‑memory killer, every subsequent W3C call returns Appium error 13 Unknown server-side error until the driver auto‑recovers (the XCUITest driver has logic to restart WDA up to N times, controlled by appium:wdaStartupRetries). Tests should be defensive around this — it manifests as flaky failures that aren't reproducible locally.

8.8 Real device vs simulator/emulator capability gaps

Many mobile: commands are explicitly simulator‑only or emulator‑only:

  • iOS simulator only: clearApp, clearKeychains, pushNotification, biometric enrollment, setAppearance, simulator pasteboard, setPermission (real devices only support resetPermission).
  • iOS real device only: killApp (via Instruments), listApps, runXCTest, installCertificate, startNetworkMonitor.
  • Android emulator only: gsmCall, gsmSignal, sendSms, powerAc, powerCapacity, networkSpeed, sensorSet, fingerprint, execEmuConsoleCommand, injectEmulatorCameraImage.
  • Android real device + emulator: almost everything else (shell, gestures, app management, clipboard, …).

Calling a simulator‑only command on a real device returns unsupported operation (Appium error code 405); WebdriverIO surfaces this as WebDriverError: unsupported operation with the device‑specific message.

8.9 Permissions on iOS

Permission modeling differs sharply between simulator and device:

  • Simulator: mobile: setPermission works for calendar, camera, contacts, homekit, microphone, motion, photos, reminders, siri, medialibrary, etc., taking values 'yes'/'no'/'unset' (and 'limited' for photos on iOS 14+). Requires applesimutils binary present on $PATH.
  • Real device: Only mobile: resetPermission is available, and it operates on TCC‑protected resource enums (integers) because Apple doesn't expose grant APIs.

8.10 Permissions / Android scope

UIAutomator2's mobile: changePermissions operates in three regimes that are not equivalent:

  • target: 'pm' calls pm grant/pm revoke — works only for AndroidManifest‑declared dangerous permissions, requires targetSdkVersion >= 23, and a freshly‑installed APK with --grant-all-permissions won't show up unless you query with type: 'requested'.
  • target: 'appops' calls appops set — covers app ops like SYSTEM_ALERT_WINDOW, WRITE_SETTINGS, PICTURE_IN_PICTURE, plus permissions auto‑converted from manifest entries.

8.11 Auto‑scroll inside tap

WebdriverIO's tap has a feature absent from the underlying drivers: if the target element is off‑screen, it automatically calls swipe until either the element comes into view or maxScrolls (default 10) is exhausted. The dispatch underneath is platform‑specific: mobile: tap on iOS, mobile: clickGesture on Android. This convenience is invisible to the user but silently changes test flakiness characteristics: a test that was "always fast" on iOS may become "sometimes does 5 swipes first" on Android because UIAutomator2's clickGesture does not have iOS's automatic scroll‑to‑element behaviour.

8.12 Settings are mutable per session

POST /session/:id/appium/settings accepts a free‑form { settings: {…} } blob. Drivers maintain a sizeable list of these:

UIAutomator2: actionAcknowledgmentTimeout, allowInvisibleElements, ignoreUnimportantViews, elementResponseAttributes, enableMultiWindows, enableTopmostWindowFromActivePackage, enableNotificationListener, keyInjectionDelay, scrollAcknowledgmentTimeout, shouldUseCompactResponses, waitForIdleTimeout, waitForSelectorTimeout, normalizeTagNames, shutdownOnPowerDisconnect, simpleBoundsCalculation, trackScrollEvents, wakeLockTimeout, serverPort, mjpegServerPort, mjpegServerFramerate, mjpegScalingFactor, mjpegServerScreenshotQuality, mjpegBilinearFiltering, useResourcesForOrientationDetection, enforceXPath1, limitXPathContextScope, disableIdLocatorAutocompletion, alwaysTraversableViewClasses, includeExtrasInPageSource, includeA11yActionsInPageSource, snapshotMaxDepth, currentDisplayId.

XCUITest has its own large list (mjpegScalingFactor, screenshotQuality, snapshotMaxDepth, keyboardAutocorrection, keyboardPrediction, acceptAlertButtonSelector, dismissAlertButtonSelector, boundElementsByIndex, …) — none of which are documented in WebdriverIO's API and must be set via browser.updateSettings({...}).

Hidden risk: settings persist for the life of the session. A test that toggles enforceXPath1 for one assertion silently slows down every following XPath find.

8.13 Image plugin / compareImages

POST /session/.../appium/compare_images (legacy route) is implemented by a separate appium-images-plugin since Appium 2. Without the plugin loaded, the WebdriverIO compareImagesByFeatures / compareImagesByOccurrences / compareImagesBySimilarity calls (Tier B Appium API) return unknown command. This is a frequent surprise during upgrades from Appium 1.x where the image comparison commands were built in.

8.14 Async command rejection swallowing

WebdriverIO clients always issue Appium commands asynchronously (async/await). Appium errors come back in W3C shape { value: { error, message, stacktrace } }; the client deserializes into typed WebDriverError subclasses. However, if a mobile: execute returns a 200 OK with a value that happens to be an object with an error field (some legacy drivers do this), WebdriverIO will not treat it as an error — the user code receives a resolved promise whose value contains the error description. This is a known footgun and a reason to prefer the high‑level Tier C wrappers, which normalize.

8.15 BiDi protocol — partial coverage

WebDriver BiDi is being layered onto Appium drivers progressively. XCUITest exposes lib/commands/bidi/* and supports a subset of BiDi event subscriptions (e.g. log entries, network events on iOS 18+). UIAutomator2 has minimal BiDi support. WebdriverIO offers BiDi APIs (browser.on('log.entryAdded', …)) but at the time of writing they no‑op on most Appium sessions. Don't rely on BiDi semantics for mobile.

8.16 Discovering the surface at runtime

The single most useful introspection command for tooling that needs to adapt to whatever the connected driver supports is GET /session/:sessionId/appium/extensions, surfaced in WebdriverIO as await browser.getAppiumExtensions(). It returns the actual executeMethodMap of the active driver including parameter schemas — i.e. exactly the data in §5.1 / §5.2 but pulled live from the server. The companion getAppiumCommands() returns the registered HTTP routes (Tier A + B). Together they answer "what can I call right now" without hard‑coding the driver version.


9. Potential issues / concerns discovered

These are observations from the upstream documentation and source — none are bugs in this repository (there is no implementation here yet); they are things a CLI or wrapper that proxies WebdriverIO → Appium should be defensively aware of:

  1. The catalogs drift constantly. Both executeMethodMaps are released frequently (XCUITest minor versions every few weeks). Any tooling that hard‑codes a list of mobile: commands will go out of sync. Prefer getAppiumExtensions().

  2. Param validation is shallow. Base‑driver only checks that required keys are present; unknown keys are silently dropped without warning. Bug reports of the form "I passed option X and nothing happened" frequently trace to a typo or to a parameter that was removed in a recent driver release without the user noticing.

  3. mobile: script name parsing is whitespace‑sensitive. 'mobile:swipe' (no space) is not the same as 'mobile: swipe'. The base‑driver matcher uses an exact Object.prototype.hasOwnProperty against the registered keys. WDIO docs always show the space; some third‑party guides miss it.

  4. Three identical command names with different semantics: mobile: scroll (XCUITest — scrolls within element by predicate or to visible), mobile: scroll (UIAutomator2 — UiSelector‑based scroll inside scrollable), mobile: scrollGesture (UIAutomator2 — coordinate‑based swipe). It is very easy to copy an example for the wrong driver.

  5. Bundled subordinate binaries get out of date. XCUITest's bundled WebDriverAgent must match the Xcode version on the host; otherwise builds fail with cryptic Swift compiler errors. UIAutomator2 server's compileSdkVersion lags Android releases by several months — calling mobile: extensions that touch Settings.Global on a fresh Android 15 image may fail until a driver bump.

  6. Real‑device test runs require code signing. A subtle but pervasive failure on iOS real devices is that appium:xcodeOrgId and appium:xcodeSigningId capabilities are needed to (re)sign WDA — without them, the session never starts and the W3C createSession returns 500 with a long stack trace. Documentation for this lives in WebDriverAgent's README, not Appium's.

  7. Deep‑link command's waitForLaunch is Android‑only. The cross‑platform WebdriverIO deepLink(link, appIdentifier, waitForLaunch) silently ignores waitForLaunch on iOS, which can mask intent and produce confusing test failures when the URL handler hasn't activated by the time the next command runs.

  8. mobile: source formats diverge. XCUITest's mobile: source supports 'xml', 'json', 'description'. UIAutomator2 only supports XML (the W3C GET /session/.../source). Tools that parse the source must branch on platform.

  9. The legacy appium/app/background route requires a positional seconds. If the client omits it, the route returns 400. The modern mobile: backgroundApp accepts it as optional, defaulting to -1 (never auto‑foreground). Migrating clients between paths needs to account for that.

  10. Element invalidation has different timing semantics. On iOS, an element ID is valid as long as XCUITest's underlying XCUIElement snapshot exists — typically the lifetime of the foregrounded app. On Android, UIAutomator2 generates fresh UUIDs aggressively (every page transition can invalidate them). Tests that cache element references across navigation work on iOS and explode on Android with stale element reference.

  11. Permission reset semantics are scoped to the active app on iOS real devices. Calling mobile: resetPermission { service: 'camera' } resets camera permission for whatever app is foregrounded, not globally. This is undocumented in older driver versions and trips tests that reset permissions while a different app is in front.

  12. Settings APK trust on Android 14+. Several mobile: commands (clipboard write, IME unicode type, toast/notification listener) need the io.appium.settings APK granted WRITE_SECURE_SETTINGS via adb shell pm grant before the test runs. Failing to set this up beforehand makes mobile: setClipboard and mobile: type silently no‑op on some devices.

  13. Webview switching introduces latent JSON Wire Protocol commands. When in webview context, Chromedriver (Android) responds to many old JSONWP endpoints that the native side doesn't. Surprises include executeAsyncScript semantics (different timeout handling) and getTimeouts returning a different shape.

  14. No native multi‑touch on UIAutomator2 above two fingers. UIAutomator2's gesture API is limited to two pointers. performActions with three+ touch pointers is accepted by the driver and proxied to the server, which silently ignores the extras. Tests that emulate three‑finger gestures pass in CI but never actually fire those touches.

  15. mobile: shell is gated by appium:allowInsecure. It (and mobile: execEmuConsoleCommand) require the Appium server to be launched with --allow-insecure adb_shell (or, when run programmatically, allowInsecure: ['adb_shell'] in the server args). The error message is "Insecure feature 'adb_shell' is not enabled" — easy to miss in a stack trace.

  16. The executeMethodMap on UIAutomator2 inherits via spread from AndroidDriver.executeMethodMap. This means a UIAutomator2 release that doesn't pin its appium-android-driver version range tightly can grow or lose commands between patch versions. Any tooling enumerating the catalog at build time should resolve both packages from node_modules of the running server, not from a hard‑coded list.


10. Summary — what this means for a tool consuming WebdriverIO → Appium

Given the goals implied by this project's directory name (aco — likely an Appium‑related command‑line operator) and the prior plan notes that this MVP targets "Appium command‑line operator" functionality on top of WebdriverIO, the salient design constraints from this research are:

  1. The "command surface" is not enumerable statically. It depends on the driver version, the plugin set, and whether the session is currently in native or web context. The right primitive to expose is getAppiumExtensions() + getAppiumCommands(), augmented with WDIO's known Tier C catalog.

  2. Three wire encodings, one operation. A faithful wrapper should normalize on the modern mobile: path (Tier C semantics, Tier A endpoints when applicable) and only fall back to legacy when the driver advertises no mobile: equivalent — exactly mirroring WebdriverIO's own pattern.

  3. iOS/Android command names overlap but their params don't. A command‑line UX that accepts aco tap <selector> must internally know that "tap" routes to mobile: tap (XCUITest, accepts elementId or x/y) or mobile: clickGesture (UIAutomator2, same params but different validation rules).

  4. Discoverability beats completeness. Because the catalog drifts, exposing aco mobile list (or similar) that dumps live getAppiumExtensions() output is more durable than baking in a frozen list.

  5. Settings and capabilities are first‑class. Half of the practical issues users hit (slow XPath, stale elements, IME failures, biometrics) trace to per‑session settings or session‑creation caps. Any operator surface should distinguish "transient action" (mobile: tap) from "persistent tuning" (updateSettings) explicitly in its UX.

  6. Session lifecycle is owned by the operator. Unlike Selenium, Appium sessions take seconds to minutes to come up (WDA install + boot on iOS; APK install + grant on Android). A CLI should explicitly attach to existing sessions (via POST /session with a known session ID, or by reusing a sidecar process) rather than implicitly creating one per invocation.

Sources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment