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

View File

@@ -3,7 +3,7 @@
"strings" : {
"CFBundleDisplayName" : {
"comment" : "Bundle display name",
"shouldTranslate" : false,
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -419,12 +419,6 @@
"value" : "iNat Next"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "iNat Next"
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
@@ -461,11 +455,12 @@
"value" : "iNat Next"
}
}
}
},
"shouldTranslate" : false
},
"CFBundleName" : {
"comment" : "Bundle name",
"shouldTranslate" : false,
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -881,12 +876,6 @@
"value" : "iNaturalistReactNative"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "iNaturalistReactNative"
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
@@ -923,10 +912,12 @@
"value" : "iNaturalistReactNative"
}
}
}
},
"shouldTranslate" : false
},
"NSAppleMusicUsageDescription" : {
"comment" : "Description of why we ask for permission to access the music library.",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -1342,12 +1333,6 @@
"value" : "เพิ่มเสียงที่บันทึกไว้ไปยังการสังเกตของคุณ"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Add existing sounds to your observations."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -1804,12 +1789,6 @@
"value" : "Camera access lets you use the AI Camera and take photos."
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Camera access lets you use the AI Camera and take photos."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -1850,468 +1829,464 @@
},
"NSHumanReadableCopyright" : {
"comment" : "Copyright (human-readable)",
"shouldTranslate" : false,
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ar" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"be" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"bg" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"br" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"bs" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ca" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"cs" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"da" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"de" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"el" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "(c) iNaturalist"
"value" : ""
}
},
"en-AU" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"en-NZ" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"eo" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"es" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"es-AR" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"es-CO" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"es-CR" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"et" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"eu" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"fa" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"fi" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"fil" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"fr" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"gd" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"gl" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"gu" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"he" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"hi" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"hr" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"hu" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"id" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"it" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ja" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ka" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"kk" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"kn" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"lb" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"lt" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"lv" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"mi" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"mk" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ml" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"mr" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ms" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"nb-NO" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"nl" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"pa" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"pl" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ro" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ru" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sat" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"si" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sk" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sl" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sq" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sr" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sv" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"sw" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"ta" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"te" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"th" : {
"stringUnit" : {
"state" : "new",
"value" : "(c) iNaturalist"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"uk" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"vi" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
},
"zh-Hant-HK" : {
"stringUnit" : {
"state" : "new",
"state" : "needs_review",
"value" : "(c) iNaturalist"
}
}
}
},
"shouldTranslate" : false
},
"NSLocationAlwaysAndWhenInUseUsageDescription" : {
"comment" : "Privacy - Location Always and When In Use Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -2727,12 +2702,6 @@
"value" : "เราไม่ได้ร้องขอสิทธิ์นี้โดยเจตนา หากคุณพบข้อความนี้ กรุณาถ่ายภาพหน้าจอและส่งอีเมลไปที่ help@inaturalist.org"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -2773,6 +2742,7 @@
},
"NSLocationWhenInUseUsageDescription" : {
"comment" : "Privacy - Location When In Use Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -3188,12 +3158,6 @@
"value" : "เพิ่มพิกัด GPS ให้กับการสังเกต แสดงตำแหน่งของคุณบนแผนที่ และอื่น ๆ"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Add GPS coordinates to observations, show your location on maps, and more."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -3234,6 +3198,7 @@
},
"NSMicrophoneUsageDescription" : {
"comment" : "Privacy - Microphone Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -3649,12 +3614,6 @@
"value" : "บันทึกเสียงจากธรรมชาติ"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Record sound observations of nature."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -3695,6 +3654,7 @@
},
"NSPhotoLibraryAddUsageDescription" : {
"comment" : "Privacy - Photo Library Additions Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -4110,12 +4070,6 @@
"value" : "ส่งออกภาพถ่ายจาก iNaturalist Next ไปยังคลังภาพของคุณ"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Export iNaturalist Next photos to your library."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
@@ -4156,6 +4110,7 @@
},
"NSPhotoLibraryUsageDescription" : {
"comment" : "Privacy - Photo Library Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"af" : {
"stringUnit" : {
@@ -4571,12 +4526,6 @@
"value" : "ส่งออกและนำเข้าภาพถ่ายจาก iNaturalist Next ไปยังคลังภาพของคุณและจากคลังภาพของคุณ"
}
},
"tl" : {
"stringUnit" : {
"state" : "new",
"value" : "Export and import iNaturalist Next photos to and from your library."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",

View File

@@ -29,11 +29,11 @@ import {
useTranslation
} from "sharedHooks";
import ObsDetailsHeaderRight from "../ObsDetailsDefaultMode/ObsDetailsDefaultModeHeaderRight";
import ActivityTab from "./ActivityTab/ActivityTab";
import FloatingButtons from "./ActivityTab/FloatingButtons";
import DetailsTab from "./DetailsTab/DetailsTab";
import FaveButton from "./FaveButton";
import ObsDetailsHeader from "./ObsDetailsHeader";
import ObsDetailsOverview from "./ObsDetailsOverview";
import ObsMediaDisplayContainer from "./ObsMediaDisplayContainer";
import AgreeWithIDSheet from "./Sheets/AgreeWithIDSheet";
@@ -129,7 +129,6 @@ const ObsDetails = ( {
const scrollViewRef = useRef( );
const insets = useSafeAreaInsets();
const { t } = useTranslation( );
const [invertToWhiteBackground, setInvertToWhiteBackground] = useState( false );
// Scroll the scrollview to this y position once if set, then unset it.
// Could be refactored into a hook if we need this logic elsewher
@@ -150,14 +149,6 @@ const ObsDetails = ( {
}
}, [addingActivityItem] );
const handleScroll = e => {
const scrollY = e.nativeEvent.contentOffset.y;
const shouldInvert = !!( scrollY > 150 );
if ( shouldInvert !== invertToWhiteBackground ) {
setInvertToWhiteBackground( shouldInvert );
}
};
const dynamicInsets = useMemo( () => ( {
backgroundColor: "#ffffff",
paddingTop: insets.top,
@@ -196,6 +187,16 @@ const ObsDetails = ( {
</HideView>
);
const renderHeaderRight = ( ) => (
<ObsDetailsHeaderRight
belongsToCurrentUser={belongsToCurrentUser}
observationId={observation?.id}
uuid={observation?.uuid}
refetchSubscriptions={refetchSubscriptions}
subscriptions={subscriptions}
/>
);
const renderTablet = () => (
<View className="flex-1 flex-row bg-white">
<View className="w-[33%]">
@@ -226,7 +227,6 @@ const ObsDetails = ( {
className="flex-1 flex-column"
stickyHeaderHiddenOnScroll
endFillColor="white"
onScroll={handleScroll}
>
<View className="bg-white h-full">
{renderActivityTab( )}
@@ -246,15 +246,7 @@ const ObsDetails = ( {
/>
)}
</View>
<ObsDetailsHeader
belongsToCurrentUser={belongsToCurrentUser}
invertToWhiteBackground={invertToWhiteBackground}
observationId={observation?.id}
rightIconDarkGray
uuid={observation?.uuid}
refetchSubscriptions={refetchSubscriptions}
subscriptions={subscriptions}
/>
{renderHeaderRight( )}
</View>
);
@@ -266,17 +258,9 @@ const ObsDetails = ( {
stickyHeaderIndices={[0, 3]}
scrollEventThrottle={16}
endFillColor="white"
onScroll={handleScroll}
>
<ObsDetailsHeader
belongsToCurrentUser={belongsToCurrentUser}
subscriptions={subscriptions}
invertToWhiteBackground={invertToWhiteBackground}
observationId={observation?.id}
uuid={observation?.uuid}
refetchSubscriptions={refetchSubscriptions}
/>
<View className="-mt-[64px]">
{renderHeaderRight( )}
<View>
<ObsMediaDisplayContainer observation={observation} />
{ currentUser && (
<FaveButton

View File

@@ -1,94 +0,0 @@
import classnames from "classnames";
import {
INatIconButton,
OverlayHeader
} from "components/SharedComponents";
import {
LinearGradient
} from "components/styledComponents";
import React from "react";
import DeviceInfo from "react-native-device-info";
import type { RealmObservation } from "realmModels/types";
import {
useLocalObservation,
useNavigateToObsEdit,
useTranslation
} from "sharedHooks";
import colors from "styles/tailwindColors";
import HeaderKebabMenu from "./HeaderKebabMenu";
const isTablet = DeviceInfo.isTablet( );
interface Props {
belongsToCurrentUser?: boolean,
invertToWhiteBackground: boolean,
subscriptions: Object,
observationId: number,
refetchSubscriptions: Function,
rightIconDarkGray?: boolean,
uuid: string
}
const ObsDetailsHeader = ( {
belongsToCurrentUser,
invertToWhiteBackground,
subscriptions,
observationId,
refetchSubscriptions,
rightIconDarkGray = false,
uuid
}: Props ) => {
const localObservation = useLocalObservation( uuid );
const navigateToObsEdit = useNavigateToObsEdit( );
const { t } = useTranslation( );
const whiteIcon = !rightIconDarkGray && !invertToWhiteBackground;
return (
<LinearGradient
className={classnames(
"h-16 transparent"
)}
colors={[
isTablet
? "rgba(0,0,0,0.1)"
: "rgba(0,0,0,0.6)",
"transparent"
]}
>
<OverlayHeader
testID="ObsDetails.BackButton"
invertToWhiteBackground={invertToWhiteBackground}
headerRight={
belongsToCurrentUser
? (
<INatIconButton
testID="ObsDetail.editButton"
// TODO remove this cast when useLocalObservation is properly typed
onPress={() => navigateToObsEdit( localObservation as RealmObservation )}
icon="pencil"
color={String(
whiteIcon
? colors?.white
: colors?.darkGray
)}
accessibilityLabel={t( "Edit" )}
/>
)
: (
<HeaderKebabMenu
observationId={observationId}
white={whiteIcon}
subscriptions={subscriptions}
uuid={uuid}
refetchSubscriptions={refetchSubscriptions}
/>
)
}
/>
</LinearGradient>
);
};
export default ObsDetailsHeader;

View File

@@ -21,7 +21,7 @@ interface Props {
subscriptions: Object
}
const ObsDetailsDefaultModeHeader = ( {
const ObsDetailsDefaultModeHeaderRight = ( {
belongsToCurrentUser,
observationId,
uuid,
@@ -73,4 +73,4 @@ const ObsDetailsDefaultModeHeader = ( {
return null;
};
export default ObsDetailsDefaultModeHeader;
export default ObsDetailsDefaultModeHeaderRight;

View File

@@ -170,6 +170,12 @@ const LIST_OPTIONS = {
lazy: true
};
const OBS_DETAILS_OPTIONS = {
unmountOnBlur: true,
...showHeader,
...blankHeaderTitle
};
const Stack = createNativeStackNavigator( );
export const SCREEN_NAME_OBS_LIST = "ObsList";
@@ -216,20 +222,14 @@ const TabStackNavigator = ( ): Node => {
<Stack.Screen
name="ObsDetails"
component={FadeInObsDetailsDefaultModeContainer}
options={{
unmountOnBlur: true,
...showHeader,
...blankHeaderTitle
}}
options={OBS_DETAILS_OPTIONS}
/>
)
: (
<Stack.Screen
name="ObsDetails"
component={FadeInObsDetailsContainer}
options={{
unmountOnBlur: true
}}
options={OBS_DETAILS_OPTIONS}
/>
)}
</Stack.Group>

View File

@@ -21,7 +21,7 @@ const baseHeaderOptions: Object = {
headerShown: true,
headerBackTitleVisible: false,
headerShadowVisible: false,
headerLeft: () => <BackButton inCustomHeader />
headerLeft: () => <BackButton inCustomHeader testID="header-back-button" />
};
const showHeader: Object = {

View File

@@ -258,58 +258,7 @@ describe( "ObsDetails", () => {
// );
} );
describe( "viewing own observation", ( ) => {
async function expectEditAndNotMenu( ) {
const editLabelText = t( "Edit" );
const editButton = await screen.findByLabelText( editLabelText );
expect( editButton ).toBeTruthy( );
const kebabMenuLabelText = t( "Observation-options" );
const kebabMenu = screen.queryByLabelText( kebabMenuLabelText );
expect( kebabMenu ).toBeFalsy( );
}
it( "should show the edit button and not the menu", async ( ) => {
const mockOwnObservation = factory( "LocalObservation", { user: mockUser } );
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => mockOwnObservation );
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => mockOwnObservation.user );
renderObsDetails( );
expect( mockOwnObservation.user.id ).toEqual( mockUser.id );
await expectEditAndNotMenu( );
} );
it(
"should show the edit button and not the menu when the observation has never been uploaded",
async ( ) => {
const observation = factory( "LocalObservation" );
jest.spyOn( useLocalObservation, "default" )
.mockImplementation( () => observation );
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => observation.user );
renderObsDetails( );
// An unuploaded observation *should* be the only situation where an
// observation has no user, b/c a user can make observations before
// signing in
expect( observation.user ).toBeFalsy( );
await expectEditAndNotMenu( );
}
);
} );
describe( "viewing someone else's observation", ( ) => {
it( "should show the menu and not the edit button", async ( ) => {
expect( mockObservation.user.id ).not.toEqual( mockUser.id );
renderObsDetails( );
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => mockObservation );
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => null );
renderObsDetails( );
expect( await screen.findByTestId( `ObsDetails.${mockObservation.uuid}` ) ).toBeTruthy( );
const kebabMenuLabelText = t( "Observation-options" );
const kebabMenu = await screen.findByLabelText( kebabMenuLabelText );
expect( kebabMenu ).toBeTruthy( );
const editLabelText = t( "Edit" );
const editButton = screen.queryByLabelText( editLabelText );
expect( editButton ).toBeFalsy( );
} );
it( "should agree with another user's identification when agree button pressed", async ( ) => {
const firstIdentification = factory( "RemoteIdentification", {
taxon: factory( "RemoteTaxon", {

View File

@@ -1,63 +0,0 @@
import { fireEvent, screen } from "@testing-library/react-native";
import ObsDetailsHeader from "components/ObsDetails/ObsDetailsHeader.tsx";
import i18next from "i18next";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
jest.mock( "react-native/Libraries/Share/Share", () => ( {
share: jest.fn( () => Promise.resolve( "mockResolve" ) )
} ) );
jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
OS: "ios",
select: jest.fn( )
} ) );
describe( "ObsDetailsHeader", () => {
it( "shows options menu when viewing someone else's observation", async ( ) => {
renderComponent(
<ObsDetailsHeader
observation={factory( "RemoteObservation" )}
belongsToCurrentUser={false}
/>
);
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeTruthy( );
fireEvent.press( anchorButton );
const shareButton = await screen.findByTestId( "MenuItem.Share" );
expect( shareButton ).toBeTruthy( );
const enableNotificationsButton = await screen.findByTestId( "MenuItem.EnableNotifications" );
expect( enableNotificationsButton ).toBeTruthy( );
} );
it( "shows ignore notification button in options menu when subscribed to obs", async ( ) => {
renderComponent(
<ObsDetailsHeader
observation={factory( "RemoteObservation" )}
belongsToCurrentUser={false}
subscriptions={[{ id: 324810661, user_id: 6280741 }]}
/>
);
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeTruthy( );
fireEvent.press( anchorButton );
const shareButton = await screen.findByTestId( "MenuItem.Share" );
expect( shareButton ).toBeTruthy( );
const ignoreNotificationsButton = await screen.findByTestId( "MenuItem.IgnoreNotifications" );
expect( ignoreNotificationsButton ).toBeTruthy( );
} );
it( "does not show options menu when observation belongs to current user", ( ) => {
renderComponent(
<ObsDetailsHeader
observation={factory( "RemoteObservation" )}
belongsToCurrentUser
/>
);
const editButton = screen.getByTestId( "ObsDetail.editButton" );
expect( editButton ).toBeVisible( );
const anchorButton = screen.queryByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeFalsy( );
} );
} );