diff --git a/.gitignore b/.gitignore index 6b2d8b4092..5d4e948584 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ android/.settings # Local environment (direnv) .envrc +# e2e +*Example/artifacts diff --git a/Example/.detoxrc.js b/Example/.detoxrc.js index 4269d09526..41ed27fecc 100644 --- a/Example/.detoxrc.js +++ b/Example/.detoxrc.js @@ -1,2 +1,2 @@ -const utils = require('../scripts/detox-utils.cjs'); +const utils = require('../scripts/e2e/detox-utils.cjs'); module.exports = utils.commonDetoxConfigFactory('ScreensExample'); diff --git a/Example/package.json b/Example/package.json index c53f43b3da..1a40d54538 100644 --- a/Example/package.json +++ b/Example/package.json @@ -9,10 +9,10 @@ "format": "prettier --write --list-different './src/**/*.{js,ts,tsx}'", "lint": "eslint --ext '.js,.ts,.tsx' --fix src && yarn check-types && yarn format", "check-types": "tsc --noEmit", - "build-e2e-ios": "detox build --configuration ios.release", - "build-e2e-android": "detox build --configuration android.release", - "test-e2e-ios": "detox test --configuration ios.release --take-screenshots failing", - "test-e2e-android": "detox test --configuration android.release --take-screenshots failing", + "build-e2e-ios": "detox build --configuration ios.sim.release", + "build-e2e-android": "detox build --configuration android.emu.release", + "test-e2e-ios": "detox test --configuration ios.sim.release --take-screenshots failing", + "test-e2e-android": "detox test --configuration android.emu.release --take-screenshots failing", "postinstall": "patch-package" }, "dependencies": { diff --git a/FabricExample/.detoxrc.js b/FabricExample/.detoxrc.js index 477f13da53..0b18322286 100644 --- a/FabricExample/.detoxrc.js +++ b/FabricExample/.detoxrc.js @@ -1,3 +1,3 @@ -const utils = require('../scripts/detox-utils.cjs'); +const utils = require('../scripts/e2e/detox-utils.cjs'); module.exports = utils.commonDetoxConfigFactory('FabricExample'); diff --git a/FabricExample/package.json b/FabricExample/package.json index 22c0ed09bb..91199b09f3 100644 --- a/FabricExample/package.json +++ b/FabricExample/package.json @@ -8,10 +8,10 @@ "lint": "eslint .", "start": "npx react-native start", "test": "jest", - "build-e2e-ios": "detox build --configuration ios.release", - "build-e2e-android": "detox build --configuration android.release", - "test-e2e-ios": "detox test --configuration ios.release --take-screenshots failing", - "test-e2e-android": "detox test --configuration android.release --take-screenshots failing", + "build-e2e-ios": "detox build --configuration ios.sim.release", + "build-e2e-android": "detox build --configuration android.emu.release", + "test-e2e-ios": "detox test --configuration ios.sim.release --take-screenshots failing", + "test-e2e-android": "detox test --configuration android.emu.release --take-screenshots failing", "postinstall": "patch-package" }, "dependencies": { diff --git a/scripts/e2e/android-devices.js b/scripts/e2e/android-devices.js new file mode 100644 index 0000000000..0a1abe06d5 --- /dev/null +++ b/scripts/e2e/android-devices.js @@ -0,0 +1,100 @@ +const ChildProcess = require('node:child_process'); + +// Should be kept in sync with the constant defined in e2e workflow file +const DEFAULT_CI_AVD_NAME = 'e2e_emulator'; + +function detectLocalAndroidEmulator() { + // "RNS_E2E_AVD_NAME" can be set for local developement + const avdName = process.env.RNS_AVD_NAME ?? null; + if (avdName !== null) { + return avdName + } + + // Fallback: try to use Android SDK + try { + let stdout = ChildProcess.execSync("emulator -list-avds"); + + // Possibly convert Buffer to string + if (typeof stdout !== 'string') { + stdout = stdout.toString(); + } + + const avdList = stdout.trim().split('\n').map(name => name.trim()); + + if (avdList.length === 0) { + throw new Error('No installed AVDs detected on the device'); + } + + // Just select first one in the list. + // TODO: consider giving user a choice here. + return avdList[0]; + } catch (error) { + const errorMessage = `Failed to find Android emulator. Set "RNS_E2E_AVD_NAME" env variable pointing to one. Cause: ${error}`; + console.error(errorMessage); + throw new Error(errorMessage); + } +} + +/** + * @param {boolean} isRunningCI whether this script is run in CI environment + */ +function detectAndroidEmulatorName(isRunningCI) { + return isRunningCI ? DEFAULT_CI_AVD_NAME : detectLocalAndroidEmulator(); +} + +/** + * @returns {string | null} Device serial as requested by user, first serial from adb list or null + */ +function resolveAndroidDeviceSerial() { + const deviceSerial = process.env.RNS_DEVICE_SERIAL ?? null; + + if (deviceSerial !== null) { + return deviceSerial; + } + + // Fallback: try to use adb + try { + let stdout = ChildProcess.execSync("adb devices"); + + // Possibly convert Buffer to string + if (typeof stdout !== 'string') { + stdout = stdout.toString(); + } + + /** @type {string} */ + const stringStdout = stdout; + + // Example `adb devices` output: + // + // List of devices attached + // 6lh6huzhr48lu8t8 device + // emulator-5554 device + + const deviceList = stringStdout + .trim() + .split('\n') + .map(line => line.trim()) + .filter((line, index) => line !== '' && index !== 0) // empty lines & header + .map(line => line.split(' ', 1)[0]); + + + if (deviceList.length === 0) { + throw new Error("Seems that the attached device list is empty"); + } + + // Just select first one in the list. + // TODO: consider giving user a choice here. + return deviceList[0]; + } catch (error) { + console.error(`Failed to find attached device. Try setting "RNS_DEVICE_SERIAL" env variable pointing to one. Cause: ${error}`); + } + + return null; +} + +module.exports = { + DEFAULT_CI_AVD_NAME, + detectLocalAndroidEmulator, + detectAndroidEmulatorName, + resolveAndroidDeviceSerial, +} diff --git a/scripts/detox-utils.cjs b/scripts/e2e/detox-utils.cjs similarity index 73% rename from scripts/detox-utils.cjs rename to scripts/e2e/detox-utils.cjs index 8f66557200..81728ad051 100644 --- a/scripts/detox-utils.cjs +++ b/scripts/e2e/detox-utils.cjs @@ -1,6 +1,5 @@ -const ChildProcess = require('node:child_process'); - -const CI_AVD_NAME = 'e2e_emulator'; +const AppleDeviceUtil = require('./ios-devices'); +const AndroidDeviceUtil = require('./android-devices'); const isRunningCI = process.env.CI != null; @@ -10,44 +9,24 @@ const apkBulidArchitecture = isRunningCI ? 'x86_64' : 'arm64-v8a'; // it is assumed here that arm64-v8a AOSP emulator is not available in local setup. const testButlerApkPath = isRunningCI ? ['../Example/e2e/apps/test-butler-app-2.2.1.apk'] : undefined; -function detectLocalAndroidEmulator() { - // "DETOX_AVD_NAME" can be set for local developement - const detoxAvdName = process.env.DETOX_AVD_NAME ?? null; - if (detoxAvdName !== null) { - return detoxAvdName - } - - // Fallback: try to use Android SDK - try { - let stdout = ChildProcess.execSync("emulator -list-avds") - - // Possibly convert Buffer to string - if (typeof stdout !== 'string') { - stdout = stdout.toString(); - } - - const avdList = stdout.trim().split('\n').map(name => name.trim()); - - if (avdList.length === 0) { - throw new Error('No installed AVDs detected on the device'); - } - - // Just select first one in the list. - // TODO: consider giving user a choice here. - return avdList[0]; - } catch (error) { - const errorMessage = `Failed to find Android emulator. Set "DETOX_AVD_NAME" env variable pointing to one. Cause: ${error}` - console.error(errorMessage); - throw new Error(errorMessage); - } -} - -function detectAndroidEmulatorName() { - return isRunningCI ? CI_AVD_NAME : detectLocalAndroidEmulator(); -} - /** - * @type {Detox.DetoxConfig} + * The output of this function can be controlled through couple of env vars. + * + * * `RNS_DEVICE_SERIAL` env var can be specified in case of running + * tests with an attached Android device. It can also be an emulator. + * The expected value here is the same as you would pass to `adb -s`. + * You can find device serial by running `adb devices` command. + * + * * `RNS_AVD_NAME` env var can be specified in case of running tests on Android emulator. + * The exepected value here is the same as displayed in Android Studio or listed by + * `emulator -list-avds`. + * + * * `RNS_APPLE_SIM_NAME` env var can be set in case of running tests on iOS simulator. + * The expected value here is exactly as one listed in XCode. + * + * * `RNS_IOS_VERSION` env var can be specified to request particular iOS version + * for the given simulator. Note that required SDK & simulators must be installed. + * * @param {string} applicationName name (FabricExample / ScreensExample) * @returns {Detox.DetoxConfig} */ @@ -95,20 +74,21 @@ function commonDetoxConfigFactory(applicationName) { simulator: { type: 'ios.simulator', device: { - type: 'iPhone 16 Pro', + type: AppleDeviceUtil.resolveAppleSimulatorName(), + os: AppleDeviceUtil.getIOSVersion(), }, }, attached: { type: 'android.attached', device: { - adbName: CI_AVD_NAME, + adbName: AndroidDeviceUtil.resolveAndroidDeviceSerial(), }, utilBinaryPaths: testButlerApkPath, }, emulator: { type: 'android.emulator', device: { - avdName: detectAndroidEmulatorName(), + avdName: AndroidDeviceUtil.detectAndroidEmulatorName(), }, utilBinaryPaths: testButlerApkPath, }, @@ -147,10 +127,10 @@ function commonDetoxConfigFactory(applicationName) { app: 'android.release', }, }, - } + }; } module.exports = { commonDetoxConfigFactory, isRunningCI, -} +}; diff --git a/scripts/e2e/ios-devices.js b/scripts/e2e/ios-devices.js new file mode 100644 index 0000000000..cd14e974f6 --- /dev/null +++ b/scripts/e2e/ios-devices.js @@ -0,0 +1,28 @@ +const DEFAULT_APPLE_SIMULATOR_NAME = 'iPhone 17'; +const DEFAULT_IOS_VERSION = 'iOS 26.2'; + +/** + * @return {string} + */ +function resolveAppleSimulatorName() { + return process.env.RNS_APPLE_SIM_NAME ?? DEFAULT_APPLE_SIMULATOR_NAME; +} +/** + * @return {`iOS ${string}`} requested version of ios, or default if not specified + */ +function getIOSVersion() { + const passedVersion = process.env.RNS_IOS_VERSION; + if (passedVersion) { + if (passedVersion.startsWith('iOS ')) { + return /** @type {`iOS ${string}`} */ (passedVersion); + } + return `iOS ${passedVersion}`; + } + return DEFAULT_IOS_VERSION; +} + +module.exports = { + resolveAppleSimulatorName, + getIOSVersion, +}; +