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