- 测试
- 移动开发
- 开发工具
- CLI
【免费下载链接】Maestro
Painless E2E Automation for Mobile and Web
本文以 Maestro 开源仓库中e2e/demo_app/CLAUDE.md为骨架,完整讲解这个 Flutter 测试目标应用(demo_app)的定位、构建方式、lib/测试屏幕架构、.maestro/Flow 组织规范,以及编写权限相关 Flow 时最容易踩的坑。读完本文,你将掌握如何为 Maestro 框架的某项能力构造可观察的测试场景、如何组织跨平台 Flow 并利用config.yaml控制执行范围,以及如何在新增测试屏幕时遵循仓库约定。
demo_app 是什么:为 Maestro 而生的 E2E 测试目标
demo_app(e2e/demo_app/)是一个基于 Flutter 构建的跨平台(Android 与 iOS)示例应用,其唯一职责是充当 Maestro 移动端 UI 自动化测试框架的目标应用。正如 CLAUDE.md 开头所强调的:它不是生产应用——它的每一个屏幕都是为了"锻炼" Maestro 的某项具体功能、或复现某个已报告的 Bug 而存在。
这带来两个重要推论,贯穿整个仓库的设计:
- 屏幕只是载体,Flow 才是主体。新增测试屏幕的目的不是"测试这个屏幕本身",而是让某个 Maestro 行为变得可观察、可断言;
- 当需要验证新的 Maestro 功能时,新屏幕/新行为会持续被添加到这里。因此这个应用实际上是一份"Maestro 能力矩阵"的活文档,与 lib/ 下的屏幕一一对应。
应用的分发方式(见 e2e/demo_app/README.md):应用二进制被构建后上传到存储桶,链接记录在 e2e/manifest.txt;Maestro 的 E2E 流水线运行时下载这些二进制,并执行 .maestro/ 中的 Flow(连同其他目标应用一起)。
构建与运行:四个核心 Flutter 命令
对 demo_app 的常规开发操作全部走 Flutter CLI,命令如下(摘自 CLAUDE.md):
# 运行应用(需要已连接的设备或模拟器) flutter run # 构建 Android APK flutter build apk # 构建 iOS 模拟器应用 flutter build ios --simulator # 静态分析代码 flutter analyze其中flutter build ios --simulator用于产出 iOS 模拟器可安装的 Runner.app,是本地 E2E 调试最常用的一条;flutter build apk对应 Android 侧产物。flutter analyze配合 analysis_options.yaml(仓库使用 flutter_lints 规则集,见 pubspec.yaml)做代码质量把关。
该应用的依赖列表(见 pubspec.yaml)本身就是"测试点清单":webview_flutter(WebView 测试)、permission_handler(权限测试)、sensors_plus(传感器测试)、geolocator(定位测试)、connectivity_plus(网络连接测试)、flutter_launch_arguments(启动参数注入)、app_links(Deep Link 测试)、shared_preferences(状态持久化测试)等。
应用架构:lib/测试屏幕与启动参数注入
屏幕即场景:一张表看懂 lib/
main.dart 是应用首页,用一个GridView.count(3 列)铺满各测试入口按钮,每个按钮Navigator.push到对应的独立测试屏幕。每个屏幕是一个独立 Dart 文件、对应一个特定测试场景,CLAUDE.md 给出了完整的映射表:
| 文件 | 用途 |
|---|---|
form_screen.dart | 带邮箱/密码校验的登录表单 |
input_screen.dart | 键盘与文本输入行为 |
swiping_screen.dart | 滑动手势测试 |
nesting_screen.dart | 深层嵌套的 Widget 层级 |
location_screen.dart | 通过geolocator获取 GPS 定位,流式推送位置更新 |
sensors_screen.dart | 设备传感器(仅 Android) |
webview.dart | 通过webview_flutter嵌入 WebView |
defects_screen.dart | 故意制造的 UI 怪癖,用于缺陷回归 |
cropped_screenshot_screen.dart | 截图裁剪的边界情况 |
notifications_permission_screen.dart | 通知权限请求流程 |
permission_check_screen.dart | 被动展示权限状态(location、all-files),通过permission_handler读取,绝不调用requestPermission(),从而确定性地反映预先授予的状态 |
issue_1619_repro.dart、issue_1677_repro.dart | 具体 Bug 的复现用例 |
此外,从 lib/ 目录可以看到比文档更全的屏幕集合,包括animation_screen.dart、carousel_screen.dart、connectivity_screen.dart、gesture_tester_screen.dart、orientation_screen.dart、patient_care_screen.dart、scrollable_list_screen.dart、webview_deep_dom_test_screen.dart、webview_devtools_test_screen.dart等,分别覆盖动画、轮播、网络切换、手势、横竖屏、assertScreenshot阈值、长列表滚动、WebView 深层 DOM、WebView DevTools 等场景,并全部在 main.dart 中注册了入口按钮。
几个值得注意的平台条件渲染(见 main.dart):
Sensors入口仅在!kIsWeb时渲染(传感器场景面向 Android/iOS 真机);Health Access入口仅在 iOS 上渲染,通过MethodChannel('com.example.demo_app/health_access')调用原生层(对应 iOS 工程中的 HealthAccessManager.swift);Password autofill Test通过MethodChannel('com.example.demo_app/password_test')打开原生密码自动填充页面(对应 PasswordTestViewController.swift);issue 1619/1677 repro、Webview系列、Cropped Screenshot、Notifications Permission、Permission Check等均以独立按钮暴露。
启动参数注入:Maestro 在启动时配置应用状态
demo_app 通过flutter_launch_arguments插件读取启动参数,使得Maestro Flow 可以在launchApp时配置应用初始状态。在 main.dart 中:
final counterValue = await _flutterLaunchArgumentsPlugin.getInt('initialCounter'); final delayValue = await _flutterLaunchArgumentsPlugin.getInt('delay'); setState(() { _counter = counterValue ?? 0; _delay = delayValue ?? 0; });即支持initialCounter(初始计数值)与delay(点击加一按钮时的模拟延迟秒数)两个参数。delay的典型用途是制造"点击后界面延迟变化"的时序窗口,用于测试 Maestro 的waitForAnimationToEnd等同步机制(对应 .maestro/commands/waitForAnimationToEnd.yaml)。
顺带一提,应用还内置了 Deep Link 处理:example://form会直接导航到表单页(main.dart),对应 Android 侧AndroidManifest.xml中注册的examplescheme intent-filter 与https://maestro.mobile.dev的 App Links 声明。
Maestro Flow:.maestro/目录的组织规范
目录结构
CLAUDE.md 给出了.maestro/下各子目录的语义:
- 根目录
*.yaml:主要的通过/失败测试用例,打passing标签,或用断言预期失败; commands/:可复用的 Maestro 命令定义(如assertVisible.yaml、inputText.yaml);android_device_configuration/与ios_device_configuration/:测试前运行的设备设置 Flow(关闭自动纠错、设置时区、启用传感器等);issues/:专门复现已报告 Maestro Bug 的 Flow;experimental/:不稳定/进行中的 Flow,不纳入 CI;scripts/:被evalScript命令使用的 JavaScript 辅助脚本。
实际仓库目录与此完全吻合:commands/下已积累 50+ 个命令定义文件(assertVisible.yaml、inputText.yaml、scrollUntilVisible.yaml、setLocation.yaml、takeScreenshot.yaml、assertScreenshotCropped.yaml、retry.yaml等),issues/下存放issue1777.yaml、issue1677.yaml、issue565_form.yaml、maestro_issue_1619/等复现用例。
config.yaml:控制 Flow 收录范围
.maestro/config.yaml 决定了执行maestro test .maestro/时包含哪些 Flow 目录:
flows: - "*" - "ios_device_configuration/*" - "android_device_configuration/*" - "commands/*"注意config.yaml并没有显式列出issues/与experimental/——experimental/不纳入 CI 正是靠这个配置实现的(其存在本身即被配置排除)。
Flow 运行命令
CLAUDE.md 给出的三条核心命令(适用于脚本化或 CI 运行场景):
# 运行全部 Flow maestro test .maestro/ # 运行单个 Flow maestro test .maestro/fill_form.yaml # 按标签运行 maestro test --include-tags passing .maestro/--include-tags passing会筛选出所有打了passing标签的用例,这是 CI 中"只跑通过用例"的惯用法。仓库根目录的 e2e/run_tests、e2e/list_workspaces 等脚本展示了这套 Flow 在更上层 E2E 流水线中的编排方式。
一个典型的端到端 Flow 示例是 .maestro/fill_form.yaml,它完整覆盖"启动清状态 → 导航 → 输入 → 断言"的标准链路:
appId: com.example.example tags: - passing --- - launchApp: clearState: true - tapOn: Form Test - tapOn: Email - inputText: correct@mobile.dev - tapOn: Password - inputText: maestro - tapOn: text: Login index: 1 - assertVisible: text: Credentials are correct optional: true # Fix me this part is flaky on CI only not local, needs to be addressed why而 .maestro/commands/assertVisible.yaml 则演示了命令定义文件的双重价值:既是"命令文档"(展示assertVisible支持字符串简写、text:与id:三种形态,其中id: 'fabAddIcon'对应 main.dart 中通过Semantics(identifier:)暴露的语义标识符),又是可直接运行的测试。
平台定向(Platform targeting):一条 Flow 跑双端
CLAUDE.md 明确了 Flow 的跨平台编写原则,这是本仓库最重要的 Flow 约定:
- 默认 Flow同时运行于 Android 与 iOS;只有行为确实平台相关时才加
android或ios标签; - 优先维护单一跨平台 Flow,而不是拆成两个平台文件;
- 用
${maestro.platform == "android" ? ... : ...}表达式给平台相关值做三元插值; - 用
runFlow的when: platform:守卫平台专属步骤。
落地示例一:.maestro/permission_interpolation.yaml 在env中定义平台化权限值:
env: ALLOW_VALUE: '${maestro.platform == "android" ? "allow" : "always"}' DENY_VALUE: '${maestro.platform == "android" ? "deny" : "never"}'落地示例二:.maestro/relatives.yaml 同时展示平台标签与相对定位断言(containsChild、leftOf、rightOf、below、above)在嵌套层级上的用法,并注明android标签的理由是 iOS 使用不同的层级结构。
落地示例三:.maestro/environment-variables.yaml 展示环境变量参与断言:assertTrue: ${MAESTRO_EXAMPLE == 'test-value'},依赖运行时注入的MAESTRO_EXAMPLE环境变量。
设备配置 Flow:测试前的环境准备
android_device_configuration/与ios_device_configuration/下的 Flow 在正式用例之前执行,负责把设备环境"驯服"到确定状态。例如 .maestro/android_device_configuration/enable_sensors.yaml 会依次断言加速度计、陀螺仪、罗盘、磁力计、气压计、光传感器、接近传感器、GPS 全部Available;ios_device_configuration/disable_autocorrect.yaml 关闭 iOS 自动纠错,避免输入文本被系统"纠正"导致断言失败。
App ID
所有 Flow 的目标应用统一为appId: com.example.example(对应 Android 工程 MainActivity.kt 与 iOS Runner 的 bundle 配置)。在launchApp之前,应用需要先安装到设备/模拟器上(本地用flutter run或flutter build+ 安装命令完成)。
权限测试陷阱:本仓库最精华的实战经验
CLAUDE.md 用一整节专门总结"编写权限 Flow 时非显而易见的坑",这些经验全部来自真实的跨平台 CI 实践,逐条展开如下。
陷阱一:iOS 侧,每个权限都必须在 Podfile 里"编译进来"
permission_handler在 iOS 上的行为是:某个权限的 handler 只有在GCC_PREPROCESSOR_DEFINITIONS中设置了对应宏时才会被编译进产物(例如PERMISSION_LOCATION=1)。如果没设宏,该权限的.status在 iOS 上会静默返回 denied——无论系统真实授权状态如何。
当前 ios/Podfile 通过post_install钩子启用的宏:
post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', 'PERMISSION_NOTIFICATIONS=1', 'PERMISSION_LOCATION=1', ] end end end即当前仅启用了notifications与location两个权限。修改 Podfile 后必须执行pod install(在ios/目录下)再flutter build ios——仅仅编辑 Podfile 不会触发重新安装。
陷阱二:Android 侧,运行时权限必须在 Manifest 里声明
Android 的运行时权限若未在AndroidManifest.xml中声明,就无法被授予(pm grant会直接报错)。当前 android/app/src/main/AndroidManifest.xml 声明了:INTERNET、ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、MANAGE_EXTERNAL_STORAGE。因此例如POST_NOTIFICATIONS目前无法在此应用上被授予——这正是notifications_permission_screen.dart与launchApp通知权限测试的设计约束。
陷阱三:权限值是平台相关的,且校验行为不对称
- Android 使用
allow/deny/unset; - iOS 的
location使用always/inuse/never/unset; - iOS 会校验 location 的值并对其他值抛异常;Android 对未知/空值则静默回退为 revoke,不报错。
跨平台 Flow 必须按平台取值,典型写法(出自 .maestro/permission_interpolation.yaml):
- launchApp: clearState: true permissions: location: ${ALLOW_VALUE} # android -> allow;ios -> always - tapOn: "Permission Check" - assertVisible: "Location: Allowed"该 Flow 的完整逻辑验证了 issue 2416(launchApp权限值必须支持变量插值):allow → "Location: Allowed",deny → "Location: Not allowed",并且仅 Android用runFlow when: platform: Android守卫"未知值/空值回退 revoke"的用例(iOS 会抛异常所以必须隔离)。
陷阱四:launchApp不带permissions:块时默认all: allow
这是本仓库反复依赖的默认行为:省略permissions:时,Maestro 会默认授予全部权限。因此在大多数非权限用例中无需显式声明权限,而权限用例则必须显式给出permissions:块来控制授予/撤销。
陷阱五:观察权限要"被动",不要用会弹窗的屏幕
这是最容易踩中的坑,涉及两个屏幕的取舍:
- permission_check_screen.dart 是一个被动观察器:它只通过
permission_handler读取Permission.location.isGranted/Permission.manageExternalStorage.isGranted并展示 "Allowed / Not allowed",从不调用requestPermission(),所以不会弹系统对话框,能确定性地反映先前launchApp { permissions: }/setPermissions留下的状态,是权限断言的首选; - location_screen.dart 则主动调用
Geolocator.requestPermission()(见_startLocationUpdates中permission == LocationPermission.denied分支),会弹出系统权限对话框(Android 不会自动关闭),并异步解析定位结果——与测试流程产生竞态,不适合用来断言权限授予结果。
结论:断言"预先授予的状态"用permission_check_screen.dart;要测试"请求权限的交互流程"才用notifications_permission_screen.dart等主动请求场景。
陷阱六:MANAGE_EXTERNAL_STORAGE是 appOps 权限
Android 的MANAGE_EXTERNAL_STORAGE属于appOps 特殊路径权限,不是标准的pm grant运行时权限——它无法像普通权限那样用pm grant授予。这一点直接影响了launchApp权限块和setPermissions在此权限上的处理方式,编写涉及"所有文件访问"的权限用例时必须注意。
用 MCP 编写与调试 Flow
CLAUDE.md 特别推荐:编写、运行、调试 Flow 时优先使用 Maestro MCP(工作流为list_devices→inspect_screen/take_screenshot→run),因为 MCP 会直接返回视图层级与截图,比解析 CLI 输出迭代效率高得多;CLI 则用于脚本化/CI 场景或无 MCP 可用时。
一个关键认知(也是新手最容易困惑的点):MCP 运行的是"已构建"的 Maestro,而不是你的工作树。如果你修改了e2e/之外的 Maestro 框架代码并想通过 MCP 在本应用上验证:
- 重新构建 Maestro;
- 重连 MCP(例如
/mcp reconnect maestro); - 再重新运行 Flow。
这与重新构建 demo_app 本身是两回事——demo_app 的 Dart/iOS/Android 改动需要先重建并重装到设备上,MCP 才会看到新行为。仓库中 e2e/cli/test-cli.sh 等脚本展示了 CLI 路径的验证方式。
新增测试屏幕的标准流程
当需要为某个 Maestro 特性新增测试场景时,CLAUDE.md 给出了四步规范,这也是向该仓库贡献测试的完整路径:
- 在
lib/下新建<feature>_screen.dart,实现一个StatefulWidget; - 在 lib/main.dart 中为该屏幕添加导航按钮;
- 编写锻炼目标 Maestro 特性的 Flow,打上
[passing]标签——再次强调,Flow 才是重点,屏幕只是让该 Maestro 行为可观察的载体;行为平台相关时才加平台标签(见上文"平台定向"); - 若涉及定位或传感器测试,确保平台专属子目录中存在相应的设备配置 Flow(如 android_device_configuration/enable_sensors.yaml)。
小结
demo_app 是理解 Maestro E2E 测试方法论的最佳入口:它把"被测应用"抽象成一张张可观察的测试屏幕,用 Flow 组织成跨平台的能力验证矩阵,并用config.yaml、平台标签、${maestro.platform}插值与runFlow when:守卫解决双端差异;权限测试一节则浓缩了permission_handler在 iOS/Android 上最真实的行为差异。读者可以直接在 e2e/demo_app/.maestro/ 与 e2e/demo_app/lib/ 之间对照阅读,把本文中的每一条约定映射到实际代码上。
- 测试
- 移动开发
- 开发工具
- CLI
【免费下载链接】Maestro
Painless E2E Automation for Mobile and Web
相关推荐
SuperPlane E2E 测试实战指南:用 Go 与 Playwright 编写可读、稳定的端到端测试
SuperPlane E2E 测试实战指南:用 Go 与 Playwright 编写可读、稳定的端到端测试 SuperPlane 的端到端(E2E)测试用 Go
NativeScript 应用 UI 端到端测试实战指南:Appium e2e 测试的执行、调试与用例编写
NativeScript 应用 UI 端到端测试实战指南:Appium e2e 测试的执行、调试与用例编写 本篇指南以 NativeScript 官方仓库中承载
Mesop 应用测试指南:用 Playwright 编写端到端测试的完整实践
Mesop 应用测试指南:用 Playwright 编写端到端测试的完整实践 导读 Mesop 是一个用 Python 快速构建 AI 应用的全栈 UI 框架,
前端后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考