mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-08-07 04:42:57 -04:00
Search screen + test coverage (#26)
* Create Explore screen * Add input field component * Add input fields * UI for list view in Explore; dropdown taxa picker * UI for list view in Explore; dropdown taxa picker * ObservationViews component is shared between Explore and My Observations * Get tests passing with Explore + ObservationViews * Add map view, iOS location permission whenInUse, and geolocation fetch * Add RN permissions and geolocation to jest mocks * Explore filters, testing, explore provider/navigation stack * Crash fix for grid items with no observation photos * Code cleanup; move fetch search results to shared folder * Code cleanup; remove duplicate files * Use shared hooks for search * Remove more duplication * Display search results, similar to on web * Show a list of search results for users/taxa and allow toggling * Consolidate pickers into a single component * Move copyRealmSchema into Observation model * Move copyRealmSchema into Observation model * Move observation photo logic to Obs model * Obs details fetches an observation from API instead of from realm/exploreList * Rename hooks files with 'use' instead of 'fetch' * Add user to observation schema * Change realm keys from camelcase to snakecase with mapping * Simplify model code * Fix tests for ObsDetails * Attempt to clean up ObsList code; move useObservations hook into provider * Simplify copyRealmSchema code * Code cleanup * Add test coverage for Search * Add testing for users search * Add gitguardian pre-commit hook * Update ggshield * Testing ggshield * Add .env back to gitignore
This commit is contained in:
1 parent
9ef85a6818
commit
846b73a861
19 files changed
+527
-19
No files matched your search
@@ -0,0 +1,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/gitguardian/ggshield
|
||||
rev: v1.10.7
|
||||
hooks:
|
||||
- id: ggshield
|
||||
language_version: python3
|
||||
stages: [commit]
|
||||
@@ -8,7 +8,7 @@ import { Image } from "react-native";
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
|
||||
import useRemoteSearchResults from "./hooks/useRemoteSearchResults";
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { imageStyles, viewStyles } from "../../styles/explore/explore";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { Node } from "react";
|
||||
// TODO: we'll probably need a custom dropdown picker which looks like a search bar
|
||||
// and allows users to input immediately instead of first tapping the dropdown
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
|
||||
import useFetchSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
|
||||
type Props = {
|
||||
location: string,
|
||||
search: string => { },
|
||||
setPlaceId: number => { },
|
||||
placeId: number
|
||||
}
|
||||
|
||||
const PlacePicker = ( { location, search, setPlaceId, placeId }: Props ): Node => {
|
||||
const places = useFetchSearchResults( location, "places" );
|
||||
|
||||
const [open, setOpen] = useState( false );
|
||||
|
||||
const items = places.map( place => {
|
||||
return {
|
||||
// TODO: match styling on the web
|
||||
label: place.name,
|
||||
// value needs to be place.uuid once that's returned from api v2
|
||||
value: place.uuid
|
||||
};
|
||||
} );
|
||||
|
||||
return (
|
||||
<DropDownPicker
|
||||
open={open}
|
||||
value={placeId}
|
||||
items={items}
|
||||
setOpen={setOpen}
|
||||
setValue={setPlaceId}
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
onChangeSearchText={search}
|
||||
placeholder="Search places"
|
||||
style={viewStyles.dropdown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlacePicker;
|
||||
@@ -0,0 +1,49 @@
|
||||
// flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { Node } from "react";
|
||||
// TODO: we'll probably need a custom dropdown picker which looks like a search bar
|
||||
// and allows users to input immediately instead of first tapping the dropdown
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
|
||||
import useFetchSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
|
||||
type Props = {
|
||||
searchTerm: string,
|
||||
search: string => { },
|
||||
setProjectId: number => { },
|
||||
projectId: number
|
||||
}
|
||||
|
||||
const ProjectPicker = ( { searchTerm, search, setProjectId, projectId }: Props ): Node => {
|
||||
const autocomplete = useFetchSearchResults( searchTerm, "projects" );
|
||||
|
||||
const [open, setOpen] = useState( false );
|
||||
|
||||
const items = autocomplete.map( project => {
|
||||
return {
|
||||
// TODO: match styling on the web
|
||||
label: project.title,
|
||||
value: project.id
|
||||
};
|
||||
} );
|
||||
|
||||
return (
|
||||
<DropDownPicker
|
||||
open={open}
|
||||
value={projectId}
|
||||
items={items}
|
||||
setOpen={setOpen}
|
||||
setValue={setProjectId}
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
onChangeSearchText={search}
|
||||
placeholder="Search projects"
|
||||
style={viewStyles.dropdown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectPicker;
|
||||
@@ -0,0 +1,52 @@
|
||||
// flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
// TODO: we'll probably need a custom dropdown picker which looks like a search bar
|
||||
// and allows users to input immediately instead of first tapping the dropdown
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
|
||||
import useFetchSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { imageStyles, viewStyles } from "../../styles/explore/explore";
|
||||
|
||||
type Props = {
|
||||
searchTerm: string,
|
||||
search: string => { },
|
||||
setTaxonId: number => { },
|
||||
taxonId: number
|
||||
}
|
||||
|
||||
const TaxaPicker = ( { searchTerm, search, setTaxonId, taxonId }: Props ): Node => {
|
||||
const autocomplete = useFetchSearchResults( searchTerm, "taxa" );
|
||||
|
||||
const [open, setOpen] = useState( false );
|
||||
|
||||
const items = autocomplete.map( taxa => {
|
||||
return {
|
||||
// TODO: match styling on the web; only show matched_term if the common name isn't clearly
|
||||
// linked to the search result
|
||||
label: `${taxa.preferred_common_name} (${taxa.matched_term})`,
|
||||
value: taxa.id,
|
||||
icon: ( ) => <Image source={{ uri: taxa.default_photo.url }} style={imageStyles.pickerIcon} />
|
||||
};
|
||||
} );
|
||||
|
||||
return (
|
||||
<DropDownPicker
|
||||
open={open}
|
||||
value={taxonId}
|
||||
items={items}
|
||||
setOpen={setOpen}
|
||||
setValue={setTaxonId}
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
onChangeSearchText={search}
|
||||
placeholder="Search taxon"
|
||||
style={viewStyles.dropdown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaxaPicker;
|
||||
@@ -0,0 +1,51 @@
|
||||
// flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
// import { Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
// TODO: we'll probably need a custom dropdown picker which looks like a search bar
|
||||
// and allows users to input immediately instead of first tapping the dropdown
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
|
||||
import useFetchSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
|
||||
type Props = {
|
||||
searchTerm: string,
|
||||
search: string => { },
|
||||
setUserId: number => { },
|
||||
userId: number
|
||||
}
|
||||
|
||||
const UserPicker = ( { searchTerm, search, setUserId, userId }: Props ): Node => {
|
||||
const autocomplete = useFetchSearchResults( searchTerm, "users" );
|
||||
|
||||
const [open, setOpen] = useState( false );
|
||||
|
||||
const items = autocomplete.map( user => {
|
||||
return {
|
||||
// TODO: match styling on the web
|
||||
label: user.login,
|
||||
value: user.id
|
||||
// icon: ( ) => <Image source={{ uri: taxa.default_photo.url }} style={imageStyles.pickerIcon} />
|
||||
};
|
||||
} );
|
||||
|
||||
return (
|
||||
<DropDownPicker
|
||||
open={open}
|
||||
value={userId}
|
||||
items={items}
|
||||
setOpen={setOpen}
|
||||
setValue={setUserId}
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
onChangeSearchText={search}
|
||||
placeholder="Search users"
|
||||
style={viewStyles.dropdown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPicker;
|
||||
@@ -16,14 +16,15 @@ const useObservation = ( uuid: string ): Object => {
|
||||
const realmRef = useRef( null );
|
||||
|
||||
const openObservationFromRealm = useCallback( async ( ) => {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
|
||||
try {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
const obs = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
setObservation( obs );
|
||||
}
|
||||
catch ( err ) {
|
||||
console.error( "Error opening realm: ", err.message );
|
||||
console.error( `Error finding Observation with primary key: ${uuid} `, err.message );
|
||||
}
|
||||
}, [realmRef, uuid] );
|
||||
|
||||
@@ -57,7 +58,7 @@ const useObservation = ( uuid: string ): Object => {
|
||||
setObservation( obs );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observation:", e.message, );
|
||||
console.log( `Couldn't fetch observation with uuid ${uuid}: `, e.message, );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, Pressable, Text, Image, View } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import useFetchSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import { viewStyles, imageStyles } from "../../styles/search/search";
|
||||
|
||||
const Search = ( ): React.Node => {
|
||||
const navigation = useNavigation( );
|
||||
const [q, setQ] = React.useState( "" );
|
||||
const [queryType, setQueryType] = React.useState( "taxa" );
|
||||
// choose users or taxa
|
||||
const list = useFetchSearchResults( q, queryType );
|
||||
|
||||
const renderItem = ( { item } ) => {
|
||||
// TODO: make sure TaxonDetails navigates back to Search
|
||||
// instead of defaulting back to ObsList (first item in stack)
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: item.id } );
|
||||
const navToUserProfile = ( ) => navigation.navigate( "UserProfile", { userId: item.id } );
|
||||
if ( queryType === "taxa" ) {
|
||||
const imageUrl = ( item && item.default_photo ) && { uri: item.default_photo.square_url };
|
||||
return (
|
||||
<Pressable
|
||||
onPress={navToTaxonDetails}
|
||||
style={viewStyles.row}
|
||||
testID={`Search.taxa.${item.id}`}
|
||||
>
|
||||
<Image source={imageUrl} style={imageStyles.squareImage} testID={`Search.${item.id}.photo`} />
|
||||
<Text>{`${item.preferred_common_name} (${item.rank} ${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={navToUserProfile}
|
||||
style={viewStyles.row}
|
||||
testID={`Search.user.${item.login}`}
|
||||
>
|
||||
{/* TODO: add an empty icon when user doesn't have an icon */}
|
||||
<Image source={{ uri: item.icon }} style={imageStyles.circularImage} testID={`Search.${item.login}.photo`}/>
|
||||
<Text>{`${item.login} (${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setTaxaSearch = ( ) => setQueryType( "taxa" );
|
||||
const setUserSearch = ( ) => setQueryType( "users" );
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<View style={viewStyles.toggleRow}>
|
||||
<Pressable
|
||||
onPress={setTaxaSearch}
|
||||
testID="Search.taxa"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text>search taxa</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={setUserSearch}
|
||||
testID="Search.users"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text>search users</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<InputField
|
||||
handleTextChange={setQ}
|
||||
placeholder={queryType === "taxa" ? "search for taxa" : "search for users"}
|
||||
text={q}
|
||||
type="none"
|
||||
/>
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
testID="Search.listView"
|
||||
/>
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -3,14 +3,10 @@ import Taxon from "./Taxon";
|
||||
class Identification {
|
||||
static copyRealmSchema( id ) {
|
||||
return {
|
||||
uuid: id.uuid,
|
||||
body: id.body,
|
||||
category: id.category,
|
||||
...id,
|
||||
createdAt: id.created_at,
|
||||
id: id.id,
|
||||
taxon: Taxon.mapApiToRealm( id.taxon ),
|
||||
user: User.mapApiToRealm( id.user ),
|
||||
vision: id.vision
|
||||
user: User.mapApiToRealm( id.user )
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,9 @@ class Observation {
|
||||
const user = User.mapApiToRealm( obs.user );
|
||||
|
||||
return {
|
||||
uuid: obs.uuid,
|
||||
...obs,
|
||||
comments: comments || [],
|
||||
createdAt: obs.created_at,
|
||||
description: obs.description,
|
||||
identifications: identifications || [],
|
||||
latitude: obs.geojson ? obs.geojson.coordinates[1] : null,
|
||||
longitude: obs.geojson ? obs.geojson.coordinates[0] : null,
|
||||
|
||||
+2
-4
@@ -2,11 +2,9 @@ import Photo from "./Photo";
|
||||
class Taxon {
|
||||
static copyRealmSchema( taxon ) {
|
||||
return {
|
||||
...taxon,
|
||||
default_photo: Photo.mapApiToRealm( taxon.default_photo ),
|
||||
id: taxon.id,
|
||||
name: taxon.name,
|
||||
preferredCommonName: taxon.preferred_common_name,
|
||||
rank: taxon.rank
|
||||
preferredCommonName: taxon.preferred_common_name
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createDrawerNavigator } from "@react-navigation/drawer";
|
||||
import PlaceholderComponent from "../components/PlaceholderComponent";
|
||||
import MyObservationsStackNavigator from "./myObservationsStackNavigation";
|
||||
import ExploreStackNavigator from "./exploreStackNavigation";
|
||||
import Search from "../components/Search/Search";
|
||||
|
||||
// this removes the default hamburger menu from header
|
||||
const screenOptions = { headerLeft: ( ) => <></> };
|
||||
@@ -31,7 +32,7 @@ const App = ( ): React.Node => (
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Drawer.Screen name="missions/seen nearby" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="search" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="search" component={Search} />
|
||||
<Drawer.Screen name="identify" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="following (dashboard)" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="impact" component={PlaceholderComponent} />
|
||||
|
||||
@@ -85,7 +85,6 @@ const useObservations = ( ): boolean => {
|
||||
setLoading( false );
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observations:", e.message, );
|
||||
console.trace( e );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
// const FIELDS = {
|
||||
// record: {
|
||||
// name: true
|
||||
// }
|
||||
// };
|
||||
|
||||
const useRemoteSearchResults = ( q: string, sources: string ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 10,
|
||||
q,
|
||||
// TODO: get fields param working
|
||||
sources
|
||||
};
|
||||
const response = await inatjs.search( params );
|
||||
const results = response.results.map( result => result.record );
|
||||
if ( !isCurrent ) { return; }
|
||||
setSearchResults( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch search results with sources ${sources}:`, e.message, );
|
||||
}
|
||||
};
|
||||
|
||||
fetchSearchResults( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [q, sources] );
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
export default useRemoteSearchResults;
|
||||
@@ -0,0 +1,48 @@
|
||||
// @flow strict-local
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import type { ImageStyleProp, TextStyleProp, ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../global";
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
paddingVertical: 5,
|
||||
borderBottomColor: colors.gray,
|
||||
borderBottomWidth: 1
|
||||
},
|
||||
toggleRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-around"
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
text: { }
|
||||
} );
|
||||
|
||||
const imageWidth = 40;
|
||||
|
||||
const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
|
||||
circularImage: {
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
borderRadius: 50,
|
||||
marginRight: 10
|
||||
},
|
||||
squareImage: {
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
marginRight: 10
|
||||
}
|
||||
} );
|
||||
|
||||
export {
|
||||
imageStyles,
|
||||
textStyles,
|
||||
viewStyles
|
||||
};
|
||||
@@ -65,6 +65,7 @@ test( "navigates to observer profile on button press", ( ) => {
|
||||
|
||||
test( "navigates to identifier profile on button press", ( ) => {
|
||||
const { getByTestId } = renderObsDetails( );
|
||||
|
||||
fireEvent.press( getByTestId( `ObsDetails.identifier.${mockObservation.identifications[0].user.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", {
|
||||
userId: mockObservation.identifications[0].user.id
|
||||
@@ -73,6 +74,7 @@ test( "navigates to identifier profile on button press", ( ) => {
|
||||
|
||||
test( "navigates to taxon details on button press", ( ) => {
|
||||
const { getByTestId } = renderObsDetails( );
|
||||
|
||||
fireEvent.press( getByTestId( `ObsDetails.taxon.${mockObservation.taxon.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "TaxonDetails", {
|
||||
id: mockObservation.taxon.id
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import { render, fireEvent } from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import Search from "../../../../src/components/Search/Search";
|
||||
|
||||
const testTaxaList = [
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" )
|
||||
];
|
||||
|
||||
const mockExpected = testTaxaList;
|
||||
jest.mock( "../../../../src/sharedHooks/useRemoteSearchResults", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockExpected
|
||||
} ) );
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: ( ) => ( {
|
||||
navigate: mockedNavigate
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
const renderSearch = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
test( "renders taxon search results from API call", ( ) => {
|
||||
const { getByTestId, getByText } = renderSearch( );
|
||||
|
||||
const taxon = testTaxaList[0];
|
||||
|
||||
const commonName = taxon.preferred_common_name;
|
||||
expect( getByTestId( "Search.taxa" ) ).toBeTruthy( );
|
||||
expect( getByTestId( `Search.${taxon.id}.photo` ).props.source ).toStrictEqual( { "uri": taxon.default_photo.square_url } );
|
||||
// using RegExp to be able to search within a string
|
||||
expect( getByText( new RegExp( commonName ) ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
// right now this is failing on react-native-modal, since there's a TouchableWithFeedback
|
||||
// that allows the user to tap the backdrop and exit the modal
|
||||
test.todo( "should not have accessibility errors" );
|
||||
|
||||
test( "navigates to TaxonDetails on button press", ( ) => {
|
||||
const { getByTestId } = renderSearch( );
|
||||
|
||||
const taxon = testTaxaList[0];
|
||||
|
||||
fireEvent.press( getByTestId( `Search.taxa.${taxon.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "TaxonDetails", { id: taxon.id } );
|
||||
} );
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { render, fireEvent } from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import Search from "../../../../src/components/Search/Search";
|
||||
|
||||
// TODO: figure out how to clear jest mocks correctly or return a different
|
||||
// value from jest mocks so these can all live in a single file?
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: ( ) => ( {
|
||||
navigate: mockedNavigate
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
const renderSearch = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
const testUserList = [
|
||||
factory( "RemoteUser" )
|
||||
];
|
||||
|
||||
const mockExpectedUsers = testUserList;
|
||||
jest.mock( "../../../../src/sharedHooks/useRemoteSearchResults", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockExpectedUsers
|
||||
} ) );
|
||||
|
||||
test( "displays user search results on button press", ( ) => {
|
||||
const { getByTestId, getByText } = renderSearch( );
|
||||
|
||||
const user = testUserList[0];
|
||||
const { login } = user;
|
||||
const button = getByTestId( "Search.users" );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( getByTestId( `Search.user.${login}` ) ).toBeTruthy( );
|
||||
expect( getByTestId( `Search.${login}.photo` ).props.source ).toStrictEqual( { "uri": user.icon } );
|
||||
expect( getByText( new RegExp( login ) ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "navigates to user profile on button press", ( ) => {
|
||||
const { getByTestId } = renderSearch( );
|
||||
|
||||
const user = testUserList[0];
|
||||
const { login } = user;
|
||||
const button = getByTestId( "Search.users" );
|
||||
|
||||
fireEvent.press( button );
|
||||
fireEvent.press( getByTestId( `Search.user.${login}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", { userId: user.id } );
|
||||
} );
|
||||
|
||||
Reference in new issue
Block a user