Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,5 @@ android/.settings
# Local environment (direnv)
.envrc

# e2e
*Example/artifacts
2 changes: 1 addition & 1 deletion Example/.detoxrc.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
const utils = require('../scripts/detox-utils.cjs');
const utils = require('../scripts/e2e/detox-utils.cjs');
module.exports = utils.commonDetoxConfigFactory('ScreensExample');
8 changes: 4 additions & 4 deletions Example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion FabricExample/.detoxrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const utils = require('../scripts/detox-utils.cjs');
const utils = require('../scripts/e2e/detox-utils.cjs');
module.exports = utils.commonDetoxConfigFactory('FabricExample');

28 changes: 28 additions & 0 deletions FabricExample/e2e/component-objects/back-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { device, element, by } from 'detox';
import { getiosVersion } from '../../../scripts/e2e/ios-devices.js';
import semverSatisfies from 'semver/functions/satisfies';
import semverCoerce from 'semver/functions/coerce';

const backButtonElement = element(by.id('BackButton'));

export async function getBackButton() {
const platform = device.getPlatform();
if (platform === 'ios') {
return getiOSBackButton();
} else if (platform === 'android') {
return backButtonElement;
} else throw new Error(`Platform "${platform}" not supported`);
}
async function getiOSBackButton() {
const iosVersion = semverCoerce(getiosVersion())!;
if (semverSatisfies(iosVersion, '>=26.0')) {
const elementsByAttributes = await backButtonElement.getAttributes() as unknown as { elements: { className: string }[] };
const elements = elementsByAttributes.elements;
if (Array.isArray(elements)) {
return backButtonElement.atIndex(elements.findIndex(
elem => elem.className === '_UIButtonBarButton'
));
}
}
return backButtonElement;
}
5 changes: 4 additions & 1 deletion FabricExample/e2e/e2e-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export async function selectTestScreen(screenName: string) {
// More details: https://github.com/software-mansion/react-native-screens/pull/2919
await device.pressBack();
} else {
await element(by.type('UISearchBarTextField')).replaceText(screenName);
await waitFor(element(by.id('root-screen-tests-' + screenName)))
.toBeVisible()
.whileElement(by.id('root-screen-examples-scrollview'))
.scroll(600, 'down', Number.NaN, 0.85);
}

await expect(element(by.id(`root-screen-tests-${screenName}`))).toBeVisible();
Expand Down
3 changes: 2 additions & 1 deletion FabricExample/e2e/issuesTests/Test2926.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { describeIfiOS, selectTestScreen } from '../e2e-utils';
import { getBackButton } from '../component-objects/back-button';

// PR related to iOS search bar
describeIfiOS('Test2926', () => {
Expand All @@ -26,7 +27,7 @@ describeIfiOS('Test2926', () => {
await element(by.type('UISearchBarTextField')).replaceText('Item 2');
await element(by.id('home-button-open-second')).tap();

await element(by.id('BackButton')).tap();
await (await getBackButton()).tap();

await expect(element(by.type('UISearchBarTextField'))).toBeVisible();
await expect(element(by.type('UISearchBarTextField'))).toHaveText('Item 2');
Expand Down
5 changes: 3 additions & 2 deletions FabricExample/e2e/issuesTests/Test432.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { selectTestScreen } from '../e2e-utils';
import { getBackButton } from '../component-objects/back-button';

describe('Test432', () => {
beforeAll(async () => {
Expand All @@ -26,7 +27,7 @@ describe('Test432', () => {
await expect(element(by.id('details-headerRight-red'))).toBeVisible(100);

if (device.getPlatform() === 'ios') {
await element(by.id('BackButton')).tap();
await (await getBackButton()).tap();
} else {
await device.pressBack();
}
Expand All @@ -47,7 +48,7 @@ describe('Test432', () => {
waitFor(element(by.id('info-headerRight-green-1'))).toBeVisible(100);

if (device.getPlatform() === 'ios') {
await element(by.id('BackButton')).tap();
await (await getBackButton()).tap();
} else {
await device.pressBack();
}
Expand Down
4 changes: 3 additions & 1 deletion FabricExample/e2e/issuesTests/Test528.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { device, expect, element, by } from 'detox';
import { describeIfiOS, selectTestScreen } from '../e2e-utils';
import { getBackButton } from '../component-objects/back-button';

// Detox currently supports orientation only on iOS
describeIfiOS('Test528', () => {
Expand All @@ -22,7 +23,8 @@ describeIfiOS('Test528', () => {
it('headerRight button should be visible after coming back from horizontal screen', async () => {
await element(by.text('Go to Screen 2')).tap();
await device.setOrientation('landscape');
await element(by.id('BackButton')).tap();

await (await getBackButton()).tap();
await expect(element(by.text('Custom Button'))).toBeVisible(100);
await device.setOrientation('portrait');
await expect(element(by.text('Custom Button'))).toBeVisible(100);
Expand Down
10 changes: 6 additions & 4 deletions FabricExample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -51,13 +51,15 @@
"@types/jest": "^29.5.13",
"@types/react": "^19.1.1",
"@types/react-test-renderer": "^19.1.0",
"@types/semver": "^7",
"babel-jest": "^29.6.3",
"detox": "^20.45.1",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"patch-package": "^8.0.0",
"prettier": "2.8.8",
"react-test-renderer": "19.1.1",
"semver": "^7.7.3",
"ts-jest": "^29.0.3",
"typescript": "5.0.4"
},
Expand Down
5 changes: 4 additions & 1 deletion FabricExample/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "../tsconfig.json"
"extends": "../tsconfig.json",
"compilerOptions": {
"allowJs": true,
},
}
18 changes: 18 additions & 0 deletions FabricExample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3076,6 +3076,13 @@ __metadata:
languageName: node
linkType: hard

"@types/semver@npm:^7":
version: 7.7.1
resolution: "@types/semver@npm:7.7.1"
checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268
languageName: node
linkType: hard

"@types/stack-utils@npm:^2.0.0":
version: 2.0.3
resolution: "@types/stack-utils@npm:2.0.3"
Expand Down Expand Up @@ -3316,6 +3323,7 @@ __metadata:
"@types/jest": "npm:^29.5.13"
"@types/react": "npm:^19.1.1"
"@types/react-test-renderer": "npm:^19.1.0"
"@types/semver": "npm:^7"
babel-jest: "npm:^29.6.3"
detox: "npm:^20.45.1"
eslint: "npm:^8.19.0"
Expand All @@ -3333,6 +3341,7 @@ __metadata:
react-native-screens: "link:../"
react-native-worklets: "npm:~0.6.0"
react-test-renderer: "npm:19.1.1"
semver: "npm:^7.7.3"
ts-jest: "npm:^29.0.3"
typescript: "npm:5.0.4"
languageName: unknown
Expand Down Expand Up @@ -9464,6 +9473,15 @@ __metadata:
languageName: node
linkType: hard

"semver@npm:^7.7.3":
version: 7.7.3
resolution: "semver@npm:7.7.3"
bin:
semver: bin/semver.js
checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e
languageName: node
linkType: hard

"send@npm:0.19.0":
version: 0.19.0
resolution: "send@npm:0.19.0"
Expand Down
164 changes: 164 additions & 0 deletions scripts/e2e/android-devices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const semverSatisfies = require('semver/functions/satisfies');
const semverCoerce = require('semver/functions/coerce');
const semverMaxSatisfying = require('semver/ranges/max-satisfying');
const SemVer = require('semver/classes/semver');
const { bootDevices } = require('./turn-on-android-devices');
const { getOneLineCommandLineResponse, getCommandLineResponse } = require('./command-line-helpers');
const { assertError } = require('./errors-helpers');

const CI_AVD_NAME = 'e2e_emulator';
const SUPPORTED_API_LEVEL_RANGE = '>=25'; // Android 7.1.1
const isRunningCI = process.env.CI;

function detectAndroidEmulatorName() {
return isRunningCI ? CI_AVD_NAME : detectLocalAndroidEmulator();
}

function detectLocalAndroidEmulator() {
// "DETOX_AVD_NAME" can be set for local developement
const detoxAvdName = process.env.DETOX_AVD_NAME;
if (detoxAvdName) return detoxAvdName;
bootInactiveDevices();

const deviceIds = getDeviceIds();
const devices = deviceIds.map(id => ({id, name: getDeviceName(id)}));
const requestedAPILevel = getPassedAndroidAPILevel();
if (!requestedAPILevel) {
return findDeviceWithTheHighestAPILevel(devices).name;
}
const requestedEmulator = devices.find(emulator => getEmulatorAPILevel(emulator.id) === requestedAPILevel);
if (requestedEmulator) {
return requestedEmulator.name
}
throw new Error(`Android emulator with API level ${requestedAPILevel} is not available (Create a new one).`)
}

/**
* attaches all available devices to be able to call them via adb
*/
function bootInactiveDevices() {
const allAvailableEmulatorNames = getAvailableEmulatorNames();
try {
const nowRunningDevices = new Set(getDeviceIds().map(getDeviceName));
bootDevices(allAvailableEmulatorNames.filter(deviceName => !nowRunningDevices.has(deviceName)));
} catch(e) {
assertError(e);
bootDevices(allAvailableEmulatorNames);
}
}

function getAvailableEmulatorNames() {
try {
const outputText = getCommandLineResponse("emulator -list-avds");
const avdList = outputText.trim().split('\n').map(name => name.trim());
if (avdList.length === 0) {
throw new Error('No installed AVDs detected on the device');
}

return avdList;
} catch (error) {
const errorMessage = `Failed to find any Android emulator. Set "DETOX_AVD_NAME" env variable pointing to one. Cause:\n${error}`
throw new Error(errorMessage);
}
}

function getPassedAndroidAPILevel() {
const passedAPILevel = process.env.E2E_ANDROID_API_LEVEL;
if (passedAPILevel) {
const semverVersion = semverCoerce(passedAPILevel);
if (!semverVersion) {
throw new Error(`Android API version ${passedAPILevel}. Doesn't seem right.`);
}
if (!semverSatisfies(semverVersion, SUPPORTED_API_LEVEL_RANGE)) {
console.warn(`⚠️Android API version ${passedAPILevel} may be not supported!⚠️`);
};
return passedAPILevel;
}
}

/**
* @param {string} emulatorId
* @returns device's API Level or null if failed
*/
function getEmulatorAPILevel(emulatorId) {
try {
return getOneLineCommandLineResponse(`adb -s ${emulatorId} shell getprop ro.build.version.sdk`);
} catch (error) {
assertError(error);
const errorMessage = `Android emulator "${emulatorId}" doesn't want to share its API Level 👹.\nCause: ${error?.message}`
console.warn(errorMessage);
console.warn('SKIPPING THIS DEVICE...');
return null;
}
}

/**
* @param {{name: string, id: string}[]} devices
*/
function findDeviceWithTheHighestAPILevel(devices){
/**
* @type {Map<string, {name: string, id: string}>}
*/
const versionToDeviceName = new Map();
for (const device of devices) {
const apiLevel = getEmulatorAPILevel(device.id);
if (!apiLevel) continue;
const parsedVersion = semverCoerce(apiLevel);
if (!parsedVersion) continue;
versionToDeviceName.set(parsedVersion.toString(), device);
}
const versions = Array.from(versionToDeviceName.keys());
const highestAvailableVersion = semverMaxSatisfying(
versions.filter(isValidSemVer).map(levelAPIVersion => semverCoerce(levelAPIVersion)).filter(isValidSemVer),
SUPPORTED_API_LEVEL_RANGE
);
const result = versionToDeviceName.get(String(highestAvailableVersion));
if (!result) {
throw new Error('Something went wrong (Implementation error?)');
}
return result;
}

/**
* @returns {string[]}
*/
function getDeviceIds() {
const nonEmptyLines = getCommandLineResponse('adb devices').split('\n').map(line => line.trim()).filter(Boolean);
nonEmptyLines.shift(); // The first line is always the "List of devices attached" (header) so we remove it
if (nonEmptyLines.length === 0) {
throw new Error('The device list (from adb) is empty.');
}
return nonEmptyLines.map(line => {
const [id, state] = line.split('\t');
if (state !== 'device') {
console.warn(`THE DEVICE (ID ${id}) HAS STATUS "${state}". ITS STATUS SHOULD BE 'device' TO CONTINUE!`);
}
return id;
});
}

/**
* @param {unknown} value
* @returns {value is SemVer}
*/
function isValidSemVer(value) {
return value instanceof SemVer || typeof value === 'string' && Boolean(new SemVer(value));
}

/**
* Turns on (attaches) the device
* @param {string} deviceId
* @returns {string} device name (avd name)
*/
function getDeviceName(deviceId) {
const deviceName = getCommandLineResponse(`adb -s ${deviceId} emu avd name`).split('\r\n')[0];
if (deviceName) {
return deviceName;
}
throw new Error(`Failed to get device name for id "${deviceId}"`);
}


module.exports = {
detectAndroidEmulatorName
}
Loading
Loading