Files
iNaturalistReactNative/e2e/helpers.js
Johannes Klein 626b25314f Update existing android detox tests (#3632)
* Remove android specific launch and early return

* Remove no longer needed other launchApp command

* Update installData e2e mock

* Tap container to dismiss keyboard

* Add testID to login container

* Push a test image into the app's external files directory

* Mock camer takePhoto on Android

* Use a jpg that results in suggestions

* Update e2e camera mock to not use frameProcessor when not active

* Change vision camera mock to create a destination path with additional /

* To remove possibility for flakiness prescribe an emulator

* Add a geocoder wrapper file

* Revert "Add a geocoder wrapper file"

This reverts commit 6c7b6c61a5.
2026-05-19 20:35:38 +02:00

157 lines
5.3 KiB
JavaScript

import { exec, execSync } from "child_process";
import resetUserForTesting from "./sharedFlows/resetUserForTesting";
function execPromise( command ) {
return new Promise( ( resolve, reject ) => {
exec( command, ( error, stdout, stderr ) => {
if ( error ) {
console.log( `Error executing command: ${command}` );
console.log( `stderr: ${stderr}` );
reject( error );
return;
}
resolve( stdout );
} );
} );
}
export async function iNatE2eBeforeAll( device ) {
await resetUserForTesting();
if ( device.getPlatform( ) === "android" ) {
// Push a test image into the app's external files directory so the mock
// camera can use it as a photo source (copyAssetsFileIOS is iOS-only).
// The directory is created by the app on first launch, so we ensure it
// exists before pushing.
await execPromise(
"adb shell mkdir -p /sdcard/Android/data/org.inaturalist.iNaturalistMobile/files/",
);
await execPromise(
"adb push e2e/animal.jpg"
+ " /sdcard/Android/data/org.inaturalist.iNaturalistMobile/files/e2e_test.jpg",
);
}
}
export async function iNatE2eBeforeEach( device ) {
const launchAppOptions = {
newInstance: true,
permissions: {
location: "always",
camera: "YES",
medialibrary: "YES",
photos: "YES",
},
};
try {
await device.launchApp( launchAppOptions );
} catch ( launchAppError ) {
if ( !launchAppError.message.match( /unexpectedly disconnected/ ) ) {
throw launchAppError;
}
// Try it one more time
await device.launchApp( launchAppOptions );
}
if ( device.getPlatform( ) === "ios" ) {
// disable password autofill
execSync(
// eslint-disable-next-line max-len
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
);
execSync(
// eslint-disable-next-line max-len
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
);
execSync(
// eslint-disable-next-line max-len
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
);
}
}
async function getSimulatorId() {
try {
// List all available simulators
const output = await execPromise( "xcrun simctl list devices --json" );
const { devices } = JSON.parse( output );
// Use Object.values and Array.find instead of loops
const bootedDevice = Object.entries( devices )
.flatMap( ( [_runtime, deviceList] ) => deviceList ) // Use _ prefix for unused variables
.find( device => device.state === "Booted" );
if ( bootedDevice ) {
console.log( `Found booted simulator: ${bootedDevice.name} (${bootedDevice.udid})` );
return bootedDevice.udid;
}
console.log( "No booted simulator found" );
return null;
} catch ( error ) {
console.log( "Error getting simulator ID:", error.message );
return null;
}
}
export function terminateApp( deviceId, bundleId ) {
try {
console.log( `Attempting to terminate ${bundleId} on device ${deviceId}...` );
const result = execSync( `/usr/bin/xcrun simctl terminate ${deviceId} ${bundleId}` );
console.log( "App terminated successfully", result.toString() );
return true;
} catch ( error ) {
if ( error.stderr && error.stderr.toString().includes( "found nothing to terminate" ) ) {
console.log( "App is not running, nothing to terminate." );
return true;
}
console.error( "Error during app termination:", error.message );
return false;
}
}
export async function iNatE2eAfterEach( device ) {
await resetUserForTesting();
if ( device && device.getPlatform() === "android" ) {
return;
}
try {
// Try to use device.terminateApp first (the built-in Detox method)
if ( device ) {
try {
await device.terminateApp();
console.log( "App terminated through Detox" );
// Add a small delay to let Detox processes settle
await new Promise( resolve => { setTimeout( resolve, 300 ); } );
return;
} catch ( detoxError ) {
console.log(
"Detox terminateApp failed, falling back to manual termination:",
detoxError.message,
);
}
}
// Fall back to manual termination if Detox method fails or device is unavailable
const deviceId = await getSimulatorId();
const bundleId = "org.inaturalist.iNaturalistMobile";
if ( deviceId && bundleId ) {
console.log( "Using manual termination via simctl" );
// Use existing terminateApp, but don't throw errors
try {
await terminateApp( deviceId, bundleId );
} catch ( error ) {
console.log( "Manual termination error (non-fatal):", error.message );
}
// Add a delay to let processes settle
await new Promise( resolve => { setTimeout( resolve, 500 ); } );
}
} catch ( error ) {
console.log( "Error during cleanup (non-fatal):", error.message );
}
}