MOB-1347: tests

This commit is contained in:
Seth Peterson authored and GitHub committed 2026-08-26 17:32:52 -05:00
1 parent 64e3218af6
commit bfec7984df
6 files changed
+362 -3

No files matched your search

@@ -36,7 +36,7 @@ const EMPTY_TABS: Tab[] = [];
// first two tabs need to occupy less than 100% of the screen width
// so the user can be cued by a third option peeking out
const SCROLLABLE_TAB_WIDTH_RATIO = 0.42;
export const SCROLLABLE_TAB_WIDTH_RATIO = 0.42;
const SCROLL_STYLE = { flexGrow: 0 };
@@ -2,6 +2,7 @@ import { screen, userEvent } from "@testing-library/react-native";
import ExploreV2Tabs from "components/Explore/ExploreV2/components/ExploreV2Tabs";
import { ExploreV2Provider, initialExploreV2State } from "providers/ExploreV2Context";
import React from "react";
import useStore from "stores/useStore";
import { renderComponent } from "tests/helpers/render";
const actor = userEvent.setup( );
@@ -12,7 +13,14 @@ const renderTabs = props => renderComponent(
</ExploreV2Provider>,
);
const setAdvancedSearchMode = advancedSearchMode => useStore
.getState( ).exploreV2AdvancedSearch.setAdvancedSearchMode( advancedSearchMode );
describe( "ExploreV2Tabs", () => {
beforeEach( () => {
setAdvancedSearchMode( false );
} );
it( "renders both the observations and species tabs", () => {
renderTabs();
@@ -62,4 +70,49 @@ describe( "ExploreV2Tabs", () => {
expect( screen.getAllByText( "--" ) ).toHaveLength( 2 );
} );
it( "does not render the observers and identifiers tabs in default search mode", () => {
renderTabs();
expect( screen.queryByTestId( "ExploreV2Tabs.observers" ) ).toBeNull();
expect( screen.queryByTestId( "ExploreV2Tabs.identifiers" ) ).toBeNull();
} );
describe( "in advanced search mode", () => {
beforeEach( () => {
setAdvancedSearchMode( true );
} );
it( "renders all four tabs", () => {
renderTabs();
expect( screen.getByTestId( "ExploreV2Tabs.observations" ) ).toBeTruthy();
expect( screen.getByTestId( "ExploreV2Tabs.species" ) ).toBeTruthy();
expect( screen.getByTestId( "ExploreV2Tabs.observers" ) ).toBeTruthy();
expect( screen.getByTestId( "ExploreV2Tabs.identifiers" ) ).toBeTruthy();
} );
it( "renders the provided observer and identifier counts", () => {
renderTabs( { observersCount: 12, identifiersCount: 3 } );
expect( screen.getByText( "12" ) ).toBeTruthy();
expect( screen.getByText( "3" ) ).toBeTruthy();
} );
it.each( ["observers", "identifiers"] )(
"switches the active tab when the %s tab is pressed",
async tab => {
renderTabs();
await actor.press( screen.getByTestId( `ExploreV2Tabs.${tab}` ) );
expect(
screen.getByTestId( `ExploreV2Tabs.${tab}` ).props.accessibilityState.selected,
).toBe( true );
expect(
screen.getByTestId( "ExploreV2Tabs.observations" ).props.accessibilityState.selected,
).toBe( false );
},
);
} );
} );
@@ -0,0 +1,51 @@
import { act, waitFor } from "@testing-library/react-native";
import useUserTabCounts from "components/Explore/ExploreV2/hooks/useUserTabCounts";
import { renderHookInApp } from "tests/helpers/render";
jest.mock( "api/observations" );
const { fetchIdentifiers, fetchObservers } = require( "api/observations" );
const PARAMS = { taxon_id: 12, place_id: 1 };
beforeEach( ( ) => {
fetchObservers.mockReset( );
fetchIdentifiers.mockReset( );
fetchObservers.mockResolvedValue( { total_results: 8, results: [] } );
fetchIdentifiers.mockResolvedValue( { total_results: 5, results: [] } );
} );
describe( "useUserTabCounts", ( ) => {
it( "returns the observer and identifier totals", async ( ) => {
const { result } = renderHookInApp( ( ) => useUserTabCounts( PARAMS ) );
await waitFor( ( ) => expect( result.current.observersCount ).toBe( 8 ) );
expect( result.current.identifiersCount ).toBe( 5 );
} );
it( "asks for no results, only the totals", async ( ) => {
renderHookInApp( ( ) => useUserTabCounts( PARAMS ) );
await waitFor( ( ) => expect( fetchObservers ).toHaveBeenCalled( ) );
const [observersParams] = fetchObservers.mock.calls.at( -1 );
expect( observersParams.per_page ).toBe( 0 );
expect( observersParams.taxon_id ).toBe( 12 );
const [identifiersParams] = fetchIdentifiers.mock.calls.at( -1 );
expect( identifiersParams.per_page ).toBe( 0 );
} );
it( "returns null counts and does not fetch when disabled", async ( ) => {
const { result } = renderHookInApp(
( ) => useUserTabCounts( { taxon_id: 999 }, { enabled: false } ),
);
await act( async ( ) => {
await new Promise( resolve => {
setTimeout( resolve, 0 );
} );
} );
expect( fetchObservers ).not.toHaveBeenCalled( );
expect( fetchIdentifiers ).not.toHaveBeenCalled( );
expect( result.current.observersCount ).toBeNull( );
expect( result.current.identifiersCount ).toBeNull( );
} );
} );
@@ -2,7 +2,9 @@ import { refresh, useNetInfo } from "@react-native-community/netinfo";
import {
act, screen, userEvent, waitFor,
} from "@testing-library/react-native";
import { SPECIES_TAB } from "appConstants/tabs";
import {
IDENTIFIERS_TAB, OBSERVATIONS_TAB, OBSERVERS_TAB, SPECIES_TAB,
} from "appConstants/tabs";
import ExploreResults from "components/Explore/ExploreV2/screens/ExploreResults";
import initI18next from "i18n/initI18next";
import {
@@ -12,6 +14,7 @@ import {
} from "providers/ExploreV2Context";
import React from "react";
import { SPECIES_SORT } from "sharedHelpers/speciesSort";
import useStore from "stores/useStore";
import { renderComponent } from "tests/helpers/render";
jest.mock( "@react-navigation/native", ( ) => {
@@ -62,6 +65,24 @@ jest.mock( "components/Explore/ExploreV2/screens/ExploreV2SpeciesView", ( ) => (
default: props => mockExploreV2SpeciesView( props ),
} ) );
const mockExploreV2UsersView = jest.fn( ( ) => null );
jest.mock( "components/Explore/ExploreV2/screens/ExploreV2UsersView", ( ) => ( {
__esModule: true,
default: props => mockExploreV2UsersView( props ),
} ) );
const lastUsersViewTab = ( ) => mockExploreV2UsersView.mock.calls.at( -1 )?.[0]?.tab;
let mockUserTabCounts = { observersCount: null, identifiersCount: null };
const mockUseUserTabCounts = jest.fn( );
jest.mock( "components/Explore/ExploreV2/hooks/useUserTabCounts", ( ) => ( {
__esModule: true,
default: ( ...args ) => {
mockUseUserTabCounts( ...args );
return mockUserTabCounts;
},
} ) );
let mockLayout = "map";
const mockWriteLayoutToStorage = jest.fn( );
jest.mock( "sharedHooks/useStoredLayout", ( ) => ( {
@@ -91,6 +112,10 @@ beforeEach( ( ) => {
mockDispatch.mockClear( );
mockRequestPermissions.mockClear( );
mockExploreV2SpeciesView.mockClear( );
mockExploreV2UsersView.mockClear( );
mockUseUserTabCounts.mockClear( );
mockUserTabCounts = { observersCount: null, identifiersCount: null };
useStore.getState( ).exploreV2AdvancedSearch.setAdvancedSearchMode( false );
fetchCoarseUserLocation.mockReset( );
mockUseInfiniteExploreScroll.mockReset( );
mockUseInfiniteExploreScroll.mockReturnValue( {
@@ -448,3 +473,72 @@ describe( "ExploreResults observations view", ( ) => {
} );
} );
} );
describe( "ExploreResults observers and identifiers tabs", ( ) => {
const advancedState = tab => ( {
...mockState( { placeMode: EXPLORE_V2_PLACE_MODE.WORLDWIDE } ),
activeTab: tab,
} );
beforeEach( ( ) => {
useStore.getState( ).exploreV2AdvancedSearch.setAdvancedSearchMode( true );
} );
it.each( [OBSERVERS_TAB, IDENTIFIERS_TAB] )(
"shows the users list on the %s tab",
async tab => {
useExploreV2.mockReturnValue( {
state: advancedState( tab ),
dispatch: mockDispatch,
} );
renderComponent( <ExploreResults /> );
await waitFor( ( ) => {
expect( lastUsersViewTab( ) ).toBe( tab );
} );
},
);
it( "shows the observer and identifier counts in the tabs", async ( ) => {
mockUserTabCounts = { observersCount: 8, identifiersCount: 5 };
useExploreV2.mockReturnValue( {
state: advancedState( OBSERVERS_TAB ),
dispatch: mockDispatch,
} );
renderComponent( <ExploreResults /> );
expect( await screen.findByText( "8" ) ).toBeVisible( );
expect( screen.getByText( "5" ) ).toBeVisible( );
} );
it( "offers no sort on the observers tab", async ( ) => {
useExploreV2.mockReturnValue( {
state: advancedState( OBSERVERS_TAB ),
dispatch: mockDispatch,
} );
renderComponent( <ExploreResults /> );
await waitFor( ( ) => {
expect( mockExploreV2UsersView ).toHaveBeenCalled( );
} );
expect( screen.queryByLabelText( "Change observations sort order" ) ).toBeNull( );
} );
it( "does not count observers or identifiers outside of advanced search mode", async ( ) => {
useStore.getState( ).exploreV2AdvancedSearch.setAdvancedSearchMode( false );
useExploreV2.mockReturnValue( {
state: advancedState( OBSERVATIONS_TAB ),
dispatch: mockDispatch,
} );
renderComponent( <ExploreResults /> );
await waitFor( ( ) => {
expect( mockUseUserTabCounts ).toHaveBeenCalled( );
} );
expect( mockUseUserTabCounts.mock.calls.at( -1 )[1] ).toEqual( { enabled: false } );
} );
} );
@@ -0,0 +1,126 @@
import { screen, userEvent } from "@testing-library/react-native";
import { fetchIdentifiers, fetchObservers } from "api/observations";
import { IDENTIFIERS_TAB, OBSERVERS_TAB } from "appConstants/tabs";
import ExploreV2UsersView
from "components/Explore/ExploreV2/screens/ExploreV2UsersView";
import initI18next from "i18n/initI18next";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
const mockObservers = [
{ user: factory( "RemoteUser", { id: 1, login: "carrieseltzer" } ), observation_count: 22 },
{ user: factory( "RemoteUser", { id: 2, login: "kueda" } ), observation_count: 11 },
];
const mockIdentifiers = [
{ user: factory( "RemoteUser", { id: 3, login: "loarie" } ), count: 9 },
{ user: factory( "RemoteUser", { id: 4, login: "tiwane" } ), count: 4 },
];
const mockedNavigate = jest.fn( );
jest.mock( "@react-navigation/native", () => {
const actualNav = jest.requireActual( "@react-navigation/native" );
return {
...actualNav,
useNavigation: () => ( { navigate: mockedNavigate } ),
};
} );
const mockUseInfiniteScroll = jest.fn( );
jest.mock( "sharedHooks/useInfiniteScroll", () => ( {
__esModule: true,
default: ( ...args ) => mockUseInfiniteScroll( ...args ),
} ) );
const PARAMS = { taxon_id: 12, place_id: 1 };
const renderUsers = ( props = {} ) => renderComponent(
<ExploreV2UsersView
enabled
isConnected
params={PARAMS}
tab={OBSERVERS_TAB}
{...props}
/>,
);
const mockResults = results => mockUseInfiniteScroll.mockReturnValue( {
data: results,
fetchNextPage: jest.fn( ),
isFetchingNextPage: false,
totalResults: results.length,
} );
beforeAll( async ( ) => {
await initI18next( );
} );
beforeEach( ( ) => {
mockedNavigate.mockClear( );
mockUseInfiniteScroll.mockReset( );
mockResults( mockObservers );
} );
describe( "ExploreV2UsersView", ( ) => {
describe( "observers tab", ( ) => {
it( "lists observers with their observation counts", ( ) => {
renderUsers( );
expect( screen.getByText( "carrieseltzer" ) ).toBeVisible( );
expect( screen.getByText( "22 Observations" ) ).toBeVisible( );
expect( screen.getByText( "kueda" ) ).toBeVisible( );
} );
it( "navigates to a user profile when an observer is pressed", async ( ) => {
const actor = userEvent.setup( );
renderUsers( );
await actor.press( screen.getByTestId( "UserProfile.1" ) );
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", { userId: 1 } );
} );
it( "queries observers, sorted by observation count, with the search params", ( ) => {
renderUsers( );
const [queryKey, fetchUsers, params] = mockUseInfiniteScroll.mock.calls.at( -1 );
expect( queryKey ).toBe( "exploreV2Observers" );
expect( fetchUsers ).toBe( fetchObservers );
expect( params.order_by ).toBe( "observation_count" );
expect( params.taxon_id ).toBe( 12 );
expect( params.place_id ).toBe( 1 );
} );
} );
describe( "identifiers tab", ( ) => {
beforeEach( ( ) => {
mockResults( mockIdentifiers );
} );
it( "lists identifiers with their identification counts", ( ) => {
renderUsers( { tab: IDENTIFIERS_TAB } );
expect( screen.getByText( "loarie" ) ).toBeVisible( );
expect( screen.getByText( "9 Identifications" ) ).toBeVisible( );
expect( screen.getByText( "tiwane" ) ).toBeVisible( );
} );
it( "queries identifiers, without an observers sort, with the search params", ( ) => {
renderUsers( { tab: IDENTIFIERS_TAB } );
const [queryKey, fetchUsers, params] = mockUseInfiniteScroll.mock.calls.at( -1 );
expect( queryKey ).toBe( "exploreV2Identifiers" );
expect( fetchUsers ).toBe( fetchIdentifiers );
expect( params.order_by ).toBeUndefined( );
expect( params.taxon_id ).toBe( 12 );
expect( params.place_id ).toBe( 1 );
} );
} );
it( "passes the disabled flag through to the query", ( ) => {
renderUsers( { enabled: false } );
expect( mockUseInfiniteScroll.mock.calls.at( -1 )[3] ).toEqual( { enabled: false } );
} );
} );
@@ -1,9 +1,12 @@
import { fireEvent, render, screen } from "@testing-library/react-native";
import { Tabs } from "components/SharedComponents";
import Tabs, { SCROLLABLE_TAB_WIDTH_RATIO } from "components/SharedComponents/Tabs/Tabs";
import React from "react";
import { Dimensions, ScrollView } from "react-native";
const TAB_1 = "TAB_1";
const TAB_2 = "TAB_2";
const TAB_3 = "TAB_3";
const TAB_4 = "TAB_4";
const tab1Click = jest.fn();
const tab2Click = jest.fn();
@@ -63,3 +66,35 @@ describe( "Tabs", () => {
expect( tab2Click ).toHaveBeenCalled();
} );
} );
describe( "scrollable Tabs", () => {
const { width } = Dimensions.get( "window" );
const tabWidth = width * SCROLLABLE_TAB_WIDTH_RATIO;
const scrollableTabs = [TAB_1, TAB_2, TAB_3, TAB_4].map( id => ( {
id,
text: id,
onPress: jest.fn( ),
} ) );
beforeEach( () => {
ScrollView.prototype.scrollTo.mockClear( );
} );
it( "should keep the strip at the start when the first tab is active", () => {
render( <Tabs tabs={scrollableTabs} activeId={TAB_1} scrollable /> );
expect( ScrollView.prototype.scrollTo ).toHaveBeenCalledWith(
{ x: 0, animated: false },
);
} );
it( "should scroll the newly active tab into view when the user changes tabs", () => {
render( <Tabs tabs={scrollableTabs} activeId={TAB_1} scrollable /> );
ScrollView.prototype.scrollTo.mockClear( );
screen.update( <Tabs tabs={scrollableTabs} activeId={TAB_3} scrollable /> );
expect( ScrollView.prototype.scrollTo ).toHaveBeenCalledWith(
{ x: 2.5 * tabWidth - width / 2, animated: true },
);
} );
} );