Get the FREE Ultimate OpenClaw Setup Guide →

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 --openclaw
Files (1)
SKILL.md
21.4 KB

Maestro 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 of appId:, 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

CommandAndroidiOSWebNotes
tapOnSupports repeat, delay, retryTapIfNoChange, waitToSettleTimeoutMs
doubleTapOnSame selectors as tapOn, adds delay between taps
longPressOn3-second press, same selectors as tapOn
inputTextUnicode NOT supported on Android (ASCII only)
eraseTextDefault removes 50 chars. eraseText: 100 for more. Flaky on iOS
copyTextFromStores in ${maestro.copiedText}
pasteTextPastes from Maestro's internal clipboard only, NOT OS clipboard
setClipboardSets device clipboard text
hideKeyboard✓(flaky)Workaround: tap non-interactive element
pressKey✓(subset)Keys: enter, backspace, home, lock, back(Android), volume up/down, tab(Android)
swipeBy direction, coordinates, or from element. duration param (ms, default 400)
scrollSimple downward scroll
scrollUntilVisibledirection, 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

CommandNotes
assertVisibleWaits ~7s for element to appear. All text/id selectors are regex (full match)
assertNotVisibleWaits ~7s for element to disappear before failing
assertTrueJavaScript expression: assertTrue: ${output.count > 0}
assertWithAIExperimental. Sends screenshot to LLM. Requires maestro login. Free tier works
assertNoDefectsWithAIExperimental. Auto-detects cut-off text, overlapping elements, centering issues
assertScreenshotVisual regression — compares against baseline

App Lifecycle

CommandAndroidiOSWebNotes
launchAppclearState, clearKeychain(iOS), permissions, arguments, stopApp(default true)
stopAppStops without clearing data
killAppSystem-initiated process death. Use with launchApp: stopApp: false to test cold start
clearStateAndroid: pm clear. iOS: reinstalls entire app
clearKeychainClears ENTIRE iOS keychain

Device Control

CommandAndroidiOSWeb
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

CommandNotes
runFlowFile, inline commands:, conditional when:, env params
runScriptExternal .js file. Path relative to calling flow's location
evalScriptSingle-line inline JS: evalScript: ${output.counter = 0}
repeattimes: (N), while: (condition), or both (AND logic)
retrymaxRetries: 0-3. Either commands: or file: — not both
extendedWaitUntilvisible:/notVisible: + timeout: (ms, REQUIRED). Completes immediately when met
waitForAnimationToEndWaits until screen static. Optional timeout: param

Capture

CommandNotes
takeScreenshot: "name"Saves .png. Supports cropOn: selector to crop to specific element
startRecording: "name"Saves .mp4
stopRecordingFinalizes video
copyTextFromCopies element text to ${maestro.copiedText}
extractTextWithAIExperimental. AI-powered text extraction from screenshot

Navigation

CommandNotes
openLinkDeep links, universal links. Android: autoVerify:, browser: params
backAndroid only
travelGPS 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

  1. id: — Most stable. Maps to platform-specific accessibility identifier (see reference files)
  2. text: — Visible text content. Shorthand: tapOn: "Submit"
  3. enabled: trueAuto-waits for element to become interactive. Critical for API-dependent buttons
  4. Positional: below:, above:, leftOf:, rightOf:, childOf:, containsChild:, containsDescendants:
  5. Traits: checked: true, focused: true, selected: true
  6. Size: width:, height:, tolerance: (±px)
  7. index: — 0-based, supports negative (-1 = last). Fragile — last resort
  8. point: — Coordinates (percentage or absolute). Most fragile

Platform Selector Mapping

Platformtext: maps toid: maps to
Android Viewsandroid:text, contentDescription, hintandroid:id (resource ID)
Jetpack ComposeText content, contentDescriptiontestTag (requires testTagsAsResourceId = true)
iOS UIKitView text, accessibilityLabel (precedence)accessibilityIdentifier
iOS SwiftUIView text, .accessibilityLabel().accessibilityIdentifier()
React NativeComponent text, placeholdertestID
FluttersemanticLabel (precedence), text contentSemantics(identifier:) (Flutter ≥3.19)
WebUser-visible textcss 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

  • onFlowStart fails → main flow body SKIPPED, but onFlowComplete STILL RUNS
  • onFlowComplete fails → 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 — last copyTextFrom result
  • maestro.platformios, android, or web
  • MAESTRO_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:

  1. Parse each acceptance criterion
  2. Map to one or more flows (happy path + edge cases per criterion)
  3. Group by feature area, tag appropriately
  4. Create traceability comment linking to requirement
  5. Generate both positive and negative test cases
  6. 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 studio or Maestro Studio Desktop — visual inspector, recorder, AI assistant
  • maestro test --debug-output ./debug — saves maestro.log + screenshots
  • maestro hierarchy — dump accessibility tree from CLI
  • maestro bugreport — diagnostic zip for bug reports
  • --continuous flag — hot-reload testing during development
  • takeScreenshot with cropOn: for targeted visual debugging
  • startRecording / stopRecording for video evidence
  • androidWebViewHierarchy: devtools in 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

  • clearText does NOT exist — use eraseText
  • pasteText only pastes from Maestro's internal clipboard, NOT OS clipboard
  • Unicode inputText not supported on Android (ASCII only)
  • iOS clearState reinstalls the entire app (not just clearing data)
  • Permissions default to ALL ALLOW even with clearState: true
  • launchApp default stopApp: true — kills and relaunches app every time
  • iOS accessibilityLabel takes precedence over text content for text: selector
  • Jetpack Compose testTag requires testTagsAsResourceId = true in semantics
  • Flutter Key class is NOT exposed to accessibility layer — use Semantics(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 one
  • runFlow paths 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

  1. Step 1: Install Maestro (Java 17+) with the provided curl command
  2. Step 2: Read framework reference (RN/Flutter/native/hybrid or QA workflows) and decide a base flow structure
  3. 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

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers