Maestro mobile E2E testing patterns for React Native/Expo apps: YAML test flows, testID selectors, adaptive auth state, optimistic update verification, GraalJS scripting, cross-platform stability, CI/CD integration, Maestro Cloud, and MCP server integration
---
name: maestro-mobile-testing
description: "Maestro mobile E2E testing patterns for React Native/Expo apps: YAML test flows, testID selectors, adaptive auth state, optimistic update verification, GraalJS scripting, cross-platform stability, CI/CD integration, Maestro Cloud, and MCP server integration"
version: 1.1.0
category: toolchain
author: tovimx
license: MIT
progressive_disclosure:
entry_point:
summary: "Write reliable Maestro mobile E2E tests with declarative YAML flows, testID selectors, auth-aware adaptive patterns, and optimistic update verification"
when_to_use: "When writing mobile E2E tests, debugging flaky flows, testing authentication, verifying optimistic updates, capturing screenshots, or setting up Maestro CI for React Native/Expo apps"
quick_start: "1. Install Maestro CLI 2. Add testID props to components 3. Write YAML flows with testID selectors 4. Use auth-loaded pre-flight pattern 5. Start mock API server for backend-dependent tests 6. Run with maestro test"
token_estimate:
entry: 150
full: 9000
context_limit: 900
tags:
- react-native
- expo
- testing
- maestro
- e2e
- mobile
- ios
- android
- yaml
- ci-cd
- mcp
requires_tools: []
---
# Maestro Mobile E2E Testing
## Overview
Maestro is a declarative YAML-based mobile E2E testing framework. It provides automatic waiting, built-in retry logic, and fast execution without boilerplate. It's more stable than Detox or Appium for React Native apps.
### Key Features
- **Declarative YAML** — no imperative test code, just steps
- **Automatic waiting** — no manual `sleep()` or flaky waits
- **Built-in retry** — reduces test flakiness
- **Fast execution** — runs quickly without setup overhead
- **Maestro Studio** — interactive test builder (`maestro studio`)
- **Sub-flows** — reusable YAML sequences for DRY tests
- **JavaScript scripting** — GraalJS runtime for HTTP calls and data manipulation
- **Maestro Cloud** — real device testing in CI without local simulators
## Quick Start
### Install
```bash
curl -Ls "https://get.maestro.mobile.dev" | bash
brew install openjdk@17
export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
```
### Minimal test
```yaml
appId: com.myapp
---
- launchApp
- tapOn:
id: "my-button"
- assertVisible: "Expected Text"
```
### Run
```bash
maestro test .maestro/smoke-test.yaml
maestro test --debug .maestro/smoke-test.yaml # step through
maestro studio # interactive builder
```
---
## Core Patterns
### 1. Selector Strategy: testID vs Text
Choose your selector approach based on project context. Both are valid — the right choice depends on whether your app is localized and your team's testing philosophy.
| Context | Recommended Selector | Rationale |
|---------|---------------------|-----------|
| **Multi-language / i18n** | `id:` (testID) | Stable across translations |
| **Single language** | Text labels | Human-readable, self-documenting tests |
| **Agent-maintained tests** | Either — ask the developer | Readability matters less for AI-maintained flows |
| **System dialogs** | Text (always) | No testID possible on native alerts |
```yaml
# testID selector — stable across translations
- tapOn:
id: "submit-button"
# Text selector — human-readable, self-documenting
- tapOn: "Submit"
```
**When to prefer testIDs:**
- App supports multiple languages or will be translated
- UI text is dynamic or frequently changes
- Multiple elements share the same visible text
**When to prefer text selectors:**
- Single-language app with stable copy
- Readability and self-documentation are a priority
- Testing user-visible behavior exactly as it appears
In React Native, add `testID` props when using ID-based selectors:
```tsx
<TouchableOpacity testID="submit-button" onPress={handleSubmit}>
<Text>{t('submit')}</Text>
</TouchableOpacity>
```
### testID Naming Convention
When using ID-based selectors:
```
{component}-{action/type}[-{variant}]
Examples:
- auth-prompt-login-button
- product-card-{id}
- otp-input-0
- tab-home
- dashboard-loading
```
### 2. Auth Pre-Flight Pattern
Prevent race conditions where Maestro interacts with the UI before auth state resolves. Add a zero-size `auth-loaded` marker that only renders when auth loading completes:
```tsx
// In your tab bar or root layout
{!isLoading && <View testID="auth-loaded" style={{ width: 0, height: 0 }} />}
```
Then in every test:
```yaml
- launchApp
# Prevent XCTest crash on cold boot (iOS)
- swipe:
direction: DOWN
duration: 100
# Wait for auth state to resolve
- extendedWaitUntil:
visible:
id: "auth-loaded"
timeout: 15000
# Now safe to interact
- tapOn:
id: "tab-home"
```
### 3. Adaptive Tests (Handle Both Auth States)
Tests should work regardless of whether the user is authenticated:
```yaml
# Auth flow — only runs if login prompt is visible
- runFlow:
when:
visible: "Sign In"
file: flows/auth-flow.yaml
# Already authenticated — proceed directly
- runFlow:
when:
visible:
id: "tab-home"
file: flows/authenticated-action.yaml
```
### 4. Testing Optimistic Updates
Use short timeouts to verify UI changes happen before server response:
```yaml
# Trigger mutation
- tapOn:
id: "action-button"
# OPTIMISTIC: UI must change within 3s (not waiting for server)
- extendedWaitUntil:
visible:
id: "undo-button"
timeout: 3000
# Verify derived UI state
- extendedWaitUntil:
visible:
id: "user-indicator"
timeout: 5000
```
| Action | Expected Change | Timeout |
|--------|----------------|---------|
| Mutation trigger | Button state flips | < 3s |
| List update | Item appears/disappears | < 5s |
| Re-do action | Proves persistence | < 3s |
### 5. Dismissing Native Alerts
React Native `Alert.alert()` creates native dialogs that block the UI:
```yaml
- tapOn:
id: "action-button"
# Wait for expected state change first
- extendedWaitUntil:
visible:
id: "new-state-element"
timeout: 5000
# Dismiss alert (optional in case it already closed)
- tapOn:
text: "OK"
optional: true
# Brief delay for alert animation
- swipe:
direction: DOWN
duration: 300
```
### 6. Sub-Flows for Reusability
Break repeated sequences into sub-flow files:
```
.maestro/
├── flows/
│ ├── auth-and-return.yaml
│ ├── complete-purchase.yaml
│ └── verify-result.yaml
├── smoke-test.yaml
└── feature-test.yaml
```
```yaml
# In main test
- runFlow:
file: flows/auth-and-return.yaml
```
### 7. Deep Links (Expo)
Use the Expo scheme from `app.json`, not the bundle ID:
```yaml
# WRONG
- openLink: "com.myapp://profile/settings"
# CORRECT
- openLink: "myapp://profile/settings"
```
Deep links must be registered in your app's deep link handler. Unregistered routes silently fail.
### 8. Platform-Specific Logic
```yaml
- runFlow:
when:
platform: ios
file: flows/ios-specific.yaml
- runFlow:
when:
platform: android
file: flows/android-specific.yaml
```
### 9. Environment Variables
```yaml
appId: com.myapp
env:
TEST_EMAIL: maestro-test@example.com
API_BASE_URL: http://localhost:3000
---
- inputText: ${TEST_EMAIL}
```
### 10. Selector State Properties
Use `enabled`, `selected`, `checked`, and `focused` to target elements by their current state. This is useful for validating interactive element states before or after actions.
```yaml
# Only tap the submit button if it's enabled
- tapOn:
id: "submit-button"
enabled: true
# Assert a checkbox is checked
- assertVisible:
id: "terms-checkbox"
checked: true
# Wait for an input to be focused
- extendedWaitUntil:
visible:
id: "email-input"
focused: true
timeout: 3000
```
| Property | Values | Use Case |
|----------|--------|----------|
| `enabled` | `true` / `false` | Buttons that disable during submission or until form is valid |
| `checked` | `true` / `false` | Checkboxes, toggle switches |
| `selected` | `true` / `false` | Tab items, segmented controls |
| `focused` | `true` / `false` | Input fields with auto-focus |
### 11. Relative Position Selectors
Distinguish between similar elements by their spatial relationship to other elements. This is more idiomatic and resilient than index-based selection.
```yaml
# BAD — fragile, breaks if order changes
- tapOn:
text: "Add to Basket"
index: 1
# GOOD — contextual, self-documenting
- tapOn:
text: "Add to Basket"
below:
text: "Awesome Shoes"
```
Available relative selectors:
```yaml
# Target element below another
- tapOn:
text: "Buy Now"
below: "Product Title"
# Target element that is a child of a parent
- tapOn:
text: "Delete"
childOf:
id: "item-card-42"
# Target a parent that contains a specific child
- tapOn:
containsChild: "Urgent"
# Target by multiple descendants
- tapOn:
containsDescendants:
- id: title_id
text: "Specific Title"
- "Another descendant text"
# Horizontal positioning
- tapOn:
text: "Edit"
rightOf: "Username"
```
| Selector | Meaning |
|----------|---------|
| `below:` | Element is positioned below the referenced element |
| `above:` | Element is positioned above the referenced element |
| `leftOf:` | Element is to the left of the referenced element |
| `rightOf:` | Element is to the right of the referenced element |
| `childOf:` | Element is a direct child of the referenced parent |
| `containsChild:` | Element contains a direct child matching the reference |
| `containsDescendants:` | Element contains all specified descendant elements |
---
## Authentication Testing
### Architecture
Testing OTP or magic-link authentication in E2E requires capturing emails programmatically. The general pattern:
```
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Maestro │────▶│ Auth │────▶│ Email Capture │
│ Test │ │ Provider │ │ Service │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │
│ ┌──────────────────────────────┘
│ ▼
│ ┌─────────────┐
└──▶│ REST API │ ─── GET /api/v1/messages
│ (email) │ ─── Extract OTP code
└─────────────┘
```
Common email capture services: [Mailpit](https://github.com/axllent/mailpit), [MailHog](https://github.com/mailhog/MailHog), [Ethereal](https://ethereal.email/).
### OTP Fetch Script
Fetch OTP codes from your email capture service using Maestro's GraalJS runtime:
```javascript
// CRITICAL: Maestro uses GraalJS — NO async/await, NO fetch()
var email = typeof EMAIL !== "undefined" ? EMAIL : "test@example.com";
var emailServiceUrl = typeof EMAIL_SERVICE_URL !== "undefined"
? EMAIL_SERVICE_URL : "http://localhost:8025";
var response = http.get(emailServiceUrl + "/api/v1/messages");
if (!response.ok) {
throw new Error("Failed to fetch emails: " + response.status);
}
var data = json(response.body);
// Find the latest email and extract OTP code
var body = data.messages[0].Content.Body;
var match = body.match(/(\d{6})/);
output.OTP_CODE = match[1];
```
### OTP Input Strategy
OTP components with auto-focus need individual digit entry. Tap each input before typing:
```yaml
# Split OTP into digits via helper script
- runScript:
file: scripts/split-otp.js
env:
OTP_CODE: ${output.OTP_CODE}
# Enter each digit by tapping its input
- tapOn:
id: "otp-input-0"
- inputText: ${output.OTP_0}
- tapOn:
id: "otp-input-1"
- inputText: ${output.OTP_1}
# ... repeat for all digits
```
> For provider-specific implementations (Supabase + Mailpit, Firebase Auth, Auth0), create a project-level skill that extends this one.
---
## GraalJS Script Rules
Maestro uses the GraalJS runtime. These constraints are non-negotiable:
| Feature | Status |
|---------|--------|
| `async/await` | **NOT supported** |
| `fetch()` | **NOT supported** |
| `http.get()`, `http.post()` | Use these instead |
| `json()` | Use to parse response bodies |
| `output.VAR` | Set variables for use in YAML flow |
| `var` declarations | Required (use `var`, not `const`/`let` for safety) |
```javascript
// Script template
var response = http.get("http://localhost:8025/api/endpoint");
if (!response.ok) {
throw new Error("Request failed: " + response.status);
}
var data = json(response.body);
output.RESULT = data.value;
```
---
## Critical Gotchas
### clearState Does NOT Clear iOS Keychain
`clearState: true` clears the app sandbox (UserDefaults, files, caches) but does **NOT** clear the iOS Keychain. Auth tokens stored via `expo-secure-store` (or any Keychain-based storage) persist across `clearState` resets and even app reinstalls.
```yaml
# WRONG — user may still be authenticated
- launchApp:
clearState: true
- assertVisible: "Welcome" # Fails if Keychain has tokens
# CORRECT — wait for auth resolution, then adapt
- launchApp
- extendedWaitUntil:
visible:
id: "auth-loaded"
timeout: 15000
```
**Rules:**
- Never rely on `clearState` to produce guest state on iOS
- For auth tests: skip `clearState`, use `auth-loaded` pre-flight
- For guest tests: use adaptive flows that handle both states
- Never assert guest-only UI after `clearState`
**Note:** On Android, `clearState: true` fully resets app data including credentials. This is an iOS-only gotcha.
### XCTest kAXErrorInvalidUIElement Crash (iOS)
The XCTest driver may crash if Maestro interacts with the accessibility tree before the first render cycle completes on cold boot.
**Fix:** Add a no-op swipe immediately after `launchApp`:
```yaml
- launchApp
- swipe:
direction: DOWN
duration: 100
```
### API Server Dependency
Mobile apps calling backend APIs on localhost need either the full server or a mock server running. Without it, all API-dependent screens show loading spinners or empty states (queries fail silently).
**Fix:** Start a mock API server before running Maestro tests:
```bash
# Start mock server (serves canned responses on your API port)
npx tsx scripts/mock-api-server.ts &
# Then run tests
maestro test .maestro/my-test.yaml
```
Create a lightweight mock that returns canned JSON for each endpoint your app calls. This is faster and more deterministic than running your full backend.
### Auth-Aware Tab Bars
Tab bars that show different tabs for guest vs authenticated users will cause selector failures:
| State | Typical Tabs |
|-------|-------------|
| Guest | home, search, cart, profile |
| Auth | home, feed, create, messages, profile |
Only assert tabs that exist in both states, or use adaptive `when:` conditions.
---
## Test File Template
```yaml
# {Feature} {Action} Test
#
# Tests: {what this validates}
# Prerequisites:
# - Simulator/emulator running with app installed
# - Backend or mock server running (if API-dependent)
appId: com.myapp
env:
TEST_EMAIL: maestro-{feature}@example.com
EMAIL_SERVICE_URL: http://localhost:8025
---
# ==========================================
# STEP 1: LAUNCH + AUTH PRE-FLIGHT
# ==========================================
- launchApp
- swipe:
direction: DOWN
duration: 100
- extendedWaitUntil:
visible:
id: "auth-loaded"
timeout: 15000
- takeScreenshot: 01-initial-state
# ==========================================
# STEP 2: {ACTION}
# ==========================================
- tapOn:
id: "target-element"
# ==========================================
# STEP 3: VERIFY
# ==========================================
- extendedWaitUntil:
visible:
id: "expected-result"
timeout: 5000
- takeScreenshot: 02-final-state
```
---
## Folder Structure
```
.maestro/
├── README.md # Quick reference + testID inventory
├── config.yaml # Shared configuration
├── flows/ # Reusable sub-flows
│ ├── auth-and-return.yaml
│ ├── complete-action.yaml
│ └── verify-result.yaml
├── scripts/ # GraalJS helpers
│ ├── fetch-otp.js
│ └── split-otp.js
├── smoke-test.yaml # Guest navigation
├── auth-signin.yaml # OTP sign-in flow
├── feature-screenshots.yaml # Screenshot capture flows
└── feature-action.yaml # Feature-specific tests
scripts/
├── mock-api-server.ts # Lightweight mock for E2E
└── run-e2e.sh # Orchestration script
```
### Naming Conventions
| Type | Pattern | Example |
|------|---------|---------|
| Main test | `{feature}-{action}.yaml` | `checkout-purchase.yaml` |
| Sub-flow | `{action}-{context}.yaml` | `auth-and-return-to-dashboard.yaml` |
| Script | `{verb}-{noun}.js` | `fetch-otp.js` |
---
## Infrastructure
### Mock Server for API-Dependent Tests
Create a lightweight mock server that serves canned responses for your API layer. This is faster and more deterministic than running your full backend during E2E tests.
```bash
# Start mock server before running Maestro tests
npx tsx scripts/mock-api-server.ts &
```
### Orchestration Script
Automate the full E2E setup with a shell script that:
1. Starts backend services (database, auth)
2. Seeds test data
3. Starts mock API server
4. Runs Maestro tests
5. Cleans up all services
```bash
bash scripts/run-e2e.sh
```
### Seed Data
Tests that depend on specific data require seeded databases. Keep seed scripts alongside your test infrastructure and run them before each test suite.
---
## Android-Specific Patterns
### Emulator Setup
Android tests require an emulator or a USB-connected physical device. Maestro auto-detects connected devices.
```bash
# List available system images
sdkmanager --list | grep system-images
# Create emulator
avdmanager create avd -n maestro_test \
-k "system-images;android-34;google_apis;arm64-v8a"
# Start emulator
emulator -avd maestro_test
```
### iOS vs Android Differences
| Aspect | iOS | Android |
|--------|-----|---------|
| Device type | Simulator only (no physical) | Emulator + physical via ADB |
| `clearState` | Does NOT clear Keychain | Fully resets app data |
| Cold boot crash | XCTest kAXError (add swipe delay) | No equivalent issue |
| Performance | Runs natively (fast) | ARM emulation (slower on x86) |
| Permission dialogs | System alerts | System dialogs with different text |
### ADB Debugging
```bash
adb devices # List connected devices
adb shell am start -n com.myapp/.MainActivity # Launch app
adb logcat | grep Maestro # Filter Maestro logs
adb shell input keyevent 82 # Unlock screen
```
### Android Permission Handling
Android permissions appear as system dialogs. Dismiss with optional taps:
```yaml
- tapOn:
text: "Allow"
optional: true
- tapOn:
text: "While using the app"
optional: true
```
---
## CI/CD Integration
### GitHub Actions with Maestro Cloud
Maestro Cloud provides real devices in CI without local simulators. Use the official action:
```yaml
name: Mobile E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
maestro-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Build Android APK
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Android APK
run: |
cd apps/mobile
npx expo prebuild --platform android --no-install
cd android && ./gradlew assembleRelease
- name: Run Maestro Cloud Tests
id: maestro
uses: mobile-dev-inc/action-maestro-cloud@v2
with:
api-key: ${{ secrets.MAESTRO_API_KEY }}
app-file: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
workspace: .maestro
include-tags: ci
# Access results
# ${{ steps.maestro.outputs.MAESTRO_CLOUD_CONSOLE_URL }}
# ${{ steps.maestro.outputs.MAESTRO_CLOUD_UPLOAD_STATUS }}
# ${{ steps.maestro.outputs.MAESTRO_CLOUD_FLOW_RESULTS }}
```
### Tag-Based Flow Filtering
Use tags to control which tests run in CI vs locally:
```yaml
# In your flow file header
appId: com.myapp
tags:
- ci
- smoke
---
- launchApp
# ... test steps
```
```bash
# Run only CI-tagged flows locally
maestro test --include-tags ci .maestro/
# Exclude work-in-progress flows
maestro test --exclude-tags wip .maestro/
```
### Local CI with Docker (Android Only)
```dockerfile
FROM openjdk:17-slim
RUN curl -Ls "https://get.maestro.mobile.dev" | bash
ENV PATH="/root/.maestro/bin:${PATH}"
COPY .maestro/ /app/.maestro/
WORKDIR /app
CMD ["maestro", "test", ".maestro/"]
```
**Note:** iOS tests cannot run in Docker (requires macOS). Use Maestro Cloud for iOS in CI.
---
## Maestro Cloud
[Maestro Cloud](https://cloud.maestro.dev/) runs tests on real devices without local simulator setup.
### Setup
1. Create account at cloud.maestro.dev
2. Generate API key from dashboard
3. Store as `MAESTRO_API_KEY` secret in your CI provider
### Running from CLI
```bash
# Upload and run on Maestro Cloud
maestro cloud --api-key $MAESTRO_API_KEY \
--app-file ./app-release.apk \
.maestro/
# With tag filtering
maestro cloud --api-key $MAESTRO_API_KEY \
--app-file ./app-release.apk \
--include-tags smoke \
.maestro/
```
### Key Points
- **iOS testing**: Supported on Maestro Cloud (not on local physical devices)
- **Android testing**: Both local physical devices and Maestro Cloud
- **Results**: Dashboard with video recordings, logs, and screenshots
- **CI outputs**: `MAESTRO_CLOUD_CONSOLE_URL`, `MAESTRO_CLOUD_FLOW_RESULTS`
---
## Maestro MCP Server
The [Maestro MCP server](https://docs.maestro.dev/getting-started/maestro-mcp) exposes Maestro's full command set as Model Context Protocol tools, letting AI agents **execute tests and interact with devices directly** — not just write YAML.
### How It Complements This Skill
| | **This Skill** | **Maestro MCP** |
|---|---|---|
| **Role** | Teaches correct patterns | Provides runtime execution |
| **Layer** | Authoring (write good YAML) | Execution (run, tap, assert, screenshot) |
| **Output** | Better test files | Live device interaction |
Use both together: this skill ensures the AI writes correct tests; the MCP lets it run them immediately, see failures, and iterate.
### Setup
The MCP ships with the Maestro CLI — no extra install needed:
```bash
# Verify it's available
maestro mcp
```
**Claude Code** — add to project `.mcp.json` or global settings:
```json
{
"mcpServers": {
"maestro": {
"command": "maestro",
"args": ["mcp"]
}
}
}
```
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"maestro": {
"command": "maestro",
"args": ["mcp"]
}
}
}
```
Also supported on Cursor, Windsurf, VS Code, and JetBrains IDEs. See the [Maestro MCP docs](https://docs.maestro.dev/getting-started/maestro-mcp) for IDE-specific setup.
### Key Capabilities
The MCP server exposes 47 tools organized by category:
| Category | Tools | Examples |
|----------|-------|---------|
| UI Interaction | tap, swipe, scroll, long press | `tapOn`, `scrollUntilVisible` |
| Text Input | type, erase, paste, copy | `inputText`, `eraseText` |
| Assertions | visibility, AI-powered | `assertVisible`, `assertWithAI` |
| App Lifecycle | launch, stop, clear state | `launchApp`, `clearState` |
| Device Control | location, orientation, airplane | `setLocation`, `hideKeyboard` |
| Flow Control | run flows, repeat, eval scripts | `runFlow`, `evalScript` |
| Media | screenshots, recording | `takeScreenshot`, `startRecording` |
| AI-Powered | visual assertions, defect detection | `assertWithAI`, `assertNoDefectsWithAI` |
### Write-Run-Fix Loop
With both this skill and the MCP active, the AI can:
1. **Write** a test YAML using patterns from this skill
2. **Run** it via MCP's `runFlow` / `launchApp` + interaction tools
3. **See** failures via screenshots and assertions
4. **Fix** the YAML and re-run — all in one conversation
---
## Debugging
```bash
maestro test --debug .maestro/test.yaml # Step through interactively
maestro record .maestro/test.yaml # Record as video
maestro studio # Interactive UI builder
maestro hierarchy # View element tree
```
Screenshots saved to `~/.maestro/tests/{timestamp}/`.
---
## Checklist for New Tests
```
[ ] Unique test email (maestro-{feature}@example.com)
[ ] Selector strategy chosen (testID for i18n apps, text for single-language — see Pattern 1)
[ ] Selectors use state properties where relevant (enabled, checked — see Pattern 10)
[ ] Similar elements distinguished with relative selectors, not index (see Pattern 11)
[ ] Auth pre-flight pattern used (auth-loaded)
[ ] Post-launch swipe added (iOS crash prevention)
[ ] Both auth states handled (adaptive flows)
[ ] Native alerts dismissed after mutations
[ ] Short timeouts for optimistic updates (3-5s)
[ ] Sub-flows created for reusable sequences
[ ] Descriptive screenshots at key points
[ ] Header comment with prerequisites
[ ] Added to README.md test table
[ ] Mock API server started if backend-dependent
[ ] Tags added for CI filtering (ci, smoke, wip)
```
---
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Unable to locate Java Runtime" | Java not in PATH | `export JAVA_HOME=/opt/homebrew/opt/openjdk@17/...` |
| "Element not found" after tap | Native alert blocking | Add `tapOn: text: "OK" optional: true` |
| OTP digits not entering | Auto-focus interference | Use individual `otp-input-N` testIDs |
| Test passes but nothing happened | `optional: true` misused | Only use optional for truly optional actions |
| "Assertion is false" on visibility | Element not rendered yet | Increase timeout or verify testID exists |
| Script output empty | Wrong JS API | Use `http.get()` not `fetch()` |
| Auth state inconsistent after clearState | iOS Keychain not cleared | Don't use `clearState`, use adaptive flows |
| kAXErrorInvalidUIElement crash | Cold boot race (iOS) | Add post-launch swipe delay |
| Loading spinners / empty screens | No API server running | Start mock API server before tests |
| Permission dialog blocking (Android) | System dialog not dismissed | Add `tapOn: text: "Allow" optional: true` |
---
## Resources
- [Maestro Documentation](https://docs.maestro.dev/)
- [Maestro Selectors Reference](https://docs.maestro.dev/api-reference/selectors)
- [Maestro Cloud](https://cloud.maestro.dev/)
- [Maestro MCP Server](https://docs.maestro.dev/getting-started/maestro-mcp)
- [Maestro GitHub](https://github.com/mobile-dev-inc/Maestro)
- [GraalJS HTTP Requests](https://docs.maestro.dev/advanced/javascript/make-http-s-requests)
- [Conditions & Adaptive Flows](https://docs.maestro.dev/advanced/conditions)
- [GitHub Actions Integration](https://docs.maestro.dev/cloud/ci-integration/github-actions)
- [BrowserStack: Maestro Testing Guide](https://www.browserstack.com/guide/maestro-testing)
- [DEV.to: Tips & Tricks for Maestro + React Native](https://dev.to/retyui/best-tips-tricks-for-e2e-maestro-with-react-native-2kaa)
Creator's repository · tovimx/maestro-mobile-testing-skill
License: MIT