diff --git a/src/components/Explore/ExploreV2/helpers/universalSearchSubject.ts b/src/components/Explore/ExploreV2/helpers/universalSearchSubject.ts
index aa7f431f9..7d597e8e9 100644
--- a/src/components/Explore/ExploreV2/helpers/universalSearchSubject.ts
+++ b/src/components/Explore/ExploreV2/helpers/universalSearchSubject.ts
@@ -46,7 +46,7 @@ export const resultToSubject = ( result: UniversalSearchResultItem ): ExploreV2S
export const subjectToText = (
subject: ExploreV2Subject,
- prefersCommonNames: boolean,
+ commonNameIsPrimary: boolean,
): string => {
switch ( subject.type ) {
case "user":
@@ -55,7 +55,7 @@ export const subjectToText = (
return subject.project.title;
case "taxon":
default:
- return ( prefersCommonNames && subject.taxon.preferred_common_name )
+ return ( commonNameIsPrimary && subject.taxon.preferred_common_name )
? generateTaxonPieces( subject.taxon ).commonName ?? subject.taxon.name
: subject.taxon.name;
}
diff --git a/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx b/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
index fca7dd875..8ffe931af 100644
--- a/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
+++ b/src/components/Explore/ExploreV2/screens/UniversalSearch.tsx
@@ -55,7 +55,8 @@ const UniversalSearch = ( ) => {
const { t } = useTranslation( );
const { dispatch } = useExploreV2( );
const currentUser = useCurrentUser( );
- const prefersCommonNames = currentUser?.prefers_common_names !== false;
+ const commonNameIsPrimary = currentUser?.prefers_common_names !== false
+ && currentUser?.prefers_scientific_name_first !== true;
const [taxonText, setTaxonText] = useState( "" );
const [locationText, setLocationText] = useState( "" );
@@ -80,11 +81,11 @@ const UniversalSearch = ( ) => {
}, [debounceQuery] );
const handleSelect = useCallback( ( subject: ExploreV2Subject ) => {
- setTaxonText( subjectToText( subject, prefersCommonNames ) );
+ setTaxonText( subjectToText( subject, commonNameIsPrimary ) );
setQueryImmediately( "" );
dispatch( { type: EXPLORE_V2_ACTION.SET_SUBJECT, subject } );
locationInputRef.current?.focus( );
- }, [dispatch, prefersCommonNames, setQueryImmediately] );
+ }, [commonNameIsPrimary, dispatch, setQueryImmediately] );
const handleReset = useCallback( ( ) => {
setTaxonText( "" );
diff --git a/src/sharedHooks/useUniversalSearch.ts b/src/sharedHooks/useUniversalSearch.ts
index 5af566568..8fdb40591 100644
--- a/src/sharedHooks/useUniversalSearch.ts
+++ b/src/sharedHooks/useUniversalSearch.ts
@@ -17,7 +17,6 @@ export type UniversalSearchResultItem =
const useUniversalSearch = ( query: string ) => {
const trimmedQuery = query.trim( );
- // TODO look into some sort of throttle here
const shouldFetch = trimmedQuery.length > 0;
const {
diff --git a/tests/unit/components/Explore/ExploreV2/screens/UniversalSearch.test.js b/tests/unit/components/Explore/ExploreV2/screens/UniversalSearch.test.js
index f558194e5..dbd80fa48 100644
--- a/tests/unit/components/Explore/ExploreV2/screens/UniversalSearch.test.js
+++ b/tests/unit/components/Explore/ExploreV2/screens/UniversalSearch.test.js
@@ -1,4 +1,4 @@
-import { fireEvent, screen } from "@testing-library/react-native";
+import { act, fireEvent, screen } from "@testing-library/react-native";
import UniversalSearch from "components/Explore/ExploreV2/screens/UniversalSearch";
import initI18next from "i18n/initI18next";
import i18next from "i18next";
@@ -18,10 +18,92 @@ jest.mock( "@react-navigation/native", ( ) => {
};
} );
+const mockDispatch = jest.fn( );
+jest.mock( "providers/ExploreV2Context", ( ) => {
+ const actual = jest.requireActual( "providers/ExploreV2Context" );
+ return {
+ ...actual,
+ useExploreV2: jest.fn( ),
+ };
+} );
+const { useExploreV2 } = require( "providers/ExploreV2Context" );
+
+jest.mock( "sharedHooks/useUniversalSearch" );
+const useUniversalSearch = require( "sharedHooks/useUniversalSearch" ).default;
+
+jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
+ __esModule: true,
+ default: jest.fn( ),
+} ) );
+const useCurrentUser = require( "sharedHooks/useCurrentUser" ).default;
+
+const CURRENT_USER = {
+ id: 99,
+ login: "tester",
+ icon_url: null,
+ observations_count: 42,
+ prefers_common_names: true,
+ prefers_scientific_name_first: false,
+};
+
+const MIXED_RESULTS = [
+ {
+ type: "user",
+ user: {
+ id: 7,
+ login: "carrieseltzer",
+ icon_url: "https://example.com/u.jpg",
+ observations_count: 5,
+ },
+ },
+ {
+ type: "taxon",
+ taxon: {
+ id: 12,
+ name: "Eumyias thalassinus",
+ preferred_common_name: "Verditer Flycatcher",
+ iconic_taxon_name: "Aves",
+ default_photo: { url: "https://example.com/t.jpg" },
+ },
+ },
+ {
+ type: "project",
+ project: {
+ id: 9,
+ title: "InverteFest",
+ project_type: "collection",
+ rule_preferences: [],
+ icon: "https://example.com/p.jpg",
+ },
+ },
+];
+
+const typeQuery = text => {
+ fireEvent.changeText( screen.getByTestId( "UniversalSearch.taxonInput" ), text );
+ // Only the timer advancement needs an act wrapper; fireEvent is already
+ // wrapped internally by Testing Library.
+ act( ( ) => {
+ jest.advanceTimersByTime( 400 );
+ } );
+};
+
beforeAll( async ( ) => {
await initI18next( );
} );
+beforeEach( ( ) => {
+ jest.useFakeTimers( );
+ mockNavigate.mockClear( );
+ mockDispatch.mockClear( );
+ useExploreV2.mockReturnValue( { dispatch: mockDispatch, state: {} } );
+ useCurrentUser.mockReturnValue( CURRENT_USER );
+ useUniversalSearch.mockReturnValue( { results: [], isLoading: false, refetch: jest.fn( ) } );
+} );
+
+afterEach( ( ) => {
+ jest.useRealTimers( );
+} );
+
describe( "UniversalSearch screen", ( ) => {
it( "renders the core layout", ( ) => {
renderComponent( );
@@ -53,11 +135,55 @@ describe( "UniversalSearch screen", ( ) => {
expect( screen.queryByDisplayValue( "cup plant" ) ).toBeNull( );
expect( screen.queryByDisplayValue( "California" ) ).toBeNull( );
- expect(
- screen.getByPlaceholderText( i18next.t( "Search-for-species-user-or-project" ) ),
- ).toBeTruthy( );
- expect(
- screen.getByPlaceholderText( i18next.t( "Search-for-a-location" ) ),
- ).toBeTruthy( );
+ } );
+
+ it( "shows mixed autocomplete results while typing", ( ) => {
+ useUniversalSearch.mockReturnValue( {
+ results: MIXED_RESULTS,
+ isLoading: false,
+ refetch: jest.fn( ),
+ } );
+ renderComponent( );
+
+ typeQuery( "ver" );
+
+ expect( screen.getByText( "carrieseltzer" ) ).toBeTruthy( );
+ expect( screen.getByText( "InverteFest" ) ).toBeTruthy( );
+ expect( screen.getByTestId( "UniversalSearchResult.taxon.12" ) ).toBeTruthy( );
+ // each row has a (no-op) info icon
+ expect( screen.getByTestId( "UniversalSearchResult.info.7" ) ).toBeTruthy( );
+ } );
+
+ it( "fills the field and sets the subject when a result is tapped", ( ) => {
+ useUniversalSearch.mockReturnValue( {
+ results: MIXED_RESULTS,
+ isLoading: false,
+ refetch: jest.fn( ),
+ } );
+ renderComponent( );
+
+ typeQuery( "ver" );
+
+ fireEvent.press( screen.getByTestId( "UniversalSearchResult.user.7" ) );
+
+ expect( mockDispatch ).toHaveBeenCalledWith(
+ expect.objectContaining( {
+ type: "SET_SUBJECT",
+ subject: expect.objectContaining( {
+ type: "user",
+ user: expect.objectContaining( { id: 7, login: "carrieseltzer" } ),
+ } ),
+ } ),
+ );
+ // the search field is filled with the selected suggestion
+ expect( screen.getByDisplayValue( "carrieseltzer" ) ).toBeTruthy( );
+ } );
+
+ it( "navigates to Advanced Search", ( ) => {
+ renderComponent( );
+
+ fireEvent.press( screen.getByText( i18next.t( "Advanced-Search" ) ) );
+
+ expect( mockNavigate ).toHaveBeenCalledWith( "AdvancedSearch" );
} );
} );