mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-10-06 18:12:22 -04:00
MOB-1339: tests
This commit is contained in:
1 parent
7a1760d2c1
commit
9e1376bcea
5 files changed
+435
-6
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import UniversalSearchResult
|
||||
from "components/Explore/ExploreV2/components/UniversalSearchResult";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: jest.fn( ),
|
||||
} ) );
|
||||
const useCurrentUser = require( "sharedHooks/useCurrentUser" ).default;
|
||||
|
||||
const TAXON_WITH_PHOTO = {
|
||||
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" },
|
||||
},
|
||||
};
|
||||
|
||||
const TAXON_WITHOUT_PHOTO = {
|
||||
type: "taxon",
|
||||
taxon: {
|
||||
id: 13,
|
||||
name: "Plantae",
|
||||
iconic_taxon_name: "Plantae",
|
||||
},
|
||||
};
|
||||
|
||||
const USER_RESULT = {
|
||||
type: "user",
|
||||
user: {
|
||||
id: 7,
|
||||
login: "seth_msp",
|
||||
icon_url: "https://example.com/u.jpg",
|
||||
observations_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const PROJECT_RESULT = {
|
||||
type: "project",
|
||||
project: {
|
||||
id: 9,
|
||||
title: "InverteFest",
|
||||
project_type: "collection",
|
||||
rule_preferences: [],
|
||||
icon: "https://example.com/p.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
beforeEach( ( ) => {
|
||||
useCurrentUser.mockReturnValue( {
|
||||
prefers_common_names: true,
|
||||
prefers_scientific_name_first: false,
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "UniversalSearchResult", ( ) => {
|
||||
it( "renders a taxon result with its photo", ( ) => {
|
||||
renderComponent(
|
||||
<UniversalSearchResult result={TAXON_WITH_PHOTO} onPress={jest.fn( )} />,
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "UniversalSearchResult.taxon.12" ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( "UniversalSearchResult.taxonImage" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Verditer Flycatcher" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "falls back to the iconic taxon icon when a taxon has no photo", ( ) => {
|
||||
renderComponent(
|
||||
<UniversalSearchResult result={TAXON_WITHOUT_PHOTO} onPress={jest.fn( )} />,
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "UniversalSearchResult.taxon.13" ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "UniversalSearchResult.taxonImage" ) ).toBeNull( );
|
||||
expect( screen.getByTestId( "IconicTaxonName.iconicTaxonIcon" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "renders a user result with its login and observation count", ( ) => {
|
||||
renderComponent(
|
||||
<UniversalSearchResult result={USER_RESULT} onPress={jest.fn( )} />,
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "UniversalSearchResult.user.7" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "seth_msp" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "renders a project result with its title", ( ) => {
|
||||
renderComponent(
|
||||
<UniversalSearchResult result={PROJECT_RESULT} onPress={jest.fn( )} />,
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "UniversalSearchResult.project.9" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "InverteFest" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "calls onPress when the row is tapped", ( ) => {
|
||||
const onPress = jest.fn( );
|
||||
renderComponent(
|
||||
<UniversalSearchResult result={USER_RESULT} onPress={onPress} />,
|
||||
);
|
||||
|
||||
fireEvent.press( screen.getByTestId( "UniversalSearchResult.user.7" ) );
|
||||
|
||||
expect( onPress ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
resultToSubject,
|
||||
subjectToText,
|
||||
} from "components/Explore/ExploreV2/helpers/universalSearchSubject";
|
||||
|
||||
describe( "resultToSubject", ( ) => {
|
||||
it( "maps a user result to a user subject", ( ) => {
|
||||
const result = {
|
||||
type: "user",
|
||||
user: {
|
||||
id: 7,
|
||||
login: "seth_msp",
|
||||
icon_url: "https://example.com/u.jpg",
|
||||
observations_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
expect( resultToSubject( result ) ).toEqual( {
|
||||
type: "user",
|
||||
user: {
|
||||
id: 7,
|
||||
login: "seth_msp",
|
||||
icon_url: "https://example.com/u.jpg",
|
||||
},
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "maps a project result to a project subject", ( ) => {
|
||||
const result = {
|
||||
type: "project",
|
||||
project: {
|
||||
id: 9,
|
||||
title: "InverteFest",
|
||||
project_type: "collection",
|
||||
icon: "https://example.com/p.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
expect( resultToSubject( result ) ).toEqual( {
|
||||
type: "project",
|
||||
project: {
|
||||
id: 9,
|
||||
title: "InverteFest",
|
||||
icon: "https://example.com/p.jpg",
|
||||
},
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "maps a taxon result to a taxon subject", ( ) => {
|
||||
const result = {
|
||||
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" },
|
||||
},
|
||||
};
|
||||
|
||||
expect( resultToSubject( result ) ).toEqual( {
|
||||
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" },
|
||||
},
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "subjectToText", ( ) => {
|
||||
it( "returns the login for a user subject", ( ) => {
|
||||
const subject = { type: "user", user: { id: 7, login: "seth_msp" } };
|
||||
|
||||
expect( subjectToText( subject, true ) ).toEqual( "seth_msp" );
|
||||
} );
|
||||
|
||||
it( "returns the title for a project subject", ( ) => {
|
||||
const subject = { type: "project", project: { id: 9, title: "InverteFest" } };
|
||||
|
||||
expect( subjectToText( subject, true ) ).toEqual( "InverteFest" );
|
||||
} );
|
||||
|
||||
it( "returns the common name for a taxon when the common name is primary", ( ) => {
|
||||
const subject = {
|
||||
type: "taxon",
|
||||
taxon: { id: 2, name: "Aves", preferred_common_name: "Birds" },
|
||||
};
|
||||
|
||||
expect( subjectToText( subject, true ) ).toEqual( "Birds" );
|
||||
} );
|
||||
|
||||
it( "returns the scientific name for a taxon when the scientific name is primary", ( ) => {
|
||||
const subject = {
|
||||
type: "taxon",
|
||||
taxon: { id: 2, name: "Aves", preferred_common_name: "Birds" },
|
||||
};
|
||||
|
||||
expect( subjectToText( subject, false ) ).toEqual( "Aves" );
|
||||
} );
|
||||
|
||||
it( "falls back to the scientific name when there is no common name", ( ) => {
|
||||
const subject = {
|
||||
type: "taxon",
|
||||
taxon: { id: 2, name: "Aves" },
|
||||
};
|
||||
|
||||
expect( subjectToText( subject, true ) ).toEqual( "Aves" );
|
||||
} );
|
||||
} );
|
||||
@@ -51,7 +51,7 @@ const MIXED_RESULTS = [
|
||||
type: "user",
|
||||
user: {
|
||||
id: 7,
|
||||
login: "carrieseltzer",
|
||||
login: "seth_msp",
|
||||
icon_url: "https://example.com/u.jpg",
|
||||
observations_count: 5,
|
||||
},
|
||||
@@ -147,7 +147,7 @@ describe( "UniversalSearch screen", ( ) => {
|
||||
|
||||
typeQuery( "ver" );
|
||||
|
||||
expect( screen.getByText( "carrieseltzer" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "seth_msp" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "InverteFest" ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( "UniversalSearchResult.taxon.12" ) ).toBeTruthy( );
|
||||
// each row has a (no-op) info icon
|
||||
@@ -171,12 +171,12 @@ describe( "UniversalSearch screen", ( ) => {
|
||||
type: "SET_SUBJECT",
|
||||
subject: expect.objectContaining( {
|
||||
type: "user",
|
||||
user: expect.objectContaining( { id: 7, login: "carrieseltzer" } ),
|
||||
user: expect.objectContaining( { id: 7, login: "seth_msp" } ),
|
||||
} ),
|
||||
} ),
|
||||
);
|
||||
// the search field is filled with the selected suggestion
|
||||
expect( screen.getByDisplayValue( "carrieseltzer" ) ).toBeTruthy( );
|
||||
expect( screen.getByDisplayValue( "seth_msp" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "clears the field but keeps the subject when tapping back in after a selection", ( ) => {
|
||||
@@ -189,17 +189,55 @@ describe( "UniversalSearch screen", ( ) => {
|
||||
|
||||
typeQuery( "ver" );
|
||||
fireEvent.press( screen.getByTestId( "UniversalSearchResult.user.7" ) );
|
||||
expect( screen.getByDisplayValue( "carrieseltzer" ) ).toBeTruthy( );
|
||||
expect( screen.getByDisplayValue( "seth_msp" ) ).toBeTruthy( );
|
||||
|
||||
mockDispatch.mockClear( );
|
||||
fireEvent( screen.getByTestId( "UniversalSearch.subjectInput" ), "focus" );
|
||||
|
||||
// the field is cleared for a fresh search...
|
||||
expect( screen.queryByDisplayValue( "carrieseltzer" ) ).toBeNull( );
|
||||
expect( screen.queryByDisplayValue( "seth_msp" ) ).toBeNull( );
|
||||
// ...but the committed subject persists (only Reset / a new selection clears it)
|
||||
expect( mockDispatch ).not.toHaveBeenCalledWith( { type: "CLEAR_SUBJECT" } );
|
||||
} );
|
||||
|
||||
it( "shows an empty-state message when a query returns no results", ( ) => {
|
||||
useUniversalSearch.mockReturnValue( { results: [], isLoading: false, refetch: jest.fn( ) } );
|
||||
renderComponent( <UniversalSearch /> );
|
||||
|
||||
typeQuery( "zzzzz" );
|
||||
|
||||
expect(
|
||||
screen.getByText( i18next.t( "No-results-found-for-that-search" ) ),
|
||||
).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "does not show the empty-state message while results are loading", ( ) => {
|
||||
useUniversalSearch.mockReturnValue( { results: [], isLoading: true, refetch: jest.fn( ) } );
|
||||
renderComponent( <UniversalSearch /> );
|
||||
|
||||
typeQuery( "ver" );
|
||||
|
||||
expect(
|
||||
screen.queryByText( i18next.t( "No-results-found-for-that-search" ) ),
|
||||
).toBeNull( );
|
||||
} );
|
||||
|
||||
it( "does not show results until the user has typed a query", ( ) => {
|
||||
useUniversalSearch.mockReturnValue( {
|
||||
results: MIXED_RESULTS,
|
||||
isLoading: false,
|
||||
refetch: jest.fn( ),
|
||||
} );
|
||||
renderComponent( <UniversalSearch /> );
|
||||
|
||||
// No query typed yet, so the result list is not rendered even though the
|
||||
// hook would return data.
|
||||
expect( screen.queryByText( "seth_msp" ) ).toBeNull( );
|
||||
expect(
|
||||
screen.queryByText( i18next.t( "No-results-found-for-that-search" ) ),
|
||||
).toBeNull( );
|
||||
} );
|
||||
|
||||
it( "navigates to Advanced Search", ( ) => {
|
||||
renderComponent( <UniversalSearch /> );
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { act, renderHook } from "@testing-library/react-native";
|
||||
import useDebouncedValue from "sharedHooks/useDebouncedValue";
|
||||
|
||||
const DELAY = 400;
|
||||
|
||||
beforeEach( ( ) => {
|
||||
jest.useFakeTimers( );
|
||||
} );
|
||||
|
||||
afterEach( ( ) => {
|
||||
jest.useRealTimers( );
|
||||
} );
|
||||
|
||||
describe( "useDebouncedValue", ( ) => {
|
||||
it( "returns the initial value immediately", ( ) => {
|
||||
const { result } = renderHook( ( ) => useDebouncedValue( "start", DELAY ) );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "start" );
|
||||
} );
|
||||
|
||||
it( "does not update the value until the delay elapses", ( ) => {
|
||||
const { result } = renderHook( ( ) => useDebouncedValue( "", DELAY ) );
|
||||
|
||||
act( ( ) => {
|
||||
result.current.debounce( "later" );
|
||||
jest.advanceTimersByTime( DELAY - 1 );
|
||||
} );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "" );
|
||||
|
||||
act( ( ) => {
|
||||
jest.advanceTimersByTime( 1 );
|
||||
} );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "later" );
|
||||
} );
|
||||
|
||||
it( "coalesces rapid calls so only the last value lands", ( ) => {
|
||||
const { result } = renderHook( ( ) => useDebouncedValue( "", DELAY ) );
|
||||
|
||||
act( ( ) => {
|
||||
result.current.debounce( "a" );
|
||||
jest.advanceTimersByTime( DELAY - 100 );
|
||||
// A second call before the first fired resets the timer.
|
||||
result.current.debounce( "b" );
|
||||
jest.advanceTimersByTime( DELAY - 100 );
|
||||
} );
|
||||
|
||||
// The pending "a" was cancelled, and "b" hasn't reached its full delay yet.
|
||||
expect( result.current.debouncedValue ).toEqual( "" );
|
||||
|
||||
act( ( ) => {
|
||||
jest.advanceTimersByTime( 100 );
|
||||
} );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "b" );
|
||||
} );
|
||||
|
||||
it( "setImmediately applies the value now and cancels a pending debounce", ( ) => {
|
||||
const { result } = renderHook( ( ) => useDebouncedValue( "", DELAY ) );
|
||||
|
||||
act( ( ) => {
|
||||
result.current.debounce( "pending" );
|
||||
result.current.setImmediately( "now" );
|
||||
} );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "now" );
|
||||
|
||||
// The pending debounce must not overwrite the immediate value later.
|
||||
act( ( ) => {
|
||||
jest.advanceTimersByTime( DELAY );
|
||||
} );
|
||||
|
||||
expect( result.current.debouncedValue ).toEqual( "now" );
|
||||
} );
|
||||
|
||||
it( "cancels a pending debounce on unmount", ( ) => {
|
||||
const clearTimeoutSpy = jest.spyOn( global, "clearTimeout" );
|
||||
const { result, unmount } = renderHook( ( ) => useDebouncedValue( "", DELAY ) );
|
||||
|
||||
act( ( ) => {
|
||||
result.current.debounce( "never" );
|
||||
} );
|
||||
|
||||
unmount( );
|
||||
|
||||
expect( clearTimeoutSpy ).toHaveBeenCalled( );
|
||||
clearTimeoutSpy.mockRestore( );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,73 @@
|
||||
import { waitFor } from "@testing-library/react-native";
|
||||
import useUniversalSearch from "sharedHooks/useUniversalSearch";
|
||||
import { renderHookInApp } from "tests/helpers/render";
|
||||
|
||||
jest.mock( "api/search" );
|
||||
const { search } = require( "api/search" );
|
||||
|
||||
const TAXON_RESULT = {
|
||||
taxon: {
|
||||
id: 12,
|
||||
name: "Eumyias thalassinus",
|
||||
preferred_common_name: "Verditer Flycatcher",
|
||||
},
|
||||
};
|
||||
const USER_RESULT = {
|
||||
user: { id: 7, login: "seth_msp" },
|
||||
};
|
||||
const PROJECT_RESULT = {
|
||||
project: { id: 9, title: "InverteFest" },
|
||||
};
|
||||
|
||||
beforeEach( ( ) => {
|
||||
search.mockReset( );
|
||||
} );
|
||||
|
||||
describe( "useUniversalSearch", ( ) => {
|
||||
it( "does not query and returns no results for a blank query", async ( ) => {
|
||||
const { result } = renderHookInApp( ( ) => useUniversalSearch( " " ) );
|
||||
|
||||
// Give the (disabled) query a chance to fire if it were going to.
|
||||
await waitFor( ( ) => expect( result.current.isLoading ).toBe( false ) );
|
||||
|
||||
expect( search ).not.toHaveBeenCalled( );
|
||||
expect( result.current.results ).toEqual( [] );
|
||||
} );
|
||||
|
||||
it( "trims the query before sending it to the API", async ( ) => {
|
||||
search.mockResolvedValue( { results: [] } );
|
||||
|
||||
renderHookInApp( ( ) => useUniversalSearch( " ver " ) );
|
||||
|
||||
await waitFor( ( ) => expect( search ).toHaveBeenCalled( ) );
|
||||
expect( search ).toHaveBeenCalledWith(
|
||||
expect.objectContaining( { q: "ver", sources: ["taxa", "users", "projects"] } ),
|
||||
expect.anything( ),
|
||||
);
|
||||
} );
|
||||
|
||||
it( "maps a mixed response to a tagged union, preserving order", async ( ) => {
|
||||
search.mockResolvedValue( {
|
||||
results: [USER_RESULT, TAXON_RESULT, PROJECT_RESULT],
|
||||
} );
|
||||
|
||||
const { result } = renderHookInApp( ( ) => useUniversalSearch( "ver" ) );
|
||||
|
||||
await waitFor( ( ) => expect( result.current.results.length ).toEqual( 3 ) );
|
||||
|
||||
expect( result.current.results ).toEqual( [
|
||||
{ type: "user", user: USER_RESULT.user },
|
||||
{ type: "taxon", taxon: TAXON_RESULT.taxon },
|
||||
{ type: "project", project: PROJECT_RESULT.project },
|
||||
] );
|
||||
} );
|
||||
|
||||
it( "returns an empty list when the API responds with null", async ( ) => {
|
||||
search.mockResolvedValue( null );
|
||||
|
||||
const { result } = renderHookInApp( ( ) => useUniversalSearch( "ver" ) );
|
||||
|
||||
await waitFor( ( ) => expect( search ).toHaveBeenCalled( ) );
|
||||
expect( result.current.results ).toEqual( [] );
|
||||
} );
|
||||
} );
|
||||
Reference in new issue
Block a user