diff --git a/.github/workflows/scripts/firebase.emulator.template.json b/.github/workflows/scripts/firebase.emulator.template.json
new file mode 100644
index 0000000000..613f219da5
--- /dev/null
+++ b/.github/workflows/scripts/firebase.emulator.template.json
@@ -0,0 +1,26 @@
+{
+ "name": "react-native-firebase-testing",
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "functions": {
+ "predeploy": ["cd functions && yarn", "cd functions && yarn --cwd \"$RESOURCE_DIR\" build"],
+ "source": "functions",
+ "ignore": [".yarn", "yarn.lock", "*.log", "node_modules"]
+ },
+ "database": {
+ "rules": "database.rules"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ },
+ "emulators": {
+ "auth": { "port": 9099 },
+ "database": { "port": 9000 },
+ "firestore": { "port": 8080 },
+ "functions": { "port": 5001 },
+ "storage": { "port": 9199 },
+ "ui": { "enabled": false }
+ }
+}
diff --git a/.gitignore b/.gitignore
index 074c95edb1..91411f14bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -548,6 +548,8 @@ google-services.json
GoogleService-Info.plist
tests/harness.overrides.js
+tests/e2e-slots.env.json
+.github/workflows/scripts/.e2e-emulator-*.json
# RNFB TurboModule codegen — misplaced iOS provider stubs under Android output paths
# (NewArch-AD-5: okf-bundle/new-architecture/architecture-decisions.md — commit generated
diff --git a/okf-bundle/testing/running-e2e.md b/okf-bundle/testing/running-e2e.md
index 5f4ca2a84d..f8847bcbe4 100644
--- a/okf-bundle/testing/running-e2e.md
+++ b/okf-bundle/testing/running-e2e.md
@@ -58,7 +58,7 @@ yarn tests:macos:test-cover
5. **Report locations** — [Coverage design](coverage-design.md).
-6. **One e2e at a time** — never overlap `:test-cover` runs on one host. All platforms share Metro `:8081` and the test-runner WebSocket port (default **8090**); parallel runs race on coverage/device/emulator state. Every run starts after [clean pre-flight](#pre-flight-is-the-host-clear-to-start). Log triage for port/orchestration markers: [test-runner host orchestration](#test-runner-host-orchestration-log-triage-only).
+6. **One e2e at a time (default)** — never overlap `:test-cover` runs on one host unless each run uses a distinct port/device slot via [configurable e2e environment](#configurable-e2e-environment). Serial runs share Metro `:8081` and the test-runner WebSocket port (default **8090**); parallel runs race on coverage/device/emulator state without slotted env. Every run starts after [clean pre-flight](#pre-flight-is-the-host-clear-to-start). Log triage for port/orchestration markers: [test-runner host orchestration](#test-runner-host-orchestration-log-triage-only).
7. **No source edits during e2e** — wait/cancel cleanly before editing `packages/**`, `tests/**`, or bundle-affecting OKF docs. Saves can hot reload/rebundle and invalidate tests/coverage.
@@ -502,6 +502,29 @@ See also: [unit-focused-tier loop](#unit-focused-tier-iteration-loop), [dispatch
- **adb empty** — `adb kill-server && adb start-server && adb devices`
- **Stale processes** — one Metro (`:8081`), one emulator set (`:8080`, `:9099`, `:9000`, `:4400`, …). Stray listener on `:8090` after a run → [pre-flight recovery](#pre-flight-recovery), then restart background services with [Rules §1–2](#rules) (`yarn tests:packager:jet`, `yarn tests:emulator:start`).
+
+
+### Configurable e2e environment
+
+Serial e2e uses committed defaults (Metro `:8081`, Jet `:8090` / `:8190` / `:8290`, emulators `:8080` / `:9099` / …). To run multiple platform jobs on one host — or to pin a non-default slot — export env vars **before** Metro/native build and `:test-cover`. Unset vars keep legacy serial behaviour.
+
+| Variable | Purpose |
+|----------|---------|
+| `RCT_METRO_PORT`, `RNFB_METRO_PORT` | Metro bundler port (global fallback) |
+| `RNFB_{ANDROID,IOS,MACOS}_METRO_PORT` | Per-platform Metro port |
+| `JET_REMOTE_PORT`, `JET_METRO_PORT` | Test-runner WebSocket + Metro hint (global) |
+| `RNFB_{ANDROID,IOS,MACOS}_JET_PORT` | Per-platform Jet WebSocket port |
+| `RNFB_JET_CONTROL_PORT` | Jet HTTP control plane (default `JET_REMOTE_PORT + 1`) |
+| `RNFB_{ANDROID,IOS,MACOS}_EMULATOR_{FIRESTORE,AUTH,DATABASE,FUNCTIONS,STORAGE,HUB,LOGGING}_PORT` | Per-platform Firebase emulator suite |
+| `RNFB_DETOX_ANDROID_CONFIG`, `RNFB_DETOX_IOS_CONFIG` | Detox configuration name (e.g. `android.emu.debug.slot1`, `ios.sim.debug.slot1`) |
+| `RNFB_E2E_PLATFORM`, `RNFB_E2E_SLOT` | Active platform label and slot index for orchestration scripts |
+| `RNFB_ANDROID_AVD`, `RNFB_IOS_SIMULATOR`, `RNFB_ANDROID_EMULATOR_BOOT_ARGS` | Device selection overrides |
+| `ORG_GRADLE_PROJECT_reactNativeDevServerPort` | Android Gradle Metro port (set to match `RNFB_ANDROID_METRO_PORT`) |
+| `SIMCTL_CHILD_RCT_METRO_PORT` | iOS simulator child Metro port |
+| `RNFB_E2E_DEBUG` | Verbose `[rnfb-e2e]` port resolution logging in app helpers |
+
+Helper scripts (not canonical `:test-cover` entrypoints): `scripts/e2e/start-emulator-slotted.sh`, `yarn tests:e2e:setup-android-avds`, `yarn tests:e2e:setup-ios-sims`.
+
### Android emulator gray screen / Quick Boot (blocking)
Detox's default emulator launch **restores the AVD Quick Boot snapshot** unless told otherwise. On `TestingAVD` that can leave the device **`offline` on a gray screen** — `adb devices` shows `emulator-XXXX offline` and Detox hangs on `wait-for-device`.
diff --git a/package.json b/package.json
index a33b152201..1ce750dc93 100644
--- a/package.json
+++ b/package.json
@@ -41,19 +41,19 @@
"tests:emulator:start:windows": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.bat --no-daemon",
"tests:emulator:start-ci": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
"tests:android:build": "run-script-os",
- "tests:android:build:default": "cd tests && yarn detox build --configuration android.emu.debug",
+ "tests:android:build:default": "bash -c 'cd tests && yarn detox build --configuration \"${RNFB_DETOX_ANDROID_CONFIG:-android.emu.debug}\"'",
"tests:android:build:windows": "cd tests && yarn detox build --configuration android.emu.debug.windows",
"tests:android:build:release": "cd tests && yarn detox build --configuration android.emu.release",
"tests:android:manual": "cd tests && yarn react-native run-android",
- "tests:android:test": "cd tests && yarn detox test --configuration android.emu.debug",
+ "tests:android:test": "bash -c 'cd tests && yarn detox test --configuration \"${RNFB_DETOX_ANDROID_CONFIG:-android.emu.debug}\"'",
"tests:android:test:debug": "cd tests && yarn detox test --configuration android.emu.debug --inspect",
"tests:android:test-reuse": "cd tests && yarn detox test --configuration android.emu.debug --reuse",
- "tests:android:test-cover": "cd tests && yarn detox test --configuration android.emu.debug --loglevel verbose",
+ "tests:android:test-cover": "bash -c 'cd tests && yarn detox test --configuration \"${RNFB_DETOX_ANDROID_CONFIG:-android.emu.debug}\" --loglevel verbose'",
"tests:android:test-cover-reuse": "cd tests && yarn detox test --configuration android.emu.debug --reuse",
"tests:android:pull-native-coverage": "node tests/scripts/pull-native-coverage.js --android-pull",
"tests:android:post-e2e-coverage": "node tests/scripts/pull-native-coverage.js --android-post-e2e",
"tests:android:test:jacoco-report": "cd tests/android && ./gradlew jacocoAndroidTestReport",
- "tests:ios:build": "cd tests && yarn detox build --configuration ios.sim.debug",
+ "tests:ios:build": "bash -c 'cd tests && yarn detox build --configuration \"${RNFB_DETOX_IOS_CONFIG:-ios.sim.debug}\"'",
"tests:ios:build:release": "cd tests && yarn detox build --configuration ios.sim.release",
"tests:ios:detox-framework-cache:rebuild": "cd tests && yarn detox rebuild-framework-cache",
"tests:ios:manual": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ yarn react-native run-ios",
@@ -61,7 +61,7 @@
"tests:ios:test:release": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.release --loglevel warn",
"tests:ios:test:debug": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.debug --loglevel warn --inspect",
"tests:ios:test-reuse": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.debug --reuse --loglevel warn",
- "tests:ios:test-cover": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.debug --loglevel verbose",
+ "tests:ios:test-cover": "bash -c 'cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration \"${RNFB_DETOX_IOS_CONFIG:-ios.sim.debug}\" --loglevel verbose'",
"tests:ios:test-cover-reuse": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.debug --reuse --loglevel warn",
"tests:ios:test:process-coverage": "node tests/scripts/process-ios-native-coverage.js",
"tests:ios:test-cover-and-process": "yarn tests:ios:test-cover && yarn tests:ios:test:process-coverage",
@@ -71,6 +71,8 @@
"tests:macos:manual": "cd tests && yarn react-native run-macos",
"tests:macos:test": "cd tests && npx jet --target=macos --coverage",
"tests:macos:test-cover": "cd tests && npx jet --target=macos --coverage",
+ "tests:e2e:setup-android-avds": "bash ./scripts/e2e/create-android-avds.sh",
+ "tests:e2e:setup-ios-sims": "bash ./scripts/e2e/create-ios-simulators.sh",
"format:js": "prettier --write \"packages/**/*.{js,ts,tsx}\"",
"format:markdown": "prettier --write \"docs/**/*.md[x]\""
},
diff --git a/packages/ai/lib/requests/request.ts b/packages/ai/lib/requests/request.ts
index 0d9d9bb596..a30a9b7fec 100644
--- a/packages/ai/lib/requests/request.ts
+++ b/packages/ai/lib/requests/request.ts
@@ -58,12 +58,21 @@ export class RequestUrl {
logger.info(
'Running VertexAI in test environment, pointing to Firebase Functions emulator URL',
);
- const isAndroid = Platform.OS === 'android';
+ 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';
+ }
if (this.stream) {
- emulatorUrl = `http://${isAndroid ? '10.0.2.2' : '127.0.0.1'}:5001/react-native-firebase-testing/us-central1/testFetchStream`;
+ emulatorUrl = `http://${fnHost}:${fnPort}/react-native-firebase-testing/us-central1/testFetchStream`;
} else {
- emulatorUrl = `http://${isAndroid ? '10.0.2.2' : '127.0.0.1'}:5001/react-native-firebase-testing/us-central1/testFetch`;
+ emulatorUrl = `http://${fnHost}:${fnPort}/react-native-firebase-testing/us-central1/testFetch`;
}
return emulatorUrl;
}
diff --git a/packages/app/e2e/config.e2e.js b/packages/app/e2e/config.e2e.js
index bcaef2667c..9f0b72e14a 100644
--- a/packages/app/e2e/config.e2e.js
+++ b/packages/app/e2e/config.e2e.js
@@ -47,8 +47,9 @@ describe('config', function () {
// NOTE: "preferencesClearAll" clears Firestore settings. Set DB as emulator again.
after(async function () {
const { connectFirestoreEmulator, getFirestore } = firestoreModular;
+ const { getE2eEmulatorPort } = require('../../app/e2e/helpers');
if (Platform.other) return;
- connectFirestoreEmulator(getFirestore(), 'localhost', 8080);
+ connectFirestoreEmulator(getFirestore(), 'localhost', getE2eEmulatorPort('firestore'));
});
it('should set bool values', async function () {
diff --git a/packages/app/e2e/helpers.js b/packages/app/e2e/helpers.js
index c1362fd012..c1054de919 100644
--- a/packages/app/e2e/helpers.js
+++ b/packages/app/e2e/helpers.js
@@ -8,3 +8,90 @@ exports.getE2eEmulatorHost = function getE2eEmulatorHost() {
}
return '127.0.0.1';
};
+
+function platformKey() {
+ if (Platform.other) {
+ return 'macos';
+ }
+ if (Platform.android) {
+ return 'android';
+ }
+ return 'ios';
+}
+
+const DEFAULT_EMULATOR = {
+ firestore: 8080,
+ auth: 9099,
+ database: 9000,
+ functions: 5001,
+ storage: 9199,
+};
+
+const DEFAULT_JET = {
+ android: 8090,
+ ios: 8190,
+ macos: 8290,
+};
+
+function e2eDebug(msg, extra) {
+ if (process.env.RNFB_E2E_DEBUG !== '1') {
+ return;
+ }
+ const pk = platformKey();
+ // eslint-disable-next-line no-console
+ console.log(`[rnfb-e2e] platform=${pk} ${msg}`, extra || '');
+}
+
+function prefixedEmulatorPort(service) {
+ const pk = platformKey();
+ const key = `RNFB_${pk.toUpperCase()}_EMULATOR_${service.toUpperCase()}_PORT`;
+ if (process.env[key]) {
+ return parseInt(process.env[key], 10);
+ }
+ return null;
+}
+
+exports.getE2ePlatformKey = platformKey;
+
+exports.getE2eEmulatorPort = function getE2eEmulatorPort(service) {
+ const prefixed = prefixedEmulatorPort(service);
+ if (prefixed) {
+ e2eDebug(`emulator.${service} from env`, prefixed);
+ return prefixed;
+ }
+ const fallback = DEFAULT_EMULATOR[service] ?? 8080;
+ e2eDebug(`emulator.${service} default`, fallback);
+ return fallback;
+};
+
+exports.getJetRemotePort = function getJetRemotePort() {
+ if (process.env.JET_REMOTE_PORT) {
+ return parseInt(process.env.JET_REMOTE_PORT, 10);
+ }
+ const pk = platformKey();
+ const prefixed = process.env[`RNFB_${pk.toUpperCase()}_JET_PORT`];
+ if (prefixed) {
+ return parseInt(prefixed, 10);
+ }
+ return DEFAULT_JET[pk] ?? 8090;
+};
+
+exports.getJetRemoteUrl = function getJetRemoteUrl() {
+ const host = exports.getE2eEmulatorHost();
+ return `ws://${host}:${exports.getJetRemotePort()}`;
+};
+
+exports.getMetroPort = function getMetroPort() {
+ if (process.env.RCT_METRO_PORT) {
+ return parseInt(process.env.RCT_METRO_PORT, 10);
+ }
+ if (process.env.RNFB_METRO_PORT) {
+ return parseInt(process.env.RNFB_METRO_PORT, 10);
+ }
+ const pk = platformKey();
+ const prefixed = process.env[`RNFB_${pk.toUpperCase()}_METRO_PORT`];
+ if (prefixed) {
+ return parseInt(prefixed, 10);
+ }
+ return 8081;
+};
diff --git a/packages/auth/e2e/emailLink.e2e.js b/packages/auth/e2e/emailLink.e2e.js
index b1cc54d725..3e09b4cb20 100644
--- a/packages/auth/e2e/emailLink.e2e.js
+++ b/packages/auth/e2e/emailLink.e2e.js
@@ -1,4 +1,9 @@
const { getLastOob, signInUser } = require('./helpers');
+const { getE2eEmulatorHost, getMetroPort } = require('../../app/e2e/helpers');
+
+function emailLinkContinueUrl(random) {
+ return `http://${getE2eEmulatorHost()}:${getMetroPort()}/authLinkFoo?bar=${random}`;
+}
describe('auth() -> emailLink Provider', function () {
beforeEach(async function () {
@@ -43,7 +48,7 @@ describe('auth() -> emailLink Provider', function () {
const auth = getAuth();
const random = Utils.randString(12, '#aa');
const email = `${random}@${random}.com`;
- const continueUrl = `http://${Platform.android ? '10.0.2.2' : '127.0.0.1'}:8081/authLinkFoo?bar=${random}`;
+ const continueUrl = emailLinkContinueUrl(random);
const actionCodeSettings = {
url: continueUrl,
handleCodeInApp: true,
@@ -97,7 +102,7 @@ describe('auth() -> emailLink Provider', function () {
const auth = getAuth();
const random = Utils.randString(12, '#aa');
const email = `${random}@${random}.com`;
- const continueUrl = `http://${Platform.android ? '10.0.2.2' : '127.0.0.1'}:8081/authLinkFoo?bar=${random}`;
+ const continueUrl = emailLinkContinueUrl(random);
const actionCodeSettings = {
url: continueUrl,
handleCodeInApp: true,
diff --git a/packages/auth/e2e/helpers.js b/packages/auth/e2e/helpers.js
index fa40a5add6..ad5cef0ba8 100644
--- a/packages/auth/e2e/helpers.js
+++ b/packages/auth/e2e/helpers.js
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
-const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers');
+const { getE2eTestProject, getE2eEmulatorHost, getE2eEmulatorPort } = require('../../app/e2e/helpers');
// Call HTTP REST API URL and return JSON response parsed into object
const callRestApi = async function callRestAPI(url, returnRedirectUrl = false) {
@@ -27,8 +27,9 @@ exports.clearAllUsers = async function clearAllUsers() {
const response = await fetch(
'http://' +
getE2eEmulatorHost() +
- ':9099' +
- '/emulator/v1/projects/' +
+ ':' +
+ getE2eEmulatorPort('auth') +
+ '/emulator/v1/projects/' +
getE2eTestProject() +
'/accounts',
{
@@ -51,7 +52,8 @@ exports.disableUser = async function disableUser(userId) {
const response = await fetch(
'http://' +
getE2eEmulatorHost() +
- ':9099' +
+ ':' +
+ getE2eEmulatorPort('auth') +
'/identitytoolkit.googleapis.com/v1/projects/' +
getE2eTestProject() +
'/accounts:update',
@@ -77,7 +79,9 @@ async function getLastSmsCode(specificPhone) {
const getSmsCodesUrl =
'http://' +
getE2eEmulatorHost() +
- ':9099/emulator/v1/projects/' +
+ ':' +
+ getE2eEmulatorPort('auth') +
+ '/emulator/v1/projects/' +
getE2eTestProject() +
'/verificationCodes';
@@ -117,7 +121,9 @@ async function getLastOob(specificEmail) {
const getOobCodesUrl =
'http://' +
getE2eEmulatorHost() +
- ':9099/emulator/v1/projects/' +
+ ':' +
+ getE2eEmulatorPort('auth') +
+ '/emulator/v1/projects/' +
getE2eTestProject() +
'/oobCodes';
@@ -154,7 +160,9 @@ exports.resetPassword = async function resetPassword(oobCode, newPassword) {
const resetPasswordUrl =
'http://' +
getE2eEmulatorHost() +
- ':9099/emulator/action?mode=resetPassword&lang=en&oobCode=' +
+ ':' +
+ getE2eEmulatorPort('auth') +
+ '/emulator/action?mode=resetPassword&lang=en&oobCode=' +
oobCode +
'&apiKey=fake-api-key&newPassword=' +
newPassword;
@@ -165,7 +173,9 @@ async function verifyEmail(oobCode) {
const verifyEmailUrl =
'http://' +
getE2eEmulatorHost() +
- ':9099/emulator/action?mode=verifyEmail&lang=en&oobCode=' +
+ ':' +
+ getE2eEmulatorPort('auth') +
+ '/emulator/action?mode=verifyEmail&lang=en&oobCode=' +
oobCode +
'&apiKey=fake-api-key';
return await callRestApi(verifyEmailUrl);
diff --git a/packages/database/e2e/helpers.js b/packages/database/e2e/helpers.js
index ce16f48c50..0b6d5cdd54 100644
--- a/packages/database/e2e/helpers.js
+++ b/packages/database/e2e/helpers.js
@@ -13,7 +13,7 @@ const testingUtils = require('@firebase/rules-unit-testing');
// eslint-disable-next-line no-console
console.warn = originalConsoleWarn;
-const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers');
+const { getE2eTestProject, getE2eEmulatorHost, getE2eEmulatorPort } = require('../../app/e2e/helpers');
// TODO make more unique?
const ID = Date.now();
@@ -78,7 +78,7 @@ exports.seed = function seed(path) {
databaseName: DB_NAME,
rules: JSON.stringify(DB_RULES),
host: getE2eEmulatorHost(),
- port: 9000,
+ port: getE2eEmulatorPort('database'),
},
}),
]);
diff --git a/packages/firestore/e2e/helpers.js b/packages/firestore/e2e/helpers.js
index c44aa4e156..e59a7d92ae 100644
--- a/packages/firestore/e2e/helpers.js
+++ b/packages/firestore/e2e/helpers.js
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
-const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers');
+const { getE2eTestProject, getE2eEmulatorHost, getE2eEmulatorPort } = require('../../app/e2e/helpers');
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
@@ -20,8 +20,9 @@ const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers
exports.wipe = async function wipe(debug = false, databaseId = '(default)', retries = 3) {
const host = getE2eEmulatorHost();
+ const fsPort = getE2eEmulatorPort('firestore');
const url =
- `http://${host}:8080/emulator/v1/projects/` +
+ `http://${host}:${fsPort}/emulator/v1/projects/` +
getE2eTestProject() +
`/databases/${databaseId}/documents`;
@@ -105,7 +106,7 @@ exports.setDocumentOutOfBand = async function setDocumentOutOfBand(
databaseId = '(default)',
) {
const url =
- `http://${getE2eEmulatorHost()}:8080/v1/projects/` +
+ `http://${getE2eEmulatorHost()}:${getE2eEmulatorPort('firestore')}/v1/projects/` +
getE2eTestProject() +
`/databases/${databaseId}/documents/${path}`;
diff --git a/packages/functions/e2e/functions.e2e.js b/packages/functions/e2e/functions.e2e.js
index 630fd014c6..6c292f89c4 100644
--- a/packages/functions/e2e/functions.e2e.js
+++ b/packages/functions/e2e/functions.e2e.js
@@ -88,6 +88,21 @@ const SAMPLE_DATA = {
],
};
+const { getE2eEmulatorHost, getE2eEmulatorPort } = require('../../app/e2e/helpers');
+
+function functionsEmulatorConnectHost() {
+ const host = getE2eEmulatorHost();
+ return host === '10.0.2.2' ? '10.0.2.2' : 'localhost';
+}
+
+function functionsEmulatorPort() {
+ return getE2eEmulatorPort('functions');
+}
+
+function functionsCallableUrl(fnPath) {
+ return `http://${getE2eEmulatorHost()}:${functionsEmulatorPort()}/react-native-firebase-testing/us-central1/${fnPath}`;
+}
+
const E2E_CALLABLE_TIMEOUT_MS = 120000;
function e2eCallableTimeoutOptions(extra = {}) {
@@ -184,7 +199,7 @@ describe('functions() modular', function () {
const region = 'us-central1';
const fnName = 'helloWorldV2';
const functions = getFunctions(getApp(), region);
- connectFunctionsEmulator(functions, 'localhost', 5001);
+ connectFunctionsEmulator(functions, functionsEmulatorConnectHost(), functionsEmulatorPort());
const response = await httpsCallable(functions, fnName, e2eCallableTimeoutOptions())();
response.data.should.equal('Hello from Firebase!');
});
@@ -195,7 +210,7 @@ describe('functions() modular', function () {
const region = 'us-central1';
const fnName = 'helloWorldV2';
const functions = getFunctions(getApp(), region);
- connectFunctionsEmulator(functions, 'localhost', 5001);
+ connectFunctionsEmulator(functions, functionsEmulatorConnectHost(), functionsEmulatorPort());
const response = await httpsCallable(functions, fnName, e2eCallableTimeoutOptions())();
response.data.should.equal('Hello from Firebase!');
});
@@ -207,7 +222,7 @@ describe('functions() modular', function () {
const { getApp } = modular;
const { getFunctions, httpsCallable, connectFunctionsEmulator } = functionsModular;
const functions = getFunctions(getApp(), 'us-central1');
- connectFunctionsEmulator(functions, 'localhost', 5001);
+ connectFunctionsEmulator(functions, functionsEmulatorConnectHost(), functionsEmulatorPort());
const response = await httpsCallable(functions, 'helloWorldV2', { timeout: 10000 })();
response.data.should.equal('Hello from Firebase!');
});
@@ -225,7 +240,7 @@ describe('functions() modular', function () {
const functions = getFunctions(getApp());
const functionRunner = httpsCallableFromUrl(
functions,
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/helloWorldV2`,
+ functionsCallableUrl('helloWorldV2'),
e2eCallableTimeoutOptions(),
);
const response = await functionRunner();
@@ -890,7 +905,7 @@ describe('functions() modular', function () {
}
const functionRunner = httpsCallableFromUrl(
getFunctions(getApp()),
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/testStreamWithHttpsErrorFromUrl`,
+ functionsCallableUrl('testStreamWithHttpsErrorFromUrl'),
e2eCallableTimeoutOptions(),
);
@@ -973,7 +988,7 @@ describe('functions() modular', function () {
}
const functionRunner = httpsCallableFromUrl(
getFunctions(getApp()),
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/testStreamingCallable`,
+ functionsCallableUrl('testStreamingCallable'),
e2eCallableTimeoutOptions(),
);
const { stream, data } = await functionRunner.stream({ count: 3, delay: 400 });
@@ -1003,7 +1018,7 @@ describe('functions() modular', function () {
}
const functionRunner = httpsCallableFromUrl(
getFunctions(getApp()),
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/testStreamingCallable`,
+ functionsCallableUrl('testStreamingCallable'),
e2eCallableTimeoutOptions(),
);
const { stream, data } = await functionRunner.stream({ count: 2, delay: 300 });
@@ -1033,7 +1048,7 @@ describe('functions() modular', function () {
}
const functionRunner = httpsCallableFromUrl(
getFunctions(getApp()),
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/testStreamingCallable`,
+ functionsCallableUrl('testStreamingCallable'),
e2eCallableTimeoutOptions(),
);
const { stream, data } = await functionRunner.stream(
@@ -1066,7 +1081,7 @@ describe('functions() modular', function () {
}
const functionRunner = httpsCallableFromUrl(
getFunctions(getApp()),
- `http://${hostname}:5001/react-native-firebase-testing/us-central1/testStreamingCallable`,
+ functionsCallableUrl('testStreamingCallable'),
e2eCallableTimeoutOptions(),
);
const result = await functionRunner.stream({ count: 2, delay: 200 });
diff --git a/scripts/e2e/create-android-avds.sh b/scripts/e2e/create-android-avds.sh
new file mode 100755
index 0000000000..119cb3783e
--- /dev/null
+++ b/scripts/e2e/create-android-avds.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# Clone TestingAVD into TestingAVD-1 … TestingAVD-4 for parallel e2e slots.
+# Same AVD *definition* cannot run two read-write instances concurrently; clones are required.
+set -euo pipefail
+
+COUNT="${1:-4}"
+BASE_AVD="${RNFB_ANDROID_BASE_AVD:-TestingAVD}"
+ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}}"
+AVD_DIR="${ANDROID_AVD_HOME:-$HOME/.android/avd}"
+EMU="${ANDROID_HOME}/emulator/emulator"
+SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager"
+
+if [[ ! -x "$EMU" ]]; then
+ echo "error: emulator not found at $EMU (set ANDROID_HOME)" >&2
+ exit 1
+fi
+
+if ! "$EMU" -list-avds | grep -qx "$BASE_AVD"; then
+ echo "error: base AVD '$BASE_AVD' not found. Create it first (Android Studio AVD Manager)." >&2
+ exit 1
+fi
+
+clone_avd() {
+ local src=$1 dst=$2
+ if "$EMU" -list-avds | grep -qx "$dst"; then
+ echo "[avd] $dst already exists"
+ return 0
+ fi
+
+ local src_ini="${AVD_DIR}/${src}.ini"
+ local src_avd="${AVD_DIR}/${src}.avd"
+ local dst_ini="${AVD_DIR}/${dst}.ini"
+ local dst_avd="${AVD_DIR}/${dst}.avd"
+
+ echo "[avd] cloning $src → $dst"
+ cp -R "$src_avd" "$dst_avd"
+ cp "$src_ini" "$dst_ini"
+ 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"
+}
+
+for i in $(seq 1 "$COUNT"); do
+ clone_avd "$BASE_AVD" "${BASE_AVD}-${i}"
+done
+
+echo "[avd] available AVDs:"
+"$EMU" -list-avds | grep -E "^${BASE_AVD}" || true
+echo "[avd] done — slot 0 uses ${BASE_AVD}, slots 1+ use ${BASE_AVD}-N"
diff --git a/scripts/e2e/create-ios-simulators.sh b/scripts/e2e/create-ios-simulators.sh
new file mode 100755
index 0000000000..639da1af39
--- /dev/null
+++ b/scripts/e2e/create-ios-simulators.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# Create dedicated iOS simulators for e2e slots 1–4 (slot 0 uses default iPhone 17).
+set -euo pipefail
+
+COUNT="${1:-4}"
+BASE_NAME="${RNFB_IOS_BASE_SIMULATOR:-iPhone 17}"
+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);
+")
+
+for i in $(seq 1 "$COUNT"); do
+ name="RNFB E2E iOS slot-${i}"
+ if xcrun simctl list devices available | grep -q "${name}"; then
+ echo "[sim] ${name} exists"
+ continue
+ fi
+ echo "[sim] creating ${name}"
+ xcrun simctl create "$name" "$BASE_NAME" "$RUNTIME"
+done
+
+echo "[sim] done"
diff --git a/scripts/e2e/start-emulator-slotted.sh b/scripts/e2e/start-emulator-slotted.sh
new file mode 100755
index 0000000000..a1f6a4903d
--- /dev/null
+++ b/scripts/e2e/start-emulator-slotted.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Start one Firebase emulator suite for a platform using RNFB__EMULATOR_* env vars.
+set -euo pipefail
+
+PLATFORM="${1:?platform required: android|ios|macos}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+SCRIPTS="${REPO_ROOT}/.github/workflows/scripts"
+
+prefix="$(echo "${PLATFORM}" | tr '[:lower:]' '[:upper:]')"
+
+eval "FS_PORT=\$RNFB_${prefix}_EMULATOR_FIRESTORE_PORT"
+eval "AUTH_PORT=\$RNFB_${prefix}_EMULATOR_AUTH_PORT"
+eval "DB_PORT=\$RNFB_${prefix}_EMULATOR_DATABASE_PORT"
+eval "FN_PORT=\$RNFB_${prefix}_EMULATOR_FUNCTIONS_PORT"
+eval "ST_PORT=\$RNFB_${prefix}_EMULATOR_STORAGE_PORT"
+eval "HUB_PORT=\$RNFB_${prefix}_EMULATOR_HUB_PORT"
+eval "LOG_PORT=\$RNFB_${prefix}_EMULATOR_LOGGING_PORT"
+
+for v in FS_PORT AUTH_PORT DB_PORT FN_PORT ST_PORT HUB_PORT LOG_PORT; do
+ if [[ -z "${!v:-}" ]]; then
+ echo "error: ${v} not set (export RNFB_${prefix}_EMULATOR_* ports first)" >&2
+ exit 1
+ fi
+done
+
+CONFIG="${SCRIPTS}/.e2e-emulator-${PLATFORM}-${RNFB_E2E_HOST_SLOT:-${RNFB_E2E_SLOT:-0}}.json"
+python3 - </dev/null
+yarn >/dev/null 2>&1 || yarn
+yarn build
+popd >/dev/null
+
+# shellcheck source=firebase-cli.sh
+source "${SCRIPTS}/firebase-cli.sh"
+
+cd "${SCRIPTS}"
+"${FIREBASE_CMD[@]}" emulators:start \
+ --config "${CONFIG}" \
+ --only auth,database,firestore,functions,storage \
+ --project react-native-firebase-testing
diff --git a/tests/.babelrc b/tests/.babelrc
index 8f47e44072..c427acf011 100644
--- a/tests/.babelrc
+++ b/tests/.babelrc
@@ -4,7 +4,37 @@
[
"transform-inline-environment-variables",
{
- "include": ["CI"]
+ "include": [
+ "CI",
+ "RNFB_E2E_PLATFORM",
+ "RNFB_E2E_DEBUG",
+ "RCT_METRO_PORT",
+ "RNFB_METRO_PORT",
+ "JET_METRO_PORT",
+ "JET_REMOTE_PORT",
+ "RNFB_JET_CONTROL_PORT",
+ "RNFB_ANDROID_METRO_PORT",
+ "RNFB_IOS_METRO_PORT",
+ "RNFB_MACOS_METRO_PORT",
+ "RNFB_ANDROID_JET_PORT",
+ "RNFB_IOS_JET_PORT",
+ "RNFB_MACOS_JET_PORT",
+ "RNFB_ANDROID_EMULATOR_FIRESTORE_PORT",
+ "RNFB_ANDROID_EMULATOR_AUTH_PORT",
+ "RNFB_ANDROID_EMULATOR_DATABASE_PORT",
+ "RNFB_ANDROID_EMULATOR_FUNCTIONS_PORT",
+ "RNFB_ANDROID_EMULATOR_STORAGE_PORT",
+ "RNFB_IOS_EMULATOR_FIRESTORE_PORT",
+ "RNFB_IOS_EMULATOR_AUTH_PORT",
+ "RNFB_IOS_EMULATOR_DATABASE_PORT",
+ "RNFB_IOS_EMULATOR_FUNCTIONS_PORT",
+ "RNFB_IOS_EMULATOR_STORAGE_PORT",
+ "RNFB_MACOS_EMULATOR_FIRESTORE_PORT",
+ "RNFB_MACOS_EMULATOR_AUTH_PORT",
+ "RNFB_MACOS_EMULATOR_DATABASE_PORT",
+ "RNFB_MACOS_EMULATOR_FUNCTIONS_PORT",
+ "RNFB_MACOS_EMULATOR_STORAGE_PORT"
+ ]
}
],
[
diff --git a/tests/.detoxrc.js b/tests/.detoxrc.js
index 235263035c..63ab5e4641 100644
--- a/tests/.detoxrc.js
+++ b/tests/.detoxrc.js
@@ -1,101 +1,188 @@
/** @type {Detox.DetoxConfig} */
-module.exports = {
- testRunner: {
- args: {
- $0: 'jest',
- config: 'e2e/jest.config.js',
- },
- jest: {
- setupTimeout: 120000,
- },
+
+const SERIAL_ANDROID_REVERSE = [8080, 8081, 8090, 9000, 9099, 9199, 5001];
+
+function intEnv(name, fallback) {
+ const v = process.env[name];
+ if (v === undefined || v === '') {
+ return fallback;
+ }
+ return parseInt(v, 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),
+ ];
+}
+
+const ANDROID_REVERSE_DEFAULT = androidReversePortsFromEnv();
+
+function readNativeMetroPort() {
+ if (process.env.RNFB_E2E_PLATFORM) {
+ const prefixed = process.env[`RNFB_${process.env.RNFB_E2E_PLATFORM.toUpperCase()}_METRO_PORT`];
+ if (prefixed) {
+ return prefixed;
+ }
+ }
+ if (process.env.RCT_METRO_PORT) {
+ return process.env.RCT_METRO_PORT;
+ }
+ if (process.env.RNFB_METRO_PORT) {
+ return process.env.RNFB_METRO_PORT;
+ }
+ if (process.env.RNFB_IOS_METRO_PORT) {
+ return process.env.RNFB_IOS_METRO_PORT;
+ }
+ return '8081';
+}
+
+function iosXcodebuildPrefix() {
+ return `RCT_METRO_PORT=${readNativeMetroPort()}`;
+}
+
+const IOS_SIM_NAMES = [
+ 'iPhone 17',
+ 'RNFB E2E iOS slot-1',
+ 'RNFB E2E iOS slot-2',
+ 'RNFB E2E iOS slot-3',
+ 'RNFB E2E iOS slot-4',
+];
+
+const ANDROID_AVD_NAMES = [
+ 'TestingAVD',
+ 'TestingAVD-1',
+ 'TestingAVD-2',
+ 'TestingAVD-3',
+ 'TestingAVD-4',
+];
+
+function iosSimulatorDevice(slot) {
+ const deviceType = process.env.RNFB_IOS_BASE_SIMULATOR || 'iPhone 17';
+ if (slot === 0) {
+ return {
+ type: 'ios.simulator',
+ device: { type: deviceType, name: deviceType },
+ };
+ }
+ const slotName = IOS_SIM_NAMES[slot] || `RNFB E2E iOS slot-${slot}`;
+ return {
+ type: 'ios.simulator',
+ device: { type: deviceType, name: slotName },
+ };
+}
+
+function androidEmulatorDevice(slot) {
+ return {
+ type: 'android.emulator',
+ device: { avdName: ANDROID_AVD_NAMES[slot] || `TestingAVD-${slot}` },
+ bootArgs: process.env.RNFB_ANDROID_EMULATOR_BOOT_ARGS || '-no-snapshot-load -no-snapshot-save',
+ readonly: true,
+ };
+}
+
+function androidApp(reversePorts) {
+ return {
+ type: 'android.apk',
+ binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
+ build:
+ 'cd android && ./gradlew-with-worker-cap.sh assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
+ reversePorts,
+ };
+}
+
+function androidAppWindows(reversePorts) {
+ return {
+ type: 'android.apk',
+ binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
+ build:
+ 'cd android && .\\gradlew-with-worker-cap.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
+ reversePorts,
+ };
+}
+
+const devices = {
+ simulator: iosSimulatorDevice(0),
+ attached: {
+ type: 'android.attached',
+ device: { adbName: '.*' },
},
- apps: {
- 'ios.debug': {
- type: 'ios.app',
- binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/testing.app',
- build:
- 'set -o pipefail && xcodebuild VALID_ARCHS="`uname -m`" CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build | xcbeautify',
- },
- 'ios.release': {
- type: 'ios.app',
- binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/testing.app',
- build:
- 'export RCT_NO_LAUNCH_PACKAGER=true && set -o pipefail | xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ -workspace ios/testing.xcworkspace -scheme testing -configuration Release -sdk iphonesimulator -derivedDataPath ios/build | xcbeautify',
- },
- 'android.debug': {
- type: 'android.apk',
- binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
- // keep in sync with android.debug.windows below, except gradlew vs gradlew.bat
- build:
- 'cd android && ./gradlew-with-worker-cap.sh assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
- reversePorts: [8080, 8081, 8090, 9000, 9099, 9199],
- },
- 'android.debug.windows': {
- type: 'android.apk',
- binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
- // android.debug.windows only exists to use .bat script vs shell here:
- build:
- 'cd android && .\\gradlew-with-worker-cap.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
- reversePorts: [8080, 8081, 8090, 9000, 9099, 9199],
- },
- 'android.release': {
- type: 'android.apk',
- binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
- build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
- },
+ emulator: androidEmulatorDevice(0),
+};
+
+for (let slot = 1; slot < 5; slot += 1) {
+ devices[`simulator-slot${slot}`] = iosSimulatorDevice(slot);
+ devices[`emulator-slot${slot}`] = androidEmulatorDevice(slot);
+}
+
+const apps = {
+ 'ios.debug': {
+ type: 'ios.app',
+ binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/testing.app',
+ build:
+ `set -o pipefail && ${iosXcodebuildPrefix()} xcodebuild VALID_ARCHS="\`uname -m\`" CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build | xcbeautify`,
},
- devices: {
- simulator: {
- type: 'ios.simulator',
- device: {
- type: 'iPhone 17',
- },
- },
- attached: {
- type: 'android.attached',
- device: {
- adbName: '.*',
- },
- },
- emulator: {
- type: 'android.emulator',
- device: {
- avdName: 'TestingAVD',
- },
- // Cold boot: do not load/save AVD snapshots (warm quickboot is unreliable locally).
- bootArgs:
- process.env.RNFB_ANDROID_EMULATOR_BOOT_ARGS || '-no-snapshot-load -no-snapshot-save',
- readonly: true,
- },
+ 'ios.release': {
+ type: 'ios.app',
+ binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/testing.app',
+ build:
+ `export RCT_NO_LAUNCH_PACKAGER=true && set -o pipefail && ${iosXcodebuildPrefix()} xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ -workspace ios/testing.xcworkspace -scheme testing -configuration Release -sdk iphonesimulator -derivedDataPath ios/build | xcbeautify`,
},
- configurations: {
- 'ios.sim.debug': {
- device: 'simulator',
- app: 'ios.debug',
- },
- 'ios.sim.release': {
- device: 'simulator',
- app: 'ios.release',
- },
- 'android.att.debug': {
- device: 'attached',
- app: 'android.debug',
- },
- 'android.att.release': {
- device: 'attached',
- app: 'android.release',
- },
- 'android.emu.debug': {
- device: 'emulator',
- app: 'android.debug',
- },
- 'android.emu.debug.windows': {
- device: 'emulator',
- app: 'android.debug.windows',
- },
- 'android.emu.release': {
- device: 'emulator',
- app: 'android.release',
- },
+ 'android.debug': androidApp(ANDROID_REVERSE_DEFAULT),
+ 'android.debug.windows': androidAppWindows(ANDROID_REVERSE_DEFAULT),
+ 'android.release': {
+ type: 'android.apk',
+ binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
+ build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
+ },
+};
+
+for (let slot = 1; slot < 5; slot += 1) {
+ apps[`android.debug.slot${slot}`] = androidApp(ANDROID_REVERSE_DEFAULT);
+ apps[`android.debug.slot${slot}.windows`] = androidAppWindows(ANDROID_REVERSE_DEFAULT);
+}
+
+const configurations = {
+ 'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
+ 'ios.sim.release': { device: 'simulator', app: 'ios.release' },
+ 'android.att.debug': { device: 'attached', app: 'android.debug' },
+ 'android.att.release': { device: 'attached', app: 'android.release' },
+ 'android.emu.debug': { device: 'emulator', app: 'android.debug' },
+ 'android.emu.debug.windows': { device: 'emulator', app: 'android.debug.windows' },
+ 'android.emu.release': { device: 'emulator', app: 'android.release' },
+};
+
+for (let slot = 1; slot < 5; slot += 1) {
+ configurations[`ios.sim.debug.slot${slot}`] = {
+ device: `simulator-slot${slot}`,
+ app: 'ios.debug',
+ };
+ configurations[`android.emu.debug.slot${slot}`] = {
+ device: `emulator-slot${slot}`,
+ app: `android.debug.slot${slot}`,
+ };
+ configurations[`android.emu.debug.slot${slot}.windows`] = {
+ device: `emulator-slot${slot}`,
+ app: `android.debug.slot${slot}.windows`,
+ };
+}
+
+module.exports = {
+ testRunner: {
+ args: { $0: 'jest', config: 'e2e/jest.config.js' },
+ jest: { setupTimeout: 120000 },
},
+ apps,
+ devices,
+ configurations,
};
diff --git a/tests/.jetrc.js b/tests/.jetrc.js
index 84d60d31b1..887a7b749d 100644
--- a/tests/.jetrc.js
+++ b/tests/.jetrc.js
@@ -7,6 +7,53 @@ let macOsRetries = 0;
const MACOS_BUNDLE_QUERY =
'platform=macos&dev=true&lazy=true&minify=false&inlineSourceMap=true&modulesOnly=false&runModule=true&app=org.reactjs.native.io-invertase-testing';
+const DEFAULT_JET = { android: 8090, ios: 8190, macos: 8290 };
+
+function e2ePlatform() {
+ return process.env.RNFB_E2E_PLATFORM || 'macos';
+}
+
+function readJetPort() {
+ const pk = e2ePlatform();
+ const prefixed = process.env[`RNFB_${pk.toUpperCase()}_JET_PORT`];
+ if (prefixed) {
+ return parseInt(prefixed, 10);
+ }
+ if (process.env.JET_REMOTE_PORT) {
+ return parseInt(process.env.JET_REMOTE_PORT, 10);
+ }
+ return DEFAULT_JET[pk] ?? 8090;
+}
+
+function readMetroPort() {
+ const pk = e2ePlatform();
+ const prefixed = process.env[`RNFB_${pk.toUpperCase()}_METRO_PORT`];
+ if (prefixed) {
+ return parseInt(prefixed, 10);
+ }
+ if (process.env.RCT_METRO_PORT) {
+ return parseInt(process.env.RCT_METRO_PORT, 10);
+ }
+ if (process.env.RNFB_METRO_PORT) {
+ return parseInt(process.env.RNFB_METRO_PORT, 10);
+ }
+ return 8081;
+}
+
+function killMacOsTestApp() {
+ try {
+ execSync('killall "io.invertase.testing"', { stdio: 'ignore' });
+ } catch (_e) {
+ // not running
+ }
+ try {
+ execSync('pgrep -x io.invertase.testing >/dev/null 2>&1');
+ execSync('killall -9 "io.invertase.testing"', { stdio: 'ignore' });
+ } catch (_e) {
+ // clear
+ }
+}
+
async function waitForMetroMacosBundle(metroPort = 8081, timeoutMs = 600000) {
const host = '127.0.0.1';
const statusUrl = `http://${host}:${metroPort}/status`;
@@ -32,6 +79,7 @@ async function waitForMetroMacosBundle(metroPort = 8081, timeoutMs = 600000) {
module.exports = {
config: {
+ port: readJetPort(),
slow: 3000,
reporter: 'spec',
timeout: 420000, // 7 minutes - fetchAndActivate takes 5+ sometimes
@@ -59,19 +107,22 @@ module.exports = {
},
macos: {
async before(config) {
- try {
- execSync(`killall "io.invertase.testing"`);
- } catch (_e) {
- // noop
- }
- await waitForMetroMacosBundle(config.metroPort ?? 8081);
- const macApp = spawn(
- 'open',
- ['./macos/build/Build/Products/Debug/io.invertase.testing.app'],
- {
- stdio: ['ignore', 'inherit', 'inherit'],
+ killMacOsTestApp();
+ const metroPort = readMetroPort();
+ const jetPort = readJetPort();
+ config.metroPort = metroPort;
+ config.port = jetPort;
+ await waitForMetroMacosBundle(metroPort);
+ const macBinary =
+ './macos/build/Build/Products/Debug/io.invertase.testing.app/Contents/MacOS/io.invertase.testing';
+ const macApp = spawn(macBinary, [], {
+ stdio: ['ignore', 'inherit', 'inherit'],
+ env: {
+ ...process.env,
+ RCT_METRO_PORT: String(metroPort),
+ JET_REMOTE_PORT: String(jetPort),
},
- );
+ });
macApp.on('close', code => {
if (code === 0) {
return;
@@ -95,11 +146,8 @@ module.exports = {
return config;
},
async after(_config) {
- try {
- execSync(`killall "io.invertase.testing"`);
- } catch (_e) {
- // noop
- }
+ killMacOsTestApp();
+ console.warn('[rnfb-e2e] macOS app teardown complete');
},
},
},
diff --git a/tests/app.js b/tests/app.js
index bace128e32..6fedb1d2e8 100644
--- a/tests/app.js
+++ b/tests/app.js
@@ -21,6 +21,12 @@ import { StyleSheet, View, StatusBar, AppRegistry, Text, Button } from 'react-na
import { JetProvider, ConnectionText, StatusEmoji, StatusText } from 'jet';
+import {
+ getE2eEmulatorHost,
+ getE2eEmulatorPort,
+ getJetRemoteUrl,
+} from '@react-native-firebase/app/e2e/helpers';
+
import { TestComponents } from './local-tests';
let harnessOverrides = {};
@@ -91,24 +97,29 @@ function loadTests(_) {
}
before(async function () {
+ const emuHost = getE2eEmulatorHost();
if (platformSupportedModules.includes('functions')) {
const { connectFunctionsEmulator, getFunctions } = functionsModular;
- connectFunctionsEmulator(getFunctions(), 'localhost', 5001);
+ connectFunctionsEmulator(getFunctions(), emuHost, getE2eEmulatorPort('functions'));
}
if (platformSupportedModules.includes('database')) {
const { connectDatabaseEmulator, getDatabase } = databaseModular;
- connectDatabaseEmulator(getDatabase(), 'localhost', 9000);
+ connectDatabaseEmulator(getDatabase(), emuHost, getE2eEmulatorPort('database'));
}
if (platformSupportedModules.includes('auth')) {
const { connectAuthEmulator, getAuth } = authModular;
- connectAuthEmulator(getAuth(), 'http://localhost:9099');
+ connectAuthEmulator(
+ getAuth(),
+ `http://${emuHost}:${getE2eEmulatorPort('auth')}`,
+ );
}
if (platformSupportedModules.includes('firestore')) {
const { getApp } = modular;
const { connectFirestoreEmulator, clearIndexedDbPersistence, getFirestore } =
firestoreModular;
- connectFirestoreEmulator(getFirestore(), 'localhost', 8080);
- connectFirestoreEmulator(getFirestore(getApp(), 'second-rnfb'), 'localhost', 8080);
+ const fsPort = getE2eEmulatorPort('firestore');
+ connectFirestoreEmulator(getFirestore(), emuHost, fsPort);
+ connectFirestoreEmulator(getFirestore(getApp(), 'second-rnfb'), emuHost, fsPort);
// Firestore caches documents locally (a great feature!) and that confounds tests
// as data from previous runs pollutes following runs until re-install the app. Clear it.
if (!Platform.other) {
@@ -118,12 +129,13 @@ function loadTests(_) {
if (platformSupportedModules.includes('storage')) {
const { getApp } = modular;
const { getStorage, connectStorageEmulator } = storageModular;
- connectStorageEmulator(getStorage(), 'localhost', 9199);
- connectStorageEmulator(getStorage(getApp('secondaryFromNative')), 'localhost', 9199);
+ const stPort = getE2eEmulatorPort('storage');
+ connectStorageEmulator(getStorage(), emuHost, stPort);
+ connectStorageEmulator(getStorage(getApp('secondaryFromNative')), emuHost, stPort);
connectStorageEmulator(
getStorage(getApp(), 'gs://react-native-firebase-testing'),
- 'localhost',
- 9199,
+ emuHost,
+ stPort,
);
}
});
@@ -313,7 +325,7 @@ function App() {