• 电商
  • 移动开发

【免费下载链接】opensource-ecommerce-mobile-app

This open-source mobile ecommerce app seamlessly transforms your Bagisto store into a powerful mobile platform, providing real-time synchronization of products and categories.

项目地址: https://gitcode.com/gh_mirrors/op/opensource-ecommerce-mobile-app
点击查看 免费下载

Maestro 是一套声明式 YAML 驱动的移动端 E2E 测试框架,以"自动等待、内置重试、零样板代码"著称,特别适合电商、社交等依赖复杂后端状态的应用。本文以仓库内的 maestro-mobile-testing 技能文档 为主体骨架,并结合本仓库(Bagisto 开源电商移动 App)中真实可运行的 maestro/ 测试工程展开讲解。读完本文,你将掌握:如何设计 testID/文本选择器、如何用认证预检与自适应流程消除 flaky、如何用 GraalJS 脚本抓取 OTP 验证码、如何编排 Mock API Server,以及如何把整套测试接入 Maestro Cloud 与 CI/CD。

Maestro 是什么:核心特性总览

Maestro 是移动端 E2E 测试框架,使用声明式 YAML 描述测试步骤,不需要编写命令式测试代码。其设计目标是让测试"写起来像在描述操作",并尽可能减少网络波动、动画时序带来的不确定性。

特性说明
声明式 YAML无命令式测试代码,每个步骤即一条 YAML 指令
自动等待不需要手写 sleep(),框架自动等待元素出现
内置重试retry: 块可对不稳定步骤自动重试,降低 flakiness
快速执行无需额外启动开销,测试运行迅速
Maestro Studio交互式测试构建器,命令为 maestro studio
子流程(Sub-flows)可复用的 YAML 片段,用于 DRY 测试
JavaScript 脚本基于 GraalJS 运行时,可发 HTTP 请求与处理数据
Maestro Cloud在 CI 中使用真实设备执行测试,无需本地模拟器

从当前仓库的实践看,这套方法论已经完整落地:maestro/ 目录下包含 17 个 Android 端流程文件(覆盖登录、注册、忘记密码、购物车、目录、商品详情、结算、订单、全流程回归),以及一套复用的 helpers 子流程和全局 config.yaml 配置。

快速开始:安装、最小测试与运行

安装 Maestro CLI

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

Maestro 依赖 Java 17 运行时,安装后需要将 JAVA_HOME 指向 JDK 17,否则运行测试时会报 "Unable to locate Java Runtime"(详见文末"常见错误"表)。

最小测试示例

appId: com.myapp
---
- launchApp
- tapOn:
    id: "my-button"
- assertVisible: "Expected Text"

运行测试

maestro test .maestro/smoke-test.yaml
maestro test --debug .maestro/smoke-test.yaml  # 逐步调试
maestro studio                                  # 交互式构建器

在本仓库中,由于流程依赖大量环境变量,实际运行命令需要携带 --config 参数并指定设备,例如 config.yaml 顶部注释给出的用法:

maestro test maestro/flows/android/full_app_flow.yaml --config maestro/config.yaml --device emulator-5554

--config 会把 config.yaml 中的 env: 变量(如 APP_ID_ANDROID、USER_EMAIL、PRODUCT_NAME 等)预加载进所有流程,测试文件内部只需用 ${VAR} 引用即可。

核心模式(一):选择器策略 —— testID vs 文本

选择器是 E2E 测试的基石。Maestro 支持按 id:(即 testID / accessibility id)和按可见文本两种方式定位元素,两种都合法,选择哪种取决于项目上下文:

上下文推荐选择器理由
多语言 / i18n 应用id:(testID)跨翻译稳定
单语言应用文本标签可读性强、测试自文档化
Agent 维护的测试两者皆可,询问开发者AI 维护的流程对可读性要求较低
系统对话框始终用文本原生弹窗上没有 testID 可用
# testID 选择器 —— 跨翻译稳定
- tapOn:
    id: "submit-button"

# 文本选择器 —— 可读、自文档化
- tapOn: "Submit"

何时优先用 testID: 应用支持多语言或将要翻译、UI 文案动态变化频繁、多个元素共享相同可见文本。

何时优先用文本: 单语言应用且文案稳定、可读性与自文档化优先、需要严格按用户可见行为测试。

在 React Native 中,使用 ID 选择器时需要给组件挂 testID 属性:

<TouchableOpacity testID="submit-button" onPress={handleSubmit}>
  <Text>{t('submit')}</Text>
</TouchableOpacity>

说明:SKILL.md 面向 React Native / Expo 应用举例;本仓库实际是 Flutter(Bagisto)电商 App,其 maestro/ 测试工程采用的是"文本标签优先"策略,详见下文"仓库落地实践"。

testID 命名规范

当使用 ID 选择器时,遵循统一命名模式,让选择器可预测、可维护:

{component}-{action/type}[-{variant}]

示例:
- auth-prompt-login-button
- product-card-{id}
- otp-input-0
- tab-home
- dashboard-loading

核心模式(二):认证预检(Auth Pre-Flight Pattern)

E2E 最常见的 flaky 来源,是 Maestro 在认证状态尚未解析完成时就与 UI 交互。解决办法是在根布局中放置一个零尺寸的 auth-loaded 标记,仅在认证加载完成后渲染:

// 在 tab bar 或根布局中
{!isLoading && <View testID="auth-loaded" style={{ width: 0, height: 0 }} />}

随后每个测试都先等它出现:

- launchApp

# 防止 iOS 冷启动 XCTest 崩溃
- swipe:
    direction: DOWN
    duration: 100

# 等待认证状态解析完成
- extendedWaitUntil:
    visible:
      id: "auth-loaded"
    timeout: 15000

# 现在可以安全交互
- tapOn:
    id: "tab-home"

核心模式(三):自适应测试(Adaptive Tests)

测试不应假设用户处于登录或游客态,而应通过 when: 条件动态分支:

# 认证流程 —— 仅当登录提示可见时执行
- runFlow:
    when:
      visible: "Sign In"
    file: flows/auth-flow.yaml

# 已登录 —— 直接继续
- runFlow:
    when:
      visible:
        id: "tab-home"
    file: flows/authenticated-action.yaml

这一模式在本仓库的 helpers 中被大量复用。例如 auth_logout.yaml 中,先尝试进入账户页,若发现处于未登录态(可见 Login),就动态调用登录子流程再继续登出断言:

- runFlow:
    when:
      visible:
        text: "^Login$"
    commands:
      - runFlow: helpers/open_login_from_account.yaml
      - runFlow: helpers/login_existing_customer.yaml
- assertVisible:
    text: "^My Orders$"

核心模式(四):乐观更新(Optimistic Updates)验证

很多现代 App(尤其是社交、电商类)采用乐观 UI:点击后界面立即变化,不等服务器响应。测试这类行为要用短超时验证"UI 在服务器响应前就变了":

# 触发变更
- tapOn:
    id: "action-button"

# 乐观更新:UI 必须在 3s 内变化(不是等服务器)
- extendedWaitUntil:
    visible:
      id: "undo-button"
    timeout: 3000

# 验证派生出的 UI 状态
- extendedWaitUntil:
    visible:
      id: "user-indicator"
    timeout: 5000
动作预期变化超时
触发变更按钮状态翻转< 3s
列表更新条目出现/消失< 5s
重做动作证明持久化生效< 3s

核心模式(五):原生弹窗处理

RN 的 Alert.alert() 会创建阻塞 UI 的原生对话框。处理套路是:先等待预期状态变化,再用 optional: true 的文本点击关闭弹窗:

- tapOn:
    id: "action-button"

# 先等待预期状态变化
- extendedWaitUntil:
    visible:
      id: "new-state-element"
    timeout: 5000

# 关闭弹窗(optional 以防它已自动关闭)
- tapOn:
    text: "OK"
    optional: true

# 短暂等待弹窗动画结束
- swipe:
    direction: DOWN
    duration: 300

核心模式(六):子流程复用(Sub-Flows)

把重复序列抽成子流程文件,主流程通过 runFlow: 引用:

.maestro/
├── flows/
│   ├── auth-and-return.yaml
│   ├── complete-purchase.yaml
│   └── verify-result.yaml
├── smoke-test.yaml
└── feature-test.yaml
# 主测试中
- runFlow:
    file: flows/auth-and-return.yaml

本仓库对此的实践非常典型:maestro/flows/android/helpers/ 下集中了 normalize_to_home_attached.yaml、ensure_guest_account_screen.yaml、open_login_from_account.yaml、login_existing_customer.yaml、dismiss_runtime_permissions.yaml 五个可复用子流程,被 auth_login_valid.yaml、auth_logout.yaml、checkout_*、full_app_flow_attached.yaml 等十余个主流程共享,真正做到"一次编写、处处复用"。

核心模式(七):深链接(Deep Links)

使用 app.json 中的 Expo scheme,而不是 bundle ID:

# 错误
- openLink: "com.myapp://profile/settings"

# 正确
- openLink: "myapp://profile/settings"

深链接必须在应用的深链处理器中注册,未注册的路由会静默失败。

核心模式(八):平台特定逻辑

- runFlow:
    when:
      platform: ios
    file: flows/ios-specific.yaml

- runFlow:
    when:
      platform: android
    file: flows/android-specific.yaml

核心模式(九):环境变量

appId: com.myapp
env:
  TEST_EMAIL: maestro-test@example.com
  API_BASE_URL: http://localhost:3000
---
- inputText: ${TEST_EMAIL}

本仓库把这一模式做到了极致:所有账号、商品、地址、文案选择器全部收口到 config.yaml 与 keys.yaml 的 env 中,例如:

env:
  # App IDs
  APP_ID_ANDROID: "com.webkul.bagisto.mobikul"
  APP_ID_IOS: "com.webkul.bagistoApp.iOS"

  # Credentials
  USER_EMAIL: "demo.user@example.com"
  USER_PASSWORD: "ExamplePass123!"
  FORGOT_PASSWORD_EMAIL: "demo.user@example.com"

  # Product under test
  PRODUCT_NAME: "Apple iPhone Smartphone"
  CONFIG_SIZE_OPTION: "Medium"
  CONFIG_COLOR_OPTION: "Blue"

  # Validation messages
  INVALID_LOGIN_ERROR: "Invalid credentials"
  EXISTING_USER_ERROR: "Email already exists"

  # Checkout data
  ADDRESS_FULL_NAME: "QA User"
  SHIPPING_METHOD_LABEL: "Flat Rate"
  PAYMENT_METHOD_LABEL: "Cash On Delivery"

流程文件内用 ${PRODUCT_NAME}、${USER_EMAIL} 等引用。注意 keys.yaml 与 config.yaml 存在字段差异(如 remove_item_button 一处为 "Delete" 一处为 "Remove"),说明这是两套历史配置,使用时以你实际执行的那份为准。

核心模式(十):选择器状态属性(State Properties)

用 enabled、selected、checked、focused 等状态属性定位元素,非常适合在动作前后校验交互元素的可用性:

# 仅当提交按钮可用时才点击
- tapOn:
    id: "submit-button"
    enabled: true

# 断言复选框已被勾选
- assertVisible:
    id: "terms-checkbox"
    checked: true

# 等待输入框获得焦点
- extendedWaitUntil:
    visible:
      id: "email-input"
      focused: true
    timeout: 3000
属性取值使用场景
enabledtrue / false提交中禁用、表单未合法时禁用的按钮
checkedtrue / false复选框、开关
selectedtrue / falseTab 项、分段控件
focusedtrue / false自动聚焦的输入框

核心模式(十一):相对位置选择器(Relative Position)

当多个相似元素并存时,用空间关系区分比用 index 更健壮、更自文档化:

# 差 —— 脆弱,顺序一变就挂
- tapOn:
    text: "Add to Basket"
    index: 1

# 好 —— 上下文明确、自文档化
- tapOn:
    text: "Add to Basket"
    below:
      text: "Awesome Shoes"

可选用的相对选择器完整清单:

# 目标元素在另一元素下方
- tapOn:
    text: "Buy Now"
    below: "Product Title"

# 目标元素是某父元素的子元素
- tapOn:
    text: "Delete"
    childOf:
      id: "item-card-42"

# 目标父元素包含某个直接子元素
- tapOn:
    containsChild: "Urgent"

# 目标元素包含多个后代
- tapOn:
    containsDescendants:
      - id: title_id
        text: "Specific Title"
      - "Another descendant text"

# 水平定位
- tapOn:
    text: "Edit"
    rightOf: "Username"
选择器含义
below:元素位于参照元素下方
above:元素位于参照元素上方
leftOf:元素位于参照元素左侧
rightOf:元素位于参照元素右侧
childOf:元素是参照父元素的直接子元素
containsChild:元素包含匹配参照的直接子元素
containsDescendants:元素包含全部指定的后代元素

本仓库的 helpers 中大量使用相对定位。例如 open_login_from_account.yaml 用 rightOf: "^Sign Up$" 精确定位登录按钮(同一屏存在 Sign Up 与 Login 两个按钮):

- tapOn:
    text: "^Login$"
    rightOf: "^Sign Up$"
    retryTapIfNoChange: true
    waitToSettleTimeoutMs: 1500

认证测试:OTP 与 Magic Link 的 E2E 架构

测试 OTP 或 magic-link 登录,核心挑战是"在 E2E 中程序化地捕获邮件"。通用架构如下:

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│  Maestro    │────▶│  Auth        │────▶│  Email Capture  │
│  Test       │     │  Provider    │     │  Service        │
└─────────────┘     └──────────────┘     └─────────────────┘
       │                                        │
       │         ┌──────────────────────────────┘
       │         ▼
       │   ┌─────────────┐
       └──▶│  REST API   │ ─── GET /api/v1/messages
           │  (email)    │ ─── Extract OTP code
           └─────────────┘

常见的邮件捕获服务有 Mailpit、MailHog、Ethereal。流程是:测试触发认证提供方发送邮件 → 邮件捕获服务通过 REST API 暴露邮件 → Maestro 脚本轮询 API 提取 OTP 码。

OTP 抓取脚本(GraalJS)

// 关键:Maestro 使用 GraalJS —— 不支持 async/await、不支持 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);
// 取最新一封邮件并提取 OTP 码
var body = data.messages[0].Content.Body;
var match = body.match(/(\d{6})/);
output.OTP_CODE = match[1];

OTP 输入策略

自动聚焦的 OTP 组件需要逐位输入,每位先 tap 再输入:

# 通过辅助脚本拆分 OTP 为单数字
- runScript:
    file: scripts/split-otp.js
    env:
      OTP_CODE: ${output.OTP_CODE}

# 逐位 tap 输入框再输入
- tapOn:
    id: "otp-input-0"
- inputText: ${output.OTP_0}

- tapOn:
    id: "otp-input-1"
- inputText: ${output.OTP_1}
# ... 对每位数字重复

对于具体提供方(Supabase + Mailpit、Firebase Auth、Auth0),建议在项目级 skill 中扩展本文。

GraalJS 脚本规则

Maestro 的脚本运行在 GraalJS 之上,以下约束是硬性的:

特性状态
async/await不支持
fetch()不支持
http.get()、http.post()用它们替代 fetch
json()用于解析响应体
output.VAR向 YAML 流程暴露变量
var 声明必须使用(用 var 而非 const/let 更安全)
// 脚本模板
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;

本仓库提供了一个极简但完整的 GraalJS 脚本范例 generate_signup_email.js,用于注册测试生成唯一邮箱,避免重复注册冲突:

var prefix = typeof EMAIL_PREFIX !== "undefined" ? EMAIL_PREFIX : "qa.signup";
var timestamp = String(Date.now());

output.UNIQUE_SIGNUP_EMAIL = prefix + "+" + timestamp + "@example.com";

它在 auth_signup.yaml 中通过 runScript 调用,并把结果注入注册表单:

- runScript:
    file: ../../scripts/generate_signup_email.js
    env:
      EMAIL_PREFIX: ${SIGNUP_EMAIL_PREFIX}
...
- tapOn:
    text: "^Enter your email$"
- inputText: ${output.UNIQUE_SIGNUP_EMAIL}

注意脚本通过 typeof EMAIL_PREFIX !== "undefined" 做参数兜底,这是 GraalJS 脚本处理可选环境变量的标准写法。脚本执行后,流程用 extendedWaitUntil 等待 "Login to your account|My Orders" 二选一出现,并针对两种注册结果分别断言,体现了"自适应"思想。

关键坑(Critical Gotchas)

clearState 不会清除 iOS Keychain

clearState: true 只清除 App 沙盒(UserDefaults、文件、缓存),不会清除 iOS Keychain。通过 expo-secure-store(或任何基于 Keychain 的存储)保存的认证令牌,在 clearState 重置、甚至应用重装后依然存在。

# 错误 —— 用户可能仍处于登录态
- launchApp:
    clearState: true
- assertVisible: "Welcome"  # 若 Keychain 中有令牌则失败

# 正确 —— 等待认证解析,然后自适应
- launchApp
- extendedWaitUntil:
    visible:
      id: "auth-loaded"
    timeout: 15000

规则:

  • 永远不要依赖 clearState 在 iOS 上制造游客态
  • 认证测试:跳过 clearState,用 auth-loaded 预检
  • 游客测试:用同时兼容两种状态的自适应流程
  • 永远不要在 clearState 后断言"仅游客可见"的 UI

注意: Android 上 clearState: true 会完整重置应用数据(包括凭据),这是 iOS 专属的坑。

XCTest kAXErrorInvalidUIElement 崩溃(iOS)

冷启动时,如果 Maestro 在首个渲染周期完成前就访问无障碍树,XCTest 驱动可能崩溃。

修复: 在 launchApp 后紧跟一个无操作 swipe:

- launchApp
- swipe:
    direction: DOWN
    duration: 100

API 服务器依赖

调用后端 API 的 App 在 localhost 上运行时,需要完整服务器或 mock 服务器在运行;否则所有依赖 API 的屏幕都会显示加载中或空状态(查询静默失败)。

修复: 运行测试前先启动 mock API 服务器:

# 启动 mock 服务器(在你的 API 端口上返回预设响应)
npx tsx scripts/mock-api-server.ts &

# 再运行测试
maestro test .maestro/my-test.yaml

为 App 调用的每个端点返回预设 JSON 的轻量 mock,比跑完整后端更快、更确定。

认证感知的 Tab 栏

游客与登录用户显示不同 Tab 的 Tab 栏会导致选择器失败:

状态典型 Tabs
游客home, search, cart, profile
已登录home, feed, create, messages, profile

只断言两种状态下都存在 Tab,或使用自适应 when: 条件。

测试文件模板

# {Feature} {Action} Test
#
# Tests: {这个测试验证什么}
# Prerequisites:
# - 模拟器/真机已运行且 App 已安装
# - 后端或 mock 服务器已运行(若依赖 API)

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                    # 速查手册 + testID 清单
├── config.yaml                  # 共享配置
├── flows/                       # 可复用子流程
│   ├── auth-and-return.yaml
│   ├── complete-action.yaml
│   └── verify-result.yaml
├── scripts/                     # GraalJS 辅助脚本
│   ├── fetch-otp.js
│   └── split-otp.js
├── smoke-test.yaml              # 游客导航
├── auth-signin.yaml             # OTP 登录流程
├── feature-screenshots.yaml     # 截图流程
└── feature-action.yaml          # 功能专属测试

scripts/
├── mock-api-server.ts           # 供 E2E 使用的轻量 mock
└── run-e2e.sh                   # 编排脚本

命名约定

类型模式示例
主测试{feature}-{action}.yamlcheckout-purchase.yaml
子流程{action}-{context}.yamlauth-and-return-to-dashboard.yaml
脚本{verb}-{noun}.jsfetch-otp.js

仓库的目录布局与此高度一致:maestro/flows/android/ 放主流程(如 auth_login_valid.yaml、cart_flow.yaml、checkout_guest.yaml),maestro/flows/android/helpers/ 放可复用子流程,maestro/scripts/ 放 GraalJS 脚本,config.yaml 承担共享配置,并额外提供了 LABEL_BASED_INSTRUCTIONS.md 作为选择器策略的工程约束文档。

基础设施:Mock 服务器、编排脚本与种子数据

Mock 服务器

为 API 层返回预设响应的轻量 mock,比 E2E 期间跑完整后端更快更确定:

# 运行 Maestro 测试前先启动 mock 服务器
npx tsx scripts/mock-api-server.ts &

编排脚本

用 shell 脚本自动化完整 E2E 准备流程:

  1. 启动后端服务(数据库、认证)
  2. 注入种子测试数据
  3. 启动 mock API 服务器
  4. 运行 Maestro 测试
  5. 清理所有服务
bash scripts/run-e2e.sh

种子数据

依赖特定数据的测试需要种子数据库。把种子脚本放在测试基础设施旁,在每套测试运行前执行。

Android 专属模式

模拟器搭建

Android 测试需要模拟器或 USB 连接的物理设备,Maestro 自动检测已连接设备:

# 列出可用的系统镜像
sdkmanager --list | grep system-images

# 创建模拟器
avdmanager create avd -n maestro_test \
  -k "system-images;android-34;google_apis;arm64-v8a"

# 启动模拟器
emulator -avd maestro_test

iOS 与 Android 差异

方面iOSAndroid
设备类型仅模拟器(不支持物理设备)模拟器 + 通过 ADB 的物理设备
clearState不清除 Keychain完整重置应用数据
冷启动崩溃XCTest kAXError(需加 swipe 延时)无对应问题
性能原生运行(快)ARM 模拟(x86 上较慢)
权限弹窗系统警告文本不同的系统对话框

ADB 调试

adb devices                                    # 列出已连接设备
adb shell am start -n com.myapp/.MainActivity  # 启动应用
adb logcat | grep Maestro                      # 过滤 Maestro 日志
adb shell input keyevent 82                    # 解锁屏幕

Android 权限处理

Android 权限以系统对话框形式出现,用 optional tap 关闭:

- tapOn:
    text: "Allow"
    optional: true

- tapOn:
    text: "While using the app"
    optional: true

仓库对此封装了专门的 helpers 子流程 dismiss_runtime_permissions.yaml,用 when: visible 条件一次性尝试关闭所有常见权限文案变体:

- runFlow:
    when:
      visible:
        text: "^(Allow only while using the app|While using the app|Only this time|Allow)$"
    commands:
      - tapOn:
          text: "^Allow only while using the app$"
          optional: true
          waitToSettleTimeoutMs: 750
      - tapOn:
          text: "^While using the app$"
          optional: true
          waitToSettleTimeoutMs: 750
      - tapOn:
          text: "^Only this time$"
          optional: true
          waitToSettleTimeoutMs: 750
      - tapOn:
          text: "^Allow$"
          optional: true
          waitToSettleTimeoutMs: 750

因为该子流程内层 tap 全部是 optional: true,无论当前弹出哪种权限文案都能被关闭,且不会因元素不存在而失败,可被安全地嵌入任意流程任意位置。

仓库落地实践:Flutter 电商 App 的 Maestro 测试工程

前面是 SKILL.md 的通用方法论,接下来看本仓库如何把它真正落地(结合 maestro 目录下可运行的真实工程)。

策略约束:标签优先,禁用坐标

LABEL_BASED_INSTRUCTIONS.md 明确规定了本项目(含附加流程 full_app_flow_attached.yaml)的选择器纪律:

  • 使用基于标签(label-based)的选择器
  • 不要使用 point: 坐标选择器
  • 优先用可见文本与无障碍标签进行导航和操作
  • 保持流程步骤可读,便于 UI 变化时维护

这是一条工程级约束:相比坐标点选,文本标签在屏幕尺寸、分辨率变化下更稳定。实际各流程也大量使用锚定正则(如 ^Login$、^My Orders$)来精确匹配、避免子串误命中。

会话恢复:normalize_to_home_attached

E2E 经常面临"上次运行中断,App 停在未知页面"的问题。normalize_to_home_attached.yaml 是仓库给出的解法:通过 retry: maxRetries: 3 包裹一组 runFlow: when: visible 条件,逐一识别 "Checkout"、"Orders #.*"、"Search products"、"Add to Cart|Buy Now"、"Create Account"、"Forgot Password?" 等中间页面并 back 返回,最终断言底部导航的 Home 与 Account 可见,把 App 归一化到可继续执行的根屏幕。这种"先恢复、再执行"的思路,与 SKILL.md 的 auth-loaded 预检哲学一脉相承,只是把"等待状态"升级为"主动归位"。

完整回归:full_app_flow_attached

full_app_flow_attached.yaml 是该工程的压轴流程:归一化 → 确保登录(自适应补登录)→ 搜索商品 → 加入购物车 → Buy Now → 结算(含支付方式选择)→ 下单成功 → 查看订单详情(Price Break / Grand Total)。全程依赖 retry、scrollUntilVisible、extendedWaitUntil 与 dismiss_runtime_permissions 的组合,可视为 SKILL.md 全部核心模式在电商场景下的完整示范。

CI/CD 集成

GitHub Actions + Maestro Cloud

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

      # 构建 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

      # 访问结果
      # ${{ steps.maestro.outputs.MAESTRO_CLOUD_CONSOLE_URL }}
      # ${{ steps.maestro.outputs.MAESTRO_CLOUD_UPLOAD_STATUS }}
      # ${{ steps.maestro.outputs.MAESTRO_CLOUD_FLOW_RESULTS }}

基于 Tag 的流程过滤

用 tags 控制哪些测试进 CI、哪些只在本地跑:

# 流程文件头部
appId: com.myapp
tags:
  - ci
  - smoke
---
- launchApp
# ... 测试步骤
# 本地只跑带 ci 标签的流程
maestro test --include-tags ci .maestro/

# 排除进行中的流程
maestro test --exclude-tags wip .maestro/

仓库流程也遵循了这套规范,例如 auth_login_valid.yaml 的 tags 为 auth / smoke / android,full_app_flow.yaml 为 e2e / regression / android,便于按 tag 精细调度。

本地 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 key
  3. 在 CI 提供商中把 key 存为 MAESTRO_API_KEY 密钥

从 CLI 运行

# 上传并在 Maestro Cloud 上运行
maestro cloud --api-key $MAESTRO_API_KEY \
  --app-file ./app-release.apk \
  .maestro/

# 带 tag 过滤
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 服务器:让 Agent 直接执行测试

Maestro MCP 服务器把 Maestro 的完整命令集暴露为 Model Context Protocol 工具,让 AI Agent 直接执行测试、与设备交互,而不仅仅是编写 YAML。

与本文技能的分工

本文技能Maestro MCP
角色教正确的模式提供运行时执行
层次编写(写出好 YAML)执行(运行、点击、断言、截图)
产出更好的测试文件实时设备交互

两者配合使用:技能保证 AI 写出正确的测试;MCP 让它能立即运行、看到失败、迭代修复。

安装

MCP 随 Maestro CLI 内置,无需额外安装:

# 验证可用
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。

核心能力

MCP 服务器按类别暴露 47 个工具:

类别工具示例
UI 交互tap、swipe、scroll、长按tapOn、scrollUntilVisible
文本输入type、erase、paste、copyinputText、eraseText
断言可见性、AI 驱动assertVisible、assertWithAI
应用生命周期launch、stop、清状态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   # 交互式逐步调试
maestro record .maestro/test.yaml          # 录制成视频
maestro studio                             # 交互式 UI 构建器
maestro hierarchy                          # 查看元素树

截图保存在 ~/.maestro/tests/{timestamp}/。

新测试编写清单

[ ] 使用唯一测试邮箱(maestro-{feature}@example.com)
[ ] 选择器策略已确定(i18n 应用用 testID,单语言用文本 —— 见模式一)
[ ] 相关处使用状态属性选择器(enabled、checked —— 见模式十)
[ ] 相似元素用相对选择器区分,而非 index(见模式十一)
[ ] 使用认证预检模式(auth-loaded)
[ ] 启动后添加 swipe(iOS 崩溃预防)
[ ] 处理两种认证状态(自适应流程)
[ ] 变更后关闭原生弹窗
[ ] 乐观更新使用短超时(3-5s)
[ ] 可复用序列抽成子流程
[ ] 关键节点保存描述性截图
[ ] 头部注释写明前置条件
[ ] 添加到 README.md 测试表
[ ] 依赖后端时启动 mock API 服务器
[ ] 添加 CI 过滤 tags(ci、smoke、wip)

常见错误速查表

错误原因修复
"Unable to locate Java Runtime"Java 不在 PATHexport JAVA_HOME=/opt/homebrew/opt/openjdk@17/...
点击后 "Element not found"原生弹窗阻塞添加 tapOn: text: "OK" optional: true
OTP 数字输不进去自动聚焦干扰用独立的 otp-input-N testID
测试通过但什么都没发生滥用 optional: true仅对真正可选的动作用 optional
可见性断言为 false元素尚未渲染加大超时或确认 testID 存在
脚本输出为空JS API 用错用 http.get() 而非 fetch()
clearState 后认证状态不一致iOS Keychain 未清除不用 clearState,改用自适应流程
kAXErrorInvalidUIElement 崩溃iOS 冷启动竞态启动后加 swipe 延时
加载中/空屏未运行 API 服务器测试前启动 mock API 服务器
权限弹窗阻塞(Android)系统对话框未关闭添加 tapOn: text: "Allow" optional: true

小结

Maestro 的声明式 YAML、自动等待、内置重试与 GraalJS 脚本能力,为移动端 E2E 提供了一条低样板、高稳定的路径;而选择器策略、认证预检、自适应流程、乐观更新验证、Mock API 与 CI/CD 编排这些模式,则是让这套路径真正可落地、可维护的关键。本仓库的 maestro/ 工程(config.yaml、helpers 子流程、全流程回归、GraalJS 脚本)即为上述全部方法论的真实范本,可作为你搭建自身移动端 E2E 测试体系的直接参考。

  • 电商
  • 移动开发

【免费下载链接】opensource-ecommerce-mobile-app

This open-source mobile ecommerce app seamlessly transforms your Bagisto store into a powerful mobile platform, providing real-time synchronization of products and categories.

项目地址: https://gitcode.com/gh_mirrors/op/opensource-ecommerce-mobile-app
点击查看 免费下载

更多推荐