Taxon result updates (#1254)

* Only requests remote taxon if local is missing or hasn't been synced in a
  week
* Returns a localized version of the remote taxon immediately without waiting
  to get a newly-created record from realm
* Expands tests to use a unique realm, integrate more of our code, and check
  to ensure the API gets called
* Show indicator while loading taxon on ARCamera
* Show iconic taxon found in model results if we can't load a remote taxon
* Show iconic taxon as backdrop for ObsImage whenever possible
This commit is contained in:
Ken-ichi
2024-03-05 16:54:42 -08:00
committed by GitHub
parent 7ad0cdb701
commit 6fdf3d2faf
18 changed files with 520 additions and 214 deletions

View File

@@ -43,10 +43,10 @@ async function handleError( e: Object, options: Object = {} ): Object {
if ( typeof ( options.onApiError ) === "function" ) {
options.onApiError( error );
}
// Default to throw errors. We almost never want handle supress an error at
// Default to throw errors. We almost never want supress an error at
// this low level
if ( options.throw === false ) {
return null;
return error;
}
throw error;
}

View File

@@ -3,6 +3,7 @@
import {
useState
} from "react";
import { useIconicTaxa } from "sharedHooks";
const usePredictions = ( ): Object => {
const [result, setResult] = useState( null );
@@ -11,22 +12,27 @@ const usePredictions = ( ): Object => {
const [fps, setFPS] = useState( 1 );
const [numStoredResults, setNumStoredResults] = useState( 4 );
const [cropRatio, setCropRatio] = useState( 1 );
const iconicTaxa = useIconicTaxa( );
const handleTaxaDetected = cvResult => {
console.log( "[DEBUG usePredictions.js] cvResult: ", cvResult );
if ( cvResult && !modelLoaded ) {
setModelLoaded( true );
}
let prediction = null;
const { predictions } = cvResult;
predictions.sort( ( a, b ) => a.rank_level - b.rank_level );
if ( predictions.length > 0 ) {
const finestPrediction = predictions[0];
const { predictions: branch } = cvResult;
branch.sort( ( a, b ) => a.rank_level - b.rank_level );
const branchIDs = branch.map( t => t.taxon_id );
if ( branch.length > 0 ) {
const finestPrediction = branch[0];
// Try to find a known iconic taxon in the model results so we can show
// an icon if we can't show a photo
const iconicTaxon = iconicTaxa?.find( t => branchIDs.indexOf( t.id ) >= 0 );
prediction = {
taxon: {
rank_level: finestPrediction.rank_level,
id: finestPrediction.taxon_id,
name: finestPrediction.name
name: finestPrediction.name,
iconic_taxon_name: iconicTaxon?.name
},
score: finestPrediction.score
};

View File

@@ -137,7 +137,7 @@ const DisplayTaxonName = ( {
return (
<View
testID="display-taxon-name"
className={classNames( "flex", null, {
className={classNames( "flex", {
"flex-row items-end flex-wrap w-11/12": isHorizontal
} )}
>

View File

@@ -1,7 +1,7 @@
// @flow
import classNames from "classnames";
import { IconicTaxonIcon } from "components/SharedComponents";
import { Image } from "components/styledComponents";
import { Image, View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
@@ -35,29 +35,36 @@ const ObsImage = ( {
}: Props ): Node => {
const noImg = !uri?.uri;
const iconicTaxon = (
<IconicTaxonIcon
imageClassName={[
...CLASS_NAMES,
imageClassName,
{ "bg-darkGray": white }
]}
iconicTaxonName={iconicTaxonName}
white={white}
isBackground={isBackground}
size={iconicTaxonIconSize}
/>
);
if ( noImg ) {
return (
<IconicTaxonIcon
imageClassName={[
...CLASS_NAMES,
imageClassName,
{ "bg-darkGray": white }
]}
iconicTaxonName={iconicTaxonName}
white={white}
isBackground={isBackground}
size={iconicTaxonIconSize}
/>
);
return iconicTaxon;
}
return (
<Image
source={uri}
className={classNames( ...CLASS_NAMES, { "opacity-50": opaque } )}
testID="ObsList.photo"
accessibilityIgnoresInvertColors
/>
<View className={classNames( CLASS_NAMES, "relative" )}>
<View className="absolute w-full h-full">
{iconicTaxon}
</View>
<Image
source={uri}
className={classNames( ...CLASS_NAMES, { "opacity-50": opaque } )}
testID="ObsList.photo"
accessibilityIgnoresInvertColors
/>
</View>
);
};

View File

@@ -2,7 +2,11 @@
import { useNavigation } from "@react-navigation/native";
import classnames from "classnames";
import { DisplayTaxonName, INatIconButton } from "components/SharedComponents";
import {
ActivityIndicator,
DisplayTaxonName,
INatIconButton
} from "components/SharedComponents";
import ObsImagePreview from "components/SharedComponents/ObservationsFlashList/ObsImagePreview";
import { Pressable, View } from "components/styledComponents";
import type { Node } from "react";
@@ -39,7 +43,7 @@ const TaxonResult = ( {
handleCheckmarkPress,
handlePress,
showCheckmark = true,
taxon: taxonResult,
taxon: taxonProp,
testID,
white = false
}: Props ): Node => {
@@ -50,13 +54,13 @@ const TaxonResult = ( {
// network requests for useTaxon instead of making individual API calls.
// right now, this fetches a single taxon at a time on AR camera &
// a short list of taxa from offline Suggestions
const localTaxon = useTaxon( taxonResult, fetchRemote );
const taxon = fromLocal
const { taxon: localTaxon, isLoading } = useTaxon( taxonProp, fetchRemote );
const usableTaxon = fromLocal
? localTaxon
: taxonResult;
const taxonImage = { uri: taxon?.default_photo?.url };
: taxonProp;
const taxonImage = { uri: usableTaxon.default_photo?.url };
const navToTaxonDetails = () => navigation.navigate( "TaxonDetails", { id: taxon.id } );
const navToTaxonDetails = () => navigation.navigate( "TaxonDetails", { id: usableTaxon.id } );
return (
<View
@@ -65,7 +69,6 @@ const TaxonResult = ( {
"flex-row items-center justify-between px-4 py-3",
{
"border-b-[1px] border-lightGray": !clearBackground,
"mx-4": clearBackground,
"border-t-[1px]": first
}
)
@@ -73,33 +76,43 @@ const TaxonResult = ( {
testID={testID}
>
<Pressable
className="flex-row items-center w-16 grow"
className="flex-row items-center shrink"
onPress={handlePress || navToTaxonDetails}
accessible
accessibilityRole="link"
accessibilityLabel={t( "Navigate-to-taxon-details" )}
accessibilityValue={{ text: taxon.name }}
accessibilityValue={{ text: usableTaxon.name }}
accessibilityState={{ disabled: false }}
>
<ObsImagePreview
source={taxonImage}
testID={`${testID}.photo`}
iconicTaxonName={taxon?.iconic_taxon_name}
className="rounded-xl"
isSmall
white={white}
/>
{( confidence && confidencePosition === "photo" ) && (
<View className="absolute -bottom-5 w-[62px]">
<ConfidenceInterval
confidence={confidence}
activeColor={activeColor}
/>
</View>
)}
<View className="w-[62px] h-[62px] justify-center relative">
{
isLoading
? (
<ActivityIndicator size={44} />
)
: (
<ObsImagePreview
source={taxonImage}
testID={`${testID}.photo`}
iconicTaxonName={usableTaxon?.iconic_taxon_name}
className="rounded-xl"
isSmall
white={white}
/>
)
}
{( confidence && confidencePosition === "photo" ) && (
<View className="absolute -bottom-4 w-full items-center">
<ConfidenceInterval
confidence={confidence}
activeColor={activeColor}
/>
</View>
)}
</View>
<View className="shrink ml-3">
<DisplayTaxonName
taxon={taxon}
taxon={usableTaxon}
color={clearBackground && "text-white"}
/>
{( confidence && confidencePosition === "text" ) && (
@@ -136,7 +149,7 @@ const TaxonResult = ( {
? theme.colors.onSecondary
: theme.colors.secondary
}
onPress={() => handleCheckmarkPress( taxon )}
onPress={() => handleCheckmarkPress( usableTaxon )}
accessibilityLabel={t( "Checkmark" )}
accessibilityHint={t( "Add-this-ID" )}
testID={`${testID}.checkmark`}

View File

@@ -1,5 +1,4 @@
import { Realm } from "@realm/react";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import Photo from "./Photo";
@@ -107,15 +106,6 @@ class Taxon extends Realm.Object {
static uri = item => ( item && item.default_photo ) && { uri: item.default_photo.url };
static saveRemoteTaxon = async ( remoteTaxon, realm ) => {
if ( remoteTaxon ) {
const localTaxon = Taxon.mapApiToRealm( remoteTaxon, realm );
safeRealmWrite( realm, ( ) => {
realm.create( "Taxon", localTaxon, "modified" );
}, "saving remote taxon in Taxon" );
}
};
static schema = {
name: "Taxon",
primaryKey: "id",
@@ -136,7 +126,8 @@ class Taxon extends Realm.Object {
rank: "string?",
rank_level: "float?",
isIconic: "bool?",
iconic_taxon_name: "string?"
iconic_taxon_name: "string?",
_synced_at: "date?"
}
};
}

View File

@@ -28,7 +28,7 @@ export default {
User,
Vote
],
schemaVersion: 46,
schemaVersion: 47,
path: `${RNFS.DocumentDirectoryPath}/db.realm`,
// https://github.com/realm/realm-js/pull/6076 embedded constraints
migrationOptions: {

View File

@@ -7,7 +7,8 @@ import { useAuthenticatedQuery, useIsConnected } from "sharedHooks";
const { useRealm } = RealmContext;
const useIconicTaxa = ( { reload }: Object ): Object => {
const useIconicTaxa = ( options: { reload: boolean } = { reload: false } ): Object => {
const { reload } = options;
const realm = useRealm( );
const isConnected = useIsConnected( );
const [isUpdatingRealm, setIsUpdatingRealm] = useState( );
@@ -23,10 +24,11 @@ const useIconicTaxa = ( { reload }: Object ): Object => {
if ( iconicTaxa?.length > 0 && !isUpdatingRealm ) {
setIsUpdatingRealm( true );
safeRealmWrite( realm, ( ) => {
iconicTaxa.forEach( taxa => {
iconicTaxa.forEach( taxon => {
realm.create( "Taxon", {
...taxa,
isIconic: true
...taxon,
isIconic: true,
_synced_at: new Date( )
}, "modified" );
} );
}, "modifying iconic taxa in useIconicTaxa" );

View File

@@ -3,30 +3,67 @@
import { fetchTaxon } from "api/taxa";
import { RealmContext } from "providers/contexts";
import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useAuthenticatedQuery } from "sharedHooks";
const { useRealm } = RealmContext;
const ONE_WEEK_MS = (
1000 // ms / s
* 60 // s / min
* 60 // min / hr
* 24 // hr / day
* 7 // day / wk
);
const useTaxon = ( taxon: Object, fetchRemote: boolean = true ): Object => {
const realm = useRealm( );
const existingTaxon = taxon.id && realm.objectForPrimaryKey( "Taxon", taxon?.id );
const localTaxon = taxon.id && realm.objectForPrimaryKey( "Taxon", taxon.id );
const canFetchTaxon = !!taxon?.id;
const localTaxonNeedsSync = (
// Definitely sync if there's no local copy
!localTaxon
|| (
// Sync if the local copy hasn't been synced in a week
localTaxon._synced_at && ( Date.now( ) - localTaxon._synced_at > ONE_WEEK_MS )
)
);
const enabled = canFetchTaxon && fetchRemote && localTaxonNeedsSync;
const {
data: remoteTaxon
data: remoteTaxon,
isLoading
} = useAuthenticatedQuery(
["fetchTaxon", taxon?.id],
optsWithAuth => fetchTaxon( taxon.id, { fields: Taxon.TAXON_FIELDS }, optsWithAuth ),
{
enabled: !!( taxon?.id && fetchRemote )
enabled
}
);
if ( !existingTaxon && remoteTaxon ) {
Taxon.saveRemoteTaxon( remoteTaxon, realm );
const mappedRemoteTaxon = remoteTaxon
? Taxon.mapApiToRealm( remoteTaxon, realm )
: null;
if ( localTaxonNeedsSync && mappedRemoteTaxon ) {
safeRealmWrite( realm, ( ) => {
realm.create(
"Taxon",
{ ...mappedRemoteTaxon, _synced_at: new Date( ) },
"modified"
);
}, "saving remote taxon in useTaxon" );
}
return existingTaxon || taxon;
// Local is best, local-ish version of remote will be available sooner, use
// whatever was passed in as a last resort
return {
taxon: localTaxon || mappedRemoteTaxon || taxon,
// Apparently useQuery isLoading is true if the query is disabled
isLoading: enabled && isLoading
};
};
export default useTaxon;

View File

@@ -1,10 +1,10 @@
import { define } from "factoria";
export default define( "LocalTaxon", faker => ( {
_synced_at: faker.date.past( ),
id: faker.number.int( ),
name: faker.person.fullName( ),
preferred_common_name: faker.person.fullName( ),
rank: "species",
rank_level: 10,
taxon_photos: []
rank_level: 10
} ) );

View File

@@ -23,7 +23,7 @@ const queryClient = new QueryClient( {
}
} );
function renderComponent( component, update = null ) {
function renderComponent( component, update = null, renderOptions = {} ) {
const renderMethod = update || render;
return renderMethod(
<QueryClientProvider client={queryClient}>
@@ -36,7 +36,8 @@ function renderComponent( component, update = null ) {
</BottomSheetModalProvider>
</GestureHandlerRootView>
</INatPaperProvider>
</QueryClientProvider>
</QueryClientProvider>,
renderOptions
);
}
@@ -82,9 +83,47 @@ async function renderAppWithObservations(
await screen.findByTestId( `MyObservations.obsListItem.${observations[0].uuid}` );
}
/**
* Render a hook within a component
*
* Port of equivalent in react-testing-library
* (https://github.com/testing-library/react-testing-library/blob/edb6344d578a8c224daf0cd6e2984f36cc6e8d86/src/pure.js#L264C1-L290C2),
* but using our renderComponent
*/
function renderHook( renderCallback, options = {} ) {
const { initialProps, ...renderOptions } = options;
const result = React.createRef( );
const TestComponent = ( { renderCallbackProps } ) => {
// eslint-disable-next-line testing-library/render-result-naming-convention
const pendingResult = renderCallback( renderCallbackProps );
React.useEffect( ( ) => {
result.current = pendingResult;
} );
return null;
};
const { rerender: baseRerender, unmount } = renderComponent(
<TestComponent renderCallbackProps={initialProps} />,
null,
renderOptions
);
function rerender( rerenderCallbackProps ) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />
);
}
return { result, rerender, unmount };
}
export {
renderApp,
renderAppWithComponent,
renderAppWithObservations,
renderComponent
renderComponent,
renderHook
};

View File

@@ -1,8 +1,32 @@
import { renderHook } from "@testing-library/react-native";
import { waitFor } from "@testing-library/react-native";
import inatjs from "inaturalistjs";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useTaxon } from "sharedHooks";
import factory from "tests/factory";
import factory, { makeResponse } from "tests/factory";
import faker from "tests/helpers/faker";
import { renderHook } from "tests/helpers/render";
import setupUniqueRealm from "tests/helpers/uniqueRealm";
// UNIQUE REALM SETUP
const mockRealmIdentifier = __filename;
const { mockRealmModelsIndex, uniqueRealmBeforeAll, uniqueRealmAfterAll } = setupUniqueRealm(
mockRealmIdentifier
);
jest.mock( "realmModels/index", ( ) => mockRealmModelsIndex );
jest.mock( "providers/contexts", ( ) => {
const originalModule = jest.requireActual( "providers/contexts" );
return {
__esModule: true,
...originalModule,
RealmContext: {
...originalModule.RealmContext,
useRealm: ( ) => global.mockRealms[mockRealmIdentifier]
}
};
} );
beforeAll( uniqueRealmBeforeAll );
afterAll( uniqueRealmAfterAll );
// /UNIQUE REALM SETUP
const mockRemoteTaxon = factory( "RemoteTaxon", {
default_photo: {
@@ -10,25 +34,33 @@ const mockRemoteTaxon = factory( "RemoteTaxon", {
}
} );
jest.mock( "sharedHooks/useAuthenticatedQuery", () => ( {
__esModule: true,
default: ( ) => ( {
data: mockRemoteTaxon
} )
} ) );
const mockTaxon = factory( "LocalTaxon", {
_syncedAt: faker.date.recent( { days: 1 } ),
default_photo: {
url: faker.image.url( )
}
} );
const mockOutdatedTaxon = factory( "LocalTaxon", {
_syncedAt: faker.date.recent( { days: 1, refDate: "2024-01-01" } ),
default_photo: {
url: faker.image.url( )
}
} );
describe( "useTaxon", ( ) => {
beforeAll( async () => {
jest.useFakeTimers( );
} );
beforeEach( ( ) => {
jest.restoreAllMocks( );
inatjs.taxa.fetch.mockResolvedValue( makeResponse( [mockRemoteTaxon] ) );
} );
describe( "with local taxon", ( ) => {
beforeEach( async ( ) => {
beforeEach( ( ) => {
// Write mock taxon to realm
safeRealmWrite( global.realm, ( ) => {
global.realm.create( "Taxon", mockTaxon, "modified" );
safeRealmWrite( global.mockRealms[mockRealmIdentifier], ( ) => {
global.mockRealms[mockRealmIdentifier].create( "Taxon", mockTaxon, "modified" );
}, "write mock taxon, useTaxon test" );
} );
@@ -40,23 +72,73 @@ describe( "useTaxon", ( ) => {
describe( "when there is a local taxon with taxon id", ( ) => {
it( "should return local taxon with default photo", ( ) => {
const { result } = renderHook( ( ) => useTaxon( mockTaxon ) );
expect( result.current ).toHaveProperty( "default_photo" );
expect( result.current.default_photo.url ).toEqual( mockTaxon.default_photo.url );
expect( inatjs.taxa.fetch ).not.toHaveBeenCalled( );
const { taxon: resultTaxon } = result.current;
expect( resultTaxon ).toHaveProperty( "default_photo" );
expect( resultTaxon.default_photo.url ).toEqual( mockTaxon.default_photo.url );
} );
it( "should request a taxon from the API if the local copy is out of date", async ( ) => {
safeRealmWrite( global.mockRealms[mockRealmIdentifier], ( ) => {
global.mockRealms[mockRealmIdentifier].create( "Taxon", mockOutdatedTaxon, "modified" );
}, "write mock outdated taxon, useTaxon test" );
renderHook( ( ) => useTaxon( mockOutdatedTaxon ) );
await waitFor( ( ) => expect( inatjs.taxa.fetch ).toHaveBeenCalled( ) );
} );
} );
} );
describe( "when there is no local taxon with taxon id", ( ) => {
beforeEach( async ( ) => {
safeRealmWrite( global.realm, ( ) => {
global.realm.deleteAll( );
safeRealmWrite( global.mockRealms[mockRealmIdentifier], ( ) => {
global.mockRealms[mockRealmIdentifier].deleteAll( );
}, "delete all realm, useTaxon test" );
} );
it( "should make an API call and return passed in taxon when fetchRemote is enabled", ( ) => {
const taxonId = faker.number.int( );
const { result } = renderHook( ( ) => useTaxon( { id: taxonId }, true ) );
expect( result.current ).not.toHaveProperty( "default_photo" );
describe( "with fetchRemote: true", ( ) => {
it( "should request the taxon from the API", async ( ) => {
expect(
global.mockRealms[mockRealmIdentifier].objectForPrimaryKey( "Taxon", mockTaxon.id )
).toBeNull( );
renderHook( ( ) => useTaxon( mockTaxon ) );
await waitFor( ( ) => expect( inatjs.taxa.fetch ).toHaveBeenCalled( ) );
} );
it( "should return the argument taxon if request fails", ( ) => {
// I don't love this. While it kind of mocks at the edge of the code
// we need to integrate, it doesn't test out API error handling code.
// I tried mocking inatjs to make it throw, but that always seems to
// result in a failure in the test, even though useQuery should catch
// those errors. ~~~kueda20240305
jest.mock( "@tanstack/react-query", () => ( {
useQuery: jest.fn( ( ) => ( {
error: { }
} ) )
} ) );
const partialTaxon = { id: faker.number.int( ), foo: "bar" };
const { result } = renderHook( ( ) => useTaxon( partialTaxon ) );
expect( result.current.taxon.foo ).toEqual( "bar" );
jest.unmock( "@tanstack/react-query" );
} );
it( "should return a taxon like a local taxon record if the request succeeds", async ( ) => {
const { result } = renderHook( ( ) => useTaxon( { id: mockTaxon.id } ) );
await waitFor( ( ) => expect( inatjs.taxa.fetch ).toHaveBeenCalled( ) );
expect( result.current.taxon ).toHaveProperty( "default_photo" );
} );
} );
describe( "with fetchRemote: false", ( ) => {
it( "should not call the API and return passed in taxon", ( ) => {
const taxonId = faker.number.int( );
expect(
global.mockRealms[mockRealmIdentifier].objectForPrimaryKey( "Taxon", taxonId )
).toBeNull( );
const { result } = renderHook( ( ) => useTaxon( { id: taxonId, foo: "bar" }, false ) );
expect( inatjs.taxa.fetch ).not.toHaveBeenCalled( );
expect( result.current.taxon ).not.toHaveProperty( "default_photo" );
expect( result.current.taxon.foo ).toEqual( "bar" );
} );
} );
} );
} );

View File

@@ -35,7 +35,7 @@ jest.mock( "sharedHooks/useAuthenticatedQuery", () => ( {
jest.mock( "sharedHooks/useTaxon", () => ( {
__esModule: true,
default: () => mockLocalTaxon
default: () => ( { taxon: mockLocalTaxon } )
} ) );
const mockModelLoaded = {
@@ -120,9 +120,11 @@ describe( "AR Camera", ( ) => {
it( "displays iconic taxon icon if taxon does not exist in realm", ( ) => {
jest.spyOn( useTaxon, "default" ).mockImplementation( () => ( {
...mockLocalTaxon,
default_photo: {
url: null
taxon: {
...mockLocalTaxon,
default_photo: {
url: null
}
}
} ) );
jest.spyOn( usePredictions, "default" ).mockImplementation( () => ( {

View File

@@ -31,13 +31,7 @@ exports[`ObsGridItem for an observation with a photo should render 1`] = `
}
testID="MyObservations.gridItem.00000000-0000-0000-0000-000000000000"
>
<Image
accessibilityIgnoresInvertColors={true}
source={
{
"uri": "https://inaturalist-open-data.s3.amazonaws.com/photos/1/large.jpeg",
}
}
<View
style={
[
[
@@ -47,11 +41,126 @@ exports[`ObsGridItem for an observation with a photo should render 1`] = `
{
"aspectRatio": 1,
},
{
"position": "relative",
},
],
]
}
testID="ObsList.photo"
/>
>
<View
style={
[
[
{
"position": "absolute",
},
{
"width": "100%",
},
{
"height": "100%",
},
],
]
}
>
<View
style={
[
[
{
"flexGrow": 1,
},
{
"aspectRatio": 1,
},
{
"maxHeight": 210,
},
{
"overflow": "hidden",
},
{
"position": "relative",
},
{
"borderBottomLeftRadius": 15,
"borderBottomRightRadius": 15,
"borderTopLeftRadius": 15,
"borderTopRightRadius": 15,
},
{
"width": 200,
},
{
"width": 200,
},
{
"backgroundColor": "#454545",
},
{
"flexShrink": 1,
},
{
"justifyContent": "center",
},
{
"alignItems": "center",
},
{
"backgroundColor": "#454545",
},
],
]
}
testID="IconicTaxonName.iconicTaxonIcon"
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#BFBFBF33",
"fontSize": 100,
},
undefined,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Image
accessibilityIgnoresInvertColors={true}
source={
{
"uri": "https://inaturalist-open-data.s3.amazonaws.com/photos/1/large.jpeg",
}
}
style={
[
[
{
"flexGrow": 1,
},
{
"aspectRatio": 1,
},
],
]
}
testID="ObsList.photo"
/>
</View>
<BVLinearGradient
colors={
[

View File

@@ -12,7 +12,7 @@ const mockTaxon = {
jest.mock( "sharedHooks/useTaxon", () => ( {
__esModule: true,
default: () => mockTaxon
default: () => ( { taxon: mockTaxon } )
} ) );
describe( "TaxonResult", () => {

View File

@@ -77,10 +77,7 @@ exports[`TaxonResult should render correctly 1`] = `
"alignItems": "center",
},
{
"width": 64,
},
{
"flexGrow": 1,
"flexShrink": 1,
},
],
]
@@ -91,49 +88,25 @@ exports[`TaxonResult should render correctly 1`] = `
[
[
{
"maxHeight": 210,
},
{
"overflow": "hidden",
},
{
"position": "relative",
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"width": 62,
},
{
"height": 62,
},
{
"width": 62,
"justifyContent": "center",
},
{
"position": "relative",
},
[
{
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
},
],
],
]
}
testID="undefined.photo"
>
<View
style={
[
[
{
"flexGrow": 1,
},
{
"aspectRatio": 1,
},
{
"maxHeight": 210,
},
@@ -155,77 +128,122 @@ exports[`TaxonResult should render correctly 1`] = `
{
"width": 62,
},
{
"flexShrink": 1,
},
{
"justifyContent": "center",
},
{
"alignItems": "center",
},
{
"borderBottomWidth": 1,
"borderLeftWidth": 1,
"borderRightWidth": 1,
"borderTopWidth": 1,
},
{
"borderBottomColor": "#E8E8E8",
"borderLeftColor": "#E8E8E8",
"borderRightColor": "#E8E8E8",
"borderTopColor": "#E8E8E8",
},
[
{
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
},
],
],
]
}
testID="IconicTaxonName.iconicTaxonIcon"
testID="undefined.photo"
>
<Text
allowFontScaling={false}
selectable={false}
<View
style={
[
{
"color": "rgba(103, 80, 164, 1)",
"fontSize": 22,
},
undefined,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
[
{
"flexGrow": 1,
},
{
"aspectRatio": 1,
},
{
"maxHeight": 210,
},
{
"overflow": "hidden",
},
{
"position": "relative",
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"height": 62,
},
{
"width": 62,
},
{
"flexShrink": 1,
},
{
"justifyContent": "center",
},
{
"alignItems": "center",
},
{
"borderBottomWidth": 1,
"borderLeftWidth": 1,
"borderRightWidth": 1,
"borderTopWidth": 1,
},
{
"borderBottomColor": "#E8E8E8",
"borderLeftColor": "#E8E8E8",
"borderRightColor": "#E8E8E8",
"borderTopColor": "#E8E8E8",
},
],
]
}
testID="IconicTaxonName.iconicTaxonIcon"
>
</Text>
</View>
<View
style={
[
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "rgba(103, 80, 164, 1)",
"fontSize": 22,
},
undefined,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
<View
style={
[
{
"position": "absolute",
},
{
"right": 0,
},
{
"paddingBottom": 4,
"paddingLeft": 4,
"paddingRight": 4,
"paddingTop": 4,
},
{
"bottom": 0,
},
],
]
}
/>
[
{
"position": "absolute",
},
{
"right": 0,
},
{
"paddingBottom": 4,
"paddingLeft": 4,
"paddingRight": 4,
"paddingTop": 4,
},
{
"bottom": 0,
},
],
]
}
/>
</View>
</View>
<View
style={

View File

@@ -18,7 +18,7 @@ const mockTaxon = factory( "RemoteTaxon" );
jest.mock( "sharedHooks/useTaxon", () => ( {
__esModule: true,
default: () => mockTaxon
default: () => ( { taxon: mockTaxon } )
} ) );
const mockSuggestionsList = [{

View File

@@ -33,7 +33,7 @@ jest.mock( "sharedHooks/useTaxonSearch", () => ( {
jest.mock( "sharedHooks/useTaxon", () => ( {
__esModule: true,
default: () => mockTaxaList[0]
default: () => ( { taxon: mockTaxaList[0] } )
} ) );
// react-native-paper's TextInput does a bunch of async stuff that's hard to