Claude Skill

mobile-testing-maestro

Maestro mobile E2E testing - YAML flows, selectors, flow control, environment variables, JavaScript expressions, device interactions, Maestro Studio, Maestro Cloud CI, tags, test suites

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-testing-maestro_skills_mobile-testing-maestro-3a51ef5.zip · 18 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-testing-maestro/skills/mobile-testing-maestro
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Maestro Mobile UI Testing Patterns

Quick Guide: Write E2E tests as declarative YAML flows. Use id selectors for stable element targeting (not text that changes with localization). Use runFlow to compose reusable subflows (login, setup). Use waitForAnimationToEnd before assertions on animated screens. Use onFlowStart/onFlowComplete hooks for setup/teardown. Maestro auto-retries assertions for up to 7 seconds before failing. Current stable: CLI 2.4.0.


<critical_requirements>

CRITICAL: Before Using This Skill

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:

  • Writing E2E UI tests for iOS and Android mobile apps
  • Automating user workflows (login, checkout, onboarding) with YAML flows
  • Testing cross-platform behavior from a single flow file
  • Running mobile tests in CI with Maestro Cloud
  • Recording test execution for debugging or documentation
  • Testing deep links, location, permissions, and device interactions

When NOT to use:

  • Unit testing business logic (use your unit test framework)
  • API-only testing without UI (use direct HTTP tests)
  • Testing web-only applications without mobile component
  • Performance profiling or load testing (Maestro is for functional UI flows)

Key patterns covered:

  • Flow structure with appId, YAML commands, and selectors
  • Selector strategies: id (preferred), text, point, relational, state
  • Flow control: runFlow, repeat, retry, conditions (when), hooks
  • Environment variables and parameterized flows
  • JavaScript expressions: inline ${}, evalScript, runScript, output object
  • Device interactions: swipe, scroll, setLocation, openLink, permissions
  • Workspace configuration: tags, test discovery, execution order
  • Maestro Studio for visual flow creation and element inspection
  • Maestro Cloud for CI integration with GitHub Actions

Detailed Resources:




<decision_framework>

Decision Framework

Selector Choice

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)

Flow Organization

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

When to Use JavaScript

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>

RED FLAGS

High Priority Issues:

  • Using text selectors for buttons/labels that will be localized - breaks when language changes. Use id (accessibility identifiers) instead.
  • Duplicating login/setup steps in every flow file - extract to subflow and call with runFlow
  • Missing stopRecording after startRecording - produces corrupted or zero-byte video files
  • Hardcoding credentials or API keys in YAML flow files - use environment variables with -e or MAESTRO_ prefix
  • Using arbitrary sleep or extendedWaitUntil with long timeouts instead of waitForAnimationToEnd - Maestro's built-in tolerance handles most timing issues automatically

Medium Priority Issues:

  • Not using clearState or clearKeychain in setup - test results depend on leftover app state from previous runs
  • Not using tags for flow categorization - makes it impossible to run targeted subsets (smoke, regression, etc.)
  • Using point selectors (coordinates) as primary strategy - breaks on different screen sizes and resolutions
  • Not using label on runFlow calls - test reports show file paths instead of meaningful step descriptions
  • Putting all flows in the root directory without subdirectories - becomes unmanageable beyond 20+ flows

Gotchas 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.
  • CLI parameters are always strings - use parseInt() or comparison in JavaScript if you need numeric logic
  • MAESTRO_ prefixed shell variables are automatically available in flows but only via CLI, not Maestro Studio
  • The string "false" is truthy in JavaScript - use explicit === "true" comparison in when: true: conditions
  • onFlowComplete runs even when the flow fails - design teardown logic that doesn't assume success
  • runFlow with commands (inline) and runFlow with file (external) are mutually exclusive - you cannot use both in the same runFlow call
  • Template literals (backticks) do not work inside evalScript because the command is already wrapped in ${} - use string concatenation instead
  • console.log in evalScript writes to maestro.log, not the terminal - use runScript for terminal-visible logging
  • retry maxRetries is capped at 3 - for more attempts, restructure the flow logic
  • Maestro Cloud --async flag returns immediately without waiting for results - poll the API or use webhooks for completion
  • FlashList / RecyclerView items may not have stable accessibility IDs - use scrollUntilVisible with text fallback for list items

</red_flags>


<critical_reminders>

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>

Files (skills)
  • examples
    • core.md 6.5 KB
      # Maestro - Core Patterns
      
      > Flow structure, selectors, assertions, input, and navigation. See [SKILL.md](../SKILL.md) for decision guidance and red flags.
      
      **Prerequisites:** Maestro CLI installed, emulator/simulator running or physical device connected.
      
      ---
      
      ## Pattern 1: Flow Structure
      
      Every Maestro flow file has two sections separated by `---`: the configuration block (appId, env, tags, hooks) and the commands block.
      
      ```yaml
      # flows/login-smoke.yaml
      appId: com.example.app
      tags:
        - smoke
        - auth
      env:
        DEFAULT_TIMEOUT: "10000"
      ---
      - launchApp
      - tapOn:
          id: "email_input"
      - inputText: "user@example.com"
      - tapOn:
          id: "password_input"
      - inputText: "secure_pass"
      - tapOn:
          id: "login_button"
      - assertVisible:
          id: "home_screen"
      ```
      
      **Why good:** appId identifies the target app, tags enable selective execution with --include-tags, env block defines flow-scoped constants, commands read top-to-bottom like user steps
      
      ```yaml
      # Bad: no appId, no structure
      - tapOn: Login
      - inputText: user@example.com
      - tapOn: Submit
      ```
      
      **Why bad:** missing appId causes launch failures, bare text selectors break with copy changes, no tags makes selective execution impossible
      
      ---
      
      ## Pattern 2: clearState and launchApp
      
      Use `clearState` to reset the app to a clean install state before tests. Use `launchApp` with options to control permissions and app arguments.
      
      ```yaml
      appId: com.example.app
      ---
      - clearState
      - launchApp:
          clearKeychain: true # iOS only - clears stored credentials
          clearState: true # Clears app data (alternative to separate clearState command)
          stopApp: true # Force-stops the app before launching
      
      # Or with permissions
      - launchApp:
          permissions:
            notifications: allow
            location: allow
            camera: deny
      ```
      
      **Why good:** clearState ensures no leftover data from previous runs, clearKeychain handles iOS credential caching, permissions configured declaratively
      
      ---
      
      ## Pattern 3: Selector Types
      
      ### ID Selector (Preferred)
      
      Maps to `accessibilityIdentifier` (iOS) or `resource-id` / `content-description` (Android).
      
      ```yaml
      # Exact match
      - tapOn:
          id: "submit_button"
      
      # Regex match
      - tapOn:
          id: ".*submit.*"
      ```
      
      ### Text Selector
      
      Matches visible text or accessibility label. Supports regex.
      
      ```yaml
      # Exact text
      - tapOn:
          text: "Sign In"
      
      # Regex for dynamic text
      - assertVisible:
          text: "Welcome, .*"
      
      # Simple string shorthand (uses text matching)
      - tapOn: "Sign In"
      ```
      
      ### Index Selector
      
      Select nth matching element when multiple matches exist.
      
      ```yaml
      # Tap the second "Delete" button on screen
      - tapOn:
          text: "Delete"
          index: 1 # 0-based index
      ```
      
      ### Point Selector (Last Resort)
      
      Tap absolute coordinates. Fragile -- breaks on different screen sizes.
      
      ```yaml
      - tapOn:
          point: "50%,80%" # Percentage-based (more portable)
      
      - tapOn:
          point: "200,450" # Absolute pixels (fragile)
      ```
      
      ### Relational Selectors
      
      Disambiguate elements using spatial relationships.
      
      ```yaml
      # Tap "Edit" that is below "Profile" section
      - tapOn:
          text: "Edit"
          below: "Profile"
      
      # Tap inside a specific parent container
      - tapOn:
          text: "Save"
          childOf:
            id: "settings_form"
      
      # Tap element above another
      - tapOn:
          text: "Username"
          above:
            id: "password_input"
      ```
      
      ### State Selectors
      
      Filter by element state.
      
      ```yaml
      # Only tap if enabled
      - tapOn:
          id: "submit_button"
          enabled: true
      
      # Assert a checkbox is checked
      - assertVisible:
          id: "terms_checkbox"
          checked: true
      
      # Assert element has focus
      - assertVisible:
          id: "search_input"
          focused: true
      ```
      
      ### Combined Selectors
      
      Combine multiple selector properties for precision.
      
      ```yaml
      - tapOn:
          id: "action_button"
          enabled: true
          below: "Order Summary"
      ```
      
      ---
      
      ## Pattern 4: Assertions
      
      ### assertVisible / assertNotVisible
      
      Maestro auto-retries assertions for up to 7 seconds before failing. No need for explicit waits before assertions.
      
      ```yaml
      # Assert element is on screen (waits up to 7s)
      - assertVisible:
          id: "welcome_message"
      
      # Assert element is NOT on screen
      - assertNotVisible:
          id: "loading_spinner"
      
      # Assert with text content
      - assertVisible:
          text: "Order confirmed"
      
      # Assert with label for reports
      - assertVisible:
          id: "dashboard"
          label: "Verify user lands on dashboard after login"
      ```
      
      **Why good:** built-in 7-second retry eliminates the need for explicit waits, label improves test report readability
      
      ### assertTrue (JavaScript Expressions)
      
      ```yaml
      - assertTrue: ${output.itemCount > 0}
      - assertTrue: ${output.userRole === "admin"}
      ```
      
      ### AI-Powered Assertions
      
      ```yaml
      # Natural language assertion
      - assertWithAI: "The login form has email and password fields"
      
      # Defect detection
      - assertNoDefectsWithAI
      ```
      
      ---
      
      ## Pattern 5: Text Input and Keyboard
      
      ```yaml
      # Input text into focused field
      - tapOn:
          id: "search_input"
      - inputText: "running shoes"
      
      # Erase text (character count)
      - eraseText: 5
      
      # Erase all text
      - eraseText
      
      # Hide keyboard after input
      - hideKeyboard
      
      # Press specific keys
      - pressKey: Enter
      - pressKey: Backspace
      - pressKey: Tab
      
      # Input random data (DataFaker)
      - inputRandomName
      - inputRandomEmail
      - inputRandomNumber
      - inputRandomText
      
      # Copy text from element to variable
      - copyTextFrom:
          id: "order_number"
      - evalScript: ${output.orderNumber = maestro.copiedText}
      ```
      
      **Why good:** `hideKeyboard` prevents keyboard from obscuring elements below, `inputRandom*` commands generate unique test data without external libraries
      
      ---
      
      ## Pattern 6: Navigation
      
      ```yaml
      # System back button
      - back
      
      # Open a deep link
      - openLink: "myapp://products/123"
      
      # Open a URL (opens in browser or app if registered)
      - openLink: "https://example.com/invite/abc"
      
      # Double tap
      - doubleTapOn:
          id: "like_button"
      
      # Long press (context menus, drag triggers)
      - longPressOn:
          id: "message_bubble"
      ```
      
      ---
      
      ## Pattern 7: Optional Actions
      
      Use `optional: true` for elements that may or may not appear (permission dialogs, tooltips, one-time prompts).
      
      ```yaml
      # Dismiss onboarding tooltip if it appears (no failure if absent)
      - tapOn:
          text: "Got it"
          optional: true
      
      # Dismiss cookie consent if shown
      - tapOn:
          id: "accept_cookies"
          optional: true
          label: "Dismiss cookie banner if present"
      ```
      
      **Why good:** optional prevents test failure on non-deterministic UI elements, label documents why the step is optional
      
      **When to use:** One-time prompts, A/B test variants, permission dialogs that only appear on first launch.
      
      **When NOT to use:** Critical assertions that must pass -- never mark assertVisible as optional for required elements.
      
    • device-interactions.md 6.8 KB
      # Maestro - Device Interactions
      
      > Swipe, scroll, location, links, permissions, recording, and media. See [SKILL.md](../SKILL.md) for decision guidance. See [core.md](core.md) for basic commands and selectors.
      
      ---
      
      ## Pattern 1: Swipe Gestures
      
      ### Directional Swipe
      
      ```yaml
      # Swipe left (e.g., dismiss or navigate)
      - swipe:
          direction: LEFT
      
      # Swipe up (e.g., refresh or reveal content)
      - swipe:
          direction: UP
      ```
      
      ### Coordinate-Based Swipe (Percentages)
      
      Use percentages for cross-device compatibility.
      
      ```yaml
      # Swipe from right to left (carousel navigation)
      - swipe:
          start: "90%,50%"
          end: "10%,50%"
      ```
      
      ### Element-Relative Swipe
      
      ```yaml
      # Swipe up on a specific element (e.g., dismiss a card)
      - swipe:
          from:
            id: "product_card"
          direction: LEFT
      ```
      
      ### Custom Duration
      
      ```yaml
      # Slow swipe (for drag-and-drop or deliberate gestures)
      - swipe:
          direction: LEFT
          duration: 2000
      
      # Fast swipe
      - swipe:
          direction: UP
          duration: 200
      ```
      
      **Why good:** percentage-based coordinates work across screen sizes, element-relative swipe targets specific UI components, duration controls gesture speed for different interaction types
      
      ---
      
      ## Pattern 2: Scrolling
      
      ### Basic Scroll
      
      ```yaml
      # Scroll down (default)
      - scroll
      
      # Scroll in specific direction
      - scroll:
          direction: UP
      ```
      
      ### scrollUntilVisible
      
      Automatically scrolls until a target element appears. More reliable than fixed scroll counts.
      
      ```yaml
      # Scroll down until element appears
      - scrollUntilVisible:
          element: "Add to Cart"
          direction: DOWN
      
      # Scroll with id selector
      - scrollUntilVisible:
          element:
            id: "footer_section"
          direction: DOWN
      
      # Custom timeout and speed
      - scrollUntilVisible:
          element:
            id: "item_50"
          direction: DOWN
          timeout: 30000 # Max 30 seconds
          speed: 60 # 0-100, higher = faster
      
      # Center element in viewport after finding it
      - scrollUntilVisible:
          centerElement: true
          element:
            text: "Target Item"
      ```
      
      **Why good:** scrollUntilVisible is declarative (describe what to find, not how many times to scroll), timeout prevents infinite scrolling, centerElement ensures element is fully visible and interactable
      
      **Gotcha:** `scrollUntilVisible` swipes from center toward the edge. If the element is already above the visible area and direction is DOWN, it will never find it. Match direction to where the element is expected to be.
      
      ---
      
      ## Pattern 3: Wait for Animations
      
      Use `waitForAnimationToEnd` before assertions on screens with animations, transitions, or loading indicators.
      
      ```yaml
      # Wait for animation to complete before interacting
      - tapOn:
          id: "navigate_to_details"
      - waitForAnimationToEnd
      
      # Then assert on the new screen
      - assertVisible:
          id: "details_screen"
      ```
      
      ```yaml
      # Wait with custom timeout (default varies by platform)
      - waitForAnimationToEnd:
          timeout: 5000
      ```
      
      **Why good:** prevents assertions on partially-rendered screens, handles CSS/native animations, loading indicators, and screen transitions
      
      **When to use:** After navigation transitions, after modal open/close animations, after skeleton screen loading, after pull-to-refresh.
      
      **When NOT to use:** Before simple `assertVisible` calls -- Maestro's built-in 7-second retry handles most timing issues without explicit waits.
      
      ---
      
      ## Pattern 4: Location and Device Settings
      
      ### Set GPS Location
      
      ```yaml
      # Set device location (latitude, longitude)
      - setLocation:
          latitude: "37.7749"
          longitude: "-122.4194"
      ```
      
      ### Orientation
      
      ```yaml
      # Switch to landscape
      - setOrientation: LANDSCAPE
      
      # Switch back to portrait
      - setOrientation: PORTRAIT
      ```
      
      ### Airplane Mode
      
      ```yaml
      # Enable airplane mode (test offline behavior)
      - setAirplaneMode:
          enabled: true
      
      # Disable airplane mode
      - setAirplaneMode:
          enabled: false
      
      # Toggle (switch current state)
      - toggleAirplaneMode
      ```
      
      ### Permissions
      
      ```yaml
      # Set permissions before launch
      - launchApp:
          permissions:
            notifications: allow
            location: allow
            camera: deny
            photos: allow
      
      # Set permissions at runtime
      - setPermissions:
          notifications: deny
      ```
      
      ---
      
      ## Pattern 5: Deep Links and URLs
      
      ```yaml
      # Open a deep link
      - openLink: "myapp://products/123"
      
      # Open a universal link / app link
      - openLink: "https://example.com/products/123"
      
      # Test deep link with dynamic data
      - openLink: "myapp://user/${output.userId}/profile"
      ```
      
      **Why good:** deep links test navigation without manual UI traversal, useful for testing specific screens directly
      
      ---
      
      ## Pattern 6: Screen Recording
      
      Pair `startRecording` with `stopRecording` to capture video evidence of test execution.
      
      ```yaml
      appId: com.example.app
      ---
      - launchApp
      - startRecording: recordings/checkout-flow
      
      # Test steps...
      - tapOn:
          id: "add_to_cart"
      - tapOn:
          id: "checkout"
      - assertVisible:
          id: "payment_form"
      
      - stopRecording
      ```
      
      ### With Full Options
      
      ```yaml
      - startRecording:
          path: "recordings/onboarding"
          label: "Capture full onboarding sequence"
          optional: true # Don't fail test if recording engine has issues
      # ... test steps ...
      - stopRecording
      ```
      
      **Why good:** video evidence for debugging failures, shareable with non-technical stakeholders, optional flag prevents recording issues from breaking tests
      
      **Warning:** Every `startRecording` MUST have a matching `stopRecording`. Missing the stop command produces corrupted or zero-byte video files.
      
      ---
      
      ## Pattern 7: Screenshots
      
      ```yaml
      # Take screenshot with filename
      - takeScreenshot: screenshots/login-screen
      
      # Take screenshot as visual regression baseline
      - assertScreenshot: screenshots/dashboard-baseline
      ```
      
      **Why good:** `takeScreenshot` captures state for documentation, `assertScreenshot` enables visual regression testing against saved baselines
      
      ---
      
      ## Pattern 8: Clipboard and Text Extraction
      
      ```yaml
      # Set clipboard text
      - setClipboard: "paste-this-text"
      
      # Paste from clipboard
      - tapOn:
          id: "input_field"
      - pasteText
      
      # Copy text FROM an element
      - copyTextFrom:
          id: "confirmation_code"
      - evalScript: ${output.code = maestro.copiedText}
      # Use the copied text elsewhere
      - inputText: ${output.code}
      ```
      
      ---
      
      ## Pattern 9: App Lifecycle
      
      ```yaml
      # Kill app (force stop)
      - killApp
      
      # Kill and clear data
      - killApp:
          clearState: true
      
      # Stop app without clearing data
      - stopApp
      
      # Relaunch after kill
      - launchApp
      
      # Clear app data without killing
      - clearState
      
      # Clear iOS keychain
      - clearKeychain
      
      # Add media to device gallery
      - addMedia: test-data/profile-photo.jpg
      ```
      
      **Why good:** `killApp` + `launchApp` tests cold start behavior, `clearState` ensures clean test environment, `addMedia` enables testing image/video pickers with known files
      
      ---
      
      ## Pattern 10: Time Travel
      
      ```yaml
      # Simulate 24 hours passing (test expiration, reminders)
      - travel:
          forward: "24h"
      
      # Travel forward by specific duration
      - travel:
          forward: "30m"
      ```
      
      **Why good:** tests time-dependent features (token expiry, scheduled notifications, date-based UI) without waiting real time
      
    • flow-control.md 8.7 KB
      # Maestro - Flow Control and JavaScript
      
      > Subflows, loops, retries, conditions, hooks, and JavaScript expressions. See [SKILL.md](../SKILL.md) for decision guidance. See [core.md](core.md) for basic commands and selectors.
      
      ---
      
      ## Pattern 1: Reusable Subflows with runFlow
      
      ### External Subflow File
      
      ```yaml
      # flows/checkout-test.yaml
      appId: com.example.app
      ---
      - runFlow:
          file: subflows/login.yaml
          env:
            USERNAME: "buyer@example.com"
            PASSWORD: "buyer_pass"
          label: "Log in as buyer"
      
      - tapOn:
          id: "add_to_cart"
      - tapOn:
          id: "checkout_button"
      
      - runFlow:
          file: subflows/enter-payment.yaml
          label: "Enter payment details"
      
      - assertVisible:
          id: "order_confirmation"
      ```
      
      ```yaml
      # subflows/login.yaml
      appId: com.example.app
      ---
      - tapOn:
          id: "email_input"
      - inputText: ${USERNAME}
      - tapOn:
          id: "password_input"
      - inputText: ${PASSWORD}
      - tapOn:
          id: "login_button"
      - assertVisible:
          id: "home_screen"
      ```
      
      **Why good:** login defined once, reused everywhere with different credentials via env, label makes test reports readable
      
      ### Inline Subflow (No Separate File)
      
      Use for small, flow-specific sequences that are not reused.
      
      ```yaml
      - runFlow:
          label: "Sort products by price"
          commands:
            - tapOn:
                id: "sort_icon"
            - tapOn: "Price: Low to High"
      ```
      
      **Why good:** keeps small sequences inline without creating a separate file, label documents the intent
      
      ---
      
      ## Pattern 2: Conditions with when
      
      ### Platform Conditions
      
      ```yaml
      # Handle different permission dialogs per platform
      - runFlow:
          when:
            platform: Android
          commands:
            - tapOn: "While using the app"
      
      - runFlow:
          when:
            platform: iOS
          commands:
            - tapOn: "Allow While Using App"
      ```
      
      ### Visibility Conditions
      
      ```yaml
      # Dismiss popup only if visible
      - runFlow:
          when:
            visible: "Rate this app"
          commands:
            - tapOn: "Not now"
      
      # Skip onboarding if already completed
      - runFlow:
          when:
            notVisible:
              id: "home_screen"
          file: subflows/complete-onboarding.yaml
      ```
      
      ### JavaScript Conditions
      
      ```yaml
      # Conditional based on environment variable
      - runFlow:
          when:
            true: ${FEATURE_FLAG === "true"}
          file: subflows/new-feature-test.yaml
      
      # Combined: platform AND visibility
      - runFlow:
          when:
            platform: Android
            visible: "Allow Notifications"
          commands:
            - tapOn: "Allow"
      ```
      
      **Why good:** platform conditions handle iOS/Android differences in one file, visibility conditions handle non-deterministic UI, JavaScript conditions enable feature-flag-driven testing
      
      ---
      
      ## Pattern 3: Repeat and Retry
      
      ### repeat - Fixed Count
      
      ```yaml
      # Add 3 items to cart
      - repeat:
          times: 3
          commands:
            - tapOn:
                id: "add_item"
            - tapOn:
                id: "confirm_add"
      ```
      
      ### repeat - While Condition
      
      ```yaml
      # Scroll until counter reaches target
      - repeat:
          while:
            notVisible:
              id: "end_of_list"
          commands:
            - scroll
      ```
      
      ### retry - Flaky Step Recovery
      
      ```yaml
      # Retry a flaky network-dependent step (max 3 retries)
      - retry:
          maxRetries: 3
          commands:
            - tapOn:
                id: "refresh_button"
            - assertVisible:
                id: "data_loaded"
      ```
      
      **Why good:** repeat with times for known iteration counts, repeat with while for condition-based loops, retry handles intermittent failures (max 3)
      
      **Gotcha:** `retry` maxRetries is capped at 3. For more complex retry logic, use `runScript` with a loop.
      
      ---
      
      ## Pattern 4: Hooks - onFlowStart and onFlowComplete
      
      Define in the configuration block. `onFlowStart` runs before each flow, `onFlowComplete` runs after (even on failure).
      
      ### Flow-Level Hooks
      
      ```yaml
      appId: com.example.app
      onFlowStart:
        - clearState
        - runFlow:
            file: subflows/login.yaml
            env:
              USERNAME: "test_user@example.com"
              PASSWORD: "test_pass"
      onFlowComplete:
        - runFlow: subflows/logout.yaml
        - runScript: scripts/cleanup-test-data.js
      ---
      - tapOn:
          id: "profile_icon"
      - assertVisible:
          id: "profile_screen"
      ```
      
      ### Hooks with Dynamic Environment
      
      ```yaml
      onFlowStart:
        - runFlow:
            file: subflows/login.yaml
            env:
              USERNAME: ${TEST_USER || "default@example.com"}
              ROLE: "admin"
      ```
      
      **Why good:** clearState ensures clean app state, login runs automatically before every flow, cleanup always runs even on failure
      
      **Hook failure behavior:**
      
      | Scenario               | Result                                                             |
      | ---------------------- | ------------------------------------------------------------------ |
      | `onFlowStart` fails    | Flow marked FAILED, main body skipped, `onFlowComplete` still runs |
      | `onFlowComplete` fails | Flow marked FAILED even if main test passed                        |
      
      **Warning:** Avoid slow operations in hooks -- they run before/after EVERY flow in the suite, multiplying total execution time.
      
      ---
      
      ## Pattern 5: JavaScript Expressions
      
      ### Inline Expressions ($\{\})
      
      Everything inside `${}` is evaluated as JavaScript. Use for simple dynamic values.
      
      ```yaml
      # Dynamic username with timestamp
      - inputText: user_${Date.now()}@test.com
      
      # Platform check
      - inputText: ${platform === 'ios' ? 'iOS User' : 'Android User'}
      ```
      
      ### evalScript - Set Variables
      
      Use for logic-only steps that don't interact with UI.
      
      ```yaml
      # Store a computed value
      - evalScript: ${output.sessionId = 'session_' + Date.now()}
      
      # Use the stored value later
      - inputText: ${output.sessionId}
      
      # Arithmetic
      - evalScript: ${output.total = output.price * output.quantity}
      ```
      
      **Gotcha:** Template literals (backticks) do not work inside `evalScript` because the command is already wrapped in `${}`. Use string concatenation instead.
      
      ### runScript - External JavaScript Files
      
      Use for complex logic: HTTP requests, data generation, multi-step computation.
      
      ```yaml
      # Run external script
      - runScript: scripts/generate-test-data.js
      
      # Run with environment variables
      - runScript:
          file: scripts/setup-api.js
          env:
            API_URL: "https://staging.example.com/api"
      
      # Use output from script
      - inputText: ${output.generatedEmail}
      - inputText: ${output.generatedPassword}
      ```
      
      ```javascript
      // scripts/generate-test-data.js
      const timestamp = Date.now();
      output.generatedEmail = `test_${timestamp}@example.com`;
      output.generatedPassword = `Pass_${timestamp}!`;
      output.uniqueId = `user_${Math.random().toString(36).substring(2, 10)}`;
      ```
      
      ### HTTP Requests in Scripts
      
      ```javascript
      // scripts/setup-api.js
      const response = http.get(`${API_URL}/health`);
      
      if (response.ok) {
        output.apiStatus = "healthy";
      } else {
        output.apiStatus = "unhealthy";
      }
      
      // POST request to create test data
      const createResponse = http.post(`${API_URL}/users`, {
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          email: `test_${Date.now()}@example.com`,
          role: "tester",
        }),
      });
      output.userId = json(createResponse.body).id;
      ```
      
      **Why good:** scripts handle complex logic outside YAML, HTTP requests enable API setup/teardown within flows, output object passes data between scripts and YAML commands
      
      ### The output Object
      
      All scripts and evalScript commands share a single global `output` object. Use namespacing to prevent collisions.
      
      ```yaml
      - evalScript: ${output.login = {}}
      - evalScript: ${output.login.username = 'admin@test.com'}
      - evalScript: ${output.login.token = ''}
      
      - runScript: scripts/authenticate.js
      # Script sets output.login.token
      
      - inputText: ${output.login.token}
      ```
      
      ---
      
      ## Pattern 6: Environment Variables
      
      ### Flow-Level Constants (env block)
      
      ```yaml
      appId: com.example.app
      env:
        BASE_URL: "https://staging.example.com"
        DEFAULT_USER: "qa@example.com"
        MAX_RETRIES: "3"
      ---
      - launchApp
      - inputText: ${DEFAULT_USER}
      ```
      
      ### CLI Parameters (-e flag)
      
      ```bash
      maestro test -e USERNAME=admin@test.com -e ENV=production flow.yaml
      ```
      
      ### Shell Variables (MAESTRO\_ prefix)
      
      ```bash
      export MAESTRO_API_KEY="sk-test-12345"
      maestro test flow.yaml
      # Access in flow: ${MAESTRO_API_KEY}
      ```
      
      **Note:** `MAESTRO_` prefixed variables only work via CLI, not in Maestro Studio.
      
      ### Default Values with Fallbacks
      
      ```yaml
      - inputText: ${USERNAME || "guest@example.com"}
      - evalScript: ${output.retries = parseInt(MAX_RETRIES || "1")}
      ```
      
      ### Built-in Variables
      
      | Variable              | Description                 |
      | --------------------- | --------------------------- |
      | `MAESTRO_FILENAME`    | Current flow filename       |
      | `MAESTRO_DEVICE_UDID` | Connected device identifier |
      | `MAESTRO_SHARD_ID`    | Shard ID (starts at 1)      |
      | `MAESTRO_SHARD_INDEX` | Shard index (starts at 0)   |
      
      ### Subflow Variable Passing
      
      Variables defined in `env` on `runFlow` are scoped to the subflow. Subflow constants override parent parameters with the same name.
      
      ```yaml
      - runFlow:
          file: subflows/create-user.yaml
          env:
            ROLE: "admin"
            DEPARTMENT: "engineering"
      ```
      
  • reference.md 8.8 KB
    # Maestro Quick Reference
    
    > Command reference, CLI commands, workspace config, and decision frameworks. See [SKILL.md](SKILL.md) for red flags and anti-patterns.
    
    ---
    
    ## Command Reference
    
    ### Interaction Commands
    
    | Command              | Purpose                    | Example                     |
    | -------------------- | -------------------------- | --------------------------- |
    | `tapOn`              | Tap element or coordinates | `- tapOn: id: "btn"`        |
    | `doubleTapOn`        | Double-tap element         | `- doubleTapOn: id: "like"` |
    | `longPressOn`        | Long press (context menu)  | `- longPressOn: id: "msg"`  |
    | `inputText`          | Type into focused field    | `- inputText: "hello"`      |
    | `eraseText`          | Delete text (count or all) | `- eraseText: 5`            |
    | `pressKey`           | Hardware key press         | `- pressKey: Enter`         |
    | `hideKeyboard`       | Dismiss on-screen keyboard | `- hideKeyboard`            |
    | `swipe`              | Swipe gesture              | `- swipe: direction: LEFT`  |
    | `scroll`             | Scroll in direction        | `- scroll`                  |
    | `scrollUntilVisible` | Scroll until element found | See examples                |
    | `back`               | System back button         | `- back`                    |
    | `pasteText`          | Paste clipboard content    | `- pasteText`               |
    
    ### Assertion Commands
    
    | Command                 | Purpose                       | Auto-Retry |
    | ----------------------- | ----------------------------- | ---------- |
    | `assertVisible`         | Element is on screen          | 7 seconds  |
    | `assertNotVisible`      | Element is NOT on screen      | 7 seconds  |
    | `assertTrue`            | JavaScript expression is true | No         |
    | `assertScreenshot`      | Visual regression match       | No         |
    | `assertWithAI`          | AI validates UI state         | No         |
    | `assertNoDefectsWithAI` | AI checks for visual defects  | No         |
    
    ### Data Commands
    
    | Command             | Purpose                   |
    | ------------------- | ------------------------- |
    | `inputRandomName`   | Generate random name      |
    | `inputRandomEmail`  | Generate random email     |
    | `inputRandomNumber` | Generate random number    |
    | `inputRandomText`   | Generate random text      |
    | `copyTextFrom`      | Extract text from element |
    | `setClipboard`      | Set device clipboard      |
    
    ### App Lifecycle Commands
    
    | Command         | Purpose                                    |
    | --------------- | ------------------------------------------ |
    | `launchApp`     | Start app (with permissions/state options) |
    | `stopApp`       | Stop app (preserve state)                  |
    | `killApp`       | Force stop (optional clearState)           |
    | `clearState`    | Reset app to clean install                 |
    | `clearKeychain` | Clear iOS keychain data                    |
    
    ### Device Commands
    
    | Command              | Purpose                      |
    | -------------------- | ---------------------------- |
    | `setLocation`        | Set GPS coordinates          |
    | `setOrientation`     | Portrait or landscape        |
    | `setAirplaneMode`    | Enable/disable airplane mode |
    | `toggleAirplaneMode` | Toggle airplane mode         |
    | `setPermissions`     | Grant/revoke permissions     |
    | `openLink`           | Open URL or deep link        |
    | `addMedia`           | Add image/video to gallery   |
    | `travel`             | Simulate time passage        |
    
    ### Flow Control Commands
    
    | Command                 | Purpose                            |
    | ----------------------- | ---------------------------------- |
    | `runFlow`               | Execute subflow (file or inline)   |
    | `repeat`                | Loop commands (count or condition) |
    | `retry`                 | Retry on failure (max 3)           |
    | `evalScript`            | Inline JavaScript execution        |
    | `runScript`             | External JavaScript file           |
    | `waitForAnimationToEnd` | Pause until animations complete    |
    | `extendedWaitUntil`     | Custom timeout wait                |
    
    ### Recording Commands
    
    | Command          | Purpose             |
    | ---------------- | ------------------- |
    | `startRecording` | Begin video capture |
    | `stopRecording`  | End video capture   |
    | `takeScreenshot` | Capture screenshot  |
    
    ---
    
    ## Selector Priority
    
    | Priority | Selector            | Stability   | Notes                                          |
    | -------- | ------------------- | ----------- | ---------------------------------------------- |
    | 1        | `id`                | High        | Accessibility identifier, language-independent |
    | 2        | `text` + relational | Medium-High | Text with `below`/`childOf` for disambiguation |
    | 3        | `text`              | Medium      | Breaks with i18n, copy changes                 |
    | 4        | `index`             | Medium      | Position-dependent, breaks if list changes     |
    | 5        | `point`             | Low         | Coordinate-based, breaks on different screens  |
    
    ---
    
    ## CLI Commands
    
    ### Test Execution
    
    ```bash
    # Run single flow
    maestro test flow.yaml
    
    # Run all flows in directory
    maestro test .maestro/
    
    # Continuous mode (auto-rerun on file save)
    maestro test -c flow.yaml
    
    # With environment variables
    maestro test -e USER=admin -e PASS=secret flow.yaml
    
    # Filter by tags
    maestro test --include-tags=smoke .maestro/
    maestro test --exclude-tags=wip,flaky .maestro/
    
    # Sharded execution (parallel)
    maestro test --shards 4 .maestro/
    
    # Generate JUnit report
    maestro test --format junit --output report.xml .maestro/
    
    # Generate HTML report
    maestro test --format html --output report.html .maestro/
    ```
    
    ### Maestro Studio
    
    ```bash
    # Launch Studio (visual flow editor)
    maestro studio
    ```
    
    ### Maestro Cloud
    
    ```bash
    # Upload and run on Maestro Cloud
    maestro cloud --app-file app.apk --flows .maestro/ --project-id PROJECT_ID
    
    # Async execution (returns immediately)
    maestro cloud --app-file app.apk --flows .maestro/ --async
    
    # With environment variables
    maestro cloud --app-file app.apk --flows .maestro/ -e API_KEY=sk-test
    ```
    
    ### Device Management
    
    ```bash
    # Start emulator/simulator
    maestro start-device --platform android
    maestro start-device --platform ios --os-version 17
    
    # Print view hierarchy (debug selectors)
    maestro hierarchy
    ```
    
    ### Recording
    
    ```bash
    # Record flow execution as video
    maestro record flow.yaml output.mp4
    ```
    
    ---
    
    ## Workspace Configuration (config.yaml)
    
    ```yaml
    # Root-level workspace configuration
    appId: com.example.app
    
    # Test discovery
    flows:
      - "*" # Root only (default)
      - "auth/*" # Specific subdirectory
      - "tests/**" # Recursive
    
    # Tag filtering
    includeTags:
      - production_ready
    excludeTags:
      - wip
      - flaky
    
    # Execution order
    executionOrder:
      continueOnFailure: true # Continue suite after flow failure
      flowsOrder: # Explicit execution order
        - setup-flow.yaml
        - login-test.yaml
        - checkout-test.yaml
    
    # Output directory
    testOutputDir: ./test-results
    
    # Global hooks
    onFlowStart:
      - clearState
    onFlowComplete:
      - runScript: scripts/cleanup.js
    
    # Platform-specific (Maestro Cloud)
    ios:
      disableAnimations: true
    android:
      disableAnimations: true
    
    # Maestro Cloud notifications
    notifications:
      slack:
        endpoint: "https://hooks.slack.com/services/..."
      email:
        enabled: true
        recipients:
          - team@example.com
    ```
    
    ---
    
    ## Flow File Template
    
    ```yaml
    appId: com.example.app
    tags:
      - smoke
    env:
      DEFAULT_USER: "test@example.com"
    onFlowStart:
      - clearState
    onFlowComplete:
      - runScript: scripts/cleanup.js
    ---
    - launchApp
    # Test steps here
    ```
    
    ---
    
    ## Decision Framework: Flow Organization
    
    ```
    Starting a new test suite?
    |
    +-> How many flows?
    |   |-- < 10 -> Single directory (.maestro/)
    |   +-- 10+  -> Subdirectories by feature (.maestro/auth/, .maestro/checkout/)
    |              Update config.yaml flows: ["**"] for recursive discovery
    |
    +-> Need different test subsets?
    |   |-- YES -> Use tags (smoke, regression, wip) + --include-tags/--exclude-tags
    |   +-- NO  -> Run all flows: maestro test .maestro/
    |
    +-> Need setup/teardown?
    |   |-- Same for ALL flows -> onFlowStart/onFlowComplete in config.yaml
    |   +-- Different per flow -> runFlow in individual flow files
    |
    +-> Running in CI?
        |-- Local emulator -> maestro test with --format junit for reports
        +-- Maestro Cloud -> maestro cloud with --project-id and --app-file
    ```
    
    ---
    
    ## GitHub Actions Integration
    
    ```yaml
    # .github/workflows/maestro.yml
    name: Maestro E2E Tests
    on: [push, pull_request]
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - 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
              env: |
                API_URL=${{ secrets.STAGING_API_URL }}
                TEST_USER=${{ secrets.TEST_USER }}
    ```
    
  • SKILL.md 16.5 KB
    ---
    name: mobile-testing-maestro
    description: Maestro mobile E2E testing - YAML flows, selectors, flow control, environment variables, JavaScript expressions, device interactions, Maestro Studio, Maestro Cloud CI, tags, test suites
    ---
    
    # Maestro Mobile UI Testing Patterns
    
    > **Quick Guide:** Write E2E tests as declarative YAML flows. Use `id` selectors for stable element targeting (not text that changes with localization). Use `runFlow` to compose reusable subflows (login, setup). Use `waitForAnimationToEnd` before assertions on animated screens. Use `onFlowStart`/`onFlowComplete` hooks for setup/teardown. Maestro auto-retries assertions for up to 7 seconds before failing. Current stable: CLI 2.4.0.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **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:**
    
    - Writing E2E UI tests for iOS and Android mobile apps
    - Automating user workflows (login, checkout, onboarding) with YAML flows
    - Testing cross-platform behavior from a single flow file
    - Running mobile tests in CI with Maestro Cloud
    - Recording test execution for debugging or documentation
    - Testing deep links, location, permissions, and device interactions
    
    **When NOT to use:**
    
    - Unit testing business logic (use your unit test framework)
    - API-only testing without UI (use direct HTTP tests)
    - Testing web-only applications without mobile component
    - Performance profiling or load testing (Maestro is for functional UI flows)
    
    **Key patterns covered:**
    
    - Flow structure with appId, YAML commands, and selectors
    - Selector strategies: id (preferred), text, point, relational, state
    - Flow control: runFlow, repeat, retry, conditions (when), hooks
    - Environment variables and parameterized flows
    - JavaScript expressions: inline `${}`, evalScript, runScript, output object
    - Device interactions: swipe, scroll, setLocation, openLink, permissions
    - Workspace configuration: tags, test discovery, execution order
    - Maestro Studio for visual flow creation and element inspection
    - Maestro Cloud for CI integration with GitHub Actions
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Flow structure, selectors, assertions, input, navigation
    - [examples/flow-control.md](examples/flow-control.md) - runFlow, repeat, retry, conditions, hooks, JavaScript
    - [examples/device-interactions.md](examples/device-interactions.md) - Swipe, scroll, location, links, permissions, recording
    - [reference.md](reference.md) - Command reference, CLI commands, workspace config, decision frameworks
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    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:**
    
    1. **Declarative over imperative** - YAML flows describe user intent, not implementation details
    2. **Built-in tolerance** - Maestro auto-waits up to 7 seconds for elements, auto-retries taps, and handles animation delays without explicit waits
    3. **Single flow, multiple platforms** - One YAML file can test both iOS and Android with platform conditions for differences
    4. **Composition over duplication** - Extract reusable sequences (login, setup, teardown) into subflows with `runFlow`
    5. **Stable selectors** - Use accessibility identifiers (`id`) over visible text to survive localization and copy changes
    
    **Mental 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:**
    
    - Smoke tests for critical user journeys (login, purchase, onboarding)
    - Regression tests for flows that broke before
    - Cross-platform verification with a single flow file
    - Visual recording of test runs for stakeholder review
    
    **When NOT to use Maestro:**
    
    - Isolated unit tests for business logic
    - API contract testing without UI
    - Performance benchmarking or load testing
    - Complex data-driven testing requiring heavy programmatic logic (Maestro's JS support is limited compared to full test frameworks)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Flow Structure and Basic Commands
    
    Every flow starts with a configuration block (appId, optional env/tags) separated from commands by `---`. Commands execute sequentially top to bottom.
    
    ```yaml
    appId: com.example.app
    tags:
      - smoke
      - auth
    ---
    - launchApp
    - tapOn:
        id: "email_input"
    - inputText: "user@example.com"
    - 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](examples/core.md) for complete flow structure with env blocks, labels, and clearState.
    
    ---
    
    ### Pattern 2: Selector Strategies
    
    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.
    
    ```yaml
    # 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](examples/core.md) for all selector types including index, point, and combined selectors.
    
    ---
    
    ### Pattern 3: Reusable Subflows with runFlow
    
    Extract repeated sequences into separate flow files. Pass context via `env` parameters. Use `label` for clear test reports.
    
    ```yaml
    # Main flow: checkout-test.yaml
    appId: com.example.app
    ---
    - runFlow:
        file: subflows/login.yaml
        env:
          USERNAME: "test_user@example.com"
          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"
    ```
    
    ```yaml
    # 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](examples/flow-control.md) for inline subflows, conditional flows, and nested composition.
    
    ---
    
    ### Pattern 4: Conditions and Platform-Specific Logic
    
    Use `when` with `platform`, `visible`, `notVisible`, or JavaScript `true` expressions to handle differences between iOS and Android or optional UI states.
    
    ```yaml
    # 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](examples/flow-control.md) for JavaScript conditions and combined conditions.
    
    ---
    
    ### Pattern 5: Environment Variables and Parameterized Flows
    
    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.
    
    ```yaml
    appId: com.example.app
    env:
      BASE_URL: "https://staging.example.com"
      DEFAULT_USER: "qa_user@example.com"
    ---
    - launchApp
    - tapOn:
        id: "email_input"
    - inputText: ${USERNAME || DEFAULT_USER}
    ```
    
    ```bash
    # Override from CLI
    maestro test -e USERNAME=admin@example.com -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](examples/flow-control.md) for built-in variables, runScript with env, and shell variable patterns.
    
    ---
    
    ### Pattern 6: Hooks for Setup and Teardown
    
    Use `onFlowStart` and `onFlowComplete` in the configuration block for consistent setup/teardown across all flows. `onFlowComplete` runs even if the flow fails.
    
    ```yaml
    appId: com.example.app
    onFlowStart:
      - clearState
      - runFlow:
          file: subflows/login.yaml
          env:
            USERNAME: "test_user@example.com"
            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](examples/flow-control.md) for hooks with environment variables and script-based teardown.
    
    ---
    
    ### Pattern 7: JavaScript Expressions
    
    Use inline `${}` for simple interpolation, `evalScript` for variable computation, and `runScript` for complex logic in external `.js` files. All share a global `output` object.
    
    ```yaml
    # 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](examples/flow-control.md) for HTTP requests in scripts, DataFaker, and output namespacing.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Selector Choice
    
    ```
    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)
    ```
    
    ### Flow Organization
    
    ```
    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
    ```
    
    ### When to Use JavaScript
    
    ```
    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>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using `text` selectors for buttons/labels that will be localized - breaks when language changes. Use `id` (accessibility identifiers) instead.
    - Duplicating login/setup steps in every flow file - extract to subflow and call with `runFlow`
    - Missing `stopRecording` after `startRecording` - produces corrupted or zero-byte video files
    - Hardcoding credentials or API keys in YAML flow files - use environment variables with `-e` or `MAESTRO_` prefix
    - Using arbitrary `sleep` or `extendedWaitUntil` with long timeouts instead of `waitForAnimationToEnd` - Maestro's built-in tolerance handles most timing issues automatically
    
    **Medium Priority Issues:**
    
    - Not using `clearState` or `clearKeychain` in setup - test results depend on leftover app state from previous runs
    - Not using `tags` for flow categorization - makes it impossible to run targeted subsets (smoke, regression, etc.)
    - Using `point` selectors (coordinates) as primary strategy - breaks on different screen sizes and resolutions
    - Not using `label` on runFlow calls - test reports show file paths instead of meaningful step descriptions
    - Putting all flows in the root directory without subdirectories - becomes unmanageable beyond 20+ flows
    
    **Gotchas 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.
    - CLI parameters are always strings - use `parseInt()` or comparison in JavaScript if you need numeric logic
    - `MAESTRO_` prefixed shell variables are automatically available in flows but only via CLI, not Maestro Studio
    - The string `"false"` is truthy in JavaScript - use explicit `=== "true"` comparison in `when: true:` conditions
    - `onFlowComplete` runs even when the flow fails - design teardown logic that doesn't assume success
    - `runFlow` with `commands` (inline) and `runFlow` with `file` (external) are mutually exclusive - you cannot use both in the same runFlow call
    - Template literals (backticks) do not work inside `evalScript` because the command is already wrapped in `${}` - use string concatenation instead
    - `console.log` in `evalScript` writes to `maestro.log`, not the terminal - use `runScript` for terminal-visible logging
    - `retry` maxRetries is capped at 3 - for more attempts, restructure the flow logic
    - Maestro Cloud `--async` flag returns immediately without waiting for results - poll the API or use webhooks for completion
    - FlashList / RecyclerView items may not have stable accessibility IDs - use `scrollUntilVisible` with text fallback for list items
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## 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>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related