test(e2e): make all contentious e2e resources configurable#9099
test(e2e): make all contentious e2e resources configurable#9099mikehardy wants to merge 1 commit into
Conversation
The following environment variables override serial e2e defaults when set
(before Metro build and :test-cover); unset vars keep legacy behaviour:
- RCT_METRO_PORT, RNFB_METRO_PORT, RNFB_{ANDROID,IOS,MACOS}_METRO_PORT
- JET_REMOTE_PORT, JET_METRO_PORT, RNFB_{ANDROID,IOS,MACOS}_JET_PORT, RNFB_JET_CONTROL_PORT
- RNFB_{ANDROID,IOS,MACOS}_EMULATOR_{FIRESTORE,AUTH,DATABASE,FUNCTIONS,STORAGE,HUB,LOGGING}_PORT
- RNFB_DETOX_ANDROID_CONFIG, RNFB_DETOX_IOS_CONFIG
- RNFB_E2E_PLATFORM, RNFB_E2E_SLOT, RNFB_E2E_DEBUG
- RNFB_ANDROID_AVD, RNFB_IOS_SIMULATOR, RNFB_ANDROID_EMULATOR_BOOT_ARGS
- ORG_GRADLE_PROJECT_reactNativeDevServerPort, SIMCTL_CHILD_RCT_METRO_PORT
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enables parallel E2E testing on a single host by making infrastructure resources—such as Metro bundler ports, Jet WebSocket ports, and Firebase emulator suites—configurable via environment variables. By allowing each test run to operate within a distinct 'slot' with its own ports and device instances, the changes eliminate resource contention while maintaining backward compatibility with existing serial test workflows. Highlights
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a configurable e2e environment to support parallel test runs on a single host using slotted ports and dedicated device clones. It includes scripts for cloning simulators and emulators, and updates configurations, test suites, and native AppDelegates to resolve ports dynamically. The review feedback identifies a critical issue where a dynamic require of an e2e helper in the production AI package will cause consumer build failures. Other feedback addresses a version sorting bug in simulator creation, fragile sed usage, a port-reversing limitation in Detox configurations, and inconsistent Detox global references.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let fnHost = '127.0.0.1'; | ||
| let fnPort = 5001; | ||
| try { | ||
| const { getE2eEmulatorHost, getE2eEmulatorPort } = require('@react-native-firebase/app/e2e/helpers'); | ||
| fnHost = getE2eEmulatorHost(); | ||
| fnPort = getE2eEmulatorPort('functions'); | ||
| } catch (_e) { | ||
| const isAndroid = Platform.OS === 'android'; | ||
| fnHost = isAndroid ? '10.0.2.2' : '127.0.0.1'; | ||
| } |
There was a problem hiding this comment.
Dynamic require statements with static string literals like require('@react-native-firebase/app/e2e/helpers') are statically analyzed by Metro bundler at build time. Since the e2e directory is typically excluded from published npm packages, this will cause production builds of consumer applications to fail with a module resolution error, even though it is wrapped in a try/catch block.
Instead of requiring the test helper, you can directly resolve the host and port using Platform.OS and the environment variables inlined by Babel during test runs.
const isAndroid = Platform.OS === 'android';
let fnHost = isAndroid ? '10.0.2.2' : '127.0.0.1';
let fnPort = 5001;
const platformKey = isAndroid ? 'ANDROID' : (Platform.OS as string) === 'macos' ? 'MACOS' : 'IOS';
const envPort = process.env[`RNFB_${platformKey}_EMULATOR_FUNCTIONS_PORT`];
if (envPort) {
fnPort = parseInt(envPort, 10);
}| function androidReversePortsFromEnv() { | ||
| const firestore = process.env.RNFB_ANDROID_EMULATOR_FIRESTORE_PORT; | ||
| if (!firestore) { | ||
| return SERIAL_ANDROID_REVERSE; | ||
| } | ||
| return [ | ||
| intEnv('RNFB_ANDROID_EMULATOR_FIRESTORE_PORT', 8080), | ||
| intEnv('RNFB_ANDROID_METRO_PORT', intEnv('RCT_METRO_PORT', 8081)), | ||
| intEnv('RNFB_ANDROID_JET_PORT', intEnv('JET_REMOTE_PORT', 8090)), | ||
| intEnv('RNFB_ANDROID_EMULATOR_DATABASE_PORT', 9000), | ||
| intEnv('RNFB_ANDROID_EMULATOR_AUTH_PORT', 9099), | ||
| intEnv('RNFB_ANDROID_EMULATOR_STORAGE_PORT', 9199), | ||
| intEnv('RNFB_ANDROID_EMULATOR_FUNCTIONS_PORT', 5001), | ||
| ]; | ||
| } |
There was a problem hiding this comment.
The guard if (!firestore) prevents any custom port configuration (such as RNFB_ANDROID_METRO_PORT or RNFB_ANDROID_JET_PORT) from being reversed unless RNFB_ANDROID_EMULATOR_FIRESTORE_PORT is also explicitly set.
Since intEnv already falls back to the default ports (matching SERIAL_ANDROID_REVERSE exactly) when the environment variables are unset, we can safely remove this guard to support partial environment overrides.
function androidReversePortsFromEnv() {
return [
intEnv('RNFB_ANDROID_EMULATOR_FIRESTORE_PORT', 8080),
intEnv('RNFB_ANDROID_METRO_PORT', intEnv('RCT_METRO_PORT', 8081)),
intEnv('RNFB_ANDROID_JET_PORT', intEnv('JET_REMOTE_PORT', 8090)),
intEnv('RNFB_ANDROID_EMULATOR_DATABASE_PORT', 9000),
intEnv('RNFB_ANDROID_EMULATOR_AUTH_PORT', 9099),
intEnv('RNFB_ANDROID_EMULATOR_STORAGE_PORT', 9199),
intEnv('RNFB_ANDROID_EMULATOR_FUNCTIONS_PORT', 5001),
];
}| sed -i.bak "s/${src}/${dst}/g" "$dst_ini" "${dst_avd}/config.ini" 2>/dev/null || \ | ||
| sed -i '' "s/${src}/${dst}/g" "$dst_ini" "${dst_avd}/config.ini" | ||
| rm -f "${dst_ini}.bak" |
There was a problem hiding this comment.
Using sed -i with fallback logic is fragile and leaves behind unused backup files (such as ${dst_avd}/config.ini.bak which is not cleaned up by rm -f "${dst_ini}.bak").
Using perl -pi -e is highly portable across both macOS and Linux, does not require platform-specific conditional branches, and modifies files in-place without creating backup files.
| sed -i.bak "s/${src}/${dst}/g" "$dst_ini" "${dst_avd}/config.ini" 2>/dev/null || \ | |
| sed -i '' "s/${src}/${dst}/g" "$dst_ini" "${dst_avd}/config.ini" | |
| rm -f "${dst_ini}.bak" | |
| perl -pi -e "s/\Q$src\E/$dst/g" "$dst_ini" "${dst_avd}/config.ini" |
| RUNTIME=$(xcrun simctl list runtimes available -j | node -e " | ||
| const j=JSON.parse(require('fs').readFileSync(0,'utf8')); | ||
| const ios=j.runtimes.filter(r=>r.isAvailable&&r.platform==='iOS').sort((a,b)=>b.version.localeCompare(a.version)); | ||
| if(!ios.length) process.exit(1); | ||
| console.log(ios[0].identifier); | ||
| ") |
There was a problem hiding this comment.
Sorting version strings alphabetically using localeCompare can fail when multi-digit minor or patch versions are present (e.g., treating 17.2 as newer than 17.10 because '2' > '1').
To ensure robust sorting, we should split the version strings and compare their components numerically.
| RUNTIME=$(xcrun simctl list runtimes available -j | node -e " | |
| const j=JSON.parse(require('fs').readFileSync(0,'utf8')); | |
| const ios=j.runtimes.filter(r=>r.isAvailable&&r.platform==='iOS').sort((a,b)=>b.version.localeCompare(a.version)); | |
| if(!ios.length) process.exit(1); | |
| console.log(ios[0].identifier); | |
| ") | |
| RUNTIME=$(xcrun simctl list runtimes available -j | node -e " | |
| const j=JSON.parse(require('fs').readFileSync(0,'utf8')); | |
| const ios=j.runtimes.filter(r=>r.isAvailable&&r.platform==='iOS').sort((a,b)=>{ | |
| const av = a.version.split('.').map(Number); | |
| const bv = b.version.split('.').map(Number); | |
| for (let i = 0; i < Math.max(av.length, bv.length); i++) { | |
| const an = av[i] || 0; | |
| const bn = bv[i] || 0; | |
| if (an !== bn) return bn - an; | |
| } | |
| return 0; | |
| }); | |
| if(!ios.length) process.exit(1); | |
| console.log(ios[0].identifier); | |
| ") |
| function detoxPlatformKey() { | ||
| if (process.env.RNFB_E2E_PLATFORM) { | ||
| return process.env.RNFB_E2E_PLATFORM; | ||
| } | ||
| try { | ||
| if (typeof detox !== 'undefined' && detox?.device?.getPlatform) { | ||
| return detox.device.getPlatform(); | ||
| } | ||
| } catch (_e) { | ||
| // detox not ready | ||
| } | ||
| return 'android'; | ||
| } |
There was a problem hiding this comment.
In Detox, device is the standard globally exposed object. Checking device.getPlatform() directly is more reliable and consistent with your own pattern on line 366 where device is checked directly.
| function detoxPlatformKey() { | |
| if (process.env.RNFB_E2E_PLATFORM) { | |
| return process.env.RNFB_E2E_PLATFORM; | |
| } | |
| try { | |
| if (typeof detox !== 'undefined' && detox?.device?.getPlatform) { | |
| return detox.device.getPlatform(); | |
| } | |
| } catch (_e) { | |
| // detox not ready | |
| } | |
| return 'android'; | |
| } | |
| function detoxPlatformKey() { | |
| if (process.env.RNFB_E2E_PLATFORM) { | |
| return process.env.RNFB_E2E_PLATFORM; | |
| } | |
| try { | |
| if (typeof device !== 'undefined' && device?.getPlatform) { | |
| return device.getPlatform(); | |
| } | |
| } catch (_e) { | |
| // device not ready | |
| } | |
| return 'android'; | |
| } |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9099 +/- ##
============================================
- Coverage 58.61% 58.19% -0.42%
Complexity 1616 1616
============================================
Files 503 415 -88
Lines 39178 26818 -12360
Branches 5807 4876 -931
============================================
- Hits 22962 15605 -7357
+ Misses 14811 10192 -4619
+ Partials 1405 1021 -384
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Summary
Make RNFB e2e ports, devices, and emulator suites configurable via environment variables so multiple platform runs can coexist on one host — as many iOS and Android
:test-coverjobs as you provision slots for, plus one macOS, all at once. Default behaviour is unchanged: unset env vars preserve existing serialyarn tests:*commands, ports, and workflows exactly.packages/app/e2e/helpers.js— shared Metro, Jet, and Firebase emulator host/port resolution with serial fallbacks.tests/.detoxrc.js— multi-slot Android AVD / iOS simulator devices and env-drivenreversePorts/ native Metro build prefix.tests/.jetrc.js— per-platform Jet and Metro ports from env for macOS (and host orchestration).tests/e2e/firebase.test.js— Jet spawn/orchestration and macOS stale-app guard respect slotted ports from env.tests/.babelrc— inline slotted port env vars into app bundles at build time.auth,database,firestore,functions,ai) — route emulator URLs through shared helpers.AppDelegate.mm(iOS/macOS) — honourRCT_METRO_PORTfrom the environment.package.json—RNFB_DETOX_ANDROID_CONFIG/RNFB_DETOX_IOS_CONFIGon Detox build/test scripts; generic AVD/sim setup scripts.firebase.emulator.template.json+start-emulator-slotted.sh— generate and start a per-platform Firebase emulator suite fromRNFB_*_EMULATOR_*env.create-android-avds.sh/create-ios-simulators.sh— provision cloned AVDs and named simulators for slots 1–4.okf-bundle/testing/running-e2e.md— documents the configurable env var surface.A future orchestration layer (outside this PR) will use these hooks to reserve slots and export env on demand — including parallel e2e on a single host — while RNFB itself stays coordinator-agnostic.
Test plan
yarn tests:macos:test-cover(and one native platform) unchanged vsmainRNFB_ANDROID_METRO_PORT/RNFB_ANDROID_JET_PORT/ emulator ports for slot 1 → Android build +:test-coverreaches emulators on non-default portsrunning-e2e.mdconfigurable section