Folo mobile e2e:用 Maestro 在 iOS 模拟器与 Android 模拟器上跑通认证流程的实战指南
本文围绕 Folo 仓库中的 Agent 技能文档 SKILL.md 展开,讲解如何在 apps/mobile 上运行基于 Maestro 的移动端端到端测试:从环境自检、模拟器/App 构建,到注册、登出、登录三条认证旅程的完整执行与结果判定。读完本文,你可以独立完成 iOS/Android 双平台的 E2E 认证验证,并能读懂运行器脚本 run-maestro.sh 与各 YAML 流程文件背后的实现细节。
技能文档定位与覆盖范围
mobile-e2e 是一份面向 Agent 的操作技能文档,其 frontmatter 明确了用途:
name: mobile-e2e
description: Run apps/mobile Maestro end-to-end tests in this repo. Use when an agent needs to validate mobile auth flows on iOS Simulator or Android Emulator. Current maintained coverage is register, sign out, and sign in.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
关键信息有三点:一是它只针对 apps/mobile 的 Maestro 测试;二是当前维护的覆盖范围是**注册(register)、登出(sign out)、登录(sign in)**三条认证旅程;三是 disable-model-invocation: true 表示该技能需要人工显式触发,而非由模型自动调用。
文档列出了五个“files that matter”,即整套 E2E 体系的核心组成:
| 角色 | 路径 |
|---|---|
| 运行器脚本 | apps/mobile/e2e/run-maestro.sh |
| iOS 认证流程 | apps/mobile/e2e/flows/ios/auth.yaml |
| Android 认证流程 | apps/mobile/e2e/flows/android/core.yaml |
| 共享认证流程 | apps/mobile/e2e/flows/shared/ 下的 *.yaml |
| 调试产物目录 | apps/mobile/e2e/artifacts/ |
第一步永远是环境自检
技能文档要求在仓库根目录执行:
cd apps/mobile
pnpm run e2e:doctor
pnpm run typecheck
e2e:doctor 的实际定义见 apps/mobile/package.json:
"e2e:doctor": "sh -c 'for f in e2e/flows/shared/*.yaml e2e/flows/android/*.yaml e2e/flows/ios/*.yaml; do maestro check-syntax $f; done'"
也就是说,doctor 命令会用 Maestro CLI 对 shared、android、ios 三个目录下的全部 YAML 流程做语法校验,能在真正启动模拟器之前拦住拼写错误、结构损坏的流程文件。pnpm run typecheck 则保证应用代码本身可编译。两条命令一过,才能谈得上跑设备级测试。
iOS:必须使用模拟器 .app 构建,而不是 Expo 开发客户端
SKILL.md 对 iOS 的第一条硬性要求是:Use a simulator .app build, not an Expo development client. 原因是 Expo 开发客户端依赖 Metro 服务与热更新通道,行为与最终产物不一致;E2E 要验证的是独立构建的 Folo.app 本体。
首选模拟器策略
当机器上有多台模拟器时,技能文档给出的选择策略是:优先最新的已安装 iOS 运行时 + 最新一代 iPhone 模拟器,即偏向“最新版 iOS 上的最新 iPhone 机型”。
启动模拟器
xcrun simctl boot <IOS_UDID>
xcrun simctl bootstatus <IOS_UDID> -b
open -a Simulator --args -CurrentDeviceUDID <IOS_UDID>
bootstatus <UDID> -b 会阻塞等待启动完成,保证后续安装 App 时模拟器已就绪。
App bundle 的三级解析顺序
运行器 run-maestro.sh 中 resolve_ios_app_path 函数(L43-L72)按以下顺序解析 .app 位置:
- 环境变量
MAESTRO_IOS_APP_PATH—— 可以是目录,也可以是一个.tar.gz压缩包;若是压缩包,脚本会解压到临时目录并find出其中的Folo.app(见extract_ios_app_from_tar,L35-L41); apps/mobile目录下最新的本地build-*.tar.gz(按文件名排序取最后一个);- 已在
~/Library/Developer/Xcode/DerivedData中存在的Release-iphonesimulator/Folo.app; - 以上都没有时,自动触发构建(见下节)。
README.md 还说明了这类 tar 包的典型来源:eas build --local --platform ios --profile e2e-ios-simulator 的本地构建产物。
Folo.app 不存在时如何构建
cd apps/mobile/ios
pod install
xcodebuild -workspace Folo.xcworkspace \
-scheme Folo \
-configuration Release \
-sdk iphonesimulator \
-destination 'id=<IOS_UDID>' \
build
预期产物路径为:
~/Library/Developer/Xcode/DerivedData/.../Build/Products/Release-iphonesimulator/Folo.app
对照运行器里的 build_ios_simulator_app 函数(run-maestro.sh),自动构建时还会额外传入:
PROFILE=e2e-ios-simulator \
EXPO_PUBLIC_E2E_ENV_PROFILE="${EXPO_PUBLIC_E2E_ENV_PROFILE:-}" \
EXPO_PUBLIC_E2E_LANGUAGE="${EXPO_PUBLIC_E2E_LANGUAGE:-en}" \
即默认以 e2e-ios-simulator 应用配置、英文语言打包,保证模拟器构建的形态与 E2E 预期一致;构建结束后同样用 find 在 DerivedData 中定位 Folo.app 返回给上层。
Apple Silicon 模拟器编译优化
在 Apple Silicon Mac 上、且只为当前这台机器的当前模拟器构建时,技能文档建议只编译活动架构:
xcodebuild ... \
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64
这一策略并非写在 SKILL.md 里的“口头约定”,而是运行器里的真实逻辑:append_ios_arch_args(run-maestro.sh)通过 uname -m 判断主机架构,仅在 arm64 时输出这两个参数,再追加到 xcodebuild 命令行末尾。文档同时划定了边界:该优化只适用于本机自测/模拟器构建;需要分发给其他机器的通用模拟器包、或运行在 Intel Mac 上时,都不能使用。
执行 iOS 认证流程
cd apps/mobile
MAESTRO_IOS_DEVICE_ID=<IOS_UDID> \
MAESTRO_IOS_APP_PATH=<PATH_TO_Folo.app> \
pnpm run e2e:ios
从源码看,e2e:ios 即 sh ./e2e/run-maestro.sh ios(package.json)。iOS 分支(run-maestro.sh)的实际执行序列是:
resolve_ios_device:优先取MAESTRO_IOS_DEVICE_ID,否则从xcrun simctl list devices booted中挑出第一个已启动的模拟器;- 解析并校验 App bundle,拿不到直接报错退出;
prepare_ios_simulator(L90-L115):shutdown → erase 清空数据 → 重启等待bootstatus -b→ 再次 shutdown,然后用PlistBuddy向模拟器数据目录中的三个配置 Profile plist 写入allowPasswordAutoFill = false,关闭系统密码自动填充弹窗(这类系统弹窗会干扰 UI 断言),最后再重启模拟器;xcrun simctl install安装.app并launch is.follow;- 以 JUnit 格式执行
maestro test ... e2e/flows/ios/auth.yaml,调试产物写入${MAESTRO_DEBUG_OUTPUT:-e2e/artifacts/ios}/auth。
Android:使用 release APK,而非 Expo 开发构建
Java 与 Android SDK 环境
技能文档要求使用 Android Studio 自带的 JBR:
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
以及 Android SDK 路径:
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
若 apps/mobile/android/local.properties 缺失,手动创建:
echo "sdk.dir=$HOME/Library/Android/sdk" > apps/mobile/android/local.properties
构建 release APK
如果本地还没有 apps/mobile/android 原生工程,需先用 Expo prebuild / run-android 工具生成。然后:
cd apps/mobile/android
./gradlew app:assembleRelease --console=plain
预期 APK 路径:
apps/mobile/android/app/build/outputs/apk/release/app-release.apk
安装并执行
先把 release APK 装到已启动的模拟器:
adb -s emulator-5554 install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
再运行认证流程:
cd apps/mobile
pnpm run e2e:android
从源码看,Android 分支(run-maestro.sh)在跑 Maestro 前还做了三件事:
wait_for_android_ready(L74-L88):最多轮询 90 次(每次 2 秒)等待sys.boot_completed = 1且包管理服务可用,随后再固定sleep 20秒让系统彻底静息——这是对冷启动后系统弹窗与动画延迟的工程化缓冲;adb shell pm clear is.follow:清空应用数据,保证从“全新安装”状态开始,避免旧登录态污染用例;adb shell monkey -p is.follow -c android.intent.category.LAUNCHER 1:拉起应用后再执行maestro test ... e2e/flows/android/core.yaml,同样输出 JUnit 报告并落盘到e2e/artifacts/android。
认证流程结构:register → sign out → sign in
流程编排
iOS 的入口 flows/ios/auth.yaml 非常简洁:
appId: is.follow
name: iOS auth journey
---
- runFlow:
file: ./register.yaml
- runFlow:
file: ./sign-out.yaml
- runFlow:
file: ./login.yaml
Android 的入口 flows/android/core.yaml 额外处理了开发菜单的干扰:
- runFlow:
file: ./dismiss-dev-menu.yaml
- runFlow:
file: ./register.yaml
- runFlow:
file: ../shared/sign-out.yaml
- runFlow:
file: ./dismiss-dev-menu.yaml
- runFlow:
file: ./login.yaml
可以看出两条旅程共享同一套“注册 → 登出 → 登录”骨架,平台差异被隔离在平台目录的入口文件与 dismiss-dev-menu.yaml、dismiss-system-dialogs.yaml 这类清理型子流程里,主体逻辑收敛在 flows/shared/。
注册流程的 UI 细节
flows/shared/register.yaml 的完整交互链:
runFlow: open-auth.yaml打开认证页;- 点击
login-provider-credential(凭据登录方式); - 等待并点击
register-email-input,inputText: ${E2E_EMAIL}; - 点击
register-password-input,先eraseText再inputText: ${E2E_PASSWORD},确认密码框同理; pressKey: Enter提交;- 若出现
onboarding-next(新手引导),用runFlow + when条件块连续点击 4 次跳过引导; extendedWaitUntil等待timeline-view-articles出现(30 秒超时)——到达时间线即视为注册成功。
iOS 侧的 flows/ios/register.yaml 在此之上再包了一层前置断言:先等待并断言 login-screen 可见,再 runFlow: ../shared/register.yaml。
登出与登录的断言点
flows/shared/sign-out.yaml 的路径是:点击 tab-SettingsTabScreen → 等待 settings-general-link → scrollUntilVisible 滚出 settings-sign-out(方向 DOWN,15 秒超时)→ 点击 settings-sign-out → 点击文本 Sign Out 确认 → 等待 login-provider-credential 重新出现并断言可见。这一步对应 SKILL.md 中“sign-out reaches login-screen”的结果判定。
flows/shared/login.yaml 则是:点击 auth-toggle-mode 切到登录模式 → 点击 login-provider-credential → 填入 ${E2E_EMAIL} / ${E2E_PASSWORD} → 点击 login-submit → extendedWaitUntil notVisible: login-provider-credential(30 秒超时)→ assertNotVisible 收尾。登录页消失即证明会话已建立。
测试账号从哪里来
若不显式指定 E2E_EMAIL,运行器会在 run-maestro.sh 中生成一次性账号:
: "${E2E_PASSWORD:=Password123!}"
: "${E2E_EMAIL:=folo-e2e-${platform}-${run_suffix}@example.com}"
其中 run_suffix 由秒级时间戳加进程号组成,保证每次运行注册的都是全新账户,天然规避“账号已存在”的不稳定性。若希望复用固定账号(例如配合 e2e:ios:bootstrap 的生产态登录引导),则显式导出 E2E_EMAIL / E2E_PASSWORD 即可。完整的环境变量清单见 README.md:E2E_EMAIL、E2E_PASSWORD、MAESTRO_DEBUG_OUTPUT、MAESTRO_IOS_APP_PATH、EXPO_PUBLIC_E2E_ENV_PROFILE、EXPO_PUBLIC_E2E_LANGUAGE。
成功判定的三条标准
SKILL.md 对“认证验证通过”给出了明确定义:
- register 流程跑完(最终落在
timeline-view-articles时间线); - sign-out 后回到登录页(
login-provider-credential重新可见,iOS 侧对应login-screen); - login 流程使登录页消失(
login-provider-credential不再可见)。
三条全部成立,才算本次 E2E 认证验证成功。
调试与产物排查
每次运行后,检查对应平台的产物目录:
apps/mobile/e2e/artifacts/ios/
apps/mobile/e2e/artifacts/android/
由于 Maestro 以 --format junit 输出,目录里会包含可解析的 JUnit XML 报告与 Maestro 自带的截图/日志材料。--debug-output 的落盘目录由 MAESTRO_DEBUG_OUTPUT 控制,默认值即 e2e/artifacts/${platform}(run-maestro.sh)。
SKILL.md 最后给出的技巧是:对单个流程做聚焦调试时,直接调用 Maestro 对单个 YAML 执行。以仓库内真实路径为例,命令形态为:
cd apps/mobile
maestro test --format junit --platform ios --device <IOS_UDID> \
--debug-output e2e/artifacts/ios/debug \
-e E2E_EMAIL=you@example.com -e E2E_PASSWORD='Password123!' \
e2e/flows/ios/login.yaml
这样只需几秒到几十秒即可验证某一步 UI 交互,无需重跑整条旅程。
小结
这套移动端 E2E 体系的设计思路清晰可循:用 run-maestro.sh 作为单一入口封装设备解析、环境重置(iOS 清数据并关自动填充、Android 清应用数据)、构建兜底(缺 .app 时自动 xcodebuild)等不稳定因素;用 flows/shared/ 沉淀跨平台一致的认证交互,用平台目录吸收模拟器开发菜单、系统弹窗等平台噪声;用 e2e:doctor 的语法自检把错误拦在设备启动之前。对照 SKILL.md 的操作步骤与上文源码级细节,可以稳定复现并排查 Folo 移动端“注册—登出—登录”全链路的端到端验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00