Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/scripts/firebase.emulator.template.json
Original file line number Diff line number Diff line change
@@ -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 }
}
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion okf-bundle/testing/running-e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`).

<a id="configurable-e2e-environment"></a>

### Configurable e2e environment

Serial e2e uses committed defaults (Metro `:8081`, Jet `:8090`, emulators `:8080` / `:9099` / …). Slotted parallel runs set per-platform ports via env (e.g. `RNFB_IOS_JET_PORT`, `RNFB_MACOS_JET_PORT`). 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`.
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@
"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",
"tests:ios:test": "cd tests && SIMCTL_CHILD_GULGeneratedClassDisposeDisabled=1 yarn detox test --configuration ios.sim.debug --loglevel warn",
"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",
Expand All @@ -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]\""
},
Expand Down
19 changes: 17 additions & 2 deletions packages/ai/lib/requests/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,26 @@ export class RequestUrl {
'Running VertexAI in test environment, pointing to Firebase Functions emulator URL',
);
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) {
const parsed = parseInt(envPort, 10);
if (Number.isFinite(parsed)) {
fnPort = parsed;
}
}

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;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/app/e2e/config.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
101 changes: 101 additions & 0 deletions packages/app/e2e/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,104 @@ 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_SERIAL = 8090;

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]) {
const n = parseInt(process.env[key], 10);
if (Number.isFinite(n)) {
return n;
}
}
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) {
const n = parseInt(process.env.JET_REMOTE_PORT, 10);
if (Number.isFinite(n)) {
return n;
}
}
const pk = platformKey();
const prefixed = process.env[`RNFB_${pk.toUpperCase()}_JET_PORT`];
if (prefixed) {
const n = parseInt(prefixed, 10);
if (Number.isFinite(n)) {
return n;
}
}
return DEFAULT_JET_SERIAL;
};

exports.getJetRemoteUrl = function getJetRemoteUrl() {
const host = exports.getE2eEmulatorHost();
return `ws://${host}:${exports.getJetRemotePort()}`;
};

exports.getMetroPort = function getMetroPort() {
if (process.env.RCT_METRO_PORT) {
const n = parseInt(process.env.RCT_METRO_PORT, 10);
if (Number.isFinite(n)) {
return n;
}
}
if (process.env.RNFB_METRO_PORT) {
const n = parseInt(process.env.RNFB_METRO_PORT, 10);
if (Number.isFinite(n)) {
return n;
}
}
const pk = platformKey();
const prefixed = process.env[`RNFB_${pk.toUpperCase()}_METRO_PORT`];
if (prefixed) {
const n = parseInt(prefixed, 10);
if (Number.isFinite(n)) {
return n;
}
}
return 8081;
};
9 changes: 7 additions & 2 deletions packages/auth/e2e/emailLink.e2e.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 21 additions & 7 deletions packages/auth/e2e/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* 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) {
Expand Down Expand Up @@ -27,7 +31,8 @@ exports.clearAllUsers = async function clearAllUsers() {
const response = await fetch(
'http://' +
getE2eEmulatorHost() +
':9099' +
':' +
getE2eEmulatorPort('auth') +
'/emulator/v1/projects/' +
getE2eTestProject() +
'/accounts',
Expand All @@ -51,7 +56,8 @@ exports.disableUser = async function disableUser(userId) {
const response = await fetch(
'http://' +
getE2eEmulatorHost() +
':9099' +
':' +
getE2eEmulatorPort('auth') +
'/identitytoolkit.googleapis.com/v1/projects/' +
getE2eTestProject() +
'/accounts:update',
Expand All @@ -77,7 +83,9 @@ async function getLastSmsCode(specificPhone) {
const getSmsCodesUrl =
'http://' +
getE2eEmulatorHost() +
':9099/emulator/v1/projects/' +
':' +
getE2eEmulatorPort('auth') +
'/emulator/v1/projects/' +
getE2eTestProject() +
'/verificationCodes';

Expand Down Expand Up @@ -117,7 +125,9 @@ async function getLastOob(specificEmail) {
const getOobCodesUrl =
'http://' +
getE2eEmulatorHost() +
':9099/emulator/v1/projects/' +
':' +
getE2eEmulatorPort('auth') +
'/emulator/v1/projects/' +
getE2eTestProject() +
'/oobCodes';

Expand Down Expand Up @@ -154,7 +164,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;
Expand All @@ -165,7 +177,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);
Expand Down
8 changes: 6 additions & 2 deletions packages/database/e2e/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ 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();
Expand Down Expand Up @@ -78,7 +82,7 @@ exports.seed = function seed(path) {
databaseName: DB_NAME,
rules: JSON.stringify(DB_RULES),
host: getE2eEmulatorHost(),
port: 9000,
port: getE2eEmulatorPort('database'),
},
}),
]);
Expand Down
Loading
Loading