maestro-testing
Flagged{"isSafe":false,"isSuspicious":true,"riskLevel":"high","findings":[{"category":"shell_command","severity":"high","description":"Remote installation via curl -fsSL 'https://get.maestro.mobile.dev' | bash executes a script directly from a remote URL without local verification. This is a classic remote code execution vector if the remote payload is compromised.","evidence":"curl -fsSL \"https://get.maestro.mobile.dev\" | bash"},{"category":"data_exfiltration","severity":"medium","description":"AI-assisted testing features explicitly mention sending screenshots to an external LLM. This constitutes data exfiltration to an external service by design.","evidence":"assertWithAI: **Experimental.** Sends screenshot to LLM."}],"summary":"The content includes a dangerous remote install pattern (curl ... | bash) and mentions data exfiltration by sending screenshots to external AI services. Recommend safer installation practices (e.g., verify script integrity, pin versions, avoid piping to shell) and provide opt-in data sharing with clear disclosures."}
npx machina-cli add skill eagleisbatman/maestro-skill/maestro-testing --openclawMaestro Testing Skill
Generate production-ready Maestro YAML test flows for any mobile or web app. This skill covers flow generation, selector strategy, report generation, CI/CD integration, QA workflows, and MCP-powered AI-assisted testing.
Prerequisites & Installation
Maestro requires Java 17+ and installs with a single command — zero other dependencies:
curl -fsSL "https://get.maestro.mobile.dev" | bash
- Android: Emulator or physical device with USB debugging (API 26+)
- iOS: Xcode with iOS Simulator (macOS only, simulator builds only — no real devices)
- Web: No additional setup — Maestro auto-downloads Chromium on first run
Before Generating Flows
Read the appropriate reference file for the user's framework:
- React Native / Expo →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/react-native.md - Flutter →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/flutter.md - Native Android (Kotlin/Java/Jetpack Compose) →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/native-android.md - Native iOS (Swift/SwiftUI/UIKit) →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/native-ios.md - Hybrid (Capacitor/Ionic/Cordova/PWA) →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/hybrid.md - Web Desktop Browser → use
url:instead ofappId:, same commands. No separate reference needed. - QA workflows, reports, CI/CD →
${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/qa-workflows.md
If framework is unknown, ask. Default to ${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/react-native.md.
1. Project Structure
.maestro/
├── config.yaml # Workspace-level config
├── flows/
│ ├── auth/
│ │ ├── login.yaml
│ │ ├── login-invalid.yaml
│ │ ├── signup.yaml
│ │ └── logout.yaml
│ ├── onboarding/
│ │ └── first-launch.yaml
│ ├── core/
│ │ ├── create-item.yaml
│ │ ├── edit-item.yaml
│ │ └── delete-item.yaml
│ ├── navigation/
│ │ └── tab-navigation.yaml
│ ├── edge-cases/
│ │ ├── no-network.yaml
│ │ ├── empty-state.yaml
│ │ ├── orientation-change.yaml
│ │ └── long-text-input.yaml
│ └── smoke/
│ └── critical-path.yaml
├── subflows/ # Reusable — NOT run as standalone tests
│ ├── login.yaml
│ ├── navigate-to.yaml
│ └── teardown.yaml
├── scripts/
│ ├── setup.js
│ ├── generate-data.js
│ ├── page-objects.js # Page Object Model selectors
│ └── run-tests.sh
└── media/
├── test-photo.png
└── test-video.mp4
2. Complete Command Reference
Every command accepts optional label: (string — masks sensitive data in logs/reports) and optional: true (continues on failure). AI commands default optional to true.
Interaction
| Command | Android | iOS | Web | Notes |
|---|---|---|---|---|
tapOn | ✓ | ✓ | ✓ | Supports repeat, delay, retryTapIfNoChange, waitToSettleTimeoutMs |
doubleTapOn | ✓ | ✓ | ✓ | Same selectors as tapOn, adds delay between taps |
longPressOn | ✓ | ✓ | ✓ | 3-second press, same selectors as tapOn |
inputText | ✓ | ✓ | ✓ | Unicode NOT supported on Android (ASCII only) |
eraseText | ✓ | ✓ | ✓ | Default removes 50 chars. eraseText: 100 for more. Flaky on iOS |
copyTextFrom | ✓ | ✓ | ✓ | Stores in ${maestro.copiedText} |
pasteText | ✓ | ✓ | ✓ | Pastes from Maestro's internal clipboard only, NOT OS clipboard |
setClipboard | ✓ | ✓ | ✗ | Sets device clipboard text |
hideKeyboard | ✓ | ✓(flaky) | ✗ | Workaround: tap non-interactive element |
pressKey | ✓ | ✓(subset) | ✓ | Keys: enter, backspace, home, lock, back(Android), volume up/down, tab(Android) |
swipe | ✓ | ✓ | ✓ | By direction, coordinates, or from element. duration param (ms, default 400) |
scroll | ✓ | ✓ | ✓ | Simple downward scroll |
scrollUntilVisible | ✓ | ✓ | ✓ | direction, timeout(ms), speed(0-100), visibilityPercentage, centerElement |
Random Data Input (built-in DataFaker)
- inputRandomEmail
- inputRandomPersonName
- inputRandomNumber # default 8 digits
- inputRandomNumber:
length: 10
- inputRandomText
- inputRandomText:
length: 20
Assertions
| Command | Notes |
|---|---|
assertVisible | Waits ~7s for element to appear. All text/id selectors are regex (full match) |
assertNotVisible | Waits ~7s for element to disappear before failing |
assertTrue | JavaScript expression: assertTrue: ${output.count > 0} |
assertWithAI | Experimental. Sends screenshot to LLM. Requires maestro login. Free tier works |
assertNoDefectsWithAI | Experimental. Auto-detects cut-off text, overlapping elements, centering issues |
assertScreenshot | Visual regression — compares against baseline |
App Lifecycle
| Command | Android | iOS | Web | Notes |
|---|---|---|---|---|
launchApp | ✓ | ✓ | ✓ | clearState, clearKeychain(iOS), permissions, arguments, stopApp(default true) |
stopApp | ✓ | ✓ | ✗ | Stops without clearing data |
killApp | ✓ | ✓ | ✗ | System-initiated process death. Use with launchApp: stopApp: false to test cold start |
clearState | ✓ | ✓ | ✗ | Android: pm clear. iOS: reinstalls entire app |
clearKeychain | ✗ | ✓ | ✗ | Clears ENTIRE iOS keychain |
Device Control
| Command | Android | iOS | Web |
|---|---|---|---|
setAirplaneMode: enabled/disabled | ✓ | ✗ | ✗ |
toggleAirplaneMode | ✓ | ✗ | ✗ |
setLocation (lat/lng) | ✓(API 31+) | ✓ | ✗ |
travel (waypoints + speed) | ✓(API 31+) | ✓ | ✗ |
setOrientation: PORTRAIT/LANDSCAPE | ✓ | ✓ | ✗ |
setPermissions | ✓ | ✓ | ✗ |
addMedia (png/jpg/gif/mp4) | ✓ | ✓ | ✗ |
Flow Control
| Command | Notes |
|---|---|
runFlow | File, inline commands:, conditional when:, env params |
runScript | External .js file. Path relative to calling flow's location |
evalScript | Single-line inline JS: evalScript: ${output.counter = 0} |
repeat | times: (N), while: (condition), or both (AND logic) |
retry | maxRetries: 0-3. Either commands: or file: — not both |
extendedWaitUntil | visible:/notVisible: + timeout: (ms, REQUIRED). Completes immediately when met |
waitForAnimationToEnd | Waits until screen static. Optional timeout: param |
Capture
| Command | Notes |
|---|---|
takeScreenshot: "name" | Saves .png. Supports cropOn: selector to crop to specific element |
startRecording: "name" | Saves .mp4 |
stopRecording | Finalizes video |
copyTextFrom | Copies element text to ${maestro.copiedText} |
extractTextWithAI | Experimental. AI-powered text extraction from screenshot |
Navigation
| Command | Notes |
|---|---|
openLink | Deep links, universal links. Android: autoVerify:, browser: params |
back | Android only |
travel | GPS movement simulation between waypoints at specified speed (m/s) |
3. Selector Strategy
Maestro uses the accessibility tree. All text/id fields are regex matching the entire element text.
Priority Order
id:— Most stable. Maps to platform-specific accessibility identifier (see reference files)text:— Visible text content. Shorthand:tapOn: "Submit"enabled: true— Auto-waits for element to become interactive. Critical for API-dependent buttons- Positional:
below:,above:,leftOf:,rightOf:,childOf:,containsChild:,containsDescendants: - Traits:
checked: true,focused: true,selected: true - Size:
width:,height:,tolerance:(±px) index:— 0-based, supports negative (-1 = last). Fragile — last resortpoint:— Coordinates (percentage or absolute). Most fragile
Platform Selector Mapping
| Platform | text: maps to | id: maps to |
|---|---|---|
| Android Views | android:text, contentDescription, hint | android:id (resource ID) |
| Jetpack Compose | Text content, contentDescription | testTag (requires testTagsAsResourceId = true) |
| iOS UIKit | View text, accessibilityLabel (precedence) | accessibilityIdentifier |
| iOS SwiftUI | View text, .accessibilityLabel() | .accessibilityIdentifier() |
| React Native | Component text, placeholder | testID |
| Flutter | semanticLabel (precedence), text content | Semantics(identifier:) (Flutter ≥3.19) |
| Web | User-visible text | css selector (web-only) |
Compound Selectors (AND logic)
- tapOn:
id: "submit-btn"
enabled: true # waits for button to be interactive
below: "Form Title" # positional constraint
Regex in Selectors
- assertVisible: '.*Welcome.*' # contains "Welcome"
- assertVisible: 'Order #\\d{6}' # "Order #" + 6 digits
- assertVisible: '.*\\.99' # price ending in .99
- assertVisible: 'Movies \\[NEW\\]' # escape brackets
Gotcha: YAML booleans — quote "YES", "NO", "true", "false" to prevent YAML parsing.
Gotcha: Dollar signs need escaping: \\$150.
4. Flow Configuration (YAML Header)
Every flow has a configuration section above the --- separator:
appId: com.example.app # REQUIRED for mobile
# url: https://myapp.com # Use for web instead of appId
name: "Login Happy Path" # Custom display name
tags:
- smoke
- auth
- regression
env:
TEST_EMAIL: ${TEST_EMAIL || "test@example.com"} # JS default values
TEST_PASSWORD: ${TEST_PASSWORD || "Test1234!"}
properties: # Custom JUnit XML properties
testCaseId: "TC-101"
priority: "High"
jiraTicket: "PROJ-456"
onFlowStart:
- clearState
- runScript: scripts/setup.js
onFlowComplete:
- takeScreenshot: "final-state"
- runFlow: subflows/teardown.yaml
jsEngine: graaljs # graaljs (ES2022) or rhino (ES5, default)
androidWebViewHierarchy: devtools # Chrome DevTools for WebView inspection
---
# Flow steps begin here
- launchApp
Hook Failure Behavior
onFlowStartfails → main flow body SKIPPED, butonFlowCompleteSTILL RUNSonFlowCompletefails → flow marked FAILED even if main body passed
5. launchApp — Critical Details
- launchApp:
appId: "com.example.app"
clearState: true
clearKeychain: true # iOS only, clears ENTIRE keychain
stopApp: false # false = foreground backgrounded app (default: true)
permissions:
all: deny # overrides default (all: allow)
notifications: allow
location: allow
camera: allow
photos: unset # values: allow, deny, unset
android.permission.ACCESS_FINE_LOCATION: deny # Android-specific
arguments: # access in app code
isMaestro: "true"
testMode: true
apiEndpoint: "https://staging.api.com"
Critical: Permissions default to ALL ALLOW even when clearState: true. Explicitly deny what you need denied.
6. JavaScript Integration
Engine Selection
jsEngine: graaljs # ES2022, recommended. Set in TOP-LEVEL flow only
Or: export MAESTRO_USE_GRAALJS=true
GraalJS vs Rhino: GraalJS isolates variables per script (predictable), supports ES2022, handles special characters. Rhino shares variables (leaky), ES5 only.
HTTP Requests (custom API — NOT fetch/XMLHttpRequest)
// scripts/create-test-user.js
const response = http.post('https://api.example.com/users', {
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + API_KEY },
body: JSON.stringify({ email: 'test-' + Date.now() + '@example.com', password: 'Test1234!' })
})
if (response.ok) {
const user = json(response.body)
output.userId = user.id
output.email = user.email
}
// Also available: http.get(), http.put(), http.delete(), http.request()
// Response: { ok, status, body, headers }
Built-in Variables
maestro.copiedText— lastcopyTextFromresultmaestro.platform—ios,android, orwebMAESTRO_FILENAME,MAESTRO_DEVICE_UDID,MAESTRO_SHARD_ID,MAESTRO_SHARD_INDEX- All
MAESTRO_*env vars auto-available in flows
Page Object Model Pattern
// scripts/page-objects.js
output.loginPage = {
emailField: 'email-input',
passwordField: 'password-input',
submitBtn: 'login-button',
errorMsg: 'error-message'
}
output.homePage = {
welcomeText: 'welcome-header',
profileTab: 'tab-profile',
settingsTab: 'tab-settings'
}
- runScript: scripts/page-objects.js
- tapOn:
id: ${output.loginPage.emailField}
- inputText: ${TEST_EMAIL}
- tapOn:
id: ${output.loginPage.submitBtn}
- assertVisible:
id: ${output.homePage.welcomeText}
7. Conditions and Flow Control
# Platform-specific
- runFlow:
when:
platform: Android
file: subflows/android-permissions.yaml
# Dismiss optional dialogs
- runFlow:
when:
visible: "Rate this app"
commands:
- tapOn: "Not now"
# JavaScript expression
- runFlow:
when:
true: ${IS_FEATURE_ENABLED == 'true'}
file: subflows/new-feature-test.yaml
# Combined (AND logic — ALL must match)
- runFlow:
when:
platform: iOS
visible: "Allow Notifications"
commands:
- tapOn: "Allow"
No native OR logic — use JavaScript true: condition for complex booleans.
8. Reusable Sub-Flows with Parameters
# subflows/login.yaml
appId: ${APP_ID}
env:
USERNAME: ${USERNAME || "test@example.com"}
PASSWORD: ${PASSWORD || "Test1234!"}
---
- tapOn:
id: "email-input"
- inputText: ${USERNAME}
- tapOn:
id: "password-input"
- inputText:
text: ${PASSWORD}
label: "Enter password" # masks in logs
- tapOn: "Sign In"
- extendedWaitUntil:
visible: "Home"
timeout: 10000
# Calling flow
- runFlow:
file: subflows/login.yaml
env:
USERNAME: "admin@test.com"
PASSWORD: "AdminPass!"
9. Test Reports & Output
Read ${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/qa-workflows.md for complete report generation details. Summary:
Report Formats
maestro test --format junit --output reports/results.xml .maestro/
maestro test --format html --output reports/results.html .maestro/
maestro test --format html-detailed --output reports/detailed.html .maestro/
Custom JUnit Properties (for CI dashboards)
appId: com.example.app
properties:
testCaseId: "TC-101"
priority: "High"
jiraTicket: "PROJ-456"
---
Output Directory
maestro test --test-output-dir=build/maestro-results .maestro/ # custom path
maestro test --flat-output .maestro/ # no timestamp folders (CI-friendly)
maestro test --debug-output=build/debug .maestro/ # maestro.log + debug data
Default: ~/.maestro/tests/<datetime>/ — contains screenshots, video, commands-*.json, AI reports.
AI Test Analysis
maestro test --analyze .maestro/
Generates HTML insights detecting UI regressions, spelling errors, layout breaks, i18n issues.
10. Maestro MCP Server Integration
maestro mcp # starts MCP server (bundled with CLI)
Claude Code / Claude Desktop config:
{
"mcpServers": {
"maestro": {
"command": "maestro",
"args": ["mcp"]
}
}
}
MCP tools: back, cheat_sheet, check_flow_syntax, input_text, inspect_view_hierarchy, launch_app, list_devices, query_docs, run_flow, run_flow_files, start_device, stop_app, take_screenshot, tap_on
11. CI/CD Integration
EAS Workflows (Expo / React Native — Recommended)
Expo projects use EAS Workflows with a first-class type: maestro job — see ${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/react-native.md and ${CLAUDE_PLUGIN_ROOT}/skills/maestro-testing/references/qa-workflows.md for full config.
# .eas/workflows/e2e-test.yml
name: E2E Tests
on:
pull_request:
branches: ['*']
jobs:
build:
type: build
params:
platform: android
profile: e2e-test
test:
needs: [build]
type: maestro
params:
build_id: ${{ needs.build.outputs.build_id }}
flow_path: '.maestro/'
include_tags: smoke
record_screen: true
GitHub Actions (Maestro Cloud)
- uses: mobile-dev-inc/action-maestro-cloud@v1
with:
api-key: ${{ secrets.MAESTRO_API_KEY }}
project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
app-file: app/build/outputs/apk/debug/app-debug.apk
workspace: .maestro
include-tags: smoke
env: |
TEST_EMAIL=ci@test.com
Local CI
maestro test --format junit --output build/report.xml --flat-output .maestro/
Tag-Based Execution
maestro test --include-tags smoke .maestro/
maestro test --exclude-tags wip .maestro/
Sharding
maestro test --shard-all 3 .maestro/ # ALL tests on ALL 3 devices
maestro test --shard-split 3 .maestro/ # divide tests across 3 devices
12. Workspace Config
# .maestro/config.yaml
flows:
- '**' # recursive include all subdirectories
includeTags: []
excludeTags:
- wip
- subflow
executionOrder:
continueOnFailure: false # stop on first failure (default: true)
flowsOrder: # explicit ordering for dependent flows
- flows/auth/login.yaml
- flows/core/create-item.yaml
testOutputDir: build/maestro-results
platform:
android:
disableAnimations: true # disables window/transition animations
ios:
disableAnimations: true # enables Reduce Motion
13. PRD → Test Flow Generation
When the user provides a PRD, spec, or user stories:
- Parse each acceptance criterion
- Map to one or more flows (happy path + edge cases per criterion)
- Group by feature area, tag appropriately
- Create traceability comment linking to requirement
- Generate both positive and negative test cases
- Include setup/teardown hooks for test isolation
# Requirement: US-12 — User can reset password via email
# AC: Given a registered user, when they tap "Forgot Password"
# and enter their email, then they see a confirmation message.
appId: com.example.app
name: "US-12: Password Reset - Happy Path"
tags: [auth, regression]
properties:
testCaseId: "TC-012"
requirement: "US-12"
---
14. Debugging
maestro studioor Maestro Studio Desktop — visual inspector, recorder, AI assistantmaestro test --debug-output ./debug— savesmaestro.log+ screenshotsmaestro hierarchy— dump accessibility tree from CLImaestro bugreport— diagnostic zip for bug reports--continuousflag — hot-reload testing during developmenttakeScreenshotwithcropOn:for targeted visual debuggingstartRecording/stopRecordingfor video evidenceandroidWebViewHierarchy: devtoolsin flow header for WebView inspection
Detect Maestro in App (for test-only behaviors)
- launchApp:
arguments:
isMaestro: "true"
App-side: Android intent.getStringExtra("isMaestro"), iOS ProcessInfo.processInfo.arguments, Web checks window.maestro.
15. Known Gotchas
clearTextdoes NOT exist — useeraseTextpasteTextonly pastes from Maestro's internal clipboard, NOT OS clipboard- Unicode
inputTextnot supported on Android (ASCII only) - iOS
clearStatereinstalls the entire app (not just clearing data) - Permissions default to ALL ALLOW even with
clearState: true launchAppdefaultstopApp: true— kills and relaunches app every time- iOS
accessibilityLabeltakes precedence over text content fortext:selector - Jetpack Compose
testTagrequirestestTagsAsResourceId = truein semantics - Flutter
Keyclass is NOT exposed to accessibility layer — useSemantics(identifier:) - Dollar signs in selectors need escaping:
\\$150 - YAML booleans — quote
"YES","NO","true","false" - Cloud tests ~45s slower due to device wipe/recreate overhead
console.log()in JS with multiple arguments only prints the first onerunFlowpaths in Cloud must specify folder, not single file
Source
git clone https://github.com/eagleisbatman/maestro-skill/blob/main/skills/maestro-testing/SKILL.mdView on GitHub Overview
Maestro Testing Skill creates production-ready YAML test flows for Android, iOS, and web apps. It covers flow generation, selector strategy, report generation, CI/CD integration, QA workflows, and MCP-powered AI-assisted testing to automate UI and end-to-end scenarios from PRDs, specs, or user stories.
How This Skill Works
Install Maestro (Java 17+), then read framework-specific references (RN/Expo, Flutter, native Android/iOS, hybrid, or web desktop). Build flows in the standardized project structure under .maestro/flows, reuse subflows, and define selectors with a robust strategy (Page Object Model encouraged via scripts/page-objects.js). Use the Maestro command set (tapOn, inputText, etc.) to author YAML tests, generate reports, and hook into CI/CD or MCP AI features for automated testing.
When to Use It
- You want to generate production-ready Maestro YAML flows for a mobile (Android/iOS) or web app
- You need to automate UI/end-to-end tests or create/edit Maestro flows from PRDs/specs/user stories
- You’re integrating Maestro tests into CI/CD or using MCP-powered AI-assisted testing
- You require framework-specific references and project structure guidance for flows (RN, Flutter, native, hybrid, web)
- You’re validating visual regression, test reports, or automation for web desktop testing
Quick Start
- Step 1: Install Maestro (Java 17+) with the provided curl command
- Step 2: Read framework reference (RN/Flutter/native/hybrid or QA workflows) and decide a base flow structure
- Step 3: Create a new YAML under .maestro/flows (e.g., flows/auth/login.yaml) and run tests locally
Best Practices
- Read the correct framework reference file before generating flows
- Organize tests under .maestro/flows with clear categories (auth, onboarding, edge-cases, etc.)
- Adopt a robust selector strategy and consider Page Object Model (page-objects.js) for maintainability
- Leverage the full command set (tapOn, inputText, etc.) with optional: true for resilience
- Validate flows locally first and align with QA workflows, then integrate with CI/CD and reporting
Example Use Cases
- Flutter mobile app: login.yaml under flows/auth with tapOn, inputText, and assertion steps
- React Native + Capacitor: onboarding.yaml verifying navigation and offline handling
- Native iOS (SwiftUI): end-to-end sign-up flow with verification and push notification checks
- Web Desktop: admin panel navigation.yaml testing tab navigation and data filtering
- Hybrid (Ionic) app: edge-cases.yaml for no-network and long-text input scenarios