src/skills/mobile-testing-maestro/SKILL.md
Maestro mobile E2E testing - YAML flows, selectors, flow control, environment variables, JavaScript expressions, device interactions, Maestro Studio, Maestro Cloud CI, tags, test suites
npx skillsauth add agents-inc/skills mobile-testing-maestroInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Write E2E tests as declarative YAML flows. Use
idselectors for stable element targeting (not text that changes with localization). UserunFlowto compose reusable subflows (login, setup). UsewaitForAnimationToEndbefore assertions on animated screens. UseonFlowStart/onFlowCompletehooks for setup/teardown. Maestro auto-retries assertions for up to 7 seconds before failing. Current stable: CLI 2.4.0.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use id selectors (accessibility identifiers) as primary selectors - text selectors break with localization or copy changes)
(You MUST use runFlow for reusable sequences (login, onboarding) - NEVER duplicate steps across flow files)
(You MUST use waitForAnimationToEnd before assertions on screens with animations or transitions - assertions on animated elements are flaky)
(You MUST pair every startRecording with a stopRecording - unpaired commands produce corrupted or missing video files)
(You MUST use environment variables or env blocks for credentials and environment-specific values - NEVER hardcode secrets in YAML flows)
</critical_requirements>
Auto-detection: Maestro, maestro, .maestro, maestro test, maestro cloud, maestro studio, launchApp, tapOn, assertVisible, assertNotVisible, inputText, scrollUntilVisible, runFlow, evalScript, runScript, swipe, hideKeyboard, waitForAnimationToEnd, onFlowStart, onFlowComplete, maestro.yaml, config.yaml tags
When to use:
When NOT to use:
Key patterns covered:
${}, evalScript, runScript, output objectDetailed Resources:
Maestro takes a fundamentally different approach from code-based testing frameworks: tests are declarative YAML, not imperative code. This makes flows readable by anyone on the team, not just developers. The framework handles the hard parts of mobile testing automatically -- waiting for elements, retrying taps, tolerating animation delays -- so flows focus on what to test, not how to wait.
Core principles:
runFlowid) over visible text to survive localization and copy changesMental model:
Maestro flows are recipes. Each step is an action a user would take. The framework handles timing, retries, and platform differences. You describe the journey, Maestro drives the car.
When to use Maestro:
When NOT to use Maestro:
Every flow starts with a configuration block (appId, optional env/tags) separated from commands by ---. Commands execute sequentially top to bottom.
appId: com.example.app
tags:
- smoke
- auth
---
- launchApp
- tapOn:
id: "email_input"
- inputText: "[email protected]"
- tapOn:
id: "password_input"
- inputText: "secure_password"
- tapOn:
id: "login_button"
- assertVisible:
id: "home_screen"
Why good: appId identifies the app under test, tags enable filtering with --include-tags/--exclude-tags, id selectors are stable across localizations, sequential commands read like a user story
See examples/core.md for complete flow structure with env blocks, labels, and clearState.
Use id (accessibility identifier) as the primary selector. Fall back to text for static labels, point for coordinates only as last resort. Combine selectors for precision.
# Preferred: id selector (stable, language-independent)
- tapOn:
id: "submit_button"
# Fallback: text selector (breaks with i18n changes)
- tapOn:
text: "Submit"
# Relational: below/above/childOf for disambiguation
- tapOn:
text: "Delete"
below: "Shopping Cart"
# State selectors: filter by element state
- tapOn:
id: "toggle_switch"
enabled: true
Why good: id selectors survive text changes, relational selectors disambiguate duplicate labels, state selectors prevent tapping disabled elements
See examples/core.md for all selector types including index, point, and combined selectors.
Extract repeated sequences into separate flow files. Pass context via env parameters. Use label for clear test reports.
# Main flow: checkout-test.yaml
appId: com.example.app
---
- runFlow:
file: subflows/login.yaml
env:
USERNAME: "[email protected]"
PASSWORD: "test_password"
label: "Log in as test user"
- tapOn:
id: "cart_icon"
- runFlow:
file: subflows/complete-checkout.yaml
label: "Complete purchase flow"
- assertVisible:
id: "order_confirmation"
# Subflow: subflows/login.yaml
appId: com.example.app
---
- tapOn:
id: "email_input"
- inputText: ${USERNAME}
- tapOn:
id: "password_input"
- inputText: ${PASSWORD}
- tapOn:
id: "login_button"
Why good: login sequence defined once and reused across all flows, env parameters make subflows configurable, labels improve test report readability
See examples/flow-control.md for inline subflows, conditional flows, and nested composition.
Use when with platform, visible, notVisible, or JavaScript true expressions to handle differences between iOS and Android or optional UI states.
# Platform-specific permission handling
- runFlow:
when:
platform: Android
commands:
- tapOn: "Allow"
- runFlow:
when:
platform: iOS
commands:
- tapOn: "Allow While Using App"
# Dismiss optional popup if visible
- runFlow:
when:
visible: "Rate this app"
commands:
- tapOn: "Not now"
Why good: single flow handles both platforms, visibility conditions handle non-deterministic UI (popups, tooltips), no test failure on missing optional elements
See examples/flow-control.md for JavaScript conditions and combined conditions.
Pass runtime values via CLI flags (-e), shell variables (MAESTRO_ prefix), or env blocks in flow files. Use ${} syntax for interpolation with JavaScript fallback defaults.
appId: com.example.app
env:
BASE_URL: "https://staging.example.com"
DEFAULT_USER: "[email protected]"
---
- launchApp
- tapOn:
id: "email_input"
- inputText: ${USERNAME || DEFAULT_USER}
# Override from CLI
maestro test -e [email protected] -e PASSWORD=secret flow.yaml
Why good: secrets never hardcoded in flow files, env blocks provide defaults, CLI overrides enable multi-environment testing, || fallback prevents failures when variables are missing
See examples/flow-control.md for built-in variables, runScript with env, and shell variable patterns.
Use onFlowStart and onFlowComplete in the configuration block for consistent setup/teardown across all flows. onFlowComplete runs even if the flow fails.
appId: com.example.app
onFlowStart:
- clearState
- runFlow:
file: subflows/login.yaml
env:
USERNAME: "[email protected]"
PASSWORD: "test_password"
onFlowComplete:
- runFlow: subflows/cleanup.yaml
---
- tapOn:
id: "settings_icon"
- assertVisible:
id: "settings_screen"
Why good: clearState ensures clean app state, login runs before every flow, cleanup always runs (even on failure), prevents test pollution between flows
Hook failure behavior: If onFlowStart fails, the main flow is skipped but onFlowComplete still executes. If onFlowComplete fails, the flow is marked as failed even if the main test passed.
See examples/flow-control.md for hooks with environment variables and script-based teardown.
Use inline ${} for simple interpolation, evalScript for variable computation, and runScript for complex logic in external .js files. All share a global output object.
# Inline expression
- inputText: user_${Date.now()}@test.com
# evalScript for computation
- evalScript: ${output.timestamp = Date.now()}
- inputText: ${output.timestamp}
# runScript for complex logic (external file)
- runScript: scripts/generate-test-data.js
- inputText: ${output.generatedEmail}
Why good: inline expressions handle simple dynamic values, evalScript sets variables without UI interaction, runScript keeps complex logic in testable JS files, output object passes data between steps
See examples/flow-control.md for HTTP requests in scripts, DataFaker, and output namespacing.
</patterns><decision_framework>
Can you add an accessibility identifier (testID/accessibilityIdentifier)?
|-- YES -> Use id selector (most stable)
+-- NO -> Is the text static and unique on screen?
|-- YES -> Use text selector
+-- NO -> Is there a unique parent or sibling?
|-- YES -> Use relational selector (below, childOf, etc.)
+-- NO -> Use point selector as last resort (fragile)
Is this sequence used in 2+ flows?
|-- YES -> Extract to subflows/ directory, call with runFlow
+-- NO -> Keep inline in the flow
Does the flow need setup/teardown?
|-- YES -> For ALL flows: use onFlowStart/onFlowComplete in config.yaml
| For ONE flow: use runFlow at start/end of that flow
+-- NO -> Start with launchApp directly
Is there platform-specific behavior?
|-- YES -> Use when: platform: Android/iOS conditions
+-- NO -> Single flow handles both platforms
Need a dynamic value (timestamp, random ID)?
|-- YES -> Inline ${} expression (e.g., ${Date.now()})
+-- NO -> Need to compute and store a value?
|-- YES -> evalScript for simple computation
+-- NO -> Need HTTP calls, file I/O, or complex logic?
|-- YES -> runScript with external .js file
+-- NO -> Plain YAML commands are sufficient
</decision_framework>
<red_flags>
High Priority Issues:
text selectors for buttons/labels that will be localized - breaks when language changes. Use id (accessibility identifiers) instead.runFlowstopRecording after startRecording - produces corrupted or zero-byte video files-e or MAESTRO_ prefixsleep or extendedWaitUntil with long timeouts instead of waitForAnimationToEnd - Maestro's built-in tolerance handles most timing issues automaticallyMedium Priority Issues:
clearState or clearKeychain in setup - test results depend on leftover app state from previous runstags for flow categorization - makes it impossible to run targeted subsets (smoke, regression, etc.)point selectors (coordinates) as primary strategy - breaks on different screen sizes and resolutionslabel on runFlow calls - test reports show file paths instead of meaningful step descriptionsGotchas and Edge Cases:
assertVisible auto-retries for 7 seconds before failing - this is a feature, not a bug. Don't add explicit waits before assertions.parseInt() or comparison in JavaScript if you need numeric logicMAESTRO_ prefixed shell variables are automatically available in flows but only via CLI, not Maestro Studio"false" is truthy in JavaScript - use explicit === "true" comparison in when: true: conditionsonFlowComplete runs even when the flow fails - design teardown logic that doesn't assume successrunFlow with commands (inline) and runFlow with file (external) are mutually exclusive - you cannot use both in the same runFlow callevalScript because the command is already wrapped in ${} - use string concatenation insteadconsole.log in evalScript writes to maestro.log, not the terminal - use runScript for terminal-visible loggingretry maxRetries is capped at 3 - for more attempts, restructure the flow logic--async flag returns immediately without waiting for results - poll the API or use webhooks for completionscrollUntilVisible with text fallback for list items</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST use id selectors (accessibility identifiers) as primary selectors - text selectors break with localization or copy changes)
(You MUST use runFlow for reusable sequences (login, onboarding) - NEVER duplicate steps across flow files)
(You MUST use waitForAnimationToEnd before assertions on screens with animations or transitions - assertions on animated elements are flaky)
(You MUST pair every startRecording with a stopRecording - unpaired commands produce corrupted or missing video files)
(You MUST use environment variables or env blocks for credentials and environment-specific values - NEVER hardcode secrets in YAML flows)
Failure to follow these rules will produce flaky tests, broken recordings, and security-exposed credentials in version control.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events