关于 maestro-mobile-testing
Maestro Mobile E2E Testing 是用于编写移动应用声明式 YAML 端到端测试的技能,介绍可复用流程、选择器、身份验证模式和脚本编写,适合验证不同界面状态下的应用行为。
主要功能
- 01声明式 YAML 测试流程
- 02自动等待与内置重试
- 03可复用子流程和选择器
- 04使用 GraalJS 编写脚本
- 05身份验证和界面状态测试指南
适用场景
- 01创建可重复的移动端冒烟测试
- 02测试登录和 OTP 流程
- 03验证不同登录状态下的界面行为
- 04通过模拟服务器测试依赖 API 的界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tovimx/ maestro-mobile-testing-skill --skill maestro-mobile-testing安装后在 Claude Code 中输入/maestro-mobile-testing确认已加载。
详细介绍
Maestro 移动端 E2E 测试
概述
Maestro 是一个基于声明式 YAML 的移动端 E2E 测试框架。它提供自动等待、内置重试逻辑,并且无需样板代码即可快速执行。对于 React Native 应用,它比 Detox 或 Appium 更稳定。
主要功能
- 声明式 YAML — 无需编写命令式测试代码,只需定义步骤
- 自动等待 — 无需手动使用
sleep()或不稳定的等待 - 内置重试 — 减少测试的不稳定性
- 快速执行 — 无需额外设置开销,运行迅速
- Maestro Studio — 交互式测试构建器(
maestro studio) - 子流程 — 可复用的 YAML 序列,让测试保持 DRY
- JavaScript 脚本 — 使用 GraalJS 运行时执行 HTTP 调用和数据处理
- Maestro Cloud — 在 CI 中使用真实设备进行测试,无需本地模拟器
快速开始
安装
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
最简测试
appId: com.myapp
---
- launchApp
- tapOn:
id: "my-button"
- assertVisible: "Expected Text"
运行
maestro test .maestro/smoke-test.yaml
maestro test --debug .maestro/smoke-test.yaml # step through
maestro studio # interactive builder
核心模式
1. 选择器策略:testID 与文本
根据项目背景选择选择器方案。两种方式都有效,合适的选择取决于应用是否支持本地化,以及团队的测试理念。
| 背景 | 推荐的选择器 | 原因 |
|---|---|---|
| 多语言 / i18n | id:(testID) | 翻译后仍保持稳定 |
| 单语言 | 文本标签 | 易于阅读,测试自带说明 |
| 由 Agent 维护的测试 | 两者均可 — 请询问开发者 | 对由 AI 维护的流程而言,可读性没那么重要 |
| 系统对话框 | 文本(始终如此) | 原生警告框无法设置 testID |
# testID selector — stable across translations
- tapOn:
id: "submit-button"
# Text selector — human-readable, self-documenting
- tapOn: "Submit"
优先使用 testID 的情况:
- 应用支持多种语言或计划进行翻译
- UI 文本是动态内容或经常变化
- 多个元素显示相同的文本
优先使用文本选择器的情况:
- 单语言应用,且文案稳定
- 可读性和自带说明很重要
- 需要精确测试用户实际看到的行为
在 React Native 中,使用基于 ID 的选择器时,请添加 testID 属性:
<TouchableOpacity testID="submit-button" onPress={handleSubmit}>
<Text>{t('submit')}</Text>
</TouchableOpacity>
testID 命名约定
使用基于 ID 的选择器时:
{component}-{action/type}[-{variant}]
Examples:
- auth-prompt-login-button
- product-card-{id}
- otp-input-0
- tab-home
- dashboard-loading
2. 认证预检模式
避免 Maestro 在认证状态确定前就与 UI 交互而产生竞态条件。添加一个零尺寸的 auth-loaded 标记,仅在认证加载完成后渲染:
// In your tab bar or root layout
{!isLoading && <View testID="auth-loaded" style={{ width: 0, height: 0 }} />}
然后在每个测试中:
- 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. 自适应测试(兼容两种认证状态)
无论用户是否已通过认证,测试都应正常运行:
# 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. 测试乐观更新
使用较短的超时时间,验证 UI 是否在服务器响应前完成更改:
# 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
| 操作 | 预期更改 | 超时时间 |
|---|---|---|
| 触发变更 | 按钮状态切换 | < 3s |
| 列表更新 | 项目出现或消失 | < 5s |
| 再次执行操作 | 验证持久性 | < 3s |
5. 关闭原生警告框
React Native 的 Alert.alert() 会创建阻塞 UI 的原生对话框:
- 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. 使用子流程实现复用
将重复的序列拆分为子流程文件:
.maestro/
├── flows/
│ ├── auth-and-return.yaml
│ ├── complete-purchase.yaml
│ └── verify-result.yaml
├── smoke-test.yaml
└── feature-test.yaml
# In main test
- runFlow:
file: flows/auth-and-return.yaml
7. 深层链接(Expo)
使用 app.json 中的 Expo scheme,而非 bundle ID:
# WRONG
- openLink: "com.myapp://profile/settings"
# CORRECT
- openLink: "myapp://profile/settings"
深层链接必须在应用的深层链接处理器中注册。未注册的路由会静默失败。
8. 平台专属逻辑
- runFlow:
when:
platform: ios
file: flows/ios-specific.yaml
- runFlow:
when:
platform: android
file: flows/android-specific.yaml
9. 环境变量
appId: com.myapp
env:
TEST_EMAIL: [email protected]
API_BASE_URL: http://localhost:3000
---
- inputText: ${TEST_EMAIL}
10. 选择器状态属性
使用 enabled、selected、checked 和 focused,根据元素当前状态定位元素。这对于在操作前后验证交互元素的状态很有用。
# 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
| 属性 | 值 | 用例 |
|---|---|---|
enabled | true / false | 提交期间或表单有效前会禁用的按钮 |
checked | true / false | 复选框、切换开关 |
selected | true / false | 标签项、分段控件 |
focused | true / false | 自动聚焦的输入字段 |
11. 相对位置选择器
根据元素之间的空间关系区分相似元素。这种方式比基于索引的选择更符合习惯,也更稳健。
# 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"
可用的相对位置选择器:
# 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"
| 选择器 | 含义 |
|---|---|
below: | 元素位于所引用元素的下方 |
above: | 元素位于所引用元素的上方 |
leftOf: | 元素位于所引用元素的左侧 |
rightOf: | 元素位于所引用元素的右侧 |
childOf: | 元素是所引用父元素的直接子元素 |
containsChild: | 元素包含与引用匹配的直接子元素 |
containsDescendants: | 元素包含所有指定的后代元素 |
身份验证测试
架构
在端到端测试中测试 OTP 或魔法链接身份验证,需要以编程方式捕获电子邮件。一般模式如下:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Maestro │────▶│ Auth │────▶│ Email Capture │
│ Test │ │ Provider │ │ Service │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │
│ ┌──────────────────────────────┘
│ ▼
│ ┌─────────────┐
└──▶│ REST API │ ─── GET /api/v1/messages
│ (email) │ ─── Extract OTP code
└─────────────┘
常用的电子邮件捕获服务:Mailpit、MailHog、Ethereal。
OTP 获取脚本
使用 Maestro 的 GraalJS 运行时,从电子邮件捕获服务获取 OTP 代码:
// CRITICAL: Maestro uses GraalJS — NO async/await, NO fetch()
var email = typeof EMAIL !== "undefined" ? EMAIL : "[email protected]";
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 输入策略
带有自动聚焦功能的 OTP 组件需要逐个输入数字。输入前先点击每个输入框:
# 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
对于特定提供商的实现(Supabase + Mailpit、Firebase Auth、Auth0),请创建项目级技能来扩展此技能。
GraalJS 脚本规则
Maestro 使用 GraalJS 运行时。以下限制不可妥协:
| 功能 | 状态 |
|---|---|
async/await | 不支持 |
fetch() | 不支持 |
http.get()、http.post() | 请改用这些方法 |
json() | 用于解析响应正文 |
output.VAR | 设置供 YAML 工作流使用的变量 |
var 声明 | 必须使用(为确保安全,请使用 var,而不是 const/let) |
// 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;
关键注意事项
clearState 不会清除 iOS 钥匙串
clearState: true 会清除应用沙盒(UserDefaults、文件、缓存),但不会清除 iOS 钥匙串。通过 expo-secure-store(或任何基于钥匙串的存储)保存的身份验证令牌,在 clearState 重置甚至应用重新安装后仍会保留。
# 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
规则:
- 在 iOS 上,切勿依赖
clearState来进入访客状态 - 对于身份验证测试:跳过
clearState,使用auth-loaded预检 - 对于访客测试:使用能够处理两种状态的自适应工作流
clearState后,切勿断言仅访客可见的 UI
**注意:**在 Android 上,clearState: true 会彻底重置应用数据,包括凭据。这是仅限 iOS 的注意事项。
XCTest kAXErrorInvalidUIElement 崩溃(iOS)
如果 Maestro 在冷启动时的首次渲染周期完成前与辅助功能树交互,XCTest 驱动程序可能会崩溃。
**修复方法:**在 launchApp 后立即添加一个无操作滑动:
- launchApp
- swipe:
direction: DOWN
duration: 100
API 服务依赖
调用本地主机后端 API 的移动应用需要运行完整服务或模拟服务。否则,所有依赖 API 的屏幕都会显示加载指示器或空状态(查询会静默失败)。
**修复方法:**运行 Maestro 测试前启动模拟 API 服务:
# 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
创建一个轻量级模拟服务,为应用调用的每个端点返回预设 JSON。与运行完整后端相比,这种方式更快且结果更可预测。
感知身份验证状态的标签栏
访客用户与已通过身份验证的用户看到的标签栏不同,这会导致选择器失败:
| 状态 | 常见标签 |
|---|---|
| 访客 | home、search、cart、profile |
| 已验证 | home、feed、create、messages、profile |
仅断言两种状态下都存在的标签,或使用自适应的 when: 条件。
测试文件模板
# {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
文件夹结构
.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
命名约定
| 类型 | 模式 | 示例 |
|---|---|---|
| 主测试 | {feature}-{action}.yaml | checkout-purchase.yaml |
| 子流程 | {action}-{context}.yaml | auth-and-return-to-dashboard.yaml |
| 脚本 | {verb}-{noun}.js | fetch-otp.js |
基础设施
面向 API 依赖测试的模拟服务器
创建一个轻量级模拟服务器,为 API 层提供预设响应。与在 E2E 测试期间运行完整后端相比,这种方式速度更快,也更可预测。
# Start mock server before running Maestro tests
npx tsx scripts/mock-api-server.ts &
编排脚本
使用 shell 脚本自动完成完整的 E2E 设置:
- 启动后端服务(数据库、身份验证)
- 填充测试数据
- 启动模拟 API 服务器
- 运行 Maestro 测试
- 清理所有服务
bash scripts/run-e2e.sh
种子数据
依赖特定数据的测试需要预先填充数据库。将种子脚本与测试基础设施放在一起,并在每个测试套件运行前执行。
Android 专属模式
模拟器设置
Android 测试需要模拟器或通过 USB 连接的实体设备。Maestro 会自动检测已连接的设备。
# 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 与 Android 的差异
| 方面 | iOS | Android |
|---|---|---|
| 设备类型 | 仅模拟器(不支持实体设备) | 模拟器 + 通过 ADB 连接的实体设备 |
clearState | 不会清除 Keychain | 完全重置应用数据 |
| 冷启动崩溃 | XCTest kAXError(添加滑动延迟) | 没有同类问题 |
| 性能 | 原生运行(快速) | ARM 仿真(在 x86 上较慢) |
| 权限对话框 | 系统警告框 | 文本不同的系统对话框 |
ADB 调试
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 权限处理
Android 权限会显示为系统对话框。可通过可选点击将其关闭:
- tapOn:
text: "Allow"
optional: true
- tapOn:
text: "While using the app"
optional: true
CI/CD 集成
使用 Maestro Cloud 的 GitHub Actions
Maestro Cloud 可在 CI 中提供实体设备,无需本地模拟器。使用官方 action:
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 }}
基于标签的流程筛选
使用标签控制在 CI 中和本地运行哪些测试:
# In your flow file header
appId: com.myapp
tags:
- ci
- smoke
---
- launchApp
# ... test steps
# Run only CI-tagged flows locally
maestro test --include-tags ci .maestro/
# Exclude work-in-progress flows
maestro test --exclude-tags wip .maestro/
使用 Docker 的本地 CI(仅限 Android)
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/"]
注意: iOS 测试无法在 Docker 中运行(需要 macOS)。在 CI 中对 iOS 使用 Maestro Cloud。
Maestro Cloud
Maestro Cloud 无需本地模拟器设置,即可在实体设备上运行测试。
设置
- 在 cloud.maestro.dev 创建账户
- 从控制面板生成 API 密钥
- 在 CI 提供商中将其保存为
MAESTRO_API_KEY密钥
从 CLI 运行
# 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/
要点
- iOS 测试:Maestro Cloud 支持(不支持本地实体设备)
- Android 测试:支持本地实体设备和 Maestro Cloud
- 结果:控制面板提供视频录制、日志和屏幕截图
- CI 输出:
MAESTRO_CLOUD_CONSOLE_URL、MAESTRO_CLOUD_FLOW_RESULTS
Maestro MCP 服务
Maestro MCP 服务 将 Maestro 的完整命令集作为模型上下文协议工具公开,让 AI Agent 可以直接执行测试并与设备交互,而不只是编写 YAML。
如何与此技能配合
| 此技能 | Maestro MCP | |
|---|---|---|
| 作用 | 教授正确的模式 | 提供运行时执行能力 |
| 层级 | 编写(编写优质 YAML) | 执行(运行、点击、断言、截图) |
| 输出 | 更好的测试文件 | 实时设备交互 |
两者配合使用:此技能可确保 AI 编写正确的测试;MCP 则能让它立即运行测试、查看失败并迭代。
设置
MCP 随 Maestro CLI 一同提供,无需额外安装:
# Verify it's available
maestro mcp
Claude Code — 添加到项目 .mcp.json 或全局设置:
{
"mcpServers": {
"maestro": {
"command": "maestro",
"args": ["mcp"]
}
}
}
Claude Desktop — 添加到 ~/Library/Application Support/Claude/claude_desktop_config.json(macOS):
{
"mcpServers": {
"maestro": {
"command": "maestro",
"args": ["mcp"]
}
}
}
也支持 Cursor、Windsurf、VS Code 和 JetBrains IDE。有关 IDE 专属设置,请参阅 Maestro MCP 文档。
核心功能
MCP 服务按类别提供 47 个工具:
| 类别 | 工具 | 示例 |
|---|---|---|
| UI 交互 | 点击、滑动、滚动、长按 | tapOn、scrollUntilVisible |
| 文本输入 | 输入、擦除、粘贴、复制 | inputText、eraseText |
| 断言 | 可见性、AI 驱动 | assertVisible、assertWithAI |
| 应用生命周期 | 启动、停止、清除状态 | launchApp、clearState |
| 设备控制 | 位置、方向、飞行模式 | setLocation、hideKeyboard |
| 流程控制 | 运行流程、重复、执行脚本 | runFlow、evalScript |
| 媒体 | 屏幕截图、录制 | takeScreenshot、startRecording |
| AI 驱动 | 视觉断言、缺陷检测 | assertWithAI、assertNoDefectsWithAI |
编写-运行-修复循环
同时启用此技能和 MCP 后,AI 可以:
- 编写测试 YAML,使用此技能中的模式
- 运行测试,通过 MCP 的
runFlow/launchApp和交互工具执行 - 查看失败情况,包括屏幕截图和断言
- 修复 YAML 并重新运行——所有操作都在一次对话中完成
调试
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
屏幕截图保存在 ~/.maestro/tests/{timestamp}/。
新测试检查清单
[ ] 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)
常见错误
| 错误 | 原因 | 解决方法 |
|---|---|---|
| "Unable to locate Java Runtime" | Java 不在 PATH 中 | export JAVA_HOME=/opt/homebrew/opt/openjdk@17/... |
| 点击后出现 "Element not found" | 原生警告框阻挡操作 | 添加 tapOn: text: "OK" optional: true |
| OTP 数字未输入 | 自动聚焦干扰 | 使用单独的 otp-input-N testID |
| 测试通过但没有任何操作 | 错误使用 optional: true | 仅对真正可选的操作使用 optional |
| 可见性检查出现 "Assertion is false" | 元素尚未渲染 | 增加超时时间,或确认 testID 存在 |
| 脚本输出为空 | JS API 错误 | 使用 http.get(),而非 fetch() |
| clearState 后认证状态不一致 | iOS Keychain 未清除 | 不要使用 clearState,改用自适应流程 |
| kAXErrorInvalidUIElement 崩溃 | 冷启动竞态(iOS) | 启动后增加滑动延迟 |
| 加载旋转图标 / 空白屏幕 | API 服务未运行 | 测试前启动模拟 API 服务 |
| 权限对话框阻挡操作(Android) | 系统对话框未关闭 | 添加 tapOn: text: "Allow" optional: true |