Driver Selection
maestro-runner has three drivers. The driver is selected based on your --driver and --platform flags.
| Driver | Platform | When Used | Dependencies |
|---|---|---|---|
| UIAutomator2 | Android | Default for Android | None (bundled) |
| DeviceLab | Android | --driver devicelab |
None (bundled). Supports WebView CDP and Chrome browser CDP |
| WDA | iOS | Auto-selected when --platform ios |
Xcode (builds automatically) |
| Appium | Both | --driver appium |
External Appium server |
The UIAutomator2 and WDA drivers communicate directly with the on-device automation server, bypassing Appium for lower latency. The Appium driver is available for cloud providers (BrowserStack, SauceLabs, LambdaTest) and setups that already have Appium infrastructure.
UIAutomator2 (Android)
The bundled UIAutomator2 server APKs are automatically installed on the device. On each run, the installed version is checked against the bundled version — if they differ or there's a signing conflict, the old APKs are uninstalled and the new ones are installed. The server starts, and maestro-runner communicates with it directly. No setup required beyond having adb available.
Device Ownership & Socket Lifecycle
When maestro-runner starts a test on an Android device, it creates two files:
/tmp/uia2-<serial>.sock— Unix socket for communicating with UIAutomator2 on the device (via adb forward)/tmp/uia2-<serial>.pid— PID file tracking which host process owns the socket
These files ensure:
- Clean shutdown — On Ctrl+C (SIGINT/SIGTERM), the signal handler cleans up the socket, adb forward, and PID file before exiting
- Stale recovery — If the process is killed with
kill -9or crashes, the next run detects the dead PID and automatically cleans up the stale socket - Device locking — If another maestro-runner instance tries to use the same device, it sees the live PID and returns a clear "device already in use" error
| Scenario | Behavior |
|---|---|
| Normal exit / Ctrl+C | Signal handler runs cleanup — socket + PID removed |
kill -9 / crash |
Next run detects dead PID → auto-cleans stale socket |
| Concurrent instance on same device | Detects live PID → "device already in use" error |
DeviceLab (Android)
An optional Android driver optimized for speed. Instead of the UIAutomator2 HTTP server, the DeviceLab driver uses a WebSocket-based on-device agent for direct RPC communication. This eliminates the HTTP overhead and reduces round-trip latency, resulting in ~2x faster test execution.
Key differences from UIAutomator2:
- WebSocket transport — single persistent connection instead of per-request HTTP calls
- Bounds stabilization — before clicking an element, waits for its position to stabilize between consecutive checks, preventing misclicks during UI animations
- Special character handling — text selectors try
textContains/descriptionContainsfirst (handles parentheses and special characters), falling back to regex with proper escaping
Benchmark on Pixel 4a (Android 13), 9 flows / 163 steps:
| Driver | Duration |
|---|---|
| DeviceLab | 1m 12s |
| UIAutomator2 | 2m 24s |
| Maestro CLI | 4m 22s |
maestro-runner --driver devicelab flows/
All existing Maestro YAML flows work without modification. The DeviceLab driver supports the same commands, selectors, and features as UIAutomator2.
WDA (iOS)
WebDriverAgent is built from source using Xcode on the first run. Builds are cached per iOS version and device type, so subsequent runs skip the build step entirely. The first build takes several minutes; after that, startup is fast.
For physical iOS devices, provide your Apple Development Team ID via --team-id for code signing.
Permission handling: On simulators, permissions are pre-granted via simctl privacy before app launch. On real devices, WDA's defaultAlertAction session capability auto-accepts all permission dialogs by default, even when no permissions are configured. Set all: deny to auto-dismiss instead.
Use maestro-runner wda update to update to the latest WebDriverAgent version.
Device management: On real iOS devices, app installation and state management uses xcrun devicectl (Apple's remoted daemon), which maintains a stable connection alongside WDA's USB port forwarding.
Appium
When using the Appium driver, a fresh session is created for each flow file. This provides clean isolation between flows -- no state leaks from one test to the next. Flows that use clearState automatically get the appropriate Appium capabilities.
newSession option: If clearState fails mid-flow (e.g., mobile: clearApp is unsupported on real iOS devices), use newSession: true on launchApp to create a fresh Appium session instead:
- launchApp:
appId: com.example.app
newSession: true
This destroys the current session and creates a new one, providing clean state. On iOS real devices with newSession: true, clearState is automatically skipped since the fresh session already provides clean state.
Element Finding
maestro-runner uses a tiered approach to find elements, starting with the fastest method and falling back to slower but more comprehensive ones.
How It Works
-
Fast native queries first -- On Android, native UIAutomator selectors are used. On iOS, optimized element type queries and predicate strings are tried. These are fast because they use the platform's built-in search.
-
Full hierarchy fallback -- If native queries don't find the element (common with hint text, custom views, or relative selectors like
below/above), maestro-runner falls back to parsing the full UI hierarchy. This is slower but supports all selector types. -
Smart optimization per operation -- Visibility checks (
assertVisible) use a fast single-call path. Interaction commands (tapOn,inputText) fetch additional details like element bounds, which requires more calls but only when needed.
Tap Handling for React Native
In React Native apps, text elements are often not directly tappable -- the tap target is a parent container. maestro-runner handles this automatically: it finds the text element, then walks up the UI tree to locate the nearest tappable ancestor.
Default Timeouts
| Scenario | Default Timeout |
|---|---|
Required elements (e.g., tapOn, assertVisible) |
17 seconds |
Optional elements (optional: true) |
7 seconds |
Quick checks (assertNotVisible) |
1 second (polls for disappearance) |
Set a step-level timeout field to override these defaults.
Note:
assertNotVisiblecorrectly polls for element disappearance — it waits for the element to be gone, rather than checking for its appearance. Page source searches also filter out elements whose coordinates are outside the visible screen bounds, preventing false matches from off-screen elements.
Parallel Execution
When you use --parallel N, maestro-runner distributes flows across devices using a shared work queue.
How It Works
- All flows are placed in a queue
- Each device pulls the next available flow from the queue when it finishes its current one
- Faster devices automatically pick up more flows, balancing the workload naturally
What You See
During parallel execution, terminal output is simplified to prevent interleaved logs -- only flow start/end status is shown. Full step-by-step details appear in the final summary and in the report files.
The reported duration is wall-clock time (actual elapsed time), not the sum of individual flow durations.
Parallel Device Selection
When --parallel is used, maestro-runner detects which devices are already in use by other instances:
- Android: Checks for an active
/tmp/uia2-<serial>.socksocket with a live PID - iOS: Checks for an active WDA port on the device
Devices detected as in-use are automatically skipped. Devices are also filtered by platform — tvOS, watchOS, and xrOS devices are excluded.
Requirements
- Each parallel Android emulator requires a unique AVD name
- Parallel execution is not yet supported with the Appium driver
- Use
--auto-start-emulatorto let maestro-runner start the required number of devices - iOS simulators: If not enough shutdown simulators exist for the requested
--parallelcount, maestro-runner automatically creates new simulators. Created simulators are deleted on shutdown - In-use detection: Devices already claimed by another maestro-runner instance are automatically skipped (via WDA port check on iOS, socket check on Android)
Report System
Generated Files
Every run produces a report directory (default: ./reports/<timestamp>/):
reports/<timestamp>/
report.html # Visual HTML report
report.json # Machine-readable JSON report
maestro-runner.log # Detailed execution log
flows/
flow-000.json # Per-flow detail with commands and artifacts
flow-001.json
...
assets/
flow-000/
cmd-000-after.png # Screenshot after step failure
...
Live Monitoring
The report.html file is updated in real-time during execution. Open it in a browser to watch test progress as flows complete. The JSON index file (report.json) is also updated live.
Failure Artifacts
By default, maestro-runner captures a screenshot and the UI hierarchy after any failed step. These are saved in the assets/ directory and linked from the HTML report.
Error Classification (Android)
On Android, errors in reports are classified by type for easier debugging:
| Error Type | Description |
|---|---|
element_not_found |
Target element was not found in the UI |
timeout |
Command timed out waiting for a condition |
assertion |
An assertion (assertVisible, assertTrue, etc.) failed |
keyboard_covering |
Soft keyboard is covering the target element |
These classifications appear in the per-flow JSON reports and the HTML report.
Wait-for-Idle Timeout
The waitForIdleTimeout controls how long maestro-runner waits for the device UI to settle before executing each command. The setting follows this priority chain (highest to lowest):
- Flow config --
waitForIdleTimeoutin the flow YAML - CLI flag --
--wait-for-idle-timeout - Workspace config --
waitForIdleTimeoutinconfig.yaml - Capabilities file --
appium:waitForIdleTimeoutin caps JSON - Default -- 200ms
Set to 0 to disable idle waiting. This is useful for apps with continuous animations that would otherwise cause timeouts.
Device Cleanup
Devices started by maestro-runner (via --start-emulator, --start-simulator, or --auto-start-emulator) are automatically shut down after tests complete. Cleanup also runs on SIGINT (Ctrl+C) and SIGTERM, so interrupted test runs still clean up properly.
Use --shutdown-after=false to keep devices running after tests finish.
Flutter App Support
maestro-runner automatically detects Flutter apps and uses the Dart VM Service for element finding when the native driver (WDA/UIAutomator2) can't find an element. This works by:
- On first element miss, checks logcat/device logs for a Dart VM Service URL
- If found, connects to the VM Service and searches the semantics tree
- If semantics tree search fails, falls back to widget tree analysis (hint text, identifiers, suffix icons)
- Cross-references widget tree nodes with semantics nodes for coordinates
This is fully automatic — no YAML changes needed. Non-Flutter apps pay only one log read on first miss, then the fallback is fully bypassed. Flutter reconnection now skips retries for non-Flutter apps instead of wasting time on connection attempts.
Works on: Android (all drivers) and iOS simulators.
Disable with --no-flutter-fallback if it causes issues.
WebView CDP Support
The DeviceLab driver (Android) supports WebView testing via Chrome DevTools Protocol (CDP). When a WebView is detected on screen, maestro-runner connects to it via CDP for element finding and JavaScript execution, instead of relying solely on the native UiAutomator accessibility tree.
This enables:
- Element finding in web content — CSS selectors and text searches work inside WebViews, not just native views
- JavaScript execution —
evalWebViewScriptandrunWebViewScriptcommands execute JS directly in the WebView context via CDP - Enriched error messages — when an element is not found, the error message includes CDP context showing whether the element exists in the web content but isn't accessible via native selectors
WebView CDP support is automatic — no configuration needed. The DeviceLab driver also supports Chrome browser CDP on Android for browser-based testing.
Network Idle Detection
After navigations (in both browser and WebView contexts), maestro-runner waits for network idle and DOM stability before proceeding. This ensures that dynamically loaded content is fully rendered before the next command executes, reducing flaky failures caused by in-flight network requests or incomplete DOM updates.