appshots-maestro-capture
Maestro Screenshot Capture
Maestro is a declarative, YAML-driven mobile UI automation tool with cross-framework support (Flutter, SwiftUI, UIKit, Compose, React Native). Use it to navigate apps, interact with elements, and capture screenshots — without writing bash scripts or manual sleep timers.
Install → Write YAML Flows → Run → Screenshots
Prerequisites
# Install Maestro CLI (official installer — NOT brew, which installs a different app)
curl -fsSL "https://get.maestro.mobile.dev" | bash
# Add to PATH (the installer adds this to .zshrc, but for current session)
export PATH="$PATH:$HOME/.maestro/bin"
# Verify installation
maestro --version
⚠️ Do NOT use
brew install maestro— that installs a code editor cask, not the mobile testing CLI.
Supported frameworks: Flutter (3.19+), SwiftUI, UIKit, Jetpack Compose, Android XML, React Native.
Core Concepts
Why Maestro over Bash + AXe?
| Problem with Bash/AXe | Maestro Solution |
|---|---|
Manual sleep timers |
Auto-waits for elements to appear |
| Coordinate-based taps break across devices | Uses semantic identifiers |
| No retry on flaky interactions | Built-in retry with configurable tolerance |
| iOS Simulator only | iOS + Android with same YAML flow |
| Long imperative scripts | Short declarative YAML |
Element Selection Priority
Maestro finds elements in this order (use the most specific one available):
id:— Matches accessibility identifiers ← preferred- Flutter:
Semantics(identifier: '...') - SwiftUI:
.accessibilityIdentifier("...") - UIKit:
.accessibilityIdentifier = "..." - Compose:
Modifier.semantics { testTag = "..." } - Android XML:
android:tag="..." - React Native:
testID="..."
- Flutter:
text:— Matches visible text on screenlabel:— Matches accessibility label- Relative selectors —
below:,above:,leftOf:,rightOf:,containedIn:
See the appshots-accessibility-ids skill for detailed per-framework identifier setup.
Writing a Capture Flow
Create .maestro/ directory at your project root. Each flow is a .yaml file.
Basic Flow: capture_home.yaml
appId: com.example.myapp # Your app's bundle ID
---
- launchApp
# Home screen
- takeScreenshot: 01_home
# Navigate to a tab
- tapOn:
id: "tab_search" # Uses accessibility identifier
- takeScreenshot: 02_search
# Scroll down
- scroll
- takeScreenshot: 03_scrolled
Advanced Flow: capture_detail.yaml
appId: com.example.myapp
---
- launchApp
# Scroll to find a card
- scrollUntilVisible:
element:
id: "card_featured_item"
direction: DOWN
timeout: 10000
# Tap the card
- tapOn:
id: "card_featured_item"
# Wait for detail screen to load
- tapOn:
id: "play_button"
# Wait for content to appear
- extendedWaitUntil:
visible: "SCORE"
timeout: 8000
- takeScreenshot: detail_gameplay
Flow with Conditional Waits
appId: com.example.myapp
---
- launchApp
# Navigate to a feature
- tapOn:
id: "feature_camera"
# Tap a sample item
- tapOn:
id: "sample_item_0"
# Wait for processing to finish (look for a result element)
- extendedWaitUntil:
visible:
id: "result_item_0"
timeout: 15000
# Tap result to show detail popup
- tapOn:
id: "result_item_0"
# Wait for popup to animate in
- extendedWaitUntil:
visible:
id: "detail_popup"
timeout: 5000
- takeScreenshot: feature_detail_popup
Running Flows
Single flow
maestro test .maestro/capture_home.yaml
All flows in directory
maestro test .maestro/
Specific device
maestro test --device <UDID> .maestro/capture_home.yaml
Custom output directory
maestro test --test-output-dir ./screenshots/raw .maestro/capture_home.yaml
Multi-Locale Screenshot Capture
Strategy: One Simulator Per Locale (Parallel)
For fastest captures, create a dedicated simulator per locale and run flows in parallel.
#!/bin/bash
# scripts/maestro-multi-locale.sh
# Usage: bash scripts/maestro-multi-locale.sh [locale1] [locale2] ...
# Example: bash scripts/maestro-multi-locale.sh en vi ja
set -e
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro-Max"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-3"
APP_PATH="build/ios/iphonesimulator/Runner.app"
BUNDLE_ID="com.example.app"
FLOW_DIR=".maestro"
OUTPUT_BASE="./screenshots/raw"
# Configuration
IS_NATIVE_IOS=false # Set to true if app uses system locale
BUILD_APP=false # Set to true to build the app before capturing
LOCALES="${@:-en}"
# Map locale codes to Apple format
get_apple_locale() {
case "$1" in
en) echo "en_US" ;;
vi) echo "vi_VN" ;;
ja) echo "ja_JP" ;;
ko) echo "ko_KR" ;;
zh) echo "zh_CN" ;;
de) echo "de_DE" ;;
fr) echo "fr_FR" ;;
es) echo "es_ES" ;;
pt) echo "pt_BR" ;;
*) echo "${1}_${1^^}" ;;
esac
}
capture_locale() {
local locale="$1"
local apple_locale
apple_locale=$(get_apple_locale "$locale")
local lang="${locale}"
local sim_name="Screenshot_${locale}"
local output_dir="${OUTPUT_BASE}/${locale}"
echo "[$locale] Creating simulator..."
local udid
udid=$(xcrun simctl create "$sim_name" "$DEVICE_TYPE" "$RUNTIME" 2>/dev/null || \
xcrun simctl list devices | grep "$sim_name" | grep -oE '[A-F0-9-]{36}' | head -1)
echo "[$locale] UDID: $udid"
# Boot and set locale
xcrun simctl boot "$udid" 2>/dev/null || true
sleep 2
if [ "$IS_NATIVE_IOS" = true ]; then
# Native iOS: change system locale
xcrun simctl spawn "$udid" defaults write "Apple Global Domain" AppleLocale "$apple_locale"
xcrun simctl spawn "$udid" defaults write "Apple Global Domain" AppleLanguages -array "$lang"
xcrun simctl shutdown "$udid"
sleep 1
xcrun simctl boot "$udid"
sleep 5
else
# Flutter / Shared Prefs: write directly to app preferences
local prefs_dir=$(xcrun simctl get_app_container "$udid" "$BUNDLE_ID" data)/Library/Preferences
defaults write "$prefs_dir/$BUNDLE_ID" selected_locale -string "$lang"
# No reboot needed for app-level locale
fi
# Install app
xcrun simctl install "$udid" "$APP_PATH"
# Run Maestro flows
mkdir -p "$output_dir"
maestro test \
--device "$udid" \
--test-output-dir "$output_dir" \
"$FLOW_DIR/"
echo "[$locale] Capture complete → $output_dir"
# Cleanup: delete the temporary simulator
xcrun simctl shutdown "$udid" 2>/dev/null || true
xcrun simctl delete "$udid" 2>/dev/null || true
}
if [ "$BUILD_APP" = true ]; then
# Build the app first (adjust for your framework)
# Flutter: flutter build ios --simulator
# Xcode (native): xcodebuild -scheme MyApp -sdk iphonesimulator -derivedDataPath build
# React Native: npx react-native build-ios --mode=Release --simulator
echo "Building app for simulator..."
# ⬇ Replace with your framework's build command
flutter build ios --simulator
fi
# Run captures (parallel for speed)
for locale in $LOCALES; do
capture_locale "$locale" &
done
wait
echo "All captures complete! Check $OUTPUT_BASE"
Maestro Studio (Interactive Discovery)
Use maestro studio to visually inspect your app's UI tree and generate YAML commands interactively:
maestro studio
This launches a browser-based UI where you can:
- Click elements to see their identifiers, text, and labels
- Generate YAML by performing actions in the studio
- Verify selectors before writing them into flow files
This is the Maestro equivalent of
axe describe-ui, but visual and interactive.
Agent Guidelines
Thinking Process
When given a task like "capture screenshots of my app across 3 locales":
- Check for
.maestro/directory — Reuse existing flows if present - Discover identifiers — Run
maestro studioor inspect code for accessibility identifiers (seeappshots-accessibility-idsskill) - Write YAML flows — One flow per logical screen group (home, detail, settings)
- Test single locale first —
maestro test .maestro/on the default simulator - Scale to multi-locale — Use
scripts/maestro-multi-locale.sh en vi ja - Hand off to design — Switch to
appshots-design-workflowskill for framing and styling
Key Tips
- Always use
id:overtext:— Text changes across locales, identifiers don't - Use
extendedWaitUntil— For screens that need loading time (network, animations) - Use
scrollUntilVisible— Instead of manual swipe coordinates - One flow per screen group — Keeps flows focused and debuggable
- Use
--test-output-dir— To control exactly where screenshots land
When to Fall Back to AXe
Maestro covers 95% of use cases. Use AXe (axe describe-ui, axe tap) only when:
- You need the raw accessibility tree JSON for debugging
- Maestro doesn't support a specific simulator interaction (e.g., hardware buttons)
- You're working in a CI environment that doesn't support Maestro
Reference: Maestro Commands for Screenshots
| Command | Purpose |
|---|---|
launchApp |
Launch the app (auto-waits for ready) |
tapOn: |
Tap an element by id, text, or label |
longPressOn: |
Long press an element |
scroll |
Scroll down on current screen |
scrollUntilVisible: |
Scroll until an element is found |
swipe: |
Swipe in a direction (LEFT, RIGHT, UP, DOWN) |
inputText: |
Type text into focused field |
takeScreenshot: |
Save screenshot with given name |
extendedWaitUntil: |
Wait for element to appear (with timeout) |
assertVisible: |
Assert an element is on screen |
back |
Press back button (Android) |
hideKeyboard |
Dismiss the keyboard |
clearState |
Clear app data |
stopApp |
Force stop the app |