Use fixed header in ObsDetails advanced mode (#2741)

* Use fixed header on ObsDetails advanced mode

* Remove test for removed component - using react navigation header instead

* Remove tests related to ObsDetails header icon; using react navigation

* Add testID to react navigation level back button and fix e2e

* Terminate app after every e2e test and maybe help flakiness

* Rework termination of app

---------

Co-authored-by: Johannes Klein <johannes.t.klein@gmail.com>
This commit is contained in:
Amanda Bullington
2025-03-12 02:38:35 -07:00
committed by GitHub
parent 1347ca3488
commit 9b1275a6eb
13 changed files with 222 additions and 400 deletions

View File

@@ -2,7 +2,7 @@ import {
by, device, element, waitFor
} from "detox";
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import { iNatE2eAfterEach, iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import closeOnboarding from "./sharedFlows/closeOnboarding";
import deleteObservation from "./sharedFlows/deleteObservation";
import signIn from "./sharedFlows/signIn";
@@ -13,6 +13,7 @@ const TIMEOUT = 10_000;
describe( "AICamera", () => {
beforeAll( async () => iNatE2eBeforeAll( device ) );
beforeEach( async () => iNatE2eBeforeEach( device ) );
afterEach( async () => iNatE2eAfterEach( device ) );
it(
"should open the ai camera, take photo, select a suggestion, upload and delete observation",

View File

@@ -1,4 +1,4 @@
import { execSync } from "child_process";
import { exec, execSync } from "child_process";
export async function iNatE2eBeforeAll( device ) {
if ( device.getPlatform() === "android" ) {
@@ -53,3 +53,100 @@ export async function iNatE2eBeforeEach( device ) {
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`
);
}
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 );
} );
} );
}
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 ) {
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 );
}
}

View File

@@ -1,16 +1,13 @@
// we don't need this or switchPowerMode.e2e.js.js since they're repetititve
// with what we're already doing in the signedIn and aiCamera tests
// and we would see any failures there
import { device } from "detox";
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import { iNatE2eAfterEach, iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import closeOnboarding from "./sharedFlows/closeOnboarding";
import signIn from "./sharedFlows/signIn";
describe( "Shared flow", () => {
beforeAll( async () => iNatE2eBeforeAll( device ) );
beforeEach( async () => iNatE2eBeforeEach( device ) );
afterEach( async ( ) => iNatE2eAfterEach( device ) );
it( "should sign in the test user", async () => {
await closeOnboarding( );

View File

@@ -2,7 +2,7 @@ import {
by, device, element, expect, waitFor
} from "detox";
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import { iNatE2eAfterEach, iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import closeOnboarding from "./sharedFlows/closeOnboarding";
import deleteObservation from "./sharedFlows/deleteObservation";
import signIn from "./sharedFlows/signIn";
@@ -11,6 +11,7 @@ import uploadObservation from "./sharedFlows/uploadObservation";
describe( "Signed in user", () => {
beforeAll( async ( ) => iNatE2eBeforeAll( device ) );
beforeEach( async ( ) => iNatE2eBeforeEach( device ) );
afterEach( async ( ) => iNatE2eAfterEach( device ) );
async function createAndUploadObservation( options = { upload: false } ) {
const addObsButton = element( by.id( "add-obs-button" ) );
@@ -101,7 +102,7 @@ describe( "Signed in user", () => {
await element( by.id( `ObsDetails.${uuid}` ) ).scrollTo( "bottom" );
const comment = element( by.text( "This is a comment" ) );
await waitFor( comment ).toBeVisible().withTimeout( 10000 );
await element( by.id( "ObsDetails.BackButton" ) ).tap( );
await element( by.id( "header-back-button" ) ).tap( );
await waitFor( username ).toBeVisible( ).withTimeout( 10000 );
/*

View File

@@ -6,12 +6,13 @@ import {
waitFor
} from "detox";
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import { iNatE2eAfterEach, iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
import closeOnboarding from "./sharedFlows/closeOnboarding";
describe( "Signed out user", () => {
beforeAll( async ( ) => iNatE2eBeforeAll( device ) );
beforeEach( async ( ) => iNatE2eBeforeEach( device ) );
afterEach( async ( ) => iNatE2eAfterEach( device ) );
it( "should start at My Observations with log in text", async () => {
await closeOnboarding( );