Skip to content

Commit 495fdac

Browse files
committed
feat(e2e-templates): run real Expo iOS e2e (prebuild + run:ios + maestro)
- runner: run mobile e2e without CI=1 so `expo run:ios` exits instead of attaching to Metro; add an explicit `expo prebuild` before the build; leave pod install to expo's own run:ios - e2e-templates: show the real e2e action for expo in the --list preview - expo sample: add run:ios/run:android (--no-bundler) + e2e:ios/e2e:android and a maestro.yaml asserting the C++ matrix result
1 parent 0858e10 commit 495fdac

4 files changed

Lines changed: 46 additions & 18 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
appId: com.anonymous.cppjssamplemobilereactnativeexpo
2+
---
3+
- launchApp
4+
- assertVisible: 'Matrix multiplier with c++.*=>.*J₃ \* \(2\*J₃\) = 6\*J₃'

cppjs-samples/cppjs-sample-mobile-reactnative-expo/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
"android": "expo run:android",
1414
"ios": "expo run:ios",
1515
"web": "expo start --web",
16-
"lint": "expo lint"
16+
"lint": "expo lint",
17+
"run:ios": "expo run:ios --no-bundler --configuration Release",
18+
"run:android": "expo run:android --no-bundler --configuration Release",
19+
"maestro": "maestro test ./maestro.yaml",
20+
"e2e:ios": "npm run run:ios && npm run maestro",
21+
"e2e:android": "npm run run:android && npm run maestro"
1722
},
1823
"dependencies": {
1924
"@cpp.js/core-embind-jsi": "2.0.0-beta.17",

scripts/e2e-templates.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ function printTable(headers, rows) {
134134
// Human-readable preview of what would run for an item, given host caps.
135135
function previewAction(item, caps, flags) {
136136
if (item.klass === 'mobile') {
137-
if (item.key === 'mobile-reactnative-expo') return 'install + expo prebuild';
138137
const e2e = pickE2e({ 'e2e:ios': 1, 'e2e:android': 1 }, 'mobile', caps);
139-
return e2e ? `e2e (${e2e.script})` : 'SKIP e2e (no device/maestro)';
138+
if (e2e) return `e2e (${e2e.script})`;
139+
return item.key === 'mobile-reactnative-expo' ? 'install + expo prebuild (no device)' : 'SKIP e2e (no device/maestro)';
140140
}
141141
const buildMiss = item.buildCaps.length ? missingCaps(item.buildCaps, caps) : [];
142142
if (buildMiss.length && (item.klass === 'web' || item.klass === 'cloud')) {

scripts/e2e-templates/runner.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* - web/cloud e2e runs `e2e:prod` (falls back to `e2e:dev`) after installing
77
* Playwright browsers
88
* - mobile picks `e2e:ios` (macOS + simulator) or `e2e:android` (attached
9-
* emulator), both gated on Maestro; Expo has no e2e flow, so it gets an
10-
* `expo prebuild` smoke instead
9+
* emulator), both gated on Maestro and run WITHOUT CI (so `expo run:ios`
10+
* exits instead of attaching to Metro); with no device/Maestro, Expo falls
11+
* back to an `expo prebuild` smoke
1112
* - backend / lib-source / lib-cmake have no e2e; their build (or just a clean
1213
* scaffold+install) is the assertion
1314
*
@@ -135,20 +136,19 @@ async function runTemplate(item, ctx) {
135136
}
136137
}
137138

138-
// Expo ships no e2e flow; a prebuild smoke proves the config plugin works.
139-
if (item.key === 'mobile-reactnative-expo') {
140-
if (flags.skipE2e) return done('pass', 'e2e skipped');
141-
const pre = await record('expo-prebuild', 'npx', ['--yes', 'expo', 'prebuild', '--no-install'], {
142-
cwd: projectDir,
143-
timeoutMs: TIMEOUTS.build,
144-
});
145-
return pre.ok ? done('pass') : done('fail', 'expo prebuild failed');
146-
}
147-
148139
if (flags.skipE2e) return done('pass', 'e2e skipped');
149140

150141
const e2e = pickE2e(scripts, item.klass, caps);
151142
if (!e2e) {
143+
// Expo with no attached device/Maestro: a prebuild smoke still proves the
144+
// config plugin generates the native project.
145+
if (item.key === 'mobile-reactnative-expo') {
146+
const pre = await record('expo-prebuild', 'npx', ['--yes', 'expo', 'prebuild', '--no-install'], {
147+
cwd: projectDir,
148+
timeoutMs: TIMEOUTS.build,
149+
});
150+
return pre.ok ? done('pass', 'prebuild smoke (no device for full e2e)') : done('fail', 'expo prebuild failed');
151+
}
152152
const reason = scripts.build ? undefined : 'no build/e2e scripts (scaffold+install only)';
153153
return done('pass', reason);
154154
}
@@ -169,16 +169,35 @@ async function runTemplate(item, ctx) {
169169
await record('playwright-install', 'npx', pwArgs, { cwd: projectDir, timeoutMs: TIMEOUTS.install });
170170
}
171171

172-
// Mirrors the iOS CI: a freshly scaffolded RN project has no Pods/ until `pod install` runs.
173-
if (e2e.script === 'e2e:ios' && fs.existsSync(path.join(projectDir, 'ios', 'Podfile'))) {
172+
// Expo ships no native project (nativeFolders:false). `expo run:ios` would
173+
// prebuild implicitly on first launch; do it explicitly up front so the native
174+
// project is guaranteed to exist before the build.
175+
if (item.key === 'mobile-reactnative-expo') {
176+
const platform = e2e.script === 'e2e:android' ? 'android' : 'ios';
177+
const pre = await record('expo-prebuild', 'npx', ['--yes', 'expo', 'prebuild', '-p', platform, '--no-install'], {
178+
cwd: projectDir,
179+
timeoutMs: TIMEOUTS.build,
180+
});
181+
if (!pre.ok) return done('fail', 'expo prebuild failed');
182+
}
183+
184+
// Mirrors the iOS CI: a freshly scaffolded RN CLI project has no Pods/ until
185+
// `pod install` runs. Expo is excluded — its own `run:ios` installs pods the
186+
// expo way (a plain `pod install` here would diverge from that proven path).
187+
if (e2e.script === 'e2e:ios' && item.key !== 'mobile-reactnative-expo' && fs.existsSync(path.join(projectDir, 'ios', 'Podfile'))) {
174188
const pod = await record('pod-install', 'pod', ['install'], { cwd: path.join(projectDir, 'ios'), timeoutMs: TIMEOUTS.install });
175189
if (!pod.ok) return done('fail', 'pod install failed');
176190
}
177191

192+
// CI=1 makes Playwright run headless for web/cloud. It is harmful for mobile:
193+
// under CI, `expo run:ios` starts Metro in CI mode and attaches (never exits),
194+
// so the `run:ios && maestro` chain hangs. Run mobile e2e without CI — exactly
195+
// how a developer runs `npm run e2e:ios` locally.
196+
const e2eEnv = item.klass === 'web' || item.klass === 'cloud' ? { CI: '1' } : undefined;
178197
const e2eRun = await record(`e2e:${e2e.script.replace('e2e:', '')}`, pm, ['run', e2e.script], {
179198
cwd: projectDir,
180199
timeoutMs: TIMEOUTS.e2e,
181-
env: { CI: '1' },
200+
env: e2eEnv,
182201
});
183202
if (!e2eRun.ok) return done('fail', `${e2e.script} failed`);
184203

0 commit comments

Comments
 (0)