maestro-ui-testing
Use this skill when writing, debugging, or maintaining Maestro E2E UI tests. Enforces best practices for flakiness-free, CI-stable mobile UI tests.
SKILL.md.claude/skills/maestro-ui-testing/SKILL.md
GitHubMaestro UI Testing Best Practices
This skill ensures all Maestro E2E tests follow best practices for reliability, especially in CI/CD environments like GitHub Actions.
When to Activate
- Writing new Maestro E2E tests
- Debugging flaky tests in CI/CD
- Refactoring existing tests
- Setting up Maestro test structure
- Fixing "Assertion is false" errors
Core Principles
1. One Flow = One User Scenario
NEVER test your entire app in a single long Flow.
| DO | DON'T |
|---|---|
01-onboarding.yaml (single scenario) |
full-app-test.yaml (entire app) |
02-paywall.yaml (single feature) |
All scenarios in one file |
Why:
- If any command fails, the entire test halts
- Can't run tests in parallel
- Hard to identify which scenario failed
2. Use accessibilityIdentifier, NOT Text or Point Selectors
| Priority | Selector Type | Reliability | Example |
|---|---|---|---|
| 1st (Best) | id: (accessibilityIdentifier) |
Stable | id: "onboarding-welcome-cta" |
| 2nd | text: (fallback) |
Localization-dependent | text: "許可|Allow" |
| 3rd (Avoid) | point: |
EXTREMELY FLAKY | point: "28,78" |
SwiftUI Implementation:
Button("Get Started") {
action()
}
.accessibilityIdentifier("onboarding-welcome-cta")
Maestro Usage:
- tapOn:
id: "onboarding-welcome-cta"
3. Never Use Static Waits
| DO | DON'T |
|---|---|
extendedWaitUntil with visibility |
- wait: 5000 (sleep) |
assertVisible for short waits |
Fixed delays |
# GOOD - Wait for element to appear
- extendedWaitUntil:
visible:
id: "onboarding-welcome-cta"
timeout: 30000
# BAD - Fixed sleep
- wait: 5000
4. Handle System Dialogs Properly
System dialogs (permissions, alerts) are non-deterministic. Always use optional: true.
# Permission dialog - may or may not appear
- tapOn:
text: "許可|Allow"
optional: true
# iOS system notification permission
- tapOn:
id: "com.apple.springboard:Allow"
optional: true
Multi-language Support (Japanese/English):
- tapOn:
text: "許可|Allow"
optional: true
5. CI/CD Environment Alignment
| Issue | Solution |
|---|---|
| Slower CI machines | Increase timeouts (15s → 30s) |
| Different screen sizes | Use id: selectors, not point: |
| State inconsistency | Always clearState + clearKeychain |
| Simulator not ready | Add initial extendedWaitUntil |
Flow Structure Template
# Feature: [Description]
# Tests: [scenario description]
# Dependencies: [what state is required]
appId: com.example.app
name: Descriptive flow name
tags:
- smokeTest # Fast critical path tests for PR
- featureName # Feature-specific tag
- phase6 # Version/phase tag
---
# ALWAYS start with clean state
- clearState
- clearKeychain
- launchApp
# Wait for app to fully load (critical for CI)
- extendedWaitUntil:
visible: true
timeout: 15000
# Step 1: [Description]
- extendedWaitUntil:
visible:
id: "element-id"
timeout: 30000
- tapOn:
id: "element-id"
# Handle optional system dialogs
- tapOn:
text: "許可|Allow"
optional: true
# Final verification
- assertVisible:
id: "expected-result-element"
# Take screenshot for debugging
- takeScreenshot: "flow-complete"
Tag Strategy for CI/CD
| Tag | Purpose | When to Run |
|---|---|---|
smokeTest |
Critical path, fast | Every PR |
regression |
Full test suite | Nightly |
phase6 |
Phase-specific tests | Phase release |
util |
Helper flows (exclude) | Never standalone |
config.yaml:
flows:
- "*/*"
excludeTags:
- util
GitHub Actions:
# PR - Smoke tests only
maestro test maestro/ --include-tags smokeTest
# Nightly - Full suite
maestro test maestro/
Debugging Flaky Tests
Step 1: Identify the Flakiness Pattern
| Symptom | Likely Cause | Solution |
|---|---|---|
| "Element not found" | Timing issue | Increase extendedWaitUntil timeout |
| "Assertion is false: X is visible" | Element not loaded yet | Add extendedWaitUntil before assertion |
| Works locally, fails in CI | Environment difference | Use id: selectors, increase timeouts |
| Random failures | Point selectors | Replace point: with id: |
Step 2: Debug Commands
# Run with debug output
maestro test --debug maestro/01-onboarding.yaml
# View hierarchy to find elements
maestro hierarchy
# Interactive testing with Maestro Studio
maestro studio
Step 3: Common Fixes
Problem: Assertion is false: "true" is visible
# BEFORE - Broken (checking literal "true")
- extendedWaitUntil:
visible: true
timeout: 15000
# AFTER - Fixed (checking for actual element)
- extendedWaitUntil:
visible:
id: "main-screen-element"
timeout: 15000
Problem: Button not found after navigation
# Add wait after navigation
- tapOn:
id: "navigate-button"
- extendedWaitUntil:
visible:
id: "destination-element"
timeout: 10000
- tapOn:
id: "destination-element"
iOS-Specific Best Practices
SwiftUI accessibilityIdentifier Naming
// Use semantic, hierarchical naming
.accessibilityIdentifier("onboarding-welcome-cta")
.accessibilityIdentifier("onboarding-struggles-next")
.accessibilityIdentifier("nudge-card-primary-button")
// For dynamic elements, include the identifier
ForEach(problems) { problem in
Button(problem.name) { ... }
.accessibilityIdentifier("onboarding-struggle-\(problem.id)")
}
Handling iOS Permission Dialogs
# Notification permission (Japanese/English)
- tapOn:
text: "許可|Allow"
optional: true
# ATT permission
- tapOn:
text: "許可|Allow Tracking"
optional: true
# Location permission
- tapOn:
text: "Appの使用中は許可|Allow While Using App"
optional: true
RevenueCat Test Store (Payment E2E)
Source: RevenueCat Test Store
RC Test Store replaces StoreKit Configuration for E2E tests. No Apple sandbox credentials needed.
Prerequisites:
- RC SDK >= 5.43.0
- Test Store API Key (
test_prefix, Debug only) - Products + Offerings in RC Dashboard
uiPreviewMode: truein SubscriptionService.configure() (DEBUG builds) iOS 18.4+/26.x シミュレータでは StoreKit が products を返さない (GitHub #4954)。 RC SDK の SimulatedStoreProductsManager は Web Billing API を使うが Test Store products 未対応。uiPreviewModeで mock products を生成して回避する。 詳細:us-005b-monetization.mdの「SubscriptionService.configure() パターン」
Purchase dialog button labels (RC SDK 5.60.0, SimulatedStorePurchaseUI.swift):
- Success:
"Test valid purchase"(⚠️ NOT "Simulate Success") - Failure:
"Test failed purchase"(⚠️ NOT "Simulate Failure") - Cancel:
"Cancel"
# Payment success test (monthly)
- tapOn:
id: "paywall_plan_monthly"
- extendedWaitUntil:
visible: "Test valid purchase"
timeout: 10000
- tapOn: "Test valid purchase"
- extendedWaitUntil:
visible:
id: "timer_countdown" # Post-purchase screen
timeout: 10000
# Payment failure test
- tapOn:
id: "paywall_plan_monthly"
- extendedWaitUntil:
visible: "Test failed purchase"
timeout: 10000
- tapOn: "Test failed purchase"
- extendedWaitUntil:
visible:
id: "paywall_plan_monthly" # Returns to paywall
timeout: 5000
Paywall Dismissal
# Soft paywall dismiss (Maybe Later)
- tapOn:
id: "paywall_maybe_later"
optional: true
File Organization
maestro/
├── config.yaml # Global configuration
├── README.md # Documentation
├── 01-onboarding.yaml # Core flow (numbered for order)
├── 02-paywall.yaml # Core flow
├── phase6/ # Feature-specific directory
│ ├── 01-llm-nudge.yaml
│ └── 02-feedback.yaml
└── common/ # Shared utility flows
└── login.yaml # Tagged with 'util', excluded from standalone runs
Checklist Before Merge
| Check | Description |
|---|---|
[ ] Uses id: selectors (not point:) |
Reliability |
| [ ] Has appropriate timeouts (30s for CI) | CI stability |
[ ] Uses optional: true for dialogs |
Non-deterministic handling |
[ ] Starts with clearState + clearKeychain |
Clean state |
[ ] Has correct tags (smokeTest, etc.) |
CI filtering |
| [ ] Takes screenshots at key points | Debugging |
| [ ] Runs locally 3x without failure | Flakiness check |
Quick Reference
Common Commands
# Wait for element
- extendedWaitUntil:
visible:
id: "element-id"
timeout: 30000
# Tap by ID
- tapOn:
id: "element-id"
# Tap by text (fallback)
- tapOn:
text: "Button Text"
# Optional tap (for dialogs)
- tapOn:
text: "Allow"
optional: true
# Assert visibility
- assertVisible:
id: "element-id"
# Input text
- inputText: "Hello World"
# Clear and type
- clearText
- inputText: "New Text"
# Scroll
- scroll
# Screenshot
- takeScreenshot: "filename"
Timeout Guidelines
| Context | Timeout |
|---|---|
| App launch | 15000ms |
| Screen transition | 5000-10000ms |
| Element visibility | 30000ms (CI) |
| Animation completion | Use waitForAnimationToEnd |
Troubleshooting: Simulator Build Issues
問題: 古いビルドがシミュレータに残っている
症状:
- 削除した画面(ATT等)がまだ表示される
- コード変更が反映されない
fastlane build_for_simulatorで「Could not find .app bundle」エラー
原因: fastlane がビルドしても .app がインストールされないことがある
解決策:
# 1. DerivedData をクリーン
rm -rf aniccaios/build/DerivedData
# 2. 再ビルド
cd aniccaios && fastlane build_for_simulator
# 3. 手動でインストール
xcrun simctl install "iPhone 16 Pro" "build/DerivedData/Build/Products/Debug-iphonesimulator/aniccaios.app"
問題: DEBUG ビルドで Paywall がスキップされる
症状: sandbox ユーザーでオンボーディング後に Paywall が表示されない
原因: sandbox の過去トライアルで isEntitled=true が残っている
解決策: DEBUG では entitlement に関係なく Paywall を強制表示
#if DEBUG
showPaywall = true
return
#endif
問題: Maestro で要素が見つからない
症状: Element not found: Text matching regex: .*次へ.*
解決策: 必ず inspect_view_hierarchy で実際のテキストを確認してからセレクターを決定
Fix Loop (Automated Test Repair)
When a Maestro test fails, follow this closed-loop repair cycle:
maestro test maestro/<flow>.yaml:
PASS → next flow
FAIL → diagnose:
YAML error (wrong id, selector mismatch):
→ Fix YAML → retry
Swift error (missing a11y identifier, wrong label):
→ Fix Swift → fastlane build_for_simulator → retry
上限なし。全フロー PASS するまで繰り返す。BLOCKED にしない。
Diagnosis Commands
# Check actual view hierarchy
maestro hierarchy
# Interactive debugging
maestro studio
# Run single flow with debug output
maestro test --debug maestro/01-onboarding.yaml
Common Fix Patterns
| Symptom | Diagnosis | Fix |
|---|---|---|
| "Element not found: id: X" | a11y ID not set in Swift | Add .accessibilityIdentifier("X") + rebuild |
| "Element not found: text: X" | Text changed or localized | Use id: selector instead |
| Timeout on "Test valid purchase" | RC Test Store not configured or uiPreviewMode missing | Check API Key in Info.plist + SubscriptionService.configure() uiPreviewMode pattern |
| Flow passes locally, fails CI | Timing issue | Increase extendedWaitUntil timeout |
Remember: The goal is zero flakiness. Every test should pass 100% of the time in CI. If a test is flaky, fix the test, not the retry count.