Add ID - initial commit (#103) (#113)

* Adds a component for adding identifications, both from Obs Detail and Obs Edit
* Minor style and eslint change to disallow all-caps string literals

Closes #103 

Co-authored-by: Ken-ichi Ueda <kenichi.ueda@gmail.com>
This commit is contained in:
budowski
2022-07-03 02:01:48 +03:00
committed by GitHub
parent 2e184b97f4
commit 4b4b0f9244
18 changed files with 2413 additions and 777 deletions

View File

@@ -6,6 +6,12 @@ module.exports = {
"comma-dangle": [2, "never"],
"space-in-parens": [2, "always"],
"prettier/prettier": 0,
"i18next/no-literal-string": [2, {
words: {
// Minor change to the default to disallow all-caps string literals as well
exclude: ["[0-9!-/:-@[-`{-~]+"]
}
}],
"no-var": 1
},
// need this so jest doesn't show as undefined in jest.setup.js

2566
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"translate": "node src/i18n/i18ncli.js build"
},
"dependencies": {
"@gorhom/bottom-sheet": "^4.3.1",
"@react-native-community/cameraroll": "^4.1.2",
"@react-native-community/checkbox": "^0.5.12",
"@react-native-community/datetimepicker": "^6.1.0",

View File

@@ -6,22 +6,18 @@ import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
const createIdentification = async ( params: Object ): Promise<?number> => {
const apiToken = await getJWTToken( false );
try {
const apiParams = {
identification: params
};
const options = {
api_token: apiToken
};
// additional keys for obs detail id creation:
// body
// vision
// disagreement
const response = await inatjs.identifications.create( apiParams, options );
return response.total_results;
} catch ( e ) {
console.log( "Couldn't create identification:", JSON.stringify( e.response ), );
}
const apiParams = {
identification: params
};
const options = {
api_token: apiToken
};
// additional keys for obs detail id creation:
// body
// vision
// disagreement
const response = await inatjs.identifications.create( apiParams, options );
return response.total_results;
};
export default createIdentification;

View File

@@ -8,7 +8,6 @@ import jwt from "react-native-jwt-io";
import {Platform} from "react-native";
import {getBuildNumber, getDeviceType, getSystemName, getSystemVersion, getVersion} from "react-native-device-info";
import Realm from "realm";
import realmConfig from "../../models/index";
// Base API domain can be overridden (in case we want to use staging URL) - either by placing it in .env file, or
@@ -302,6 +301,18 @@ const getUsername = async (): Promise<string> => {
return await RNSInfo.getItem( "username", {} );
};
/**
* Returns the logged-in user
*
* @returns {Promise<boolean>}
*/
const getUser = async (): Promise<Object | null> => {
const realm = await Realm.open( realmConfig );
return realm.objects( "User" ).filtered( "signedIn == true" )[0];
};
/**
* Returns the logged-in userId
*
@@ -310,9 +321,9 @@ const getUsername = async (): Promise<string> => {
const getUserId = async (): Promise<string | null> => {
const realm = await Realm.open( realmConfig );
const currentUser = realm.objects( "User" ).filtered( "signedIn == true" )[0];
const currentUserId = currentUser?.id?.toString( );
const userId = currentUser?.id?.toString( );
realm.close( );
return currentUserId;
return userId;
};
/**
@@ -337,5 +348,6 @@ export {
getUsername,
signOut,
getJWTToken,
getUser,
getUserId
};

View File

@@ -34,7 +34,7 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }:
}, [user] );
return (
<>
<View style={[item.temporary ? viewStyles.temporaryRow : null]}>
<View style={[viewStyles.userProfileRow, viewStyles.rowBorder]}>
{user && (
<Pressable
@@ -73,7 +73,7 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }:
<View style={viewStyles.speciesDetailRow}>
<Text>{item.body}</Text>
</View>
</>
</View>
);
};

View File

@@ -1,29 +1,33 @@
// @flow
import React, { useState, useContext } from "react";
import _ from "lodash";
import { Text, View, Image, Pressable, ScrollView, LogBox } from "react-native";
import React, {useState, useContext, useEffect} from "react";
import type { Node } from "react";
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
import { useNavigation, useRoute } from "@react-navigation/native";
import { t } from "i18next";
import { formatISO } from "date-fns";
import { Text, View, Image, Pressable, ScrollView, LogBox, Alert } from "react-native";
import { useTranslation } from "react-i18next";
import { viewStyles, textStyles } from "../../styles/obsDetails/obsDetails";
import ActivityTab from "./ActivityTab";
import UserIcon from "../SharedComponents/UserIcon";
import PhotoScroll from "../SharedComponents/PhotoScroll";
import DataTab from "./DataTab";
import { useRemoteObservation } from "./hooks/useRemoteObservation";
import Taxon from "../../models/Taxon";
import User from "../../models/User";
import { ObsEditContext } from "../../providers/contexts";
import InputField from "../SharedComponents/InputField";
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
import createComment from "./helpers/createComment";
import faveObservation from "./helpers/faveObservation";
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
import createComment from "./helpers/createComment";
import createIdentification from "../Identify/helpers/createIdentification";
import DataTab from "./DataTab";
import faveObservation from "./helpers/faveObservation";
import InputField from "../SharedComponents/InputField";
import ObsDetailsHeader from "./ObsDetailsHeader";
import PhotoScroll from "../SharedComponents/PhotoScroll";
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
import Taxon from "../../models/Taxon";
import TranslatedText from "../SharedComponents/TranslatedText";
import User from "../../models/User";
import UserIcon from "../SharedComponents/UserIcon";
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
import { ObsEditContext } from "../../providers/contexts";
import { useNavigation, useRoute } from "@react-navigation/native";
import { useRemoteObservation } from "./hooks/useRemoteObservation";
import { viewStyles, textStyles } from "../../styles/obsDetails/obsDetails";
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
import { getUser } from "../LoginSignUp/AuthenticationService";
// this is getting triggered by passing dates, like _created_at, through
// react navigation via the observation object. it doesn't seem to
@@ -33,6 +37,7 @@ LogBox.ignoreLogs( [
] );
const ObsDetails = ( ): Node => {
const { t } = useTranslation( );
const [refetch, setRefetch] = useState( false );
const [showCommentBox, setShowCommentBox] = useState( false );
const [comment, setComment] = useState( "" );
@@ -41,6 +46,7 @@ const ObsDetails = ( ): Node => {
let observation = params.observation;
const [tab, setTab] = useState( 0 );
const navigation = useNavigation( );
const [ids, setIds] = useState( [] );
// TODO: we'll probably need to redo this logic a bit now that we're
// passing an observation via navigation instead of reopening realm
@@ -54,20 +60,77 @@ const ObsDetails = ( ): Node => {
const toggleRefetch = ( ) => setRefetch( !refetch );
useEffect( () => {
if ( observation ) {setIds( observation.identifications.map( i => i ) );}
}, [observation] );
if ( !observation ) { return null; }
const ids = observation.identifications.map( i => i );
const comments = observation.comments.map( c => c );
const photos = _.compact( observation.observationPhotos.map( op => op.photo ) );
const user = observation.user;
const taxon = observation.taxon;
const uuid = observation.uuid;
const onIDAdded = async ( identification ) => {
console.log( "onIDAdded", identification );
// Add temporary ID to observation.identifications ("ghosted" ID, while we're trying to add it)
const currentUser = await getUser();
const newId = {
body: identification.body,
taxon: identification.taxon,
user: {
id: currentUser?.id,
login: currentUser?.login,
signedIn: true
},
created_at: formatISO( Date.now() ),
uuid: identification.uuid,
vision: false,
// This tells us to render is ghosted (since it's temporarily visible until getting a response from the server)
temporary: true
};
setIds( [ ...ids, newId ] );
let error = null;
try {
const results = await createIdentification( { observation_id: observation.uuid, taxon_id: newId.taxon.id, body: newId.body } );
if ( results === 1 ) {
// Remove ghosted highlighting
newId.temporary = false;
setIds( [ ...ids, newId ] );
} else {
// Couldn't create ID
error = t( "Couldnt-create-identification", { error: t( "Unknown-error" ) } );
}
} catch ( e ) {
error = t( "Couldnt-create-identification", { error: e.message } );
}
if ( error ) {
// Remove temporary ID and show error
setIds( [...ids] );
Alert.alert(
"Error",
error,
[{ text: t( "OK" ) }],
{
cancelable: true
}
);
}
};
const navToUserProfile = userId => navigation.navigate( "UserProfile", { userId } );
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: taxon.id } );
const navToCVSuggestions = ( ) => {
const navToAddID = ( ) => {
addObservations( [observation] );
navigation.navigate( "camera", { screen: "Suggestions" } );
navigation.push( "AddID", { onIDAdded: onIDAdded, goBackOnSave: true } );
};
const openCommentBox = ( ) => setShowCommentBox( true );
const submitComment = async ( ) => {
@@ -155,14 +218,14 @@ const ObsDetails = ( ): Node => {
onPress={showActivityTab}
accessibilityRole="button"
>
<Text style={textStyles.greenButtonText}>ACTIVITY</Text>
<TranslatedText style={textStyles.greenButtonText} text="ACTIVITY" />
</Pressable>
<Pressable
onPress={showDataTab}
testID="ObsDetails.DataTab"
accessibilityRole="button"
>
<Text style={textStyles.greenButtonText}>DATA</Text>
<TranslatedText style={textStyles.greenButtonText} text="DATA" />
</Pressable>
</View>
{tab === 0
@@ -182,7 +245,7 @@ const ObsDetails = ( ): Node => {
but it doesn't appear to be working on staging either (Mar 11, 2022) */}
<RoundGreenButton
buttonText="Suggest an ID"
handlePress={navToCVSuggestions}
handlePress={navToAddID}
testID="ObsDetail.cvSuggestionsButton"
/>
</View>

View File

@@ -0,0 +1,196 @@
// @flow
import * as React from "react";
import {
View,
Pressable,
TouchableOpacity, FlatList, Image
} from "react-native";
import {useNavigation} from "@react-navigation/native";
import { viewStyles, textStyles } from "../../styles/obsDetails/addID";
import { useTranslation } from "react-i18next";
import { TextInput as NativeTextInput } from "react-native";
import AddIDHeader from "./AddIDHeader";
import {useRef, useState} from "react";
import {
BottomSheetBackdrop,
BottomSheetModal,
BottomSheetModalProvider
} from "@gorhom/bottom-sheet";
import {Button, Headline, Text, TextInput} from "react-native-paper";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
import {colors} from "../../styles/global";
import uuid from "react-native-uuid";
type Props = {
route: {
params: {
onIDAdded: ( identification: {[string]: any} ) => void,
goBackOnSave: boolean,
hideComment: boolean
}
}
}
const AddID = ( { route }: Props ): React.Node => {
const { t } = useTranslation( );
const [comment, setComment] = useState( "" );
const [commentDraft, setCommentDraft] = useState( "" );
const { onIDAdded, goBackOnSave, hideComment } = route.params;
const bottomSheetModalRef = useRef( null );
const [taxonSearch, setTaxonSearch] = useState( "" );
const taxonList = useRemoteSearchResults( taxonSearch, "taxa", "taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank" );
const navigation = useNavigation( );
const renderBackdrop = ( props ) => (
<BottomSheetBackdrop {...props} pressBehavior={"close"}
appearsOnIndex={0}
disappearsOnIndex={-1}
/>
);
const editComment = ( event ) => {
setCommentDraft( comment );
bottomSheetModalRef.current?.present();
};
const createPhoto = ( photo ) => {
return {
id: photo.id,
url: photo.square_url
};
};
const createID = ( taxon ) => {
const newTaxon = {
id: taxon.id,
default_photo: taxon.default_photo ? createPhoto( taxon.default_photo ) : null,
name: taxon.name,
preferred_common_name: taxon.preferred_common_name,
rank: taxon.rank
};
const newID = {
uuid: uuid.v4( ),
body: comment,
taxon: newTaxon
};
return newID;
};
const renderTaxonResult = ( {item} ) => {
const taxonImage = item.default_photo ? { uri: item.default_photo.square_url } : Icon.getImageSourceSync( "leaf", 50, colors.inatGreen );
return <View style={viewStyles.taxonResult} testID={`Search.taxa.${item.id}`}>
<Image style={viewStyles.taxonResultIcon} source={taxonImage} testID={`Search.taxa.${item.id}.photo`}
/>
<View style={viewStyles.taxonResultNameContainer}>
<Text style={textStyles.taxonResultName}>{item.name}</Text>
<Text style={textStyles.taxonResultScientificName}>{item.preferred_common_name}</Text>
</View>
<Pressable style={viewStyles.taxonResultInfo} onPress={() => navigation.navigate( "TaxonDetails", { id: item.id } )} accessibilityRole="link"><Icon style={textStyles.taxonResultInfoIcon} name="information-outline" size={25} /></Pressable>
<Pressable style={viewStyles.taxonResultSelect} onPress={() => { onIDAdded( createID( item ) ); if ( goBackOnSave ) {navigation.goBack();} }} accessibilityRole="link"><Icon style={textStyles.taxonResultSelectIcon} name="check-bold" size={25} /></Pressable>
</View>;
};
return (
<BottomSheetModalProvider>
<ViewNoFooter>
<AddIDHeader showEditComment={!hideComment && comment.length === 0} onEditCommentPressed={editComment} />
<View>
<View style={viewStyles.scrollView}>
{comment.length > 0 && <View>
<Text>{t( "ID-Comment" )}</Text>
<View style={viewStyles.commentContainer}>
<Icon style={textStyles.commentLeftIcon} name="chat-processing-outline" size={25} />
<Text style={textStyles.comment}>{comment}</Text>
<Pressable style={viewStyles.commentRightIconContainer} onPress={editComment} accessibilityRole="link"><Icon style={textStyles.commentRightIcon} name="pencil" size={25} /></Pressable>
</View>
</View>
}
<Text>{t( "Search-Taxon-ID" )}</Text>
<TextInput
testID={"SearchTaxon"}
left={<TextInput.Icon name={() => <Icon style={textStyles.taxonSearchIcon} name={"magnify"} size={25} />} />}
style={viewStyles.taxonSearch}
value={taxonSearch}
onChangeText={setTaxonSearch}
selectionColor={colors.black}
/>
<FlatList
data={taxonList}
renderItem={renderTaxonResult}
keyExtractor={item => item.id}
style={viewStyles.taxonList}
/>
</View>
</View>
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
enableOverDrag={false}
enablePanDownToClose={false}
snapPoints={["50%"]}
backdropComponent={renderBackdrop}
style={viewStyles.bottomModal}
>
<Headline style={textStyles.commentHeader}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-optional-comment" )}</Headline>
<View style={viewStyles.commentInputContainer}>
<TextInput
keyboardType="default"
style={viewStyles.commentInput}
value={commentDraft}
selectionColor={colors.black}
activeUnderlineColor={colors.transparent}
autoFocus
multiline
onChangeText={setCommentDraft}
render={( innerProps ) => (
<NativeTextInput
{...innerProps}
style={[
innerProps.style,
viewStyles.commentInputText
]}
/>
)}
/>
<TouchableOpacity
style={viewStyles.commentClear}
onPress={() => setCommentDraft( "" )}>
<Text
style={[viewStyles.commentClearText, commentDraft.length === 0 ? textStyles.disabled : null]}>{t( "Clear" )}</Text>
</TouchableOpacity>
</View>
<View style={viewStyles.commentButtonContainer}>
<Button
style={viewStyles.commentButton}
uppercase={false}
color={colors.midGray}
onPress={() => {
bottomSheetModalRef.current?.dismiss();
}}>{t( "Cancel" )}</Button>
<Button
style={viewStyles.commentButton}
uppercase={false}
disabled={commentDraft.length === 0}
color={colors.midGray}
mode="contained"
onPress={() => {
setComment( commentDraft );
bottomSheetModalRef.current?.dismiss();
}}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-comment" )}</Button>
</View>
</BottomSheetModal>
</ViewNoFooter>
</BottomSheetModalProvider>
);
};
export default AddID;

View File

@@ -0,0 +1,32 @@
// @flow
import React from "react";
import type { Node } from "react";
import { useTranslation } from "react-i18next";
import { Headline } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import {Pressable, View} from "react-native";
import { HeaderBackButton } from "@react-navigation/elements";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { viewStyles } from "../../styles/obsDetails/addID";
type Props = {
showEditComment: boolean,
onEditCommentPressed: ( event: any ) => void
}
const AddIDHeader = ( { showEditComment, onEditCommentPressed }: Props ): Node => {
const { t } = useTranslation( );
const navigation = useNavigation( );
return (
<View style={viewStyles.headerRow}>
<HeaderBackButton onPress={( ) => navigation.goBack( )} />
<Headline>{t( "Add-ID-Header" )}</Headline>
{showEditComment ?
<Pressable onPress={onEditCommentPressed} accessibilityRole="link"><Icon name="chat-processing-outline" size={25} /></Pressable> : <View />}
</View>
);
};
export default AddIDHeader;

View File

@@ -1,7 +1,7 @@
// @flow
import React, { useContext } from "react";
import { Text, Pressable, FlatList, View } from "react-native";
import {Text, Pressable, FlatList, View} from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { Node } from "react";
import { useTranslation } from "react-i18next";
@@ -25,9 +25,14 @@ const IdentificationSection = ( ): Node => {
const currentObs = observations[currentObsIndex];
const identification = currentObs.taxon;
const onIDAdded = async ( id ) => {
console.log( "onIDAdded", id );
updateIdentification( id.taxon );
};
const updateIdentification = ( taxon ) => updateTaxon( taxon );
const navToSuggestionsPage = ( ) => navigation.navigate( "Suggestions" );
const navToAddID = ( ) => navigation.push( "AddID", { onIDAdded: onIDAdded, hideComment: true } );
const renderIconicTaxaButton = ( { item } ) => {
const id = iconicTaxaIds[item];
@@ -60,7 +65,7 @@ const IdentificationSection = ( ): Node => {
</Text>
</View>
<Pressable
onPress={navToSuggestionsPage}
onPress={navToAddID}
>
<PlaceholderText text="edit" style={[textStyles.text]} />
</Pressable>
@@ -70,7 +75,7 @@ const IdentificationSection = ( ): Node => {
return (
<>
<RoundGreenButton
handlePress={navToSuggestionsPage}
handlePress={navToAddID}
buttonText="View Identification Suggestions"
testID="ObsEdit.Suggestions"
/>

View File

@@ -10,7 +10,6 @@ type Props = {
}
const PlaceholderText = ( { text, style }: Props ): Node => (
// eslint-disable-next-line react-native/no-inline-styles
<Text style={[{ color: "red", textTransform: "uppercase", fontSize: 30 }].concat( style )}>
{ text }
</Text>

View File

@@ -14,6 +14,7 @@ import PhotoGalleryProvider from "../providers/PhotoGalleryProvider";
import PhotoGallery from "../components/PhotoLibrary/PhotoGallery";
import PermissionGate from "../components/SharedComponents/PermissionGate";
import Mortal from "../components/SharedComponents/Mortal";
import AddID from "../components/ObsEdit/AddID";
const Stack = createNativeStackNavigator( );
@@ -79,6 +80,11 @@ const CameraStackNavigation = ( ): React.Node => (
headerShown: true
}}
/>
<Stack.Screen
name="AddID"
component={AddID}
options={hideHeader}
/>
</Stack.Navigator>
</PhotoGalleryProvider>
</Mortal>

View File

@@ -9,6 +9,7 @@ import ObsDetails from "../components/ObsDetails/ObsDetails";
import UserProfile from "../components/UserProfile/UserProfile";
import TaxonDetails from "../components/TaxonDetails/TaxonDetails";
import Mortal from "../components/SharedComponents/Mortal";
import AddID from "../components/ObsEdit/AddID";
const Stack = createNativeStackNavigator( );
@@ -47,6 +48,11 @@ const MyObservationsStackNavigation = ( ): React.Node => (
component={TaxonDetails}
options={showBackButton}
/>
<Stack.Screen
name="AddID"
component={AddID}
options={hideHeader}
/>
</Stack.Navigator>
</Mortal>
);

View File

@@ -25,6 +25,7 @@ const formatObsListTime = ( date ) => {
return format( date, dateTime );
};
export {
formatDateAndTime,
timeAgo,

View File

@@ -3,6 +3,7 @@
export const colors = {
white: "#ffffff",
black: "#000000",
transparent: "#ff000000",
inatGreen: "#77b300",
gray: "#393939",
lightGray: "#f5f5f5",

View File

@@ -0,0 +1,145 @@
// @flow strict-local
import { StyleSheet, Dimensions } from "react-native";
import type { ViewStyleProp, TextStyleProp, ImageStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
import { colors } from "../global";
const { width } = Dimensions.get( "screen" );
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
headerRow: {
flexDirection: "row",
flexWrap: "nowrap",
justifyContent: "space-between",
padding: 10
},
scrollView: {
paddingTop: 10,
paddingLeft: 20,
paddingRight: 20,
paddingBottom: 60
},
commentInputContainer: {
position: "relative"
},
commentInput: {
backgroundColor: colors.white,
height: 150,
padding: 0,
marginBottom: 20
},
commentInputText: {
borderWidth: 1,
borderColor: colors.gray,
borderRadius: 10,
paddingTop: 8,
paddingBottom: 8,
height: 150
},
bottomModal: {
padding: 20
},
commentButtonContainer: {
flexDirection: "row",
flexWrap: "nowrap",
width: "100%",
justifyContent: "space-around"
},
commentButton: {
width: 150
},
commentContainer: {
flexDirection: "row",
flexWrap: "nowrap",
width: width - 40,
padding: 10,
marginTop: 10,
marginBottom: 20,
borderRadius: 10,
backgroundColor: "#D9E5B8",
alignItems: "center"
},
commentRightIconContainer: {
marginLeft: "auto"
},
taxonSearch: {
marginTop: 10
},
taxonList: {
marginBottom: "auto",
marginTop: 20
},
taxonResult: {
flexDirection: "row",
alignItems: "center",
marginBottom: 10
},
taxonResultIcon: {
width: 50,
height: 50,
marginRight: 10,
backgroundColor: colors.lightGray
},
taxonResultNameContainer: {
flexDirection: "column"
},
taxonResultInfo: {
marginLeft: "auto",
marginRight: 10
},
commentClear: {
position: "absolute",
right: 20,
bottom: 30,
zIndex: 9999,
width: 40,
height: 20
}
} );
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
disabled: {
color: colors.lightGray
},
taxonResultName: {
fontWeight: "bold"
},
taxonResultScientificName: {
color: colors.gray,
fontStyle: "italic"
},
commentHeader: {
fontSize: 22,
marginBottom: 10
},
taxonSearchIcon: {
color: colors.gray
},
taxonResultInfoIcon: {
color: colors.gray
},
taxonResultSelectIcon: {
color: colors.inatGreen
},
commentClearText: {
color: colors.black
},
commentLeftIcon: {
marginRight: 10,
color: colors.inatGreen
},
commentRightIcon: {
color: colors.inatGreen
}
} );
const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
} );
export {
viewStyles,
textStyles,
imageStyles
};

View File

@@ -8,6 +8,9 @@ import { colors } from "../global";
const { width } = Dimensions.get( "screen" );
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
temporaryRow: {
opacity: 0.5
},
hoverCommentBox: {
zIndex: 1
},

View File

@@ -0,0 +1,52 @@
import React from "react";
import {fireEvent, render, waitFor} from "@testing-library/react-native";
import { NavigationContainer } from "@react-navigation/native";
import AddID from "../../../../src/components/ObsEdit/AddID";
import factory, {makeResponse} from "../../../factory";
// Mock inaturalistjs so we can make some fake responses
jest.mock( "inaturalistjs" );
import inatjs from "inaturalistjs";
// this resolves a test failure with the Animated library:
// Animated: `useNativeDriver` is not supported because the native animated module is missing.
jest.useFakeTimers( );
jest.mock( "@react-navigation/native", ( ) => {
const actualNav = jest.requireActual( "@react-navigation/native" );
return {
...actualNav,
useRoute: ( ) => ( {
} )
};
} );
const testTaxaList = [
{ taxon: factory( "RemoteTaxon" ) },
{ taxon: factory( "RemoteTaxon" ) },
{ taxon: factory( "RemoteTaxon" ) }
];
const mockExpected = testTaxaList;
const renderAddID = ( route ) => render(
<NavigationContainer>
<AddID route={route} />
</NavigationContainer>
);
test( "renders taxon search results", async ( ) => {
inatjs.search.mockResolvedValue( makeResponse( mockExpected ) );
const route = { params: { } };
const { getByTestId } = renderAddID( route );
const input = getByTestId( "SearchTaxon" );
await waitFor( () => {
fireEvent.changeText( input, "Some taxon" );
} );
const taxon = testTaxaList[0].taxon;
expect( getByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy( );
expect( getByTestId( `Search.taxa.${taxon.id}.photo` ).props.source ).toStrictEqual( { "uri": taxon.default_photo.square_url } );
} );