| name | Maestro Mobile Testing |
| description | Black-box mobile UI testing with Maestro YAML flows - launchApp, tapOn, inputText, assertVisible, reusable runFlow subflows, environment parameters, JUnit reporting, and emulator-based CI pipelines. |
| version | 1.0.0 |
| author | thetestingacademy |
| license | MIT |
| tags | ["maestro","mobile","e2e","ios","android","yaml","flows","ui-testing","flutter","react-native"] |
| testingTypes | ["mobile","e2e"] |
| frameworks | ["appium"] |
| languages | ["typescript"] |
| domains | ["mobile"] |
| agents | ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"] |
Maestro Mobile Testing
This skill makes an AI agent write Maestro flows - declarative YAML files that drive iOS and Android apps black-box style with built-in waiting, no test harness compiled into the app, and no WebDriver session management. Trigger it when a repository contains a .maestro/ directory or *.yaml flows with appId: headers, or when the user wants fast, low-maintenance smoke tests for a native, React Native, or Flutter app without the weight of Appium.
Core Principles
- Maestro waits by default; do not teach it to sleep. Every
tapOn and assertVisible polls until the element appears or a timeout expires. Reach for extendedWaitUntil for slow screens; never add fixed delays as a first resort.
- One flow per user-visible behavior. A flow named
checkout-with-saved-card.yaml that does exactly that is debuggable. A 200-line regression.yaml is not. Compose bigger journeys from subflows with runFlow.
- Select by accessibility id first, visible text second.
tapOn: id: "submit-button" survives copy changes and localization; text selectors are acceptable for stable, single-language labels only.
- Start from a clean, deterministic state.
launchApp with clearState: true resets the app between flows; tests that inherit the previous flow's logged-in session fail in random orders.
- Parameterize with env, not copies. Credentials and hosts go in
env: blocks or -e KEY=value CLI arguments so the same flow runs against dev, staging, and CI builds.
- Treat flows as code. They live in the repo next to the app, run on every PR via emulator CI, and produce JUnit XML that the pipeline understands.
Setup
curl -fsSL "https://get.maestro.mobile.dev" | bash
export PATH="$PATH:$HOME/.maestro/bin"
maestro --version
maestro test .maestro/login.yaml
maestro test .maestro/ --format junit --output maestro-report.xml
maestro studio
Patterns
1. A complete login flow
appId: com.example.shop
env:
EMAIL: qa@example.com
PASSWORD: Str0ngPass!
---
- launchApp:
clearState: true
- tapOn: 'Log in'
- tapOn:
id: 'email-input'
- inputText: ${EMAIL}
- tapOn:
id: 'password-input'
- inputText: ${PASSWORD}
- hideKeyboard
- tapOn:
id: 'login-submit'
- assertVisible: 'Welcome back'
- assertNotVisible: 'Invalid email or password'
- takeScreenshot: logged-in-home
2. Subflows with runFlow: compose journeys, handle one-off dialogs
appId: com.example.shop
---
- launchApp:
clearState: true
- tapOn: 'Log in'
- tapOn:
id: 'email-input'
- inputText: ${EMAIL}
- tapOn:
id: 'password-input'
- inputText: ${PASSWORD}
- tapOn:
id: 'login-submit'
- assertVisible:
id: 'home-screen'
appId: com.example.shop
env:
EMAIL: qa@example.com
PASSWORD: Str0ngPass!
---
- runFlow: subflows/login.yaml
- runFlow:
when:
visible: 'Allow notifications?'
commands:
- tapOn: 'Allow'
- tapOn: 'Deals'
- scrollUntilVisible:
element:
text: 'Wireless Headphones'
direction: DOWN
timeout: 20000
- tapOn: 'Wireless Headphones'
- tapOn:
id: 'add-to-cart'
- tapOn:
id: 'cart-icon'
- assertVisible: 'Wireless Headphones'
- assertVisible:
text: 'Checkout'
enabled: true
3. Waiting, repeats, and assertions with conditions
appId: com.example.shop
---
- launchApp
- tapOn:
id: 'tab-orders'
- extendedWaitUntil:
visible:
id: 'orders-list'
timeout: 15000
- repeat:
while:
notVisible: 'Shipped'
times: 5
commands:
- swipe:
direction: DOWN
duration: 400
- assertVisible:
text: 'Shipped'
- copyTextFrom:
id: 'order-number'
- assertTrue: ${maestro.copiedText != ''}
4. CI: Android emulator on GitHub Actions
name: maestro-android
on: [pull_request]
jobs:
flows:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Install Maestro
run: |
curl -fsSL "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Run flows on emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
profile: pixel_7
script: |
adb install app/build/outputs/apk/debug/app-debug.apk
maestro test .maestro/ --format junit --output maestro-report.xml \
-e EMAIL=ci-bot@example.com -e PASSWORD="${{ secrets.TEST_USER_PASSWORD }}"
- name: Publish report
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-report
path: |
maestro-report.xml
~/.maestro/tests
For hosted device execution, the same suite uploads unchanged: maestro cloud --api-key "$MAESTRO_CLOUD_API_KEY" app-debug.apk .maestro/ runs every flow on cloud devices and returns a pass/fail exit code CI can gate on.
Best Practices
- Use
maestro studio to discover ids and text before writing selectors by hand; it shows exactly what the accessibility tree exposes.
- Add accessibility identifiers in app code (
testID in React Native, accessibilityIdentifier on iOS, contentDescription/resource-id on Android) as part of feature work.
- Keep shared steps in
subflows/ and one top-level flow per journey; flows read top to bottom like a manual test script, keep them that readable.
- Tag smoke flows with YAML
tags: [smoke] and run maestro test --include-tags smoke on every PR, the full suite nightly.
- Capture
takeScreenshot at journey milestones; screenshots plus the JUnit report make CI failures diagnosable without a local repro.
- Pin the emulator API level and profile in CI; device drift is the top source of "passes locally" mysteries.
Anti-Patterns
- Sprinkling
- waitForAnimationToEnd and long extendedWaitUntil timeouts everywhere to paper over an app that never settles; fix the spinner, not the test.
- Selecting by index (
tapOn: point: 50%,30% or index-based taps) for elements that have ids; coordinate taps break on every screen size.
- One giant flow covering login, browse, checkout, refunds, and settings: a failure at step 40 costs a full re-run to debug.
- Hardcoding production credentials in flow files; use
env: plus CI secrets.
- Running flows only on a developer's personal device before release instead of an emulator on every PR.
- Re-implementing Appium-style page objects in JavaScript wrappers around Maestro; the YAML is the abstraction, keep logic out of it.
When to Trigger This Skill
- A repository contains a
.maestro/ directory, YAML files starting with appId:, or maestro commands in CI workflows.
- The user wants mobile UI smoke tests with minimal setup, or asks to test a React Native, Flutter, or native app without Appium infrastructure.
- An existing Appium suite is too slow or flaky for PR-level smoke coverage and the team wants a lighter black-box layer on top.
- Mobile onboarding, login, or checkout journeys need regression coverage that designers and QA can read and edit.
- Choose Maestro for declarative cross-platform flows; recommend Detox when the team needs gray-box React Native synchronization, or Appium when tests must script complex logic in a full programming language.