mcpField
M
maestro-mobile-testing

maestro-mobile-testing

github.com/tovimx测试与质量281.2k 次安装37 周前更新
代码仓库

关于 maestro-mobile-testing

Maestro Mobile E2E Testing 是用于编写移动应用声明式 YAML 端到端测试的技能,介绍可复用流程、选择器、身份验证模式和脚本编写,适合验证不同界面状态下的应用行为。

主要功能

  1. 01声明式 YAML 测试流程
  2. 02自动等待与内置重试
  3. 03可复用子流程和选择器
  4. 04使用 GraalJS 编写脚本
  5. 05身份验证和界面状态测试指南

适用场景

  1. 01创建可重复的移动端冒烟测试
  2. 02测试登录和 OTP 流程
  3. 03验证不同登录状态下的界面行为
  4. 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 与文本

根据项目背景选择选择器方案。两种方式都有效,合适的选择取决于应用是否支持本地化,以及团队的测试理念。

背景推荐的选择器原因
多语言 / i18nid:(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
属性值用例
enabledtrue / false提交期间或表单有效前会禁用的按钮
checkedtrue / false复选框、切换开关
selectedtrue / false标签项、分段控件
focusedtrue / 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}.yamlcheckout-purchase.yaml
子流程{action}-{context}.yamlauth-and-return-to-dashboard.yaml
脚本{verb}-{noun}.jsfetch-otp.js

基础设施

面向 API 依赖测试的模拟服务器

创建一个轻量级模拟服务器,为 API 层提供预设响应。与在 E2E 测试期间运行完整后端相比,这种方式速度更快,也更可预测。

# Start mock server before running Maestro tests
npx tsx scripts/mock-api-server.ts &
编排脚本

使用 shell 脚本自动完成完整的 E2E 设置:

  1. 启动后端服务(数据库、身份验证)
  2. 填充测试数据
  3. 启动模拟 API 服务器
  4. 运行 Maestro 测试
  5. 清理所有服务
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 的差异
方面iOSAndroid
设备类型仅模拟器(不支持实体设备)模拟器 + 通过 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 无需本地模拟器设置,即可在实体设备上运行测试。

设置
  1. 在 cloud.maestro.dev 创建账户
  2. 从控制面板生成 API 密钥
  3. 在 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 可以:

  1. 编写测试 YAML,使用此技能中的模式
  2. 运行测试,通过 MCP 的 runFlow / launchApp 和交互工具执行
  3. 查看失败情况,包括屏幕截图和断言
  4. 修复 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

资源

评论

支持 Markdown