import { screen, userEvent, waitFor, within, } from "@testing-library/react-native"; import inatjs from "inaturalistjs"; import * as rnImagePicker from "react-native-image-picker"; import useStore from "stores/useStore"; // import os from "os"; // import path from "path"; // import Realm from "realm"; // import realmConfig from "realmModels/index"; import factory, { makeResponse } from "tests/factory"; import faker from "tests/helpers/faker"; import { renderAppWithObservations, } from "tests/helpers/render"; import setStoreStateLayout from "tests/helpers/setStoreStateLayout"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; import { signIn, signOut } from "tests/helpers/user"; // We're explicitly testing navigation here so we want react-navigation // working normally jest.unmock( "@react-navigation/native" ); // 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 const mockMultipleAssets = [{ uri: faker.image.url( ), }, { uri: faker.image.url( ), }]; jest.mock( "react-native-image-picker", ( ) => ( { launchImageLibrary: jest.fn( ), } ) ); const actor = userEvent.setup( ); const navigateToObsEditViaGroupPhotos = async ( ) => { jest.spyOn( rnImagePicker, "launchImageLibrary" ).mockImplementation( ( ) => ( { assets: mockMultipleAssets, } ), ); await waitFor( ( ) => { global.timeTravel( ); // We used toBeVisible here but the update to RN0.77 broke this expectation expect( screen.getByText( /OBSERVATIONS/ ) ).toBeOnTheScreen( ); } ); const tabBar = await screen.findByTestId( "CustomTabBar" ); const addObsButton = await within( tabBar ).findByLabelText( "Add observations" ); await actor.press( addObsButton ); const photoImporter = await screen.findByLabelText( "Photo importer" ); await actor.press( photoImporter ); const groupPhotosText = await screen.findByText( /Group Photos/ ); await waitFor( ( ) => { // user should land on GroupPhotos expect( groupPhotosText ).toBeTruthy( ); } ); const importObservationsText = await screen.findByText( /IMPORT 2 OBSERVATIONS/ ); await actor.press( importObservationsText ); await waitFor( ( ) => { const obsEditTitleText = screen.getByText( /2 Observations/ ); // We used toBeVisible here but the update to RN0.77 broke this expectation expect( obsEditTitleText ).toBeOnTheScreen( ); }, { timeout: 3_000, interval: 500 } ); }; const saveObsEditObservation = async ( ) => { const saveButton = await screen.findByText( /SAVE/ ); await actor.press( saveButton ); // missing evidence sheet pops up here, so need to press SAVE twice const okButton = await screen.findByText( /OK/ ); await actor.press( okButton ); await actor.press( saveButton ); }; const uploadObsEditObservation = async options => { const uploadButton = await screen.findByText( /UPLOAD/ ); await actor.press( uploadButton ); if ( options?.skipMissingEvidence ) { return; } // missing evidence sheet pops up here, so need to press UPLOAD twice const okButton = await screen.findByText( /OK/ ); await actor.press( okButton ); await actor.press( uploadButton ); }; beforeEach( ( ) => { setStoreStateLayout( { isDefaultMode: false, isAllAddObsOptionsMode: true, } ); } ); describe( "ObsEdit", ( ) => { async function findAndPressById( labelText ) { const pressable = await screen.findByTestId( labelText ); await actor.press( pressable ); return pressable; } const mockUser = factory( "LocalUser", { login: faker.internet.userName( ), iconUrl: faker.image.url( ), locale: "en", } ); const observation = factory( "LocalObservation", { _created_at: faker.date.past( ), taxon: factory( "LocalTaxon", { name: faker.person.firstName( ), } ), } ); const mockObservations = [observation]; beforeAll( async () => { jest.useFakeTimers( ); useStore.setState( { initialNumObservationsInQueue: 3, numUploadsAttempted: 2, } ); } ); describe( "from MyObservations", ( ) => { async function navigateToObsEditOrObsDetails( observations ) { await renderAppWithObservations( observations, __filename ); const observationGridItem = await screen.findByTestId( `MyObservations.obsGridItem.${observations[0].uuid}`, ); await actor.press( observationGridItem ); } it( "should show correct observation when navigating from MyObservations", async ( ) => { await navigateToObsEditOrObsDetails( mockObservations ); expect( await screen.findByText( /Edit Observation/ ) ).toBeTruthy( ); expect( await screen.findByText( mockObservations[0].taxon.name ) ).toBeTruthy( ); } ); describe( "while signed in", ( ) => { beforeEach( async ( ) => { await signIn( mockUser, { realm: global.mockRealms[__filename] } ); } ); afterEach( ( ) => { signOut( { realm: global.mockRealms[__filename] } ); } ); it( "should show correct observation when navigating from ObsDetails", async ( ) => { const syncedObservation = factory( "LocalObservation", { _created_at: faker.date.past( ), _synced_at: faker.date.past( ), wasSynced: jest.fn( ( ) => true ), needsSync: jest.fn( ( ) => false ), taxon: factory( "LocalTaxon", { name: faker.person.firstName( ), } ), } ); await navigateToObsEditOrObsDetails( [syncedObservation] ); await findAndPressById( "ObsDetail.editButton" ); expect( await screen.findByText( /Edit Observation/ ) ).toBeTruthy( ); expect( await screen.findByText( syncedObservation.taxon.name ) ).toBeTruthy( ); } ); it.todo( "should show photos when reached from ObsDetails" ); it( "should go back to GroupPhotos if no observations are saved/uploaded" + " in the multi-observation flow", async ( ) => { await renderAppWithObservations( mockObservations, __filename ); await navigateToObsEditViaGroupPhotos( ); const backButtonId = screen.getByTestId( "ObsEdit.BackButton" ); await actor.press( backButtonId ); const groupPhotosText = await screen.findByText( /Group Photos/ ); expect( groupPhotosText ).toBeTruthy( ); } ); it( "should show discard observations sheet if at least one observation is saved/uploaded" + " in the multi-observation flow", async ( ) => { await renderAppWithObservations( mockObservations, __filename ); await navigateToObsEditViaGroupPhotos( ); await saveObsEditObservation( ); const newTitle = await screen.findByText( /New Observation/ ); expect( newTitle ).toBeTruthy( ); const backButtonId = screen.getByTestId( "ObsEdit.BackButton" ); await actor.press( backButtonId ); const warningText = await screen.findByText( /By exiting, your observation will not be saved./ ); expect( warningText ).toBeTruthy( ); } ); it( "should clear upload queue when user lands on ObsEdit", async ( ) => { await renderAppWithObservations( mockObservations, __filename ); await navigateToObsEditViaGroupPhotos( ); const { initialNumObservationsInQueue, numUploadsAttempted } = useStore.getState( ); expect( initialNumObservationsInQueue ).toEqual( 0 ); expect( numUploadsAttempted ).toEqual( 0 ); } ); it( "should show uploading status after user starts one upload" + " in the multi-observation flow", async ( ) => { inatjs.photos.create.mockImplementation( ( ) => Promise.resolve( makeResponse( [{ id: faker.number.int(), }] ) ), ); inatjs.observations.create.mockImplementation( ( params, _opts ) => Promise.resolve( makeResponse( [{ id: faker.number.int(), uuid: params.observation.uuid, }] ) ), ); inatjs.observation_photos.create.mockImplementation( ( ) => Promise.resolve( makeResponse( [{ id: faker.number.int(), }] ) ), ); await renderAppWithObservations( mockObservations, __filename ); await navigateToObsEditViaGroupPhotos( ); await uploadObsEditObservation( ); const uploadStatus = await screen.findByText( /1 uploaded/ ); // We used toBeVisible here but the update to RN0.77 broke this expectation expect( uploadStatus ).toBeOnTheScreen( ); const newTitle = await screen.findByText( /New Observation/ ); expect( newTitle ).toBeTruthy( ); } ); } ); } ); } );