Skip to content
Back to skills

Maestro

BSecurity

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

  • 31 stars
  • 0 votes
  • 0 copies
  • 7 views
  • Added September 8, 2026
ai-agentsjavascriptjavaswiftkotlinbashreactdockertestinggitapi

Works with

  • cli
  • api
  • mcp

Security analysis

B75/100
  • criticalPipes output to a shell interpreter
  • criticalDownloads and executes remote scripts — classic supply chain attack

Pro shows the line behind each finding and how to fix it

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill maestro --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Maestro?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Maestro
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-maestro/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-maestro)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: maestro
description: |
  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`
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Maestro — E2E Mobile Testing

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `maestro`.

## Why Maestro

| Feature | Maestro | Espresso/XCUITest | Detox/Appium |
|---|---|---|---|
| Cross-platform (Android + iOS) | ✅ Single test | ❌ Separate per platform | ✅ |
| Test format | YAML (declarative) | Kotlin / Swift | JS |
| Implicit waits / retry | ✅ Built-in | ❌ Manual `waitFor` | Partial |
| Recording mode | ✅ Maestro Studio | ❌ | ❌ |
| Cloud runner | ✅ Free tier on mobile.dev | — | Sauce Labs / BrowserStack |
| Compose / Flutter / RN | ✅ All | Espresso for Compose / XCUITest | Detox: RN only |
| Setup time | < 5 min | hours | 30+ min |
| Flakiness | Low (smart waits) | High (timing) | Medium |

## Install

```bash
# 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:
```bash
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`:

```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:
```bash
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:

```yaml
appId: ${APP_ID}                  # set via env or config
---
- launchApp
```

```bash
APP_ID=com.bhodl.android maestro test flow.yaml
APP_ID=com.bhodl.ios.BHODL maestro test flow.yaml
```

Or `config.yaml`:
```yaml
appId: com.bhodl
flows:
  - flows/*.yaml
```

## Selectors

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

```yaml
- 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:

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

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

## Common Actions

```yaml
- 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`:
```yaml
appId: com.bhodl.android
---
- inputText: ${USERNAME}
- tapOn:
    id: "password_field"
- inputText: ${PASSWORD}
- tapOn: "Login"
```

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

## Conditionals & Loops

```yaml
- 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:

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

`scripts/generate_address.js`:
```js
output.address = generateAddress(env.NETWORK);

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

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

## Tags & Filtering

```yaml
tags:
    - smoke
    - critical
appId: com.bhodl.android
---
- launchApp
```

```bash
# 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:

```bash
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)

```bash
# 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

```yaml
# .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)

```yaml
- 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)

```yaml
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

```yaml
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:

```yaml
- 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.

```kotlin
// 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-pattern | Why it's bad | Correct approach |
|---|---|---|
| Hardcoded sleeps (`waitForAnimationToEnd: { timeout: 30000 }`) | Slow + brittle | Use `assertVisible` (auto-retries) |
| Selecting by absolute coordinates | Breaks across screen sizes | Use text/id/spatial selectors |
| Real biometric in CI | Can't automate | Debug bypass + Maestro test flag |
| Flow that depends on previous flow's state | Brittle | `clearState: true` + helper subFlows |
| Hardcoded test data (specific addresses) | Breaks on env change | Use env vars / setup helpers |
| No screenshots on failure | Hard to debug | `takeScreenshot` at key checkpoints |
| Running all flows on every PR | Slow CI | Tag and run subset on PR, full nightly |

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| "Element not found" but visible | Wrong selector hierarchy | Use Maestro Studio to inspect tree |
| Flaky tests in CI | Animation overlap | Add `waitForAnimationToEnd` between actions |
| iOS app not launching | Wrong bundle ID or app not installed | Verify with `xcrun simctl listapps booted` |
| Android app crashes on launch | Wrong APK arch (x86_64 vs arm) | Build matching emulator arch |
| Permission dialog appearing | Permissions not pre-granted | Use `launchApp.permissions:` |
| Maestro Studio black screen | Emulator/device not connected | `adb devices` / `xcrun simctl list` |
| Slow flow execution | Many `waitForAnimationToEnd` | Replace with `assertVisible` |
| `idb_companion` errors on macOS | Outdated companion | `brew upgrade idb-companion` |

## When NOT to Use This Skill

| Scenario | Use Instead |
|----------|-------------|
| Web E2E | `testing/playwright` |
| Compose unit/integration tests | `mobile/jetpack-compose` (testing section) |
| Compose snapshot tests | `testing/compose-snapshot` |
| Espresso / XCUITest specifics | Native test frameworks |
| Detox (React Native) | Detox-specific docs |
| Pure Kotlin unit tests | `testing/kotest` |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.