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() {