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" ); } ); } );