Skip to content

test(e2e): make all contentious e2e resources configurable#9099

Draft
mikehardy wants to merge 1 commit into
mainfrom
e2e-parallel
Draft

test(e2e): make all contentious e2e resources configurable#9099
mikehardy wants to merge 1 commit into
mainfrom
e2e-parallel

Conversation

@mikehardy

Copy link
Copy Markdown
Collaborator

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-cover jobs as you provision slots for, plus one macOS, all at once. Default behaviour is unchanged: unset env vars preserve existing serial yarn 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-driven reversePorts / 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.
  • Package e2e helpers (auth, database, firestore, functions, ai) — route emulator URLs through shared helpers.
  • AppDelegate.mm (iOS/macOS) — honour RCT_METRO_PORT from the environment.
  • package.jsonRNFB_DETOX_ANDROID_CONFIG / RNFB_DETOX_IOS_CONFIG on 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 from RNFB_*_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

  • Serial sanity: no env overrides → yarn tests:macos:test-cover (and one native platform) unchanged vs main
  • Slotted sanity: export RNFB_ANDROID_METRO_PORT / RNFB_ANDROID_JET_PORT / emulator ports for slot 1 → Android build + :test-cover reaches emulators on non-default ports
  • Docs: env var list in commit message matches running-e2e.md configurable section

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
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Configurable E2E Infrastructure: Introduced environment variable support for Metro, Jet, and Firebase emulator ports, allowing multiple E2E test runs to coexist on a single host.
  • Orchestration Scripts: Added helper scripts to provision cloned Android AVDs and named iOS simulators, facilitating parallel testing slots.
  • Dynamic Port Resolution: Updated application helpers and test configurations to dynamically resolve host and port settings based on the environment, replacing hardcoded defaults.
  • Native Build Integration: Updated iOS and macOS AppDelegate files to respect the RCT_METRO_PORT environment variable, ensuring native builds connect to the correct bundler instance.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/scripts/firebase.emulator.template.json
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +61 to +70
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';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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);
      }

Comment thread tests/.detoxrc.js
Comment on lines +13 to +27
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),
];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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),
  ];
}

Comment on lines +38 to +40
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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"

Comment on lines +7 to +12
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);
")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);
")

Comment on lines +41 to +53
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';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 58.19%. Comparing base (3933b45) to head (ffbcf2e).
⚠️ Report is 2 commits behind head on main.

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     
Flag Coverage Δ
android-native 53.56% <ø> (ø)
e2e-ts-android 49.95% <80.00%> (-0.02%) ⬇️
e2e-ts-ios ?
e2e-ts-macos 41.52% <80.00%> (+0.05%) ⬆️
ios-native ?
jest 49.55% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant