Handle merge conflicts from main

This commit is contained in:
Amanda Bullington
2025-04-07 10:52:14 -07:00
26 changed files with 322 additions and 212 deletions

View File

@@ -111,8 +111,8 @@ android {
applicationId "org.inaturalist.iNaturalistMobile"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 156
versionName "0.59.13"
versionCode 157
versionName "0.59.14"
setProperty("archivesBaseName", applicationId + "-v" + versionName + "+" + versionCode)
manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
// Detox Android setup

View File

@@ -0,0 +1 @@
Bug fixes for location fetching while creating an observation and for bulk observation uploads

View File

@@ -646,7 +646,7 @@
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNative.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CURRENT_PROJECT_VERSION = 156;
CURRENT_PROJECT_VERSION = 157;
DEVELOPMENT_TEAM = N5J7L4P93Z;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -773,7 +773,7 @@
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNativeRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CURRENT_PROJECT_VERSION = 156;
CURRENT_PROJECT_VERSION = 157;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
HEADER_SEARCH_PATHS = (
@@ -1053,7 +1053,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "iNaturalistReactNative-ShareExtension/iNaturalistReactNative-ShareExtension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 156;
CURRENT_PROJECT_VERSION = 157;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
@@ -1098,7 +1098,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 156;
CURRENT_PROJECT_VERSION = 157;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.59.13</string>
<string>0.59.14</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -40,7 +40,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>156</string>
<string>157</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>

View File

@@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.59.13</string>
<string>0.59.14</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>156</string>
<string>157</string>
</dict>
</plist>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "inaturalistreactnative",
"version": "0.59.13",
"version": "0.59.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "inaturalistreactnative",
"version": "0.59.13",
"version": "0.59.14",
"hasInstallScript": true,
"dependencies": {
"@bam.tech/react-native-image-resizer": "^3.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "inaturalistreactnative",
"version": "0.59.13",
"version": "0.59.14",
"private": true,
"scripts": {
"android": "react-native run-android",

View File

@@ -13,9 +13,10 @@ import { Alert, StatusBar } from "react-native";
import type {
TakePhotoOptions
} from "react-native-vision-camera";
import fetchAccurateUserLocation from "sharedHelpers/fetchAccurateUserLocation.ts";
import { createSentinelFile, deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles.ts";
import {
useDeviceOrientation, useTranslation, useWatchPosition
useDeviceOrientation, useTranslation
} from "sharedHooks";
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import useStore from "stores/useStore";
@@ -77,22 +78,17 @@ const CameraContainer = ( ) => {
}, [cameraType, sentinelFileName] );
const { deviceOrientation } = useDeviceOrientation( );
// Check if location permission granted b/c usePrepareStoreAndNavigate and
// useUserLocation need to know if permission has been granted to fetch the
// user's location while the camera is active. We don't want to *ask* for
// permission here b/c we want to avoid overloading a new user with
// permission requests and they will just have seen the camera permission
// request before landing here, so it's ok if we're not fetching the
// location here for the user's first observation (suggestions might be a
// bit off and we'll fetch the obs coordinates on ObsEdit)
const {
hasPermissions: hasLocationPermissions,
renderPermissionsGate: renderLocationPermissionsGate,
requestPermissions: requestLocationPermissions
} = useLocationPermission( );
const { userLocation } = useWatchPosition( {
shouldFetchLocation: !!( hasLocationPermissions )
} );
// we don't want to use this for the observation location because
// a user could be walking with the camera open for a while, so this location
// will not reflect when they actually took the photo
const [userLocationForGeomodel, setUserLocationForGeomodel] = useState( null );
const navigation = useNavigation( );
const { t } = useTranslation( );
@@ -123,13 +119,10 @@ const CameraContainer = ( ) => {
const generateSentinelFile = async ( ) => {
const fileName = await createSentinelFile( "AICamera" );
setSentinelFileName( fileName );
if ( hasLocationPermissions ) {
await logStage( fileName, "fetch_user_location_start" );
}
};
if ( cameraType !== "AI" ) { return; }
generateSentinelFile( );
}, [setSentinelFileName, cameraType, hasLocationPermissions] );
}, [setSentinelFileName, cameraType] );
const {
hasPermissions: hasSavePhotoPermission,
@@ -152,9 +145,8 @@ const CameraContainer = ( ) => {
};
const navigationOptions = useMemo( ( ) => ( {
addPhotoPermissionResult,
userLocation
} ), [addPhotoPermissionResult, userLocation] );
addPhotoPermissionResult
} ), [addPhotoPermissionResult] );
const prepareStoreAndNavigate = usePrepareStoreAndNavigate( );
@@ -165,8 +157,15 @@ const CameraContainer = ( ) => {
newPhotoState: PhotoState,
visionResult: StoredResult | null
) => {
// fetch accurate user location, with a fallback to a course location
// at the time the user taps AI shutter or multicapture checkmark
// to create an observation
// this handles checking for location, and we do *not* want to show
// location permissions in the camera, so we no longer need to check for that
const accurateUserLocation = await fetchAccurateUserLocation( );
await prepareStoreAndNavigate( {
...navigationOptions,
userLocation: accurateUserLocation,
newPhotoState,
logStageIfAICamera,
deleteStageIfAICamera,
@@ -258,6 +257,18 @@ const CameraContainer = ( ) => {
return uri;
};
useEffect( ( ) => {
const fetchLocation = async ( ) => {
const accurateUserLocation = await fetchAccurateUserLocation( );
setUserLocationForGeomodel( accurateUserLocation );
return accurateUserLocation;
};
if ( hasLocationPermissions ) {
fetchLocation( );
}
}, [hasLocationPermissions] );
if ( !device ) {
Alert.alert(
t( "No-Camera-Available" ),
@@ -282,13 +293,18 @@ const CameraContainer = ( ) => {
takePhotoOptions={takePhotoOptions}
newPhotoUris={newPhotoUris}
setNewPhotoUris={setNewPhotoUris}
userLocation={userLocation}
userLocation={userLocationForGeomodel}
hasLocationPermissions={hasLocationPermissions}
requestLocationPermissions={requestLocationPermissions}
/>
{showPhotoPermissionsGate && renderSavePhotoPermissionGate( {
onPermissionGranted: async ( ) => {
const savedPhotoUris = await savePhotosToPhotoLibrary( cameraUris, userLocation );
// we need this to make sure the very first photo after permission granted
// is saved to device. very unlikely that we'll have a location here
// since we're not prompting for permission, but there are a few scenarios where it
// could happen, like a user enabling location on Explore before visiting the camera
const accurateUserLocation = await fetchAccurateUserLocation( );
const savedPhotoUris = await savePhotosToPhotoLibrary( cameraUris, accurateUserLocation );
await logStageIfAICamera( "save_photos_to_photo_library_first_permission" );
if ( savedPhotoUris.length > 0 ) {
// Save these camera roll URIs, so later on observation editor can update

View File

@@ -186,7 +186,7 @@ const GroupPhotos = ( {
</FloatingActionBar>
<ButtonBar
sticky
containerClass="items-center z-50"
containerClass="items-center z-50 bg-white"
onLayout={onLayout}
>
<Button

View File

@@ -6,7 +6,7 @@ import {
useTranslation
} from "sharedHooks";
import fetchUserLocation from "../../sharedHelpers/fetchUserLocation";
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
import useInfiniteProjectsScroll from "./hooks/useInfiniteProjectsScroll";
import Projects from "./Projects";
@@ -48,7 +48,7 @@ const ProjectsContainer = ( ) => {
}
const getCurrentUserLocation = async ( ) => {
const currentUserLocation = await fetchUserLocation( );
const currentUserLocation = await fetchCoarseUserLocation( );
setUserLocation( currentUserLocation );
};

View File

@@ -14,7 +14,7 @@ import MapView, {
BoundingBox, LatLng, MapType, Region, UrlTile
} from "react-native-maps";
import Observation from "realmModels/Observation";
import fetchUserLocation from "sharedHelpers/fetchUserLocation.ts";
import fetchCoarseUserLocation from "sharedHelpers/fetchCoarseUserLocation.ts";
import { useDebugMode, useDeviceOrientation } from "sharedHooks";
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import colors from "styles/tailwindColors";
@@ -203,7 +203,7 @@ const Map = ( {
}, [params, currentZoom, navigation] );
const onPermissionGranted = async ( ) => {
const currentLocation = await fetchUserLocation( );
const currentLocation = await fetchCoarseUserLocation( );
if ( currentLocation && mapViewRef?.current ) {
animateToRegion( {
latitude: currentLocation.latitude,

View File

@@ -21,7 +21,7 @@ import {
import { isDebugMode } from "sharedHooks/useDebugMode";
import useStore from "stores/useStore";
import fetchCoarseUserLocation from "../../sharedHelpers/fetchUserLocation";
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
import flattenUploadParams from "./helpers/flattenUploadParams";
import useClearComputerVisionDirectory from "./hooks/useClearComputerVisionDirectory";
import useNavigateWithTaxonSelected from "./hooks/useNavigateWithTaxonSelected";

View File

@@ -5,7 +5,7 @@ import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useAuthenticatedQuery, useLocationPermission } from "sharedHooks";
import fetchUserLocation from "../../sharedHelpers/fetchUserLocation";
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
const { useRealm } = RealmContext;
@@ -54,7 +54,7 @@ const useTaxonCommonNames = ( ) => {
useEffect( ( ) => {
const fetchLocation = async ( ) => {
const location = await fetchUserLocation( );
const location = await fetchCoarseUserLocation( );
setUserLocation( location );
};
if ( hasPermissions ) {

View File

@@ -6,7 +6,7 @@ import { LatLng } from "react-native-maps";
// Please don't change this to an aliased path or the e2e mock will not get
// used in our e2e tests on Github Actions
import fetchCoarseUserLocation from "../sharedHelpers/fetchUserLocation";
import fetchCoarseUserLocation from "../sharedHelpers/fetchCoarseUserLocation";
export enum EXPLORE_ACTION {
CHANGE_SORT_BY = "CHANGE_SORT_BY",

View File

@@ -0,0 +1,57 @@
// Please don't change this to an aliased path or the e2e mock will not get
// used in our e2e tests on Github Actions
import {
checkLocationPermissions,
getCurrentPositionWithOptions,
highAccuracyOptions,
lowAccuracyOptions
} from "./geolocationWrapper";
interface UserLocation {
latitude: number;
longitude: number;
positional_accuracy: number;
altitude: number | null;
altitudinal_accuracy: number | null;
}
const fetchAccurateUserLocation = async (): Promise<UserLocation | null> => {
const permissionResult = await checkLocationPermissions( );
if ( permissionResult === null ) {
return null;
}
try {
const highAccuracyResult = await getCurrentPositionWithOptions( highAccuracyOptions )
.catch( error => {
console.warn( "High accuracy location failed, falling back to low accuracy", error );
return null;
} );
if ( highAccuracyResult ) {
return {
latitude: highAccuracyResult.coords.latitude,
longitude: highAccuracyResult.coords.longitude,
positional_accuracy: highAccuracyResult.coords.accuracy,
altitude: highAccuracyResult.coords.altitude,
altitudinal_accuracy: highAccuracyResult.coords.altitudeAccuracy
};
}
const lowAccuracyResult = await getCurrentPositionWithOptions( lowAccuracyOptions, 2 );
return {
latitude: lowAccuracyResult.coords.latitude,
longitude: lowAccuracyResult.coords.longitude,
positional_accuracy: lowAccuracyResult.coords.accuracy,
altitude: lowAccuracyResult.coords.altitude,
altitudinal_accuracy: lowAccuracyResult.coords.altitudeAccuracy
};
} catch ( e ) {
console.warn( "All location attempts failed:", e );
}
return null;
};
export default fetchAccurateUserLocation;

View File

@@ -0,0 +1,39 @@
// Please don't change this to an aliased path or the e2e mock will not get
// used in our e2e tests on Github Actions
import {
checkLocationPermissions,
getCurrentPositionWithOptions,
lowAccuracyOptions
} from "./geolocationWrapper";
interface UserLocation {
latitude: number;
longitude: number;
positional_accuracy: number;
altitude: number | null;
altitudinal_accuracy: number | null;
}
const fetchCoarseUserLocation = async ( ): Promise<UserLocation | null> => {
const permissionResult = await checkLocationPermissions( );
if ( permissionResult === null ) {
return null;
}
try {
const { coords } = await getCurrentPositionWithOptions( lowAccuracyOptions );
const userLocation = {
latitude: coords.latitude,
longitude: coords.longitude,
positional_accuracy: coords.accuracy,
altitude: coords.altitude,
altitudinal_accuracy: coords.altitudeAccuracy
};
return userLocation;
} catch ( e ) {
console.warn( e, "couldn't get latLng" );
}
return null;
};
export default fetchCoarseUserLocation;

View File

@@ -1,23 +0,0 @@
type UserLocation = {
latitude: number,
longitude: number,
positional_accuracy: number
}
const fetchUserLocation = async ( ): Promise<UserLocation> => new Promise( resolve => {
setTimeout( ( ) => resolve( {
// Darwin's house. Note that the e2e tests run in a UTC environment, so the
// observed_on_string will be set to a UTC time. If these coordinates
// fall within a time zone west of that (i.e. in the past),
// observation creation will fail during the period of the day when UTC
// time has crossed into the date after the date at these coordinates.
// The opposite (when local time is in a time zone behind the time zone of
// the coordinates) will not fail, but it might make an observation
// that's a day behind when you were expecting.
latitude: 51.3313127,
longitude: 0.0509862,
positional_accuracy: 5
} ), 1000 );
} );
export default fetchUserLocation;

View File

@@ -1,65 +0,0 @@
import Geolocation, { GeolocationResponse } from "@react-native-community/geolocation";
import {
LOCATION_PERMISSIONS,
permissionResultFromMultiple
} from "components/SharedComponents/PermissionGateContainer.tsx";
import { Platform } from "react-native";
import {
checkMultiple,
RESULTS
} from "react-native-permissions";
const options = {
enableHighAccuracy: false,
timeout: 2000,
// Setting maximumAge to 0 always causes errors on Android.
// Therefore, we conditionally apply it only if the platform is iOS.
...( Platform.OS === "ios" && { maximumAge: 0 } )
} as const;
// Issue reference for getCurrentPosition bug on Android:
// Known bug in react-native-geolocation: getCurrentPosition does not work on
// Android when enableHighAccuracy: true and maximumAge: 0.
// See: https://github.com/michalchudziak/react-native-geolocation/issues/272
// Added OS-specific conditions to handle this issue and make it work properly on Android.
const getCurrentPosition = ( ): Promise<GeolocationResponse> => new Promise(
( resolve, error ) => {
Geolocation.getCurrentPosition( resolve, error, options );
}
);
interface UserLocation {
latitude: number;
longitude: number;
positional_accuracy: number;
altitude: number | null;
altitudinal_accuracy: number | null;
}
const fetchCoarseUserLocation = async ( ): Promise<UserLocation | null> => {
const permissionResult = permissionResultFromMultiple(
await checkMultiple( LOCATION_PERMISSIONS )
);
// TODO: handle case where iOS permissions are not granted
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
return null;
}
try {
const { coords } = await getCurrentPosition( );
const userLocation = {
latitude: coords.latitude,
longitude: coords.longitude,
positional_accuracy: coords.accuracy,
altitude: coords.altitude,
altitudinal_accuracy: coords.altitudeAccuracy
};
return userLocation;
} catch ( e ) {
console.warn( e, "couldn't get latLng" );
}
return null;
};
export default fetchCoarseUserLocation;

View File

@@ -2,9 +2,33 @@ import {
GeolocationResponse
} from "@react-native-community/geolocation";
import { CHUCKS_PAD } from "appConstants/e2e.ts";
import {
LOCATION_PERMISSIONS,
permissionResultFromMultiple
} from "components/SharedComponents/PermissionGateContainer.tsx";
import { Platform } from "react-native";
import {
checkMultiple,
RESULTS
} from "react-native-permissions";
let counter = 0;
function getCurrentPosition(
success: (position: GeolocationResponse) => void,
error?: (error: any) => void,
options?: any
) {
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPosition");
setTimeout(() => {
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPosition success");
success({
coords: CHUCKS_PAD,
timestamp: Date.now()
});
}, 500);
}
function watchPosition(
success: ( position: GeolocationResponse ) => void
) {
@@ -31,8 +55,44 @@ function clearWatch( watchID: number ) {
console.log( "[DEBUG geolocationWrapper.e2e-mock] clearWatch, watchID: ", watchID );
}
export {
CHUCKS_PAD,
clearWatch,
watchPosition
const highAccuracyOptions = {
enableHighAccuracy: true,
timeout: 10000,
...( Platform.OS === "ios" && { maximumAge: 0 } )
};
const lowAccuracyOptions = {
enableHighAccuracy: false,
timeout: 2000,
...( Platform.OS === "ios" && { maximumAge: 0 } )
};
const getCurrentPositionWithOptions = (
options
): Promise<GeolocationResponse> => new Promise(
( resolve, reject ) => {
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPositionWithOptions");
getCurrentPosition( resolve, reject, options );
}
);
const checkLocationPermissions = async ( ) => {
const permissionResult = permissionResultFromMultiple(
await checkMultiple( LOCATION_PERMISSIONS )
);
console.log("[DEBUG geolocationWrapper.e2e-mock] checkLocationPermissions");
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
return null;
}
return permissionResult;
};
export {
clearWatch,
watchPosition,
highAccuracyOptions,
lowAccuracyOptions,
getCurrentPositionWithOptions,
checkLocationPermissions
};

View File

@@ -5,6 +5,27 @@ import Geolocation, {
GeolocationError,
GeolocationResponse
} from "@react-native-community/geolocation";
import {
LOCATION_PERMISSIONS,
permissionResultFromMultiple
} from "components/SharedComponents/PermissionGateContainer.tsx";
import { Platform } from "react-native";
import {
checkMultiple,
RESULTS
} from "react-native-permissions";
export function getCurrentPosition(
success: ( position: GeolocationResponse ) => void,
error?: ( error: GeolocationError ) => void,
options?: {
timeout?: number;
maximumAge?: number;
enableHighAccuracy?: boolean;
}
) {
return Geolocation.getCurrentPosition( success, error, options );
}
export function watchPosition(
success: ( position: GeolocationResponse ) => void,
@@ -25,3 +46,41 @@ export function watchPosition(
export function clearWatch( watchID: number ) {
Geolocation.clearWatch( watchID );
}
// Issue reference for getCurrentPosition bug on Android:
// Known bug in react-native-geolocation: getCurrentPosition does not work on
// Android when enableHighAccuracy: true and maximumAge: 0.
// See: https://github.com/michalchudziak/react-native-geolocation/issues/272
// Added OS-specific conditions to both options below
// to handle this issue and make it work properly on Android.
export const highAccuracyOptions = {
enableHighAccuracy: true,
timeout: 10000,
...( Platform.OS === "ios" && { maximumAge: 0 } )
} as const;
export const lowAccuracyOptions = {
enableHighAccuracy: false,
timeout: 2000,
...( Platform.OS === "ios" && { maximumAge: 0 } )
} as const;
export const getCurrentPositionWithOptions = (
options
): Promise<GeolocationResponse> => new Promise(
( resolve, reject ) => {
getCurrentPosition( resolve, reject, options );
}
);
export const checkLocationPermissions = async ( ) => {
const permissionResult = permissionResultFromMultiple(
await checkMultiple( LOCATION_PERMISSIONS )
);
// TODO: handle case where iOS permissions are not granted
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
return null;
}
return permissionResult;
};

View File

@@ -1,4 +1,3 @@
import Geolocation from "@react-native-community/geolocation";
import {
screen,
userEvent,
@@ -18,18 +17,11 @@ import { getPredictionsForImage } from "vision-camera-plugin-inatvision";
// working normally
jest.unmock( "@react-navigation/native" );
const mockWatchPosition = jest.fn( ( success, _error, _options ) => {
setTimeout( ( ) => success( {
coords: {
latitude: 1,
longitude: 1,
accuracy: 9
},
timestamp: Date.now( )
} ), 100 );
return 0;
} );
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
const mockFetchUserLocation = jest.fn( () => ( { latitude: 1, longitude: 1, accuracy: 9 } ) );
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );
const mockModelResult = {
predictions: [factory( "ModelPrediction", {

View File

@@ -1,4 +1,3 @@
import Geolocation from "@react-native-community/geolocation";
import {
useNetInfo
} from "@react-native-community/netinfo";
@@ -68,20 +67,12 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
Version: 11
} ) );
const watchPositionSuccess = jest.fn( success => success( {
coords: {
latitude: 56,
longitude: 9,
accuracy: 8
}
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );
const mockWatchPosition = jest.fn( ( success, _error, _options ) => {
setTimeout( () => watchPositionSuccess( success ), 100 );
return 0;
} );
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
// We're explicitly testing navigation here so we want react-navigation
// working normally
jest.unmock( "@react-navigation/native" );
@@ -178,21 +169,13 @@ const navigateToSuggestionsForObservationViaObsEdit = async observation => {
await actor.press( addIdButton );
};
const navigateToSuggestionsViaAICamera = async ( options = {} ) => {
const navigateToSuggestionsViaAICamera = async ( ) => {
const tabBar = await screen.findByTestId( "CustomTabBar" );
const addObsButton = await within( tabBar ).findByLabelText( "Add observations" );
await actor.press( addObsButton );
const cameraButton = await screen.findByLabelText( /AI Camera/ );
await actor.press( cameraButton );
if ( options.waitForLocation ) {
await waitFor( ( ) => {
expect( Geolocation.watchPosition ).toHaveReturnedWith( 0 );
} );
await waitFor( ( ) => {
expect( watchPositionSuccess ).toHaveReturned( );
}, 100 );
}
const takePhotoButton = await screen.findByLabelText( /Take photo/ );
await actor.press( takePhotoButton );
const addIDButton = await screen.findByText( /ADD AN ID/ );
@@ -345,21 +328,19 @@ describe( "from AICamera directly", ( ) => {
describe( "suggestions with location", ( ) => {
it( "should call score_image with location parameters on first render", async ( ) => {
await setupAppWithSignedInUser( );
await navigateToSuggestionsViaAICamera( { waitForLocation: true } );
// const ignoreLocationButton = await screen.findByText( /IGNORE LOCATION/ );
// expect( ignoreLocationButton ).toBeVisible( );
// await waitFor( ( ) => {
// expect( inatjs.computervision.score_image ).toHaveBeenCalledWith(
// expect.objectContaining( {
// // Don't care about fields here
// fields: expect.any( Object ),
// image: expect.any( Object ),
// lat: 56,
// lng: 9
// } ),
// expect.anything( )
// );
// } );
await navigateToSuggestionsViaAICamera( );
await waitFor( ( ) => {
expect( inatjs.computervision.score_image ).toHaveBeenCalledWith(
expect.objectContaining( {
// Don't care about fields here
fields: expect.any( Object ),
image: expect.any( Object ),
lat: 56,
lng: 9
} ),
expect.anything( )
);
} );
} );
} );
@@ -370,6 +351,7 @@ describe( "from AICamera directly", ( ) => {
hasPermissions: false,
renderPermissionsGate: jest.fn( )
} ) );
mockFetchUserLocation.mockReturnValue( null );
await setupAppWithSignedInUser( );
await navigateToSuggestionsViaAICamera( );
await waitFor( ( ) => {

View File

@@ -1,4 +1,3 @@
import Geolocation from "@react-native-community/geolocation";
import {
screen,
userEvent,
@@ -97,6 +96,12 @@ afterEach( ( ) => {
signOut( { realm: global.mockRealms[__filename] } );
} );
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );
const actor = userEvent.setup( );
const navToAICamera = async ( ) => {
@@ -139,14 +144,6 @@ describe( "AICamera navigation with advanced user layout", ( ) => {
describe( "to Suggestions", ( ) => {
beforeEach( ( ) => {
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
coords: {
latitude: 56,
longitude: 9,
accuracy: 8
}
} ) );
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
jest.spyOn( usePredictions, "default" ).mockImplementation( () => ( {
handleTaxaDetected: jest.fn( ),
modelLoaded: true,

View File

@@ -41,7 +41,7 @@ const mockObservations = [
];
const mockFetchUserLocation = jest.fn( () => ( { latitude: 37, longitude: 34 } ) );
jest.mock( "sharedHelpers/fetchUserLocation", () => ( {
jest.mock( "sharedHelpers/fetchCoarseUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );

View File

@@ -1,4 +1,3 @@
import Geolocation from "@react-native-community/geolocation";
import {
screen,
userEvent,
@@ -49,6 +48,12 @@ beforeAll( async () => {
const actor = userEvent.setup( );
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );
const navigateToCamera = async ( ) => {
await waitFor( ( ) => {
global.timeTravel( );
@@ -88,14 +93,6 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
} );
it( "should advance to ObsEdit when photo taken and checkmark tapped", async () => {
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
coords: {
latitude: 56,
longitude: 9,
accuracy: 8
}
} ) );
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
renderApp( );
await navigateToCamera( );
const takePhotoButton = await screen.findByLabelText( /Take photo/ );
@@ -120,14 +117,6 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
} );
it( "should advance to Suggestions when photo taken and checkmark tapped", async ( ) => {
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
coords: {
latitude: 56,
longitude: 9,
accuracy: 8
}
} ) );
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
renderApp( );
await navigateToCamera( );
const takePhotoButton = await screen.findByLabelText( /Take photo/ );

View File

@@ -23,6 +23,12 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
Version: 11
} ) );
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation()
} ) );
// We're explicitly testing navigation here so we want react-navigation
// working normally
jest.unmock( "@react-navigation/native" );