React Native + Detox:iOS 模拟器环境配置
AI 速览
这篇文章记录了在 iOS 模拟器上运行 Detox E2E 测试时遇到的各种”坑”及其解决方案。其中最有价值的是对问题 5(Metro 未知目标设备)的排查过程:从日志中看到 Metro 报告 “Unexpected request from unknown page”,最终发现是 iOS 模拟器使用了与 Android 不同的 Deep Link 处理机制。解决方案是采用两阶段启动策略——先用 Detox 启动 App 建立原生层连接,再通过 Deep Link 加载 JS Bundle。文章还提供了一份实用的快速排查检查清单盖了构建、网络、连接、日志等常见问题
系列文章导航
- 架构设计与 P00 页面巡检实践 ← 推荐先读
- iOS 模拟器环境配置(本文)
- Android AOSP 模拟器配置
- Android 模拟器运行测试排查
- 基于 CDP 的信号同步机制
- Bridge 层代码注入实践
- 页面巡检的健康检查与视觉监控
环境与目标
| 组件 | 版本 |
| iOS Simulator | iPhone 16 (iOS 18) |
| Detox | 20.42.0 |
| Expo SDK | ~51.0.39 |
| expo-dev-client | ~4.0.26 |
目标本地 iOS 模拟器上成功运行 E2E 测试。
测试策略
- 本地开发:iOS 模拟器(开发体验好,调试方便)
- 云端 CI:BrowserStack Android(成本考量)
问题 1:applesimutils 未安装
现象
Command failed: applesimutils --version
/bin/sh: applesimutils: command not found原因 applesimutils` 是 Detox 在 iOS 上的必需依赖,用于与模拟器交互(安装 App、重置状态、权限管理等)。
解决
brew tap wix/brew
brew install applesimutils问题 2:binaryPath 路径错误
现象
CommandError: No development build (com.moement.moego.business) for this project is installed.排查过程
- 误以为
expo start会安装 App,原来只是启动 Metro bundler - Detox 测试命令才会自动安装 App
detox test - 最终发现是
.detoxrc.js中的binaryPath路径配置错误
解决正 .detoxrc.js 中的路径:
// 错误
"binaryPath": "ios/build/Products/Debug-iphonesimulator/business.app"
// 正确(注意多了一层 Build 目录)
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/business.app"问题 3:Metro 服务器未启动
现象
Error: connect ECONNREFUSED 127.0.0.1:8081原因 etox 的 CDP bridge 需要连接 Metro Dev Server(端口 8081),但 Metro 未启动。
解决要两个终端协作,一个负责 expo start ,一个负责 detox test。
# 终端 1:启动 Metro(E2E 模式)
pnpm start:e2e
# EXPO_PUBLIC_IS_E2E=true BRANCH_NAME=$(git branch --show-current) CI_ACTION=LOCALLY_DEV expo start
# 注意:不要按 i,那是安装 Expo Go,不是 E2E 包
# 终端 2:运行测试
# detox build --configuration ios.sim.debug
pnpm test:run-e2e-ios
# Detox 会自动安装构建好的 App问题 4:ADB 命令在 iOS 环境报错
现象
Command failed: adb shell settings put global window_animation_scale 0
/bin/sh: adb: command not found原因码中有 Android 专用的 ADB 命令,在 iOS 环境下执行会失败。
解决加平台判断和错误捕获:
if (device.getPlatform() === 'android') {
for (const command of adbCommands) {
try {
execSync(command, { stdio: 'ignore' });
} catch {
// 防止 ADB 失败导致整个测试崩溃
}
}
}问题 5:Expo Dev Client 启动时序问题(核心问题)
这是整个排查过程中最复杂的问题,花费了大量时间定位。
现象
ReferenceError: Property 'login' doesn't exist登录用例跑不起来。
排查过程(走了不少弯路)
第一阶段:误判方向
最初以为是 window.login 注入失败。检查了 e2e/bridge/inject.ts:
(window as any).login = async (email: string, password: string) => {
await AsyncStorage.setItem(LK_MOE_STRIPE_CONFIRM_LAUNCH_FLAG, '1');
await store.dispatch(login({ byEmailPassword: { email, password } }));
// ...
};检查 index.js 的条件加载逻辑:
if (process.env.EXPO_PUBLIC_IS_E2E) {
import('./e2e/bridge/inject').then(init);
} else {
init();
}误以为是 EXPO_PUBLIC_IS_E2E 环境变量未正确传递,导致注入缺失。
尝试删除 if 语句强制加载 inject.ts,问题依旧。问了同事 Rex,说和这个 inject 文件没关系。开始排查其他方向。
第二阶段:发现真正的异常
多次运行 detox test --configuration ios.sim.debug,发现一个关键现象:
每次 App 启动后,Metro 终端没有任何 Bundle 日志,App 会停留在 Expo Dev Client 的 “Development servers” 页面。
观察 Dev Client 页面显示:
http://localhost:8081(绿点,表示可连接)- “Fetch development servers”
- “Enter URL manually”
手动点击 http://localhost:8081,Metro 终端开始构建,模拟器顶部显示 “Building…”,一段时间后进入应用页面。
结论 ev Client 能检测到 Metro server,但不会自动连接。
第三阶段:尝试 Deep Link 自动连接
参考 Detox 文档 Mocking Open With URL (Deep Links),尝试通过 launchApp({ url }) 自动完成连接:
// 尝试 1
await device.launchApp({ url: 'exp://localhost:8081' });
// 尝试 2
await device.launchApp({ url: 'http://localhost:8081' });结果更糟:不传 URL 时还能进入 Development servers 页面,传 URL 后直接卡在 Splash 页面(启动 Logo 画面)。
第四阶段:寻找正确的 URL Scheme
通过 grep 搜索 Info.plist:
grep -r "CFBundleURLSchemes" ios --include="*.plist" -A 5在 ios/MoeGo2/Info.plist 中找到:
<key>CFBundleURLSchemes</key>
<array>
<string>exp+moego-business-2</string>
</array>结合 Expo 官方文档 Deep Linking to an Updates URL,Expo Dev Client 的 Scheme 格式为 exp+{slug},slug 来自 app.config.ts:
export default (_ctx: ConfigContext) => {
return {
slug: 'moego-business-2',
}
}构建完整的 Deep Link URL:
const scheme = 'exp+moego-business-2';
const metroUrl = encodeURIComponent('http://localhost:8081');
const deepLinkUrl = `${scheme}://expo-development-client/?url=${metroUrl}`;
// 结果: exp+moego-business-2://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081使用这个 URL 调用 launchApp,依旧卡在 Splash 页面,Metro 终端没有构建日志。
第五阶段:验证 Deep Link 本身是否可用
新开终端,手动执行:
xcrun simctl openurl booted "exp+moego-business-2://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081"结果:Metro 终端显示构建被触发,模拟器顶部显示 “Building…”,最终正常进入登录页。
结论 eep Link 功能正常,问题出在 Detox 的启动方式上。
第六阶段:定位根因
对比两种启动方式的行为差异:
| 方式 | 行为 | 结果 |
| launchApp({ url }) | XCUITest 启动 App 的瞬间发送 URL | 卡死 |
| xcrun simctl openurl | 系统级调用,等待 App 就绪后分发 | 正常 |
推测时序问题:


解决方案:两阶段启动策略
将”启动 App”和”发送 Deep Link”分离,预留初始化时间:
// ============================================
// 两阶段启动策略
// 解决 Expo Dev Client + Detox 的时序问题
// ============================================
// 阶段一:启动 App,不带 URL
// 让 Expo Dev Client 完成原生模块初始化
await device.launchApp({
newInstance: true,
launchArgs: {
EXPO_PUBLIC_IS_E2E: 'true',
},
});
// 等待 Expo Dev Client 初始化完成
log.info('Waiting for Expo Dev Client to initialize...');
await new Promise((resolve) => setTimeout(resolve, 3000));
// 阶段二:发送 Deep Link 触发 Metro 连接
const scheme = 'exp+moego-business-2';
const metroUrl = encodeURIComponent('http://localhost:8081');
const deepLinkUrl = `${scheme}://expo-development-client/?url=${metroUrl}`;
log.info('Sending Deep Link to trigger Metro connection...');
await device.openURL({ url: deepLinkUrl });
// 等待 JS Bundle 加载完成
await waitFor(element(by.text('Log in')))
.toBeVisible()
.withTimeout(60000);时序图

问题 6:跨平台 UI 选择器兼容
现象
Unknown class "com.facebook.react.views.textinput.ReactEditText"原因分析
- 类名不兼容代码使用了 Android 特有的
ReactEditText,iOS 应为RCTUITextField
React Native 跨平台原生组件映射
| 组件 | Android | iOS |
| TextInput | ReactEditText | RCTUITextField |
| Text | ReactTextView | RCTText |
| View | ReactViewGroup | RCTView |
| ScrollView | ReactScrollView | RCTScrollView |
解决方案
// ============================================
// 跨平台工具函数
// ============================================
/
* 跨平台 TextInput 选择器
* 暂时使用 type 选择器,后续应使用 testID 保证一致性
*/
const getTextInputSelector = () => {
return device.getPlatform() === 'ios'
? by.type('RCTUITextField')
: by.type('com.facebook.react.views.textinput.ReactEditText');
};问题 7:App busy / Run loop 同步问题
现象
测试到某个页面后卡住不动,终端重复报错:
The app is busy with the following tasks:
• Run loop "Main Run Loop" is awake.
• 2 enqueued JavaScript timers.原因
Detox 的同步机制会等待 App 进入”空闲”状态才继续执行。某些页面存在持续的动画、定时器或轮询,导致 Detox 认为 App 永远处于繁忙状态。
常见触发场景:
- 倒计时组件
- 无限循环动画(如 Loading spinner)
- 轮询请求(如实时数据刷新)
setInterval未清理
解决方案
方案 1:临时禁用同步试特定页面时)
await device.disableSynchronization();
// 执行不稳定页面的测试
await device.enableSynchronization();方案 2:代码层面优化
在 App 代码中,E2E 模式下跳过持续动画:
const shouldAnimate = !process.env.EXPO_PUBLIC_IS_E2E;
<Animated.View
style={shouldAnimate ? animatedStyle : staticStyle}
/>对于接口轮询,需要开发者配置接口通配符跳过等待,根据特定的端点(以 URL 正则表达式表示),排除与网络活动相关的同步(即,在测试执行过程中,无需等待网络空闲即可继续执行):
await device.setURLBlacklist(['.*127.0.0.1.*', '.*my.ignored.endpoint.*']);当前状态时注释掉卡顿的页面测试步骤,后续单独排查。

参考文档:
问题 8:系统权限弹窗干扰截图
现象
访问涉及地图组件的页面时,弹窗”允许 MoeGo 2 访问你的位置吗”,后续所有截图都会带上这个弹窗。

解决方案
Detox 提供了预授权机制,在 launchApp 时预设权限:
await device.launchApp({
newInstance: true,
permissions: {
location: 'inuse', // 允许使用时定位
notifications: 'YES', // 允许通知
camera: 'YES', // 允许相机
photos: 'YES', // 允许相册
},
launchArgs: {
EXPO_PUBLIC_IS_E2E: 'true',
},
});问题 9:CDP 连接 IP 地址不匹配
现象
getCurrentRoute failed: Error: get cdp client error:
Error: connect EHOSTUNREACH 192.168.0.236:8081 - Local (192.168.0.243:59446)原因分析
CDP (Chrome DevTools Protocol) 客户端连接流程:
1. CDP 客户端连接 localhost:8081 获取 /json 目标列表 ✅
2. Metro 返回的目标 WebSocket URL 包含 192.168.0.236:8081
3. CDP 尝试连接这个 IP → EHOSTUNREACH ❌问题在于:
- Metro 启动时检测到的网络 IP 是
192.168.0.236 - 运行测试时网络环境已变化(WiFi 切换、IP 重新分配)
- 实际 IP 变成了
192.168.0.243
解决方案
重启 Metro 服务器,让它重新检测当前网络 IP:
# 终端 1:重启 Metro
# Ctrl+C
pnpm start:e2e
# 终端 2:重新运行测试
pnpm test:run-e2e-ios快速排查检查清单
再次遇到问题时,按以下顺序排查:
| 检查项 | 命令/位置 |
| applesimutils 是否安装 | applesimutils --version |
| binaryPath 是否正确 | .detoxrc.js → ios/build/Build/Products/... |
| Metro 是否运行 | lsof -i :8081 |
| URL Scheme 是否正确 | grep CFBundleURLSchemes ios//Info.plist |
| 两阶段启动等待时间 | 初始化等待建议 3-5 秒 |
| 权限预授权 | launchApp({ permissions: {...} }) |
| IP 地址是否一致 | 重启 Metro 解决 |
总结
本次踩坑的核心收获:
- Expo Dev Client + Detox 的集成两阶段启动策略,这是一个比较隐蔽的时序问题
- 跨平台测试抽象选择器和交互方法,后续应全面使用
testID - CDP 通信正确的网络环境,IP 变化需要重启 Metro
- 同步机制刃剑,需要在稳定性和测试覆盖率之间权衡