Maestro — declarative E2E mobile UI testing framework by mobile.dev. YAML-based flow files, single tool for Android + iOS (and Compose Multiplatform / Flutter / React Native). Built-in cloud runner, recording mode, JS scripting for complex assertions, screen state diffing, no flakiness from explicit waits. USE WHEN: user mentions "Maestro", "maestro test", "mobile E2E", "cross-platform UI test", "maestro studio", "mobile.dev cloud", ".maestro" folder, "launchApp" YAML DO NOT USE FOR: web E2E - use `testing/playwright` DO NOT USE FOR: unit tests - use `testing/kotest`, `testing/vitest`, etc. DO NOT USE FOR: instrumented Android tests - use Espresso/Compose Test DO NOT USE FOR: snapshot tests - use `testing/compose-snapshot`

GitHub
Install command
npx skhub add claude-dev-suite/maestro
Markdown
SKILL.md

Maestro — E2E Mobile Testing

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: maestro.

Why Maestro

FeatureMaestroEspresso/XCUITestDetox/Appium
Cross-platform (Android + iOS)✅ Single test❌ Separate per platform✅
Test formatYAML (declarative)Kotlin / SwiftJS
Implicit waits / retry✅ Built-in❌ Manual waitForPartial
Recording mode✅ Maestro Studio❌❌
Cloud runner✅ Free tier on mobile.dev—Sauce Labs / BrowserStack
Compose / Flutter / RN✅ AllEspresso for Compose / XCUITestDetox: RN only
Setup time< 5 minhours30+ min
FlakinessLow (smart waits)High (timing)Medium

Install

# macOS / Linux
curl -Ls "https://get.maestro.mobile.dev" | bash
# adds to ~/.maestro/bin

# Or via brew
brew tap mobile-dev-inc/tap
brew install maestro

# Windows
# Use WSL2 or Docker; native Windows support limited

# Verify
maestro --version

For iOS testing, also install:

brew install facebook/fb/idb-companion

Project Layout

project-root/
├── .maestro/
│   ├── flows/
│   │   ├── onboarding.yaml
│   │   ├── send_bitcoin.yaml
│   │   ├── receive_bitcoin.yaml
│   │   └── settings.yaml
│   ├── helpers/
│   │   └── common.yaml          # reusable subFlow
│   └── config.yaml              # global config
├── apps/
│   ├── android/                 # APK builds
│   └── ios/                     # IPA builds
└── ...

First Flow

.maestro/flows/onboarding.yaml:

appId: com.bhodl.android
---
- launchApp:
    clearState: true              # fresh state every run
- assertVisible: "Welcome to BHODL"
- tapOn: "Get Started"
- assertVisible: "Create Wallet"
- tapOn: "Create new wallet"
- assertVisible:
    text: "Backup your seed"
    timeout: 5000
- tapOn:
    id: "btn_continue"
- inputText: "my-secure-passphrase"
- tapOn: "Confirm"
- assertVisible: "Wallet created"

Run:

maestro test .maestro/flows/onboarding.yaml

# Run all flows in folder
maestro test .maestro/flows/

# With env var substitution
maestro test -e API_BASE=https://staging.bhodl.app .maestro/flows/

Cross-Platform appId

Same flow, different bundle IDs:

appId: ${APP_ID}                  # set via env or config
---
- launchApp
APP_ID=com.bhodl.android maestro test flow.yaml
APP_ID=com.bhodl.ios.BHODL maestro test flow.yaml

Or config.yaml:

appId: com.bhodl
flows:
  - flows/*.yaml

Selectors

Maestro finds elements by text, id, accessibility label, or content description. Composable rules.

- tapOn: "Send"                          # exact text match
- tapOn:
    text: "Send"                         # explicit text matcher
- tapOn:
    id: "send_button"                    # by accessibility id (Android: contentDescription, iOS: accessibilityIdentifier)
- tapOn:
    text: "Send"
    index: 0                             # if multiple matches
- tapOn:
    text: ".*coin.*"                     # regex
- tapOn:
    point: "50%, 50%"                    # screen coordinates
- tapOn:
    below: "Recipient"                   # spatial relations
- tapOn:
    leftOf: "Cancel"
- tapOn:
    enabled: true                        # filter by state
    text: "Continue"

For Compose/SwiftUI testability:

// Compose
Button(
    onClick = { /* ... */ },
    modifier = Modifier.testTag("send_button"),    // accessible to Maestro as id
) { Text("Send") }

// SwiftUI
Button("Send") { /* ... */ }
    .accessibilityIdentifier("send_button")

Common Actions

- launchApp:
    clearState: true               # fresh app state
    clearKeychain: true            # iOS keychain wipe
    arguments:
        debug: true
    permissions:
        camera: allow              # auto-grant on launch (iOS 14+, Android)
        location: deny

- tapOn: "Button"
- doubleTapOn: "Item"
- longPressOn: "Item"
- swipe:
    from: "30%, 50%"
    to: "70%, 50%"
- swipe:
    direction: UP
- scroll                            # default scroll
- scrollUntilVisible:
    element: "End of list"
    direction: DOWN

- inputText: "hello"
- copyTextFrom: "Address field"     # to clipboard
- pasteText                         # from clipboard (iOS only)
- eraseText: 10                     # delete N chars
- hideKeyboard

- pressKey: BACK                    # Android back button
- pressKey: ENTER

- openLink: "bitcoin:bc1q..."       # deep link
- openBrowser: "https://example.com"
- back

- waitForAnimationToEnd:
    timeout: 5000

- takeScreenshot: "after_send"
- assertVisible: "Sent successfully"
- assertNotVisible: "Error"
- assertTrue: "${output.success == true}"

SubFlows (Reusable)

helpers/login.yaml:

appId: com.bhodl.android
---
- inputText: ${USERNAME}
- tapOn:
    id: "password_field"
- inputText: ${PASSWORD}
- tapOn: "Login"

Use:

- runFlow:
    file: ../helpers/login.yaml
    env:
        USERNAME: alice@example.com
        PASSWORD: secret
- assertVisible: "Welcome, Alice"

Conditionals & Loops

- runFlow:
    when:
        visible: "Permission required"
    commands:
        - tapOn: "Allow"

- runFlow:
    when:
        notVisible: "Already onboarded"
    commands:
        - runFlow: ../helpers/onboarding.yaml

- repeat:
    times: 3
    commands:
        - tapOn: "Refresh"
        - waitForAnimationToEnd

- repeat:
    while:
        visible: "Loading..."
    commands:
        - waitForAnimationToEnd:
            timeout: 1000

JavaScript Scripting

For complex assertions or test data generation:

- runScript: scripts/generate_address.js
    env:
        NETWORK: testnet
- inputText: ${output.address}

scripts/generate_address.js:

output.address = generateAddress(env.NETWORK);

function generateAddress(network) {
    return network === "testnet" ? "tb1q..." : "bc1q...";
}

Or inline:

- evalScript: ${output.balance = parseFloat(output.amount) * 100000000}
- assertTrue: ${output.balance > 0}

Tags & Filtering

tags:
    - smoke
    - critical
appId: com.bhodl.android
---
- launchApp
# Run only smoke tests
maestro test .maestro/flows/ --include-tags smoke

# Exclude slow tests
maestro test .maestro/flows/ --exclude-tags slow

Maestro Studio (Recording / Inspection)

Interactive UI for crafting tests:

maestro studio

Opens browser at http://localhost:9999. Connects to running emulator/device. You can:

  • Inspect element tree
  • Tap elements to generate YAML
  • Record test session
  • Try selectors live
  • Export to YAML flow

Use to bootstrap tests, then refine in code.

Cloud Runner (mobile.dev)

# Run tests on cloud devices
maestro cloud --apiKey=$MAESTRO_API_KEY \
    apps/android/app-debug.apk \
    .maestro/flows/

# iOS
maestro cloud --apiKey=$MAESTRO_API_KEY \
    apps/ios/build/BHODL.app \
    .maestro/flows/

Free tier: limited monthly minutes. Paid tier for parallel runs, more devices, screenshots/videos retention.

CI Integration

GitHub Actions

# .github/workflows/e2e.yml
name: Maestro E2E
on: [pull_request]

jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { java-version: '17', distribution: 'temurin' }

      - name: Build APK
        run: ./gradlew :apps:android:assembleDebug

      - name: Setup Maestro
        run: |
            curl -Ls "https://get.maestro.mobile.dev" | bash
            echo "$HOME/.maestro/bin" >> $GITHUB_PATH

      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          arch: x86_64
          script: |
            adb install apps/android/app/build/outputs/apk/debug/app-debug.apk
            maestro test .maestro/flows/

  ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Build app for simulator
        run: |
            xcodebuild -scheme BHODL -sdk iphonesimulator \
              -destination 'platform=iOS Simulator,name=iPhone 15' build

      - name: Setup Maestro
        run: |
            curl -Ls "https://get.maestro.mobile.dev" | bash
            echo "$HOME/.maestro/bin" >> $GITHUB_PATH
            brew install facebook/fb/idb-companion

      - run: maestro test .maestro/flows/

Cloud Mode (Simpler CI)

- name: Run on Maestro Cloud
  run: |
    maestro cloud --apiKey=${{ secrets.MAESTRO_API_KEY }} \
        apps/android/app-debug.apk \
        .maestro/flows/

No emulator setup needed.

Test Patterns for Wallet Apps

Send transaction (mocked backend)

appId: com.bhodl.android
---
- launchApp:
    clearState: true
- runFlow: ../helpers/restore_test_wallet.yaml

- tapOn: "Send"
- inputText: "tb1q...test_address"
- tapOn:
    id: "amount_field"
- inputText: "1000"
- tapOn: "Continue"

- assertVisible: "Confirm send"
- assertVisible: "1000 sats"
- assertVisible:
    text: "tb1q.*"

- tapOn: "Confirm"
- tapOn: "Authenticate"            # biometric prompt — needs special handling

# Biometric: in regtest/staging build, replace with bypass button
- tapOn: "Use test biometric"

- assertVisible:
    text: "Sent successfully"
    timeout: 30000

- takeScreenshot: "after_send"

Backup & restore flow

appId: com.bhodl.android
---
- launchApp:
    clearState: true

# Create wallet, get seed
- tapOn: "Create new wallet"
- copyTextFrom:
    id: "seed_phrase"
- evalScript: ${output.seed = maestro.copiedText}
- tapOn: "I've backed up"

# Wipe and restore
- launchApp:
    clearState: true
- tapOn: "Restore wallet"
- inputText: ${output.seed}
- tapOn: "Restore"

- assertVisible: "Wallet restored"
- assertVisible: "Balance: 0 sats"

Biometric / Permission Handling

Maestro can grant permissions on launch:

- launchApp:
    permissions:
        camera: allow
        location: deny
        notifications: allow

For biometric, build a debug-only test mode in your app that accepts a fixed test PIN instead of real biometric — Maestro can't simulate Face/Touch ID prompts.

// Android
class BiometricAuthHelper {
    fun authenticate(callback: (Boolean) -> Unit) {
        if (BuildConfig.DEBUG && System.getenv("MAESTRO_TEST") == "1") {
            callback(true)                                // bypass for E2E
            return
        }
        // real biometric prompt
    }
}

Performance & Best Practices

  • Idempotent flows — clearState: true ensures predictable starting point
  • Avoid timing-dependent waits — Maestro auto-retries; use assertVisible: { timeout: 10000 } only when needed
  • Use id/accessibility over text where possible — survives copy changes and i18n
  • Tag flows by speed/criticality — run smoke on every PR, full suite nightly
  • Screenshot key states — takeScreenshot for visual diff in cloud
  • Wallet apps: use a regtest/signet backend — no real money, predictable balances

Anti-Patterns

Anti-patternWhy it's badCorrect approach
Hardcoded sleeps (waitForAnimationToEnd: { timeout: 30000 })Slow + brittleUse assertVisible (auto-retries)
Selecting by absolute coordinatesBreaks across screen sizesUse text/id/spatial selectors
Real biometric in CICan't automateDebug bypass + Maestro test flag
Flow that depends on previous flow's stateBrittleclearState: true + helper subFlows
Hardcoded test data (specific addresses)Breaks on env changeUse env vars / setup helpers
No screenshots on failureHard to debugtakeScreenshot at key checkpoints
Running all flows on every PRSlow CITag and run subset on PR, full nightly

Troubleshooting

SymptomCauseFix
"Element not found" but visibleWrong selector hierarchyUse Maestro Studio to inspect tree
Flaky tests in CIAnimation overlapAdd waitForAnimationToEnd between actions
iOS app not launchingWrong bundle ID or app not installedVerify with xcrun simctl listapps booted
Android app crashes on launchWrong APK arch (x86_64 vs arm)Build matching emulator arch
Permission dialog appearingPermissions not pre-grantedUse launchApp.permissions:
Maestro Studio black screenEmulator/device not connectedadb devices / xcrun simctl list
Slow flow executionMany waitForAnimationToEndReplace with assertVisible
idb_companion errors on macOSOutdated companionbrew upgrade idb-companion

When NOT to Use This Skill

ScenarioUse Instead
Web E2Etesting/playwright
Compose unit/integration testsmobile/jetpack-compose (testing section)
Compose snapshot teststesting/compose-snapshot
Espresso / XCUITest specificsNative test frameworks
Detox (React Native)Detox-specific docs
Pure Kotlin unit teststesting/kotest
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/testing/maestro

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1