/maestro — mobile E2E (YAML, multi-env)
Wraps maestro test. Profiles = device + app build per environment.
Profile resolution: --profile → MAESTRO_PROFILE → ~/.maestro/active_profile → [default].
Overview
Wraps maestro test for mobile E2E. Flows are declarative YAML — testers can read/write them without coding. Profiles bind a device + app build per environment. Includes flaky-test detection across multiple JUnit runs.
When to Use
- Mobile E2E flows on iOS / Android / React Native
- Recording new flows interactively (no coding required)
- Inspecting accessibility hierarchy to find selectors
- Detecting flaky tests across multiple runs (JUnit XML diff)
- Cloud-based device farm runs (Maestro Cloud)
When NOT to Use
- Pure unit / component tests → Jest / JUnit, not E2E
- Non-mobile (web, desktop) → wrong tool
- Performance / load tests → use k6 or Detox
- Personal phones (Maestro can hit any UI element including OS dialogs — dedicated test devices only)
Dependencies
curl -Ls "https://get.maestro.mobile.dev" | bash # macOS / Linux
# Windows: scoop install maestro (or use WSL)
maestro --version
iOS: Xcode + simulator. Android: Android Studio + emulator.
Profile config
~/.maestro/profiles.ini (mode 600 — may contain Cloud key):
[default]
platform = ios # ios | android
device = iPhone 15
app_id = com.example.app
flows_dir = .maestro
[android-emu]
platform = android
device = Pixel_7_API_34 # AVD name
app_id = com.example.app
flows_dir = .maestro
[staging-cloud]
platform = android
device = R5XY... # serial of physical device
app_id = com.example.app.staging
flows_dir = .maestro
cloud_api_key = xxxxxxxxxxxxxxxxxxxxxxxx # Maestro Cloud
Helpers
Shared profile/INI/
ctt_*pattern reference: profiles-and-credentials.
source "$HOME/.claude-team-toolkit/lib/credentials.sh"
ctt_load_creds maestro "$PROFILE"
# Verify device reachable
case "$CTT_PLATFORM" in
ios) xcrun simctl list devices "$CTT_DEVICE" | grep -q Booted || xcrun simctl boot "$CTT_DEVICE" ;;
android) adb devices | grep -q "$CTT_DEVICE" || { echo "Device not connected" >&2; return 1; } ;;
esac
Dispatch
run <flow.yaml> [--continuous]
ARGS=()
[ "$CONTINUOUS" = "true" ] && ARGS+=(--continuous)
maestro test "${ARGS[@]}" "$FLOW"
--continuous re-runs on file changes — great for authoring.
run-all [--tags smoke,critical]
maestro test "$CTT_FLOWS_DIR/" \
${TAGS:+--include-tags "$TAGS"} \
--format junit --output /tmp/maestro-results/
record <flow.yaml> — interactive flow recorder
maestro record "$FLOW"
Point/tap to record actions → save as YAML. Tester-friendly (no coding).
studio — visual flow editor (browser-based)
maestro studio
inspect [--app <bundle-id>] — view hierarchy
maestro hierarchy --app "${APP_ID:-$CTT_APP_ID}"
Find element selectors / accessibility IDs.
tags — list all tags used in flows
grep -h "^tags:" -A 20 "$CTT_FLOWS_DIR"/*.yaml | grep "^- " | sort -u
flaky-report [--last N] — detect flaky tests from JUnit results
LAST="${LAST:-10}"
ls -t /tmp/maestro-results/*.xml | head -n "$LAST" | xargs python3 -c '
import sys, xml.etree.ElementTree as ET
from collections import defaultdict
results = defaultdict(list)
for f in sys.argv[1:]:
for tc in ET.parse(f).iter("testcase"):
failed = tc.find("failure") is not None or tc.find("error") is not None
results[tc.get("name")].append(failed)
print("Flaky tests (passed AND failed across runs):")
for n, runs in results.items():
if any(runs) and not all(runs):
print(f" {n}: {sum(runs)}/{len(runs)} fail rate")
'
cloud upload <flow-or-folder> [--name <run-name>] — Maestro Cloud
[ -z "$CTT_CLOUD_API_KEY" ] && { echo "Set cloud_api_key in profile" >&2; return 1; }
maestro cloud --apiKey "$CTT_CLOUD_API_KEY" \
${NAME:+--name "$NAME"} \
"$APP" "$FLOW_OR_FOLDER"
<app> for cloud is the build artifact (.app/.ipa/.apk), not bundle ID.
from-xlsx <xlsx-path> — scaffold from xlsx test cases
Delegates to xlsx-testcases gen maestro — see that skill.
Flow patterns (cheat sheet)
# Common actions
- launchApp
- tapOn: "Submit" # by text
- tapOn:
id: "submit-btn" # by accessibility id
- inputText: "${EMAIL}"
- assertVisible: "Welcome"
- assertNotVisible: "Login"
- waitForAnimationToEnd
- extendedWaitUntil: { visible: "Loaded", timeout: 30000 }
- scrollUntilVisible: { element: "Footer", direction: DOWN }
- runFlow: { file: ../subflows/login.yaml }
Variables via env: EMAIL=test@example.com maestro test flow.yaml. Inside YAML: ${EMAIL}.
Common Mistakes
- Hard-coding credentials in YAML → leak on commit. Use
${ENV_VAR}placeholders. - Selectors by text only → fragile across translations. Prefer
id:(accessibility ID). - Running on real personal devices instead of simulators / test devices
- Writing one mega-flow instead of small subflows +
runFlow:composition - Cloud uploads need build artifact (
.app/.ipa/.apk), not bundle ID - "Flaky test" verdict on first run → need ≥5 runs to distinguish flake from broken
Safety
- Never commit credentials in flow YAML. Use
${ENV_VAR}+ document required vars in flow comment. - Cloud API key per-account — limited blast radius. Prefer CI-scoped key.
- Physical devices: Maestro can interact with anything on screen including OS dialogs. Run only on dedicated test devices, never personal phones.
- Recordings capture screen — don't record flows with real user data / prod accounts.
Layout
.maestro/
├── config.yaml # global config
├── subflows/{login,grant-perms}.yaml
├── smoke/{launch,home-tabs}.yaml
├── regression/...
└── iap/...