Technical Approach

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:

  1. Clean shutdown — On Ctrl+C (SIGINT/SIGTERM), the signal handler cleans up the socket, adb forward, and PID file before exiting
  2. Stale recovery — If the process is killed with kill -9 or crashes, the next run detects the dead PID and automatically cleans up the stale socket
  3. 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/descriptionContains first (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

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

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

  3. 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: assertNotVisible correctly 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

  1. All flows are placed in a queue
  2. Each device pulls the next available flow from the queue when it finishes its current one
  3. 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>.sock socket 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-emulator to let maestro-runner start the required number of devices
  • iOS simulators: If not enough shutdown simulators exist for the requested --parallel count, 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):

  1. Flow config -- waitForIdleTimeout in the flow YAML
  2. CLI flag -- --wait-for-idle-timeout
  3. Workspace config -- waitForIdleTimeout in config.yaml
  4. Capabilities file -- appium:waitForIdleTimeout in caps JSON
  5. 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:

  1. On first element miss, checks logcat/device logs for a Dart VM Service URL
  2. If found, connects to the VM Service and searches the semantics tree
  3. If semantics tree search fails, falls back to widget tree analysis (hint text, identifiers, suffix icons)
  4. 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 executionevalWebViewScript and runWebViewScript commands 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.