Maestro Mobile Testing: Declarative YAML Flows for iOS and Android

Maestro Mobile Testing: Declarative YAML Flows for iOS and Android

Maestro is a mobile testing framework that replaces imperative test code with declarative YAML flows. You describe what the user does — tap this, type that, assert visible — and Maestro handles synchronization, retries, and cross-platform execution. This guide covers the flow syntax, handling dynamic content, CI integration, and how Maestro compares to Detox for different testing scenarios.

Key Takeaways

YAML flows are readable by non-engineers. Product managers and QA analysts can read and write Maestro flows without knowing how to code, which means tests don't become a bottleneck owned by one person. Maestro auto-retries flaky interactions by default. Unlike Detox's synchronization model, Maestro uses a retry-with-timeout strategy — simpler to understand, slightly more tolerant of genuinely slow UI. runFlow enables test composition. Break long user journeys into reusable sub-flows and compose them — authentication flows reused across many tests without duplication. Maestro Studio is the fastest way to build locators. Launch it against a running app and click elements to generate correct YAML selectors rather than guessing testID values. For large cross-platform teams, Maestro Cloud provides parallelism without managing device infrastructure. Local execution is free; Cloud scales horizontally across real devices.

The barrier to writing mobile E2E tests has historically been high. You need to know how to write code, understand async programming models, configure build systems, and navigate testing framework documentation before a single test runs. Maestro lowers this barrier dramatically by replacing code with YAML that reads like a step-by-step user guide.

Installing Maestro

Maestro ships as a single binary installed via a shell script:

curl -Ls "https://get.maestro.mobile.dev" <span class="hljs-pipe">| bash

Verify the installation:

maestro --version

No project-level dependencies, no package.json changes, no Gradle configuration. Maestro connects to your running app over the iOS Simulator or Android Emulator bridge.

The Flow Syntax

A Maestro flow is a YAML file with two required sections: appId and a list of commands.

# flows/login.yaml
appId: com.example.myapp
---
- launchApp
- tapOn: "Email"
- inputText: "user@example.com"
- tapOn: "Password"
- inputText: "secret123"
- tapOn: "Log In"
- assertVisible: "Welcome back!"

Run it:

maestro test flows/login.yaml

Maestro launches the app, executes each command, and exits with a pass or fail result. That's the entire workflow for a basic test.

Core Commands

# Launch and navigate
- launchApp
- launchApp:
    clearState: true          # Fresh app state

# Tap by text or testID
- tapOn: "Sign In"
- tapOn:
    id: "submit-button"       # Uses testID/accessibilityIdentifier
- tapOn:
    text: "Confirm"
    index: 1                  # Second element matching this text

# Long press
- longPressOn: "Delete item"

# Swipe
- swipe:
    direction: LEFT
    element:
      id: "swipeable-card"

# Scroll
- scroll
- scrollUntilVisible:
    element:
      text: "Load More"
    direction: DOWN

Text Input

- tapOn: "Search"
- inputText: "React Native testing"
- clearText                   # Clears focused input
- hideKeyboard

Assertions

# Check visibility
- assertVisible: "Dashboard"
- assertVisible:
    text: "3 items"
- assertVisible:
    id: "profile-avatar"

# Check not visible
- assertNotVisible: "Error message"

# Check element contains text (partial match)
- assertVisible:
    text: "Hello,"            # matches "Hello, Alice"
    enabled: true

Waiting

Maestro retries visibility assertions automatically, but you can set explicit waits:

- waitForAnimationToEnd
- wait:
    maxRetries: 10
    interval: 500             # ms between retries

# Wait for element to appear
- assertVisible:
    text: "Processing complete"
    timeout: 10000            # ms

Handling Dynamic Content

Real apps have content that changes — timestamps, user names, counts. Maestro handles this with JavaScript expressions inside YAML:

appId: com.example.myapp
---
- launchApp
- tapOn: "Profile"
- assertVisible:
    text: ${USER_NAME}        # Injected at runtime via env var

Run with environment variables:

USER_NAME="Alice Johnson" maestro <span class="hljs-built_in">test flows/profile.yaml

For more complex conditions, use the evalScript command:

- evalScript: ${new Date().getFullYear() >= 2025}

Conditional Flows

Handle optional UI states like permission dialogs:

- runFlow:
    when:
      visible: "Allow access to camera?"
    file: flows/helpers/grant-camera-permission.yaml

The runFlow with when is idempotent — it only runs the sub-flow if the condition is true. This pattern cleanly handles OS-level permission prompts that appear only on first launch.

Composing Flows with runFlow

Long user journeys should be broken into composable sub-flows:

# flows/helpers/authenticate.yaml
appId: com.example.myapp
---
- launchApp:
    clearState: true
- tapOn: "Email"
- inputText: ${EMAIL}
- tapOn: "Password"
- inputText: ${PASSWORD}
- tapOn: "Log In"
- assertVisible: "Dashboard"
# flows/checkout.yaml
appId: com.example.myapp
---
- runFlow:
    file: flows/helpers/authenticate.yaml
    env:
      EMAIL: "buyer@example.com"
      PASSWORD: "testpass123"
- tapOn: "Shop"
- tapOn: "Add to Cart"
- tapOn: "Checkout"
- assertVisible: "Order confirmed"

This pattern keeps flows focused and eliminates the authentication boilerplate that would otherwise be duplicated across dozens of test files.

Cross-Platform Testing

The same flow runs on iOS and Android. Maestro abstracts platform differences in element interaction. The only time you need platform-specific flows is when the app renders meaningfully different UI per platform:

# flows/platform-specific.yaml
appId: com.example.myapp
---
- runFlow:
    when:
      platform: iOS
    file: flows/ios/share-sheet.yaml
- runFlow:
    when:
      platform: Android
    file: flows/android/share-sheet.yaml

To target a specific device or simulator, set the device connection before running:

# iOS — target a specific simulator
maestro <span class="hljs-built_in">test flows/login.yaml --device <span class="hljs-string">"iPhone 15"

<span class="hljs-comment"># Android — use a specific emulator or device
maestro <span class="hljs-built_in">test flows/login.yaml --device emulator-5554

Maestro Studio

Maestro Studio is a browser-based UI for inspecting your app and building flows interactively:

maestro studio

It opens a browser showing your app's current screen with an element inspector. Click any element to get its text, ID, and the exact YAML command to interact with it. For apps without testID props everywhere, Studio is the fastest way to discover reliable selectors.

Studio also supports replaying individual commands in real time — paste a command, press run, and watch the interaction happen on the device. This shortens the iteration loop from minutes to seconds.

Running Flows in CI

GitHub Actions — Local Execution

# .github/workflows/maestro.yml
name: Maestro E2E Tests

on:
  push:
    branches: [main]

jobs:
  test-android:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Maestro
        run: curl -Ls "https://get.maestro.mobile.dev" | bash

      - name: Build APK
        run: cd android && ./gradlew assembleDebug

      - name: Run Android Emulator and Tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          arch: x86_64
          script: |
            adb install android/app/build/outputs/apk/debug/app-debug.apk
            $HOME/.maestro/bin/maestro test flows/ --format junit --output results.xml

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: maestro-results
          path: results.xml

  test-ios:
    runs-on: macos-14

    steps:
      - uses: actions/checkout@v4

      - name: Install Maestro
        run: curl -Ls "https://get.maestro.mobile.dev" | bash

      - name: Build iOS app
        run: xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -sdk iphonesimulator -configuration Debug

      - name: Boot simulator
        run: |
          xcrun simctl boot "iPhone 15"
          xcrun simctl install booted path/to/MyApp.app

      - name: Run tests
        run: $HOME/.maestro/bin/maestro test flows/ --format junit --output results.xml

Maestro Cloud

For parallelism across real devices without managing infrastructure:

maestro cloud --apiKey $MAESTRO_API_KEY flows/

Maestro Cloud runs your flows on a pool of real iOS and Android devices simultaneously, returning results in parallel. It integrates with GitHub Actions, Bitrise, CircleCI, and Fastlane.

Debugging Failed Flows

When a flow fails, Maestro captures a screenshot and the app hierarchy at the point of failure:

maestro test flows/login.yaml --debug-output ./debug-output/

The debug directory contains:

  • A screenshot at the failure point
  • The view hierarchy dump (XML on Android, AXI on iOS)
  • The flow execution log with each command's result

For interactive debugging, use maestro record to capture a flow by interacting with your device manually, then clean up the recorded YAML:

maestro record flows/new-flow.yaml

Maestro vs Detox

Both are solid choices; the right answer depends on your team:

Dimension Maestro Detox
Language YAML TypeScript/JavaScript
Synchronization Retry-based Event-driven gray-box
Setup complexity Minimal Significant (native config)
Composability runFlow Jest modules
Debugger Maestro Studio None built-in
Custom logic evalScript (limited) Full JS
Real devices (CI) Maestro Cloud DIY or third-party

Maestro wins when speed of writing tests matters more than test logic complexity. Detox wins when tests require conditional logic, data manipulation, or integration with existing JavaScript tooling. Many teams use both: Maestro for happy-path smoke tests, Detox for complex flows.

The YAML-first approach makes Maestro particularly effective for teams where QA or product defines the test scenarios — the format is accessible enough that the person who knows what to test can also be the person who writes the test.

Read more