Research: Appium's supported commands / messages for XCUITestDriver and UIAutomator2Driver, assuming WebdriverIO as the client interface
Context note. The project root
/Users/tai2/acois currently a TypeScript CLI scaffold (commander.js based) with no Appium‑related source yet. The fileplan.mdis 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.
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 /XCUITestframework 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:
- 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'sroutes.js/method-map. - The driver's own custom REST routes (the
newMethodMapexport — used to add wholly‑new HTTP endpoints), generally a small handful. - The driver's
executeMethodMap— the set of"mobile: …"script names callable through the standard W3CPOST /session/:sessionId/execute/syncendpoint. 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.
+--------------------------------------------------------------+
| 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
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.
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 viaappium-ios-device/go-ios, Siri, AppleScript, performance recording viainstruments, screen recording, push notifications, certificates, audio (ffmpeg), and the entiremobile: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.
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.
| 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. |
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.
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').
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).
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.
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.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→mobileDoubleTapmobile: twoFingerTap→mobileTwoFingerTapmobile: 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→mobileScrollToElementmobile: pinch→mobilePinch(scale + velocity)mobile: rotateElement→mobileRotateElementmobile: dragFromToForDuration→mobileDragFromToForDurationmobile: dragFromToWithVelocity→mobileDragFromToWithVelocitymobile: 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: isKeyboardShownmobile: 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: viewportRectmobile: deviceScreenInfo→getScreenInfomobile: calibrateWebToRealCoordinatesTranslationmobile: updateSafariPreferences
App lifecycle
mobile: installApp(app,timeoutMs,checkVersion)mobile: isAppInstalled,mobile: removeAppmobile: launchApp(bundleId,arguments,environment) — uses XCUIApplicationmobile: terminateApp,mobile: killApp(real‑device only, viainstruments)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→backgroundmobile: 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, requiresapplesimutils; 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: setIncreaseContrastmobile: contentSize/mobile: setContentSizemobile: performAccessibilityAudit
Localization
mobile: configureLocalization(keyboard, language, locale maps)
Location
mobile: setSimulatedLocation,mobile: getSimulatedLocation,mobile: resetSimulatedLocationmobile: 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.traceor 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: stopAudioRecordingmobile: startScreenRecording,mobile: stopScreenRecordingmobile: 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: pullFoldermobile: deleteFile,mobile: deleteFolder
Other
mobile: deepLink(open URL)mobile: simctl(rawsimctlsubcommand passthrough — simulator only)
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: scrollGesturemobile: scrollBackTo,mobile: scroll(UiSelector strategy + selector)mobile: viewportScreenshot,mobile: viewportRectmobile: deepLinkmobile: acceptAlert,mobile: dismissAlertmobile: batteryInfo,mobile: deviceInfomobile: openNotificationsmobile: type(Unicode IME, requires Settings APK),mobile: replaceElementValuemobile: installMultipleApksmobile: 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: getClipboardmobile: resetAccessibilityCachemobile: 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: getNotificationsmobile: listSms(default max 100)
File ops
mobile: pushFile,mobile: pullFolder,mobile: pullFile,mobile: deleteFile
App lifecycle
mobile: isAppInstalled,mobile: listApps,mobile: queryAppStatemobile: activateApp,mobile: removeApp,mobile: terminateApp,mobile: installApp(allowTestPackages, useSdcard, grantPermissions, replace, noIncremental)mobile: clearApp,mobile: backgroundAppmobile: 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: refreshGpsCachemobile: setGeolocation(lat, lon, altitude, satellites, speed, bearing, accuracy)mobile: getGeolocation,mobile: resetGeolocationmobile: 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: getSystemBarsmobile: statusBar(expandNotifications, expandSettings, collapse, …)
Biometric (emulator) / telephony
mobile: fingerprint(fingerprintId 1‑10)mobile: sendSms,mobile: gsmCall,mobile: gsmSignal,mobile: gsmVoice
Misc
mobile: getCurrentActivity,mobile: getCurrentPackagemobile: setStylusHandwritingmobile: getAppStrings
| 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) |
- WebdriverIO command
lockis invoked. The mobile command file branches to call:return await browser.execute('mobile: lock', { seconds: 5 })
- Internally
executebuilds the W3C payload:POST /session/<id>/execute/sync Content-Type: application/json { "script": "mobile: lock", "args": [{ "seconds": 5 }] }
- The Appium server routes
/execute/synctoBaseDriver.execute(script, args). BaseDriver.executesees the"mobile: "prefix and looks upXCUITestDriver.executeMethodMap['mobile: lock']→{ command: 'lock', params: { optional: ['seconds'] } }.- It validates the args against
params, then callsxcuitestDriver.lock(5). lock(seconds)inlib/commands/lock.tsproxies to WDA atPOST /session/<wda-id>/wda/lockwith{ seconds: 5 }.- WDA invokes XCTest's
XCUIDevice.shared.perform(.lock)and schedules an unlock after 5 s. - 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."
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 usingmobile: startScreenStreaming).
For dozens of operations, the wire surface is triplicated:
- Legacy MJSONWP endpoint (deprecated but still routed):
POST /session/:id/appium/device/lock. - Driver‑specific
mobile:execute:POST /session/:id/execute/syncwith{ script: 'mobile: lock', args: [{seconds}] }. - 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 positionalargsafter the script name whereargs[0]is the named‑params object. Crossing them produces confusing400 — payload validationerrors. - Some legacy endpoints accept both
appIdandbundleIdas alternatives (see[['appId'], ['bundleId']]payload constraint inroutes.js), making cross‑platform tests less branchy on the legacy path. - The legacy
POST /session/.../appium/app/backgroundrequiresseconds; the modernmobile: backgroundAppmakes it optional. Tests that worked before WDIO 8 sometimes break after upgrade because the modern path is now preferred and the missingsecondsreaches a different default.
Two completely different gesture systems coexist:
- W3C Actions (
POST /session/.../actions) — a generic sequence of pointer events withpointerType: 'touch'or'mouse'. Used by WebdriverIO'sswipe,longPress,dragAndDropbecause they need to work cross‑platform and across web/native/hybrid. The doc explicitly notes: swipe NOT based onmobile: scrollGesture(Android) ormobile: scroll(iOS), but uses W3C Actions. mobile: …Gesturefamily — driver‑native gestures (XCUITest'sXCUIElementmethods on iOS; UIAutomator2UiObject.swipeon 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.
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.
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 tocontent-descon Android andname/accessibilityIdentifieron iOS. Fastest, most portable.xpath— works on both but slow and brittle (the page source has to be serialized first). Subject to driver settingsenforceXPath1,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 optionalappium-images-plugin; matches by image template).
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.
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 returnunknown commandwhile in webview context (e.g.mobile: swipeGesture). executeScriptswitches from "execute Node code over the driver" to "execute real JavaScript in the page".- Context switch is via
POST /session/:id/context(legacy) ormobile: switchContext. iOS also supports getting context metadata viamobile: getContextswithwaitForWebviewMs.
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.
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 supportresetPermission). - 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.
Permission modeling differs sharply between simulator and device:
- Simulator:
mobile: setPermissionworks forcalendar,camera,contacts,homekit,microphone,motion,photos,reminders,siri,medialibrary, etc., taking values'yes'/'no'/'unset'(and'limited'for photos on iOS 14+). Requiresapplesimutilsbinary present on$PATH. - Real device: Only
mobile: resetPermissionis available, and it operates on TCC‑protected resource enums (integers) because Apple doesn't expose grant APIs.
UIAutomator2's mobile: changePermissions operates in three regimes that are not equivalent:
target: 'pm'callspm grant/pm revoke— works only for AndroidManifest‑declared dangerous permissions, requirestargetSdkVersion >= 23, and a freshly‑installed APK with--grant-all-permissionswon't show up unless you query withtype: 'requested'.target: 'appops'callsappops set— covers app ops likeSYSTEM_ALERT_WINDOW,WRITE_SETTINGS,PICTURE_IN_PICTURE, plus permissions auto‑converted from manifest entries.
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.
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.
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.
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.
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.
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.
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:
-
The catalogs drift constantly. Both
executeMethodMaps are released frequently (XCUITest minor versions every few weeks). Any tooling that hard‑codes a list ofmobile:commands will go out of sync. PrefergetAppiumExtensions(). -
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 Xand nothing happened" frequently trace to a typo or to a parameter that was removed in a recent driver release without the user noticing. -
mobile:script name parsing is whitespace‑sensitive.'mobile:swipe'(no space) is not the same as'mobile: swipe'. The base‑driver matcher uses an exactObject.prototype.hasOwnPropertyagainst the registered keys. WDIO docs always show the space; some third‑party guides miss it. -
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. -
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
compileSdkVersionlags Android releases by several months — callingmobile:extensions that touchSettings.Globalon a fresh Android 15 image may fail until a driver bump. -
Real‑device test runs require code signing. A subtle but pervasive failure on iOS real devices is that
appium:xcodeOrgIdandappium:xcodeSigningIdcapabilities are needed to (re)sign WDA — without them, the session never starts and the W3CcreateSessionreturns500with a long stack trace. Documentation for this lives in WebDriverAgent's README, not Appium's. -
Deep‑link command's
waitForLaunchis Android‑only. The cross‑platform WebdriverIOdeepLink(link, appIdentifier, waitForLaunch)silently ignoreswaitForLaunchon iOS, which can mask intent and produce confusing test failures when the URL handler hasn't activated by the time the next command runs. -
mobile: sourceformats diverge. XCUITest'smobile: sourcesupports'xml','json','description'. UIAutomator2 only supports XML (the W3CGET /session/.../source). Tools that parse the source must branch on platform. -
The legacy
appium/app/backgroundroute requires a positionalseconds. If the client omits it, the route returns 400. The modernmobile: backgroundAppaccepts it as optional, defaulting to-1(never auto‑foreground). Migrating clients between paths needs to account for that. -
Element invalidation has different timing semantics. On iOS, an element ID is valid as long as XCUITest's underlying
XCUIElementsnapshot 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 withstale element reference. -
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. -
Settings APK trust on Android 14+. Several
mobile:commands (clipboard write, IME unicode type, toast/notification listener) need theio.appium.settingsAPK grantedWRITE_SECURE_SETTINGSviaadb shell pm grantbefore the test runs. Failing to set this up beforehand makesmobile: setClipboardandmobile: typesilently no‑op on some devices. -
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
executeAsyncScriptsemantics (different timeout handling) andgetTimeoutsreturning a different shape. -
No native multi‑touch on UIAutomator2 above two fingers. UIAutomator2's gesture API is limited to two pointers.
performActionswith 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. -
mobile: shellis gated byappium:allowInsecure. It (andmobile: 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. -
The
executeMethodMapon UIAutomator2 inherits via spread fromAndroidDriver.executeMethodMap. This means a UIAutomator2 release that doesn't pin itsappium-android-driverversion range tightly can grow or lose commands between patch versions. Any tooling enumerating the catalog at build time should resolve both packages fromnode_modulesof the running server, not from a hard‑coded list.
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:
-
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. -
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 nomobile:equivalent — exactly mirroring WebdriverIO's own pattern. -
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 tomobile: tap(XCUITest, accepts elementId or x/y) ormobile: clickGesture(UIAutomator2, same params but different validation rules). -
Discoverability beats completeness. Because the catalog drifts, exposing
aco mobile list(or similar) that dumps livegetAppiumExtensions()output is more durable than baking in a frozen list. -
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. -
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 /sessionwith a known session ID, or by reusing a sidecar process) rather than implicitly creating one per invocation.
Sources:
- Appium XCUITest Driver — Commands Reference
- Appium XCUITest Driver — Execute Methods reference
- Appium XCUITest Driver —
lib/execute-method-map.tson master - Appium XCUITest Driver — repository
- Appium UIAutomator2 Driver — repository
- Appium UIAutomator2 Driver —
lib/execute-method-map.tson master - Appium UIAutomator2 Driver —
lib/method-map.tson master - Appium Android Driver —
lib/execute-method-map.tson master - Appium UIAutomator2 Driver — commands reference (DeepWiki)
- Appium base‑driver —
lib/protocol/routes.js - Appium 2.0 docs — base‑driver commands
- Appium 3.0 docs — WebDriver protocol reference
- WebdriverIO — Appium API
- WebdriverIO — Mobile API
- WebdriverIO — Mobile
tap - WebdriverIO — Mobile
swipe - WebdriverIO — Mobile
longPress - WebdriverIO — Mobile
scrollIntoView - WebdriverIO — Mobile
deepLink - WebdriverIO — Mobile
getContext - WebdriverIO — Mobile
lock - WebdriverIO — Mobile
dragAndDrop - WebdriverIO — Mobile
pinch - WebdriverIO — Mobile
fingerPrint - WebdriverIO — Mobile
getClipboard - WebdriverIO — Protocols overview
- WebdriverIO — Appium Service