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
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-testing-maestro/skills/mobile-testing-maestro
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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
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>
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 - Flow structure, selectors, assertions, input, navigation
- examples/flow-control.md - runFlow, repeat, retry, conditions, hooks, JavaScript
- examples/device-interactions.md - Swipe, scroll, location, links, permissions, recording
- reference.md - Command reference, CLI commands, workspace config, decision frameworks
<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
textselectors for buttons/labels that will be localized - breaks when language changes. Useid(accessibility identifiers) instead. - Duplicating login/setup steps in every flow file - extract to subflow and call with
runFlow - Missing
stopRecordingafterstartRecording- produces corrupted or zero-byte video files - Hardcoding credentials or API keys in YAML flow files - use environment variables with
-eorMAESTRO_prefix - Using arbitrary
sleeporextendedWaitUntilwith long timeouts instead ofwaitForAnimationToEnd- Maestro's built-in tolerance handles most timing issues automatically
Medium Priority Issues:
- Not using
clearStateorclearKeychainin setup - test results depend on leftover app state from previous runs - Not using
tagsfor flow categorization - makes it impossible to run targeted subsets (smoke, regression, etc.) - Using
pointselectors (coordinates) as primary strategy - breaks on different screen sizes and resolutions - Not using
labelon 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:
assertVisibleauto-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 inwhen: true:conditions onFlowCompleteruns even when the flow fails - design teardown logic that doesn't assume successrunFlowwithcommands(inline) andrunFlowwithfile(external) are mutually exclusive - you cannot use both in the same runFlow call- Template literals (backticks) do not work inside
evalScriptbecause the command is already wrapped in${}- use string concatenation instead console.loginevalScriptwrites tomaestro.log, not the terminal - userunScriptfor terminal-visible loggingretrymaxRetries is capped at 3 - for more attempts, restructure the flow logic- Maestro Cloud
--asyncflag returns immediately without waiting for results - poll the API or use webhooks for completion - FlashList / RecyclerView items may not have stable accessibility IDs - use
scrollUntilVisiblewith 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.
Reviews (0)
No reviews yet.
No comments yet.