Files
iNaturalistReactNative/tests/integration/navigation/Explore.test.js
Ryan Stelly b78be9243d lint rule & autofix for "trailing comma" (#3299)
* (lint) MOB-1063 enforce trailing commas

* autofix trailing commas

* manually fix newly introduced maxlen violations

* add trailing comma convention to i18n build
2025-12-22 20:17:13 -06:00

354 lines
14 KiB
JavaScript

import {
screen,
userEvent,
waitFor,
within,
} from "@testing-library/react-native";
import initI18next from "i18n/initI18next";
import inatjs from "inaturalistjs";
import ReactNativePermissions from "react-native-permissions";
import Observation from "realmModels/Observation";
import factory, { makeResponse } from "tests/factory";
import faker from "tests/helpers/faker";
import { renderApp } from "tests/helpers/render";
import setStoreStateLayout from "tests/helpers/setStoreStateLayout";
import setupUniqueRealm from "tests/helpers/uniqueRealm";
import { signIn, signOut, TEST_JWT } from "tests/helpers/user";
// We're explicitly testing navigation here so we want react-navigation
// working normally
jest.unmock( "@react-navigation/native" );
const mockUser = factory( "LocalUser", {
login: faker.internet.userName( ),
iconUrl: faker.image.url( ),
locale: "en",
species_count: faker.number.int(),
created_at: "2000-05-09T01:17:05-01:00",
updated_at: "2000-05-09T01:17:05-01:00",
} );
const mockTaxon = factory( "LocalTaxon" );
const mockObservations = [
factory( "LocalObservation", {
_synced_at: faker.date.past( ),
needsSync: jest.fn( ( ) => false ),
wasSynced: jest.fn( ( ) => true ),
user: mockUser,
taxon: mockTaxon,
} ),
];
const mockFetchUserLocation = jest.fn( () => ( { latitude: 37, longitude: 34 } ) );
jest.mock( "sharedHelpers/fetchCoarseUserLocation", () => ( {
__esModule: true,
default: () => mockFetchUserLocation(),
} ) );
// 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],
useQuery: ( ) => [],
},
};
} );
beforeAll( uniqueRealmBeforeAll );
afterAll( uniqueRealmAfterAll );
// /UNIQUE REALM SETUP
beforeAll( async () => {
await initI18next();
jest.useFakeTimers( );
inatjs.observations.speciesCounts.mockResolvedValue( makeResponse( [{
count: 1,
taxon: mockTaxon,
}] ) );
inatjs.observations.search.mockImplementation( ( params, _opts ) => {
// If this is from MyObs trying to get the signed in user's obs, return nothing
if ( params.user_id && !params.verifiable ) {
return makeResponse( [] );
}
// But if it's from Explore, we want to show observations
return makeResponse( mockObservations );
} );
inatjs.taxa.fetch.mockResolvedValue( makeResponse( [mockTaxon] ) );
} );
beforeEach( ( ) => {
setStoreStateLayout( {
isDefaultMode: false,
isAllAddObsOptionsMode: true,
} );
} );
const actor = userEvent.setup( );
async function navigateToObsDetails( ) {
await waitFor( ( ) => {
global.timeTravel( );
expect( screen.getByText( /OBSERVATION/ ) ).toBeVisible( );
} );
const firstObservation = await screen.findByTestId(
`ObsPressable.${mockObservations[0].uuid}`,
);
await actor.press( firstObservation );
}
async function navigateToRootExplore( ) {
const emptyScreen = await screen.findByText( /Use iNaturalist to identify any living thing/ );
await waitFor( ( ) => expect( emptyScreen ).toBeVisible( ) );
const tabBar = await screen.findByTestId( "CustomTabBar" );
const exploreButton = await within( tabBar ).findByText( "Explore" );
await actor.press( exploreButton );
}
const landOnObservationsView = async ( ) => {
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsViewIcon ).toBeVisible( );
};
const switchToSpeciesView = async ( ) => {
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsViewIcon ).toBeVisible( );
await actor.press( observationsViewIcon );
const speciesRadioButton = await screen.findByText( "Species" );
await actor.press( speciesRadioButton );
const confirmButton = await screen.findByText( /EXPLORE SPECIES/ );
await actor.press( confirmButton );
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
expect( speciesViewIcon ).toBeVisible( );
};
describe( "logged in", ( ) => {
beforeEach( async ( ) => {
await signIn( mockUser, { realm: global.mockRealms[__filename] } );
} );
afterEach( async ( ) => {
await signOut( { realm: global.mockRealms[__filename] } );
} );
describe( "from MyObs", ( ) => {
beforeEach( ( ) => {
// Write mock observation to realm
Observation.upsertRemoteObservations( mockObservations, global.mockRealms[__filename] );
} );
global.withAnimatedTimeTravelEnabled( { skipFakeTimers: true } );
describe( "from TaxonDetails", ( ) => {
it( "should show observations view and navigate back to TaxonDetails", async ( ) => {
renderApp( );
await navigateToObsDetails( );
const taxonPressable = await screen.findByTestId(
`ObsDetails.taxon.${mockObservations[0].taxon.id}`,
);
await actor.press( taxonPressable );
const exploreButton = await screen.findByLabelText( /See observations of this taxon in explore/ );
await actor.press( exploreButton );
expect( inatjs.observations.search ).toHaveBeenCalledWith( expect.objectContaining( {
taxon_id: mockTaxon.id,
verifiable: true,
} ), {
api_token: TEST_JWT,
} );
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
expect( defaultGlobalLocation ).toBeVisible( );
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsViewIcon ).toBeVisible( );
const backButton = screen.queryByTestId( "Explore.BackButton" );
await actor.press( backButton );
expect( exploreButton ).toBeVisible( );
} );
} );
describe( "from UserProfile observations button", ( ) => {
beforeEach( async ( ) => {
// Write mock observation to realm
Observation.upsertRemoteObservations( mockObservations, global.mockRealms[__filename] );
inatjs.users.fetch.mockResolvedValue( makeResponse( [mockUser] ) );
inatjs.relationships.search.mockResolvedValue( makeResponse( {
results: [],
} ) );
} );
it( "should show observations view and navigate back to UserProfile", async ( ) => {
renderApp( );
await navigateToObsDetails( );
const userProfileButton = await screen.findByLabelText(
`User ${mockObservations[0].user.login}`,
);
await actor.press( userProfileButton );
expect( inatjs.users.fetch ).toHaveBeenCalled( );
const observationsButton = await screen.findByLabelText( /See observations by this user in Explore/ );
await actor.press( observationsButton );
expect( inatjs.observations.search ).toHaveBeenCalledWith( expect.objectContaining( {
user_id: mockUser.id,
verifiable: true,
} ), {
api_token: TEST_JWT,
} );
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
expect( defaultGlobalLocation ).toBeVisible( );
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsViewIcon ).toBeVisible( );
const backButton = await screen.findByTestId( "Explore.BackButton" );
await actor.press( backButton );
const journalPostsButton = await screen.findByText( /JOURNAL POSTS/ );
await actor.press( journalPostsButton );
} );
} );
describe( "from UserProfile species button", ( ) => {
beforeEach( async ( ) => {
// Write mock observation to realm
Observation.upsertRemoteObservations( mockObservations, global.mockRealms[__filename] );
inatjs.users.fetch.mockResolvedValue( makeResponse( [mockUser] ) );
inatjs.relationships.search.mockResolvedValue( makeResponse( {
results: [],
} ) );
} );
it( "should show species view and navigate back to UserProfile", async ( ) => {
renderApp( );
await navigateToObsDetails( );
const userProfileButton = await screen.findByLabelText(
`User ${mockObservations[0].user.login}`,
);
await actor.press( userProfileButton );
expect( inatjs.users.fetch ).toHaveBeenCalled( );
const speciesButton = await screen.findByLabelText( /See species observed by this user in Explore/ );
await actor.press( speciesButton );
expect( inatjs.observations.speciesCounts ).toHaveBeenCalledWith( expect.objectContaining( {
user_id: mockUser.id,
verifiable: true,
} ), {
api_token: TEST_JWT,
} );
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
expect( defaultGlobalLocation ).toBeVisible( );
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
expect( speciesViewIcon ).toBeVisible( );
const backButton = await screen.findByTestId( "Explore.BackButton" );
await actor.press( backButton );
const journalPostsButton = await screen.findByText( /JOURNAL POSTS/ );
await actor.press( journalPostsButton );
} );
} );
} );
describe( "from bottom tab navigator Explore button", ( ) => {
describe( "from RootExplore -> X -> Explore -> back", ( ) => {
it( "should navigate from TaxonDetails to Explore and back to TaxonDetails", async ( ) => {
renderApp( );
await navigateToRootExplore( );
await landOnObservationsView( );
await switchToSpeciesView( );
const firstTaxon = await screen.findByTestId( `TaxonGridItem.Pressable.${mockTaxon.id}` );
await actor.press( firstTaxon );
const taxonDetailsExploreButton = await screen.findByLabelText( /See observations of this taxon in explore/ );
await actor.press( taxonDetailsExploreButton );
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
expect( defaultGlobalLocation ).toBeVisible( );
const observationsIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsIcon ).toBeVisible( );
const backButton = screen.queryByTestId( "Explore.BackButton" );
await actor.press( backButton );
expect( taxonDetailsExploreButton ).toBeVisible( );
} );
it( "should navigate from UserProfile to Explore and back to UserProfile", async ( ) => {
inatjs.users.fetch.mockResolvedValue( makeResponse( [mockUser] ) );
inatjs.relationships.search.mockResolvedValue( makeResponse( {
results: [],
} ) );
inatjs.observations.fetch.mockResolvedValue( makeResponse( mockObservations ) );
renderApp( );
await navigateToRootExplore( );
await landOnObservationsView( );
const headerCount = await screen.findByText( /1 Observation/ );
expect( headerCount ).toBeVisible( );
const gridView = await screen.findByTestId( "SegmentedButton.grid" );
await actor.press( gridView );
const firstObservation = screen.queryByTestId(
`ObsPressable.${mockObservations[0].uuid}`,
);
await waitFor( ( ) => {
expect( firstObservation ).toBeVisible( );
}, { timeout: 10_000 } );
await actor.press( firstObservation );
await waitFor( ( ) => {
expect(
screen.getByTestId( `ObsDetails.${mockObservations[0].uuid}` ),
).toBeVisible( );
}, { timeout: 10_000 } );
const userProfileButton = await screen.findByLabelText( `User ${mockUser.login}` );
await actor.press( userProfileButton );
const observationsButton = await screen.findByLabelText( /See observations by this user in Explore/ );
await actor.press( observationsButton );
expect( inatjs.observations.search ).toHaveBeenCalledWith( expect.objectContaining( {
user_id: mockUser.id,
verifiable: true,
} ), {
api_token: TEST_JWT,
} );
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
expect( observationsViewIcon ).toBeVisible( );
const backButton = screen.queryByTestId( "Explore.BackButton" );
await actor.press( backButton );
expect( observationsButton ).toBeVisible( );
} );
} );
describe( "without location permissions", ( ) => {
it( "should default to global species view and not have a back button", async ( ) => {
const mockedPermissions = {
"ios.permission.LOCATION": "denied",
};
jest.spyOn( ReactNativePermissions, "checkMultiple" )
.mockResolvedValueOnce( mockedPermissions );
renderApp( );
await navigateToRootExplore( );
await landOnObservationsView( );
const defaultNearbyLocationText = await screen.findByText( /Nearby/ );
expect( defaultNearbyLocationText ).toBeVisible( );
const backButton = screen.queryByTestId( "Explore.BackButton" );
expect( backButton ).toBeFalsy( );
} );
} );
describe( "with location permissions", ( ) => {
it.todo( "should default to nearby location" );
// it( "should default to nearby location", async ( ) => {
// const mockedPermissions = {
// "ios.permission.LOCATION": "granted"
// };
// jest.spyOn( ReactNativePermissions, "checkMultiple" )
// .mockResolvedValueOnce( mockedPermissions );
// renderApp( );
// await navigateToRootExplore( );
// const speciesViewIcon = await screen.findByLabelText( /Species View/ );
// expect( speciesViewIcon ).toBeVisible( );
// const nearbyText = await screen.findByText( /Nearby/ );
// expect( nearbyText ).toBeVisible( );
// } );
} );
} );
} );