From 799e0f4c4f39d7c7003feb919b0ac46eb00a464b Mon Sep 17 00:00:00 2001 From: Ken-ichi Date: Fri, 5 Jan 2024 19:48:46 -0800 Subject: [PATCH] Restore tests removed in recent suggeston fixes (#1001) * Restored Suggestions navigation tests * Restored SuggestionsWithSyncedObs.test.js tests * Mocked vision-camera-plugin-inatvision instead of useOfflineSuggestions * Removed unnecessarily complex object from navigation params There were a lot of issues here, but the main ones (I think) were related to rendering all the navigators and waiting for asynchronous stuff to happen before proceeding with the test. --- src/api/taxa.js | 2 + .../ObsDetails/ObsDetailsContainer.js | 12 +- .../ObsEdit/EvidenceSectionContainer.js | 25 +- .../Suggestions/SuggestionsContainer.js | 2 +- .../hooks/useOfflineSuggestions.js | 46 +- .../Suggestions/hooks/useTaxonSelected.js | 2 +- src/realmModels/Observation.js | 9 +- tests/factories/ModelPrediction.js | 39 ++ tests/factories/RemoteIdentification.js | 8 +- tests/factories/RemoteObservation.js | 10 +- tests/helpers/render.js | 21 +- tests/helpers/user.js | 16 +- .../SuggestionsWithSyncedObs.test.js | 467 +++++++++--------- .../navigation/MediaViewer.test.js | 8 +- tests/integration/navigation/ObsEdit.test.js | 4 +- .../navigation/Suggestions.test.js | 84 ++-- tests/jest.setup.js | 2 +- 17 files changed, 405 insertions(+), 352 deletions(-) create mode 100644 tests/factories/ModelPrediction.js diff --git a/src/api/taxa.js b/src/api/taxa.js index 712c3ac7a..c8506856d 100644 --- a/src/api/taxa.js +++ b/src/api/taxa.js @@ -73,6 +73,8 @@ async function fetchTaxon( id: any, params: Object = {}, opts: Object = {} ): Pr try { const fetchParams = { ...PARAMS, ...params }; const { results } = await inatjs.taxa.fetch( id, fetchParams, opts ); + if ( results.length === 0 ) return null; + return mapToLocalSchema( results[0] ); } catch ( e ) { return handleError( e ); diff --git a/src/components/ObsDetails/ObsDetailsContainer.js b/src/components/ObsDetails/ObsDetailsContainer.js index 5b880fab7..ac3205e4f 100644 --- a/src/components/ObsDetails/ObsDetailsContainer.js +++ b/src/components/ObsDetails/ObsDetailsContainer.js @@ -105,7 +105,7 @@ const ObsDetailsContainer = ( ): Node => { const { params } = useRoute(); const { comment, - taxonSuggested, + suggestedTaxonId, uuid, vision } = params; @@ -310,12 +310,12 @@ const ObsDetailsContainer = ( ): Node => { ); const onIDAdded = useCallback( () => { - if ( !taxonSuggested ) return; + if ( !suggestedTaxonId ) return; // New taxon identification added by user const idParams = { observation_id: uuid, - taxon_id: taxonSuggested.id, + taxon_id: suggestedTaxonId, vision }; @@ -328,13 +328,13 @@ const ObsDetailsContainer = ( ): Node => { createIdentificationMutation.mutate( { identification: idParams } ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [taxonSuggested, uuid, vision] ); + }, [suggestedTaxonId, uuid, vision] ); useEffect( () => { - if ( !taxonSuggested ) return; + if ( !suggestedTaxonId ) return; onIDAdded(); - }, [onIDAdded, taxonSuggested] ); + }, [onIDAdded, suggestedTaxonId] ); useEffect( ( ) => { if ( diff --git a/src/components/ObsEdit/EvidenceSectionContainer.js b/src/components/ObsEdit/EvidenceSectionContainer.js index 096081fcd..b8aefb92f 100644 --- a/src/components/ObsEdit/EvidenceSectionContainer.js +++ b/src/components/ObsEdit/EvidenceSectionContainer.js @@ -6,7 +6,7 @@ import { isFuture, parseISO } from "date-fns"; -import { difference } from "lodash"; +import { difference, isNil } from "lodash"; import type { Node } from "react"; import React, { useCallback, @@ -107,16 +107,19 @@ const EvidenceSectionContainer = ( { }, [currentObservation] ); const hasValidLocation = useMemo( ( ) => { - if ( hasLocation - && ( latitude !== 0 && longitude !== 0 ) - && ( latitude >= -90 && latitude <= 90 ) - && ( longitude >= -180 && longitude <= 180 ) - && ( currentObservation?.positional_accuracy === null - || currentObservation?.positional_accuracy === undefined - || ( - currentObservation?.positional_accuracy - && currentObservation?.positional_accuracy <= DESIRED_LOCATION_ACCURACY ) - ) + const coordinatesExist = latitude !== 0 && longitude !== 0; + const latitudeInRange = latitude >= -90 && latitude <= 90; + const longitudeInRange = longitude >= -180 && longitude <= 180; + const positionalAccuracyBlank = isNil( currentObservation?.positional_accuracy ); + const positionalAccuracyDesireable = ( + currentObservation?.positional_accuracy || 0 + ) <= DESIRED_LOCATION_ACCURACY; + if ( + hasLocation + && coordinatesExist + && latitudeInRange + && longitudeInRange + && ( positionalAccuracyBlank || positionalAccuracyDesireable ) ) { return true; } diff --git a/src/components/Suggestions/SuggestionsContainer.js b/src/components/Suggestions/SuggestionsContainer.js index bff4d2c20..71f4d4904 100644 --- a/src/components/Suggestions/SuggestionsContainer.js +++ b/src/components/Suggestions/SuggestionsContainer.js @@ -27,7 +27,7 @@ const SuggestionsContainer = ( ): Node => { longitude: currentObservation?.longitude } ); - const tryOfflineSuggestions = !onlineSuggestions || onlineSuggestions?.length === 0; + const tryOfflineSuggestions = !onlineSuggestions || onlineSuggestions?.results?.length === 0; const { offlineSuggestions, loadingOfflineSuggestions diff --git a/src/components/Suggestions/hooks/useOfflineSuggestions.js b/src/components/Suggestions/hooks/useOfflineSuggestions.js index 13bca82ee..e29a2f490 100644 --- a/src/components/Suggestions/hooks/useOfflineSuggestions.js +++ b/src/components/Suggestions/hooks/useOfflineSuggestions.js @@ -6,7 +6,10 @@ import { } from "react"; import { predictImage } from "sharedHelpers/cvModel"; -const useOfflineSuggestions = ( selectedPhotoUri: string, options: Object ): { +const useOfflineSuggestions = ( + selectedPhotoUri: string, + options: Object +): { offlineSuggestions: Array, loadingOfflineSuggestions: boolean } => { @@ -18,29 +21,24 @@ const useOfflineSuggestions = ( selectedPhotoUri: string, options: Object ): { useEffect( ( ) => { const predictOffline = async ( ) => { setLoadingOfflineSuggestions( true ); - try { - const predictions = await predictImage( selectedPhotoUri ); - // using the same rank level for displaying predictions in AR Camera - // this is all temporary, since we ultimately want predictions - // returned similarly to how we return them on web; this is returning a - // single branch like on the AR Camera 2023-12-08 - const formattedPredictions = predictions?.reverse( ) - .filter( prediction => prediction.rank <= 40 ) - .map( prediction => ( { - score: prediction.score, - taxon: { - id: Number( prediction.taxon_id ), - name: prediction.name, - rank_level: prediction.rank - } - } ) ); - setOfflineSuggestions( formattedPredictions ); - setLoadingOfflineSuggestions( false ); - return formattedPredictions; - } catch ( e ) { - setLoadingOfflineSuggestions( false ); - return e; - } + const predictions = await predictImage( selectedPhotoUri ); + // using the same rank level for displaying predictions in AR Camera + // this is all temporary, since we ultimately want predictions + // returned similarly to how we return them on web; this is returning a + // single branch like on the AR Camera 2023-12-08 + const formattedPredictions = predictions?.reverse( ) + .filter( prediction => prediction.rank <= 40 ) + .map( prediction => ( { + score: prediction.score, + taxon: { + id: Number( prediction.taxon_id ), + name: prediction.name, + rank_level: prediction.rank + } + } ) ); + setOfflineSuggestions( formattedPredictions ); + setLoadingOfflineSuggestions( false ); + return formattedPredictions; }; if ( selectedPhotoUri && tryOfflineSuggestions ) { diff --git a/src/components/Suggestions/hooks/useTaxonSelected.js b/src/components/Suggestions/hooks/useTaxonSelected.js index aee083a37..8dcd60604 100644 --- a/src/components/Suggestions/hooks/useTaxonSelected.js +++ b/src/components/Suggestions/hooks/useTaxonSelected.js @@ -27,7 +27,7 @@ const useTaxonSelected = ( selectedTaxon: ?Object, options: Object ) => { uuid: currentObservation.uuid, // TODO refactor so we're not passing complex objects as params; all // obs details really needs to know is the ID of the taxon - taxonSuggested: selectedTaxon, + suggestedTaxonId: selectedTaxon.id, comment, vision } ); diff --git a/src/realmModels/Observation.js b/src/realmModels/Observation.js index cea801d43..b29f6592b 100644 --- a/src/realmModels/Observation.js +++ b/src/realmModels/Observation.js @@ -111,10 +111,9 @@ class Observation extends Realm.Object { const taxon = obs.taxon ? Taxon.mapApiToRealm( obs.taxon ) : null; - const observationPhotos = obs.observation_photos - ? obs.observation_photos.map( obsPhoto => ObservationPhoto - .mapApiToRealm( obsPhoto, existingObs ) ) - : []; + const observationPhotos = ( + obs.observation_photos || obs.observationPhotos || [] + ).map( obsPhoto => ObservationPhoto.mapApiToRealm( obsPhoto, existingObs ) ); const identifications = obs.identifications ? obs.identifications.map( id => Identification.mapApiToRealm( id ) ) @@ -234,7 +233,7 @@ class Observation extends Realm.Object { const unsyncedFilter = "_synced_at == null || _synced_at <= _updated_at"; const photosUnsyncedFilter = "ANY observationPhotos._synced_at == null"; - const obs = realm?.objects( "Observation" ); + const obs = realm.objects( "Observation" ); const unsyncedObs = obs.filtered( `${unsyncedFilter} || ${photosUnsyncedFilter}` ); return unsyncedObs; }; diff --git a/tests/factories/ModelPrediction.js b/tests/factories/ModelPrediction.js new file mode 100644 index 000000000..a519d41bc --- /dev/null +++ b/tests/factories/ModelPrediction.js @@ -0,0 +1,39 @@ +import { define } from "factoria"; + +export default define( "ModelPrediction", faker => ( { + name: faker.person.fullName( ), + rank: faker.helpers.arrayElement( [ + 100, + 70, + 60, + 57, + 53, + 50, + 47, + 45, + 44, + 43, + 40, + 37, + 35, + 34.5, + 34, + 33.5, + 33, + 32, + 30, + 27, + 26, + 25, + 24, + 20, + 15, + 13, + 12, + 11, + 10, + 5 + ] ), + score: faker.number.float( { min: 0.8, max: 1 } ), + taxon_id: faker.number.int( ) +} ) ); diff --git a/tests/factories/RemoteIdentification.js b/tests/factories/RemoteIdentification.js index 3aa24cb0d..78d15728b 100644 --- a/tests/factories/RemoteIdentification.js +++ b/tests/factories/RemoteIdentification.js @@ -1,5 +1,11 @@ import { define } from "factoria"; +import taxonFactory from "./RemoteTaxon"; +import userFactory from "./RemoteUser"; + export default define( "RemoteIdentification", faker => ( { - uuid: faker.string.uuid( ) + uuid: faker.string.uuid( ), + current: true, + user: userFactory( "RemoteUser" ), + taxon: taxonFactory( "RemoteTaxon" ) } ) ); diff --git a/tests/factories/RemoteObservation.js b/tests/factories/RemoteObservation.js index 13def97eb..dd6b0d0c9 100644 --- a/tests/factories/RemoteObservation.js +++ b/tests/factories/RemoteObservation.js @@ -1,5 +1,13 @@ import { define } from "factoria"; export default define( "RemoteObservation", faker => ( { - uuid: faker.string.uuid( ) + uuid: faker.string.uuid( ), + geojson: { + coordinates: [ + Number( faker.location.longitude( ) ), + Number( faker.location.latitude( ) ) + ] + }, + positional_accuracy: 10, + observed_on_string: "2020-04-03" } ) ); diff --git a/tests/helpers/render.js b/tests/helpers/render.js index b55c0bb19..b6ae217db 100644 --- a/tests/helpers/render.js +++ b/tests/helpers/render.js @@ -4,9 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render } from "@testing-library/react-native"; +import { render, screen } from "@testing-library/react-native"; import App from "components/App"; -import ObservationsStackNavigator from "navigation/StackNavigators/ObservationsStackNavigator"; import INatPaperProvider from "providers/INatPaperProvider"; import React from "react"; import { GestureHandlerRootView } from "react-native-gesture-handler"; @@ -49,7 +48,7 @@ function renderApp( update = null ) { return renderAppWithComponent( null, update ); } -async function renderObservationsStackNavigatorWithObservations( +async function renderAppWithObservations( observations: Array, realmIdentifier: string ): any { @@ -72,14 +71,20 @@ async function renderObservationsStackNavigatorWithObservations( } ); } ) ); } - renderComponent( - - ); + // Render the whole app with all the navigators + renderAppWithComponent( ); + // If we don't wait for the obs to render we get errors about things + // happening outside of act(). Most tests will do this anyway, but this + // caused me a lot of confusion when I was trying to debug other problems + // by removing code until I was just rendering the stack navigator... and + // that was still erroring out. Hopefully this will prevent that particular + // point of confusion in the future. ~~~kueda 20240104 + await screen.findByTestId( `MyObservations.obsListItem.${observations[0].uuid}` ); } export { renderApp, renderAppWithComponent, - renderComponent, - renderObservationsStackNavigatorWithObservations + renderAppWithObservations, + renderComponent }; diff --git a/tests/helpers/user.js b/tests/helpers/user.js index b0c07cc12..890ddd177 100644 --- a/tests/helpers/user.js +++ b/tests/helpers/user.js @@ -5,6 +5,9 @@ import nock from "nock"; import RNSInfo from "react-native-sensitive-info"; import { makeResponse } from "tests/factory"; +const TEST_JWT = "test-json-web-token"; +const TEST_ACCESS_TOKEN = "test-access-token"; + async function signOut( options = {} ) { const realm = options.realm || global.realm; i18next.language = undefined; @@ -21,23 +24,22 @@ async function signOut( options = {} ) { async function signIn( user, options = {} ) { const realm = options.realm || global.realm; await RNSInfo.setItem( "username", user.login ); - await RNSInfo.setItem( "jwtToken", "yaddayadda" ); + await RNSInfo.setItem( "jwtToken", TEST_JWT ); await RNSInfo.setItem( "jwtGeneratedAt", Date.now( ).toString( ), {} ); - await RNSInfo.setItem( "accessToken", "yaddayadda" ); + await RNSInfo.setItem( "accessToken", TEST_ACCESS_TOKEN ); inatjs.users.me.mockResolvedValue( makeResponse( [user] ) ); user.signedIn = true; realm.write( ( ) => { realm.create( "User", user, "modified" ); } ); - const accessToken = "some-token"; nock( API_HOST ) .post( "/oauth/token" ) - .reply( 200, { access_token: accessToken } ) + .reply( 200, { access_token: TEST_ACCESS_TOKEN } ) .get( "/users/edit.json" ) .reply( 200, { login: user.login, id: user.id } ); nock( API_HOST, { reqheaders: { - authorization: `Bearer ${accessToken}` + authorization: `Bearer ${TEST_ACCESS_TOKEN}` } } ) .get( "/users/api_token.json" ) @@ -46,5 +48,7 @@ async function signIn( user, options = {} ) { export { signIn, - signOut + signOut, + TEST_ACCESS_TOKEN, + TEST_JWT }; diff --git a/tests/integration/SuggestionsWithSyncedObs.test.js b/tests/integration/SuggestionsWithSyncedObs.test.js index 4b3071e99..68ff03c2d 100644 --- a/tests/integration/SuggestionsWithSyncedObs.test.js +++ b/tests/integration/SuggestionsWithSyncedObs.test.js @@ -1,43 +1,28 @@ import { faker } from "@faker-js/faker"; import { - // act, + act, screen, - userEvent + userEvent, + within } from "@testing-library/react-native"; import initI18next from "i18n/initI18next"; import inatjs from "inaturalistjs"; -import ObservationsStackNavigator from "navigation/StackNavigators/ObservationsStackNavigator"; import os from "os"; import path from "path"; -import React from "react"; import Realm from "realm"; import Identification from "realmModels/Identification"; // eslint-disable-next-line import/extensions import realmConfig from "realmModels/index"; +import useStore from "stores/useStore"; import factory, { makeResponse } from "tests/factory"; -import { renderComponent } from "tests/helpers/render"; +import { renderAppWithObservations } from "tests/helpers/render"; +import { signIn, signOut, TEST_JWT } from "tests/helpers/user"; +import { getPredictionsForImage } from "vision-camera-plugin-inatvision"; -const mockOfflinePrediction = { - score: 0.97363, - taxon: { - rank_level: 10, - name: "Felis Catus", - id: 118552 - } -}; - -// const mockSearchResultTaxon = factory( "RemoteTaxon" ); - -// TODO remove this mock. This is an integration test, so we should only mock -// things outside of this app's code, which in this case is -// vision-camera-plugin-inatvision -jest.mock( "components/Suggestions/hooks/useOfflineSuggestions", ( ) => ( { - __esModule: true, - default: ( ) => ( { - offlineSuggestions: [mockOfflinePrediction], - loadingOfflineSuggestions: false - } ) -} ) ); +const mockModelPrediction = factory( "ModelPrediction", { + // useOfflineSuggestions will filter out taxa w/ rank_level > 40 + rank: 20 +} ); // We're explicitly testing navigation here so we want react-navigation // working normally @@ -78,10 +63,13 @@ jest.mock( "providers/contexts", ( ) => { }; } ); +const initialStoreState = useStore.getState( ); + // Open a realm connection and stuff it in global beforeAll( async ( ) => { global.mockRealms = global.mockRealms || {}; global.mockRealms[__filename] = await Realm.open( mockRealmConfig ); + useStore.setState( initialStoreState, true ); await initI18next(); // userEvent recommends fake timers jest.useFakeTimers( ); @@ -94,185 +82,186 @@ afterAll( ( ) => { } ); // /REALM SETUP -const mockUser = factory( "LocalUser", { - login: "fake_login", - signedIn: true -} ); +const mockUser = factory( "LocalUser" ); const makeMockObservations = ( ) => ( [ - factory( "LocalObservation", { + factory( "RemoteObservation", { _synced_at: faker.date.past( ), + needsSync: jest.fn( ( ) => false ), + wasSynced: jest.fn( ( ) => true ), // Suggestions won't load without a photo observationPhotos: [ - factory( "LocalObservationPhoto" ) + factory( "RemoteObservationPhoto" ) ], user: mockUser, - positional_accuracy: 90, - observed_on_string: "2020-01-01", - latitude: Number( faker.location.latitude( ) ), - longitude: Number( faker.location.longitude( ) ) + observed_on_string: "2020-01-01" } ) ] ); -async function renderObservationsStackNavigatorWithObservations( observations ) { - // Save the mock observation in Realm - global.mockRealms[__filename].write( ( ) => { - global.mockRealms[__filename].create( "Observation", observations[0], "modified" ); - } ); - renderComponent( - - ); +async function setupAppWithSignedInUser( ) { + const observations = makeMockObservations( ); + useStore.setState( { observations } ); + await renderAppWithObservations( observations, __filename ); + return { observations }; } -// TODO restore these tests. I broke them but couldn't figure out how to fix -// them in the time I had ~~~~kueda 20231215 -// const mockIdentification = factory( "RemoteIdentification", { -// uuid: "123456789", -// user: factory( "LocalUser" ), -// taxon: factory( "LocalTaxon", { -// name: "Miner's Lettuce", -// rank_level: 10 -// } ) -// } ); -// -// describe( "TaxonSearch", ( ) => { -// beforeEach( ( ) => { -// inatjs.identifications.create.mockResolvedValue( { results: [mockIdentification] } ); -// inatjs.search.mockResolvedValue( makeResponse( [ -// { -// taxon: mockSearchResultTaxon -// } -// ] ) ); -// inatjs.observations.observers.mockResolvedValue( makeResponse( [ -// { -// observation_count: faker.number.int( ), -// species_count: faker.number.int( ), -// user: factory( "RemoteUser" ) -// } -// ] ) ); -// } ); +// Mock the response from inatjs.computervision.score_image +const topSuggestion = { + taxon: factory.states( "genus" )( "RemoteTaxon", { name: "Primum" } ), + combined_score: 90 +}; +const otherSuggestion = { + taxon: factory( "RemoteTaxon", { name: "Alia suggestione" } ), + combined_score: 50 +}; -// afterEach( ( ) => { -// inatjs.identifications.create.mockReset( ); -// inatjs.search.mockReset( ); -// inatjs.observations.observers.mockReset( ); -// } ); +beforeEach( async ( ) => { + const mockScoreImageResponse = makeResponse( [topSuggestion, otherSuggestion] ); + inatjs.computervision.score_image.mockResolvedValue( mockScoreImageResponse ); + inatjs.observations.observers.mockResolvedValue( makeResponse( ) ); + inatjs.taxa.fetch.mockResolvedValue( makeResponse( [topSuggestion.taxon] ) ); + inatjs.observations.viewedUpdates.mockResolvedValue( makeResponse( ) ); + inatjs.identifications.create.mockResolvedValue( { + results: [factory( "RemoteIdentification", { + taxon: topSuggestion.taxon, + user: mockUser + } )] + } ); + await signIn( mockUser, { realm: global.mockRealms[__filename] } ); +} ); -// const actor = userEvent.setup( ); +afterEach( ( ) => { + inatjs.computervision.score_image.mockReset( ); + inatjs.observations.observers.mockReset( ); + inatjs.taxa.fetch.mockReset( ); + inatjs.observations.viewedUpdates.mockReset( ); + inatjs.identifications.create.mockReset( ); + signOut( { realm: global.mockRealms[__filename] } ); +} ); -// // We need to navigate from MyObs to ObsDetails to Suggestions to TaxonSearch for all of these -// // tests -// async function navigateToTaxonSearchForObservation( observation ) { -// const observationRow = await screen.findByTestId( -// `MyObservations.obsListItem.${observation.uuid}` -// ); -// await actor.press( observationRow ); -// const suggestIdButton = await screen.findByText( "SUGGEST ID" ); -// await actor.press( suggestIdButton ); -// const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); -// await actor.press( searchButton ); -// } +describe( "TaxonSearch", ( ) => { + const mockSearchResultTaxon = factory( "RemoteTaxon" ); -// async function navigateToTaxonSearchForObservationViaObsEdit( observation ) { -// const observationRow = await screen.findByTestId( -// `MyObservations.obsListItem.${observation.uuid}` -// ); -// await actor.press( observationRow ); -// const suggestIdButton = await screen.findByLabelText( "Edit" ); -// await actor.press( suggestIdButton ); -// const addIdButton = await screen.findByText( "ADD AN ID" ); -// await actor.press( addIdButton ); -// const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); -// await actor.press( searchButton ); -// } -// -// it( -// "should create an id with false vision attribute when reached from ObsDetails via" -// + " Suggestions and search result chosen", -// async ( ) => { -// const observations = makeMockObservations( ); -// await renderObservationsStackNavigatorWithObservations( observations ); -// await navigateToTaxonSearchForObservation( observations[0] ); -// const searchInput = await screen.findByLabelText( "Search for a taxon" ); -// await act( -// async ( ) => actor.type( -// searchInput, -// "doesn't really matter since we're mocking the response" -// ) -// ); -// const taxonResultButton = await screen.findByTestId( -// `Search.taxa.${mockSearchResultTaxon.id}.checkmark` -// ); -// expect( taxonResultButton ).toBeTruthy( ); -// await actor.press( taxonResultButton ); -// expect( await screen.findByText( "ACTIVITY" ) ).toBeTruthy( ); -// expect( inatjs.identifications.create ).toHaveBeenCalledWith( { -// fields: Identification.ID_FIELDS, -// identification: { -// observation_id: observations[0].uuid, -// taxon_id: mockSearchResultTaxon.id, -// vision: false -// } -// }, { -// api_token: null -// } ); -// } -// ); - -// it( -// "should update observation with false vision attribute when reached from ObsEdit" -// + " and search result chosen", -// async ( ) => { -// const observations = makeMockObservations( ); -// await renderObservationsStackNavigatorWithObservations( observations ); -// await navigateToTaxonSearchForObservationViaObsEdit( observations[0] ); -// const searchInput = await screen.findByLabelText( "Search for a taxon" ); -// await act( -// async ( ) => actor.type( -// searchInput, -// "doesn't really matter since we're mocking the response" -// ) -// ); -// const taxonResultButton = await screen.findByTestId( -// `Search.taxa.${mockSearchResultTaxon.id}.checkmark` -// ); -// expect( taxonResultButton ).toBeTruthy( ); -// await actor.press( taxonResultButton ); -// const saveChangesButton = await screen.findByText( "SAVE CHANGES" ); -// expect( saveChangesButton ).toBeTruthy( ); -// await actor.press( saveChangesButton ); -// const savedObservation = global.mockRealms[__filename] -// .objectForPrimaryKey( "Observation", observations[0].uuid ); -// expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", false ); -// } -// ); -// } ); - -describe( "Suggestions", ( ) => { - // Mock the response from inatjs.computervision.score_image - const topSuggestion = { - taxon: factory( "RemoteTaxon", { name: "Primum suggestion" } ), - combined_score: 90 - }; - const otherSuggestion = { - taxon: factory( "RemoteTaxon", { name: "Alia suggestione" } ), - combined_score: 50 - }; beforeEach( ( ) => { - const mockScoreImageResponse = makeResponse( [topSuggestion, otherSuggestion] ); - inatjs.computervision.score_image.mockResolvedValue( mockScoreImageResponse ); - inatjs.observations.observers.mockResolvedValue( makeResponse( ) ); - inatjs.taxa.fetch.mockResolvedValue( makeResponse( [topSuggestion.taxon] ) ); + inatjs.search.mockResolvedValue( makeResponse( [ + { + taxon: mockSearchResultTaxon + } + ] ) ); + inatjs.observations.observers.mockResolvedValue( makeResponse( [ + { + observation_count: faker.number.int( ), + species_count: faker.number.int( ), + user: factory( "RemoteUser" ) + } + ] ) ); + inatjs.taxa.fetch.mockResolvedValue( makeResponse( [] ) ); } ); afterEach( ( ) => { - inatjs.computervision.score_image.mockReset( ); + inatjs.search.mockReset( ); inatjs.observations.observers.mockReset( ); inatjs.taxa.fetch.mockReset( ); } ); const actor = userEvent.setup( ); + // We need to navigate from MyObs to ObsDetails to Suggestions to TaxonSearch for all of these + // tests + async function navigateToTaxonSearchForObservation( observation ) { + const observationRow = await screen.findByTestId( + `MyObservations.obsListItem.${observation.uuid}` + ); + await actor.press( observationRow ); + const suggestIdButton = await screen.findByText( "SUGGEST ID" ); + await act( async ( ) => actor.press( suggestIdButton ) ); + await screen.findByTestId( + `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark` + ); + const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); + await actor.press( searchButton ); + } + + async function navigateToTaxonSearchForObservationViaObsEdit( observation ) { + const observationRow = await screen.findByTestId( + `MyObservations.obsListItem.${observation.uuid}` + ); + await actor.press( observationRow ); + const editButton = await screen.findByLabelText( "Edit" ); + await act( async ( ) => actor.press( editButton ) ); + const addIdButton = await screen.findByText( "ADD AN ID" ); + await actor.press( addIdButton ); + await screen.findByTestId( + `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark` + ); + const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); + await actor.press( searchButton ); + } + + it( + "should create an id with false vision attribute when reached from ObsDetails via" + + " Suggestions and search result chosen", + async ( ) => { + const { observations } = await setupAppWithSignedInUser( ); + await navigateToTaxonSearchForObservation( observations[0] ); + const searchInput = await screen.findByLabelText( "Search for a taxon" ); + expect( searchInput ).toBeVisible( ); + await act( + async ( ) => actor.type( + searchInput, + "doesn't really matter since we're mocking the response" + ) + ); + const taxonResultButton = await screen.findByTestId( + `Search.taxa.${mockSearchResultTaxon.id}.checkmark` + ); + expect( taxonResultButton ).toBeTruthy( ); + await actor.press( taxonResultButton ); + expect( await screen.findByText( "ACTIVITY" ) ).toBeTruthy( ); + expect( inatjs.identifications.create ).toHaveBeenCalledWith( { + fields: Identification.ID_FIELDS, + identification: { + observation_id: observations[0].uuid, + taxon_id: mockSearchResultTaxon.id, + vision: false + } + }, { + api_token: TEST_JWT + } ); + } + ); + + it( + "should update observation with false vision attribute when reached from ObsEdit" + + " and search result chosen", + async ( ) => { + const { observations } = await setupAppWithSignedInUser( ); + await navigateToTaxonSearchForObservationViaObsEdit( observations[0] ); + const searchInput = await screen.findByLabelText( "Search for a taxon" ); + await act( + async ( ) => actor.type( + searchInput, + "doesn't really matter since we're mocking the response" + ) + ); + const taxonResultButton = await screen.findByTestId( + `Search.taxa.${mockSearchResultTaxon.id}.checkmark` + ); + expect( taxonResultButton ).toBeTruthy( ); + await actor.press( taxonResultButton ); + const saveChangesButton = await screen.findByText( "SAVE CHANGES" ); + expect( saveChangesButton ).toBeTruthy( ); + await actor.press( saveChangesButton ); + const savedObservation = global.mockRealms[__filename] + .objectForPrimaryKey( "Observation", observations[0].uuid ); + expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", false ); + } + ); +} ); + +describe( "Suggestions", ( ) => { + const actor = userEvent.setup( ); + // We need to navigate from MyObs to ObsDetails to Suggestions for all of these // tests async function navigateToSuggestionsForObservation( observation ) { @@ -281,7 +270,7 @@ describe( "Suggestions", ( ) => { ); await actor.press( observationRow ); const suggestIdButton = await screen.findByText( "SUGGEST ID" ); - await actor.press( suggestIdButton ); + await act( async ( ) => actor.press( suggestIdButton ) ); } async function navigateToSuggestionsForObservationViaObsEdit( observation ) { @@ -289,75 +278,75 @@ describe( "Suggestions", ( ) => { `MyObservations.obsListItem.${observation.uuid}` ); await actor.press( observationRow ); - const suggestIdButton = await screen.findByLabelText( "Edit" ); - await actor.press( suggestIdButton ); + const editButton = await screen.findByLabelText( "Edit" ); + await act( async ( ) => actor.press( editButton ) ); const addIdButton = await screen.findByText( "ADD AN ID" ); await actor.press( addIdButton ); } - it( - "should create an id with true vision attribute when reached from ObsDetails" - + " and taxon chosen", - async ( ) => { - const observations = makeMockObservations( ); - await renderObservationsStackNavigatorWithObservations( observations ); - await navigateToSuggestionsForObservation( observations[0] ); - const taxonId = topSuggestion.taxon.id; - console.log( taxonId, "top computer vision suggestion" ); - const topTaxonResultButton = await screen.findByTestId( - `SuggestionsList.taxa.${taxonId}.checkmark` - ); - expect( topTaxonResultButton ).toBeTruthy( ); - await actor.press( topTaxonResultButton ); - expect( await screen.findByText( "ACTIVITY" ) ).toBeTruthy( ); - expect( inatjs.identifications.create ).toHaveBeenCalledWith( { - fields: Identification.ID_FIELDS, - identification: { - observation_id: observations[0].uuid, - taxon_id: taxonId, - vision: true - } - }, { - api_token: null - } ); - } - ); + it( "should create ident with vision=true via ObsDetails", async ( ) => { + const { observations } = await setupAppWithSignedInUser( ); + await navigateToSuggestionsForObservation( observations[0] ); + const taxonId = topSuggestion.taxon.id; + const topTaxonResultButton = await screen.findByTestId( + `SuggestionsList.taxa.${taxonId}.checkmark` + ); + expect( topTaxonResultButton ).toBeTruthy( ); + await actor.press( topTaxonResultButton ); + const activityTab = await screen.findByTestId( "ActivityTab" ); + expect( activityTab ).toBeVisible( ); + // Wait for the actual identification we created to appear + const taxonNameInIdent = await within( activityTab ).findByText( topSuggestion.taxon.name ); + expect( taxonNameInIdent ).toBeVisible( ); + expect( inatjs.identifications.create ).toHaveBeenCalledWith( { + fields: Identification.ID_FIELDS, + identification: { + observation_id: observations[0].uuid, + taxon_id: taxonId, + vision: true + } + }, { + api_token: TEST_JWT + } ); + } ); - it( - "should update observation with true vision attribute when reached from ObsEdit" - + " and taxon chosen", - async ( ) => { - const observations = makeMockObservations( ); - await renderObservationsStackNavigatorWithObservations( observations ); - await navigateToSuggestionsForObservationViaObsEdit( observations[0] ); - const topTaxonResultButton = await screen.findByTestId( - `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark` - ); - expect( topTaxonResultButton ).toBeTruthy( ); - await actor.press( topTaxonResultButton ); - const saveChangesButton = await screen.findByText( "SAVE CHANGES" ); - expect( saveChangesButton ).toBeTruthy( ); - await actor.press( saveChangesButton ); - const savedObservation = global.mockRealms[__filename] - .objectForPrimaryKey( "Observation", observations[0].uuid ); - expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", true ); - } - ); + it( "should update observation with vision=true via ObsEdit", async ( ) => { + const { observations } = await setupAppWithSignedInUser( ); + await navigateToSuggestionsForObservationViaObsEdit( observations[0] ); + const topTaxonResultButton = await screen.findByTestId( + `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark` + ); + expect( topTaxonResultButton ).toBeTruthy( ); + await actor.press( topTaxonResultButton ); + const saveChangesButton = await screen.findByText( "SAVE CHANGES" ); + expect( saveChangesButton ).toBeTruthy( ); + await actor.press( saveChangesButton ); + // Ensure we're back on MyObs + const observationRow = await screen.findByTestId( + `MyObservations.obsListItem.${observations[0].uuid}` + ); + expect( observationRow ).toBeVisible( ); + const savedObservation = global.mockRealms[__filename] + .objectForPrimaryKey( "Observation", observations[0].uuid ); + expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", true ); + } ); it( "should try offline suggestions if no online suggestions are found", async ( ) => { - const mockScoreImageResponse = makeResponse( [] ); - inatjs.computervision.score_image.mockResolvedValue( mockScoreImageResponse ); - const observations = makeMockObservations( ); - await renderObservationsStackNavigatorWithObservations( observations ); - await navigateToSuggestionsForObservationViaObsEdit( observations[0] ); - - const topOnlineTaxonResultButton = await screen.findByTestId( - `SuggestionsList.taxa.${mockOfflinePrediction.taxon.id}.checkmark` + inatjs.computervision.score_image.mockResolvedValue( makeResponse( [] ) ); + getPredictionsForImage.mockImplementation( + async ( ) => ( [mockModelPrediction] ) ); - expect( topOnlineTaxonResultButton ).toBeTruthy( ); - await actor.press( topOnlineTaxonResultButton ); + const { observations } = await setupAppWithSignedInUser( ); + await navigateToSuggestionsForObservationViaObsEdit( observations[0] ); + const offlineNotice = await screen.findByText( "Viewing Offline Suggestions" ); + expect( offlineNotice ).toBeVisible( ); + const topOfflineTaxonResultButton = await screen.findByTestId( + `SuggestionsList.taxa.${mockModelPrediction.taxon_id}.checkmark` + ); + expect( topOfflineTaxonResultButton ).toBeTruthy( ); + await act( async ( ) => actor.press( topOfflineTaxonResultButton ) ); const saveChangesButton = await screen.findByText( "SAVE CHANGES" ); expect( saveChangesButton ).toBeTruthy( ); await actor.press( saveChangesButton ); diff --git a/tests/integration/navigation/MediaViewer.test.js b/tests/integration/navigation/MediaViewer.test.js index 4338d6bb1..749a47f2a 100644 --- a/tests/integration/navigation/MediaViewer.test.js +++ b/tests/integration/navigation/MediaViewer.test.js @@ -12,7 +12,7 @@ import useStore from "stores/useStore"; import factory, { makeResponse } from "tests/factory"; import { renderApp, - renderObservationsStackNavigatorWithObservations + renderAppWithObservations } from "tests/helpers/render"; import { signIn, signOut } from "tests/helpers/user"; @@ -121,7 +121,7 @@ describe( "MediaViewer navigation", ( ) => { } ); async function navigateToObsEdit( ) { - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); const observationRow = await screen.findByTestId( `MyObservations.obsListItem.${observation.uuid}` ); @@ -203,7 +203,7 @@ describe( "MediaViewer navigation", ( ) => { useStore.setState( { observations } ); async function navigateToObsDetail( ) { - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); const observationRow = await screen.findByTestId( `MyObservations.obsListItem.${observation.uuid}` ); @@ -276,7 +276,7 @@ describe( "MediaViewer navigation", ( ) => { } ); async function navigateToTaxonDetail( ) { - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); const observationRow = await screen.findByTestId( `MyObservations.obsListItem.${observation.uuid}` ); diff --git a/tests/integration/navigation/ObsEdit.test.js b/tests/integration/navigation/ObsEdit.test.js index dde6bf978..2373dde93 100644 --- a/tests/integration/navigation/ObsEdit.test.js +++ b/tests/integration/navigation/ObsEdit.test.js @@ -10,7 +10,7 @@ import Realm from "realm"; import realmConfig from "realmModels/index"; import factory from "tests/factory"; import { - renderObservationsStackNavigatorWithObservations + renderAppWithObservations } from "tests/helpers/render"; import { signIn, signOut } from "tests/helpers/user"; @@ -96,7 +96,7 @@ describe( "ObsEdit", ( ) => { } ); async function navigateToObsEditOrObsDetails( observations ) { - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); const observationRow = await screen.findByTestId( `MyObservations.obsListItem.${observations[0].uuid}` ); diff --git a/tests/integration/navigation/Suggestions.test.js b/tests/integration/navigation/Suggestions.test.js index a33e88f5e..c01255d9a 100644 --- a/tests/integration/navigation/Suggestions.test.js +++ b/tests/integration/navigation/Suggestions.test.js @@ -1,5 +1,5 @@ import { - // act, + act, screen, userEvent } from "@testing-library/react-native"; @@ -12,9 +12,7 @@ import Realm from "realm"; import realmConfig from "realmModels/index"; import useStore from "stores/useStore"; import factory, { makeResponse } from "tests/factory"; -import { renderObservationsStackNavigatorWithObservations } from "tests/helpers/render"; - -const initialStoreState = useStore.getState( ); +import { renderAppWithObservations } from "tests/helpers/render"; // We're explicitly testing navigation here so we want react-navigation // working normally @@ -56,6 +54,8 @@ jest.mock( "providers/contexts", ( ) => { }; } ); +const initialStoreState = useStore.getState( ); + // Open a realm connection and stuff it in global beforeAll( async ( ) => { global.mockRealms = global.mockRealms || {}; @@ -127,7 +127,7 @@ describe( "Suggestions", ( ) => { async ( ) => { const observations = makeMockObservations( ); useStore.setState( { observations } ); - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); await navigateToSuggestionsForObservation( observations[0] ); const topTaxonResultButton = await screen.findByTestId( `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark` @@ -141,7 +141,7 @@ describe( "Suggestions", ( ) => { it( "should navigate back to ObsEdit when another suggestion chosen", async ( ) => { const observations = makeMockObservations( ); - await renderObservationsStackNavigatorWithObservations( observations, __filename ); + await renderAppWithObservations( observations, __filename ); await navigateToSuggestionsForObservation( observations[0] ); const otherTaxonResultButton = await screen.findByTestId( `SuggestionsList.taxa.${otherSuggestion.taxon.id}.checkmark` @@ -152,40 +152,40 @@ describe( "Suggestions", ( ) => { } ); } ); - // TODO restore these tests. I broke them but couldn't figure out how to fix - // them in the time I had ~~~~kueda 20231215 - // describe( "TaxonSearch", ( ) => { - // it( - // "should navigate back to ObsEdit with expected observation" - // + " when reached from ObsEdit via Suggestions and search result chosen", - // async ( ) => { - // const observations = makeMockObservations( ); - // useStore.setState( { observations } ); - // await renderObservationsStackNavigatorWithObservations( observations, __filename ); - // await navigateToSuggestionsForObservation( observations[0] ); - // const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); - // await actor.press( searchButton ); - // const searchInput = await screen.findByLabelText( "Search for a taxon" ); - // const mockSearchResultTaxon = factory( "RemoteTaxon" ); - // inatjs.search.mockResolvedValue( makeResponse( [ - // { - // taxon: mockSearchResultTaxon - // } - // ] ) ); - // await act( - // async ( ) => actor.type( - // searchInput, - // "doesn't really matter since we're mocking the response" - // ) - // ); - // const taxonResultButton = await screen.findByTestId( - // `Search.taxa.${mockSearchResultTaxon.id}.checkmark` - // ); - // expect( taxonResultButton ).toBeTruthy( ); - // await actor.press( taxonResultButton ); - // expect( await screen.findByText( "EVIDENCE" ) ).toBeTruthy( ); - // expect( await screen.findByText( /Obscured/ ) ).toBeVisible( ); - // } - // ); - // } ); + describe( "TaxonSearch", ( ) => { + it( + "should navigate back to ObsEdit with expected observation" + + " when reached from ObsEdit via Suggestions and search result chosen", + async ( ) => { + const observations = [ + factory( "LocalObservation", { geoprivacy: "obscured" } ) + ]; + useStore.setState( { observations } ); + await renderAppWithObservations( observations, __filename ); + await navigateToSuggestionsForObservation( observations[0] ); + const searchButton = await screen.findByText( "SEARCH FOR A TAXON" ); + await actor.press( searchButton ); + const searchInput = await screen.findByLabelText( "Search for a taxon" ); + const mockSearchResultTaxon = factory( "RemoteTaxon" ); + inatjs.search.mockResolvedValue( makeResponse( [ + { + taxon: mockSearchResultTaxon + } + ] ) ); + await act( + async ( ) => actor.type( + searchInput, + "doesn't really matter since we're mocking the response" + ) + ); + const taxonResultButton = await screen.findByTestId( + `Search.taxa.${mockSearchResultTaxon.id}.checkmark` + ); + expect( taxonResultButton ).toBeTruthy( ); + await actor.press( taxonResultButton ); + expect( await screen.findByText( "EVIDENCE" ) ).toBeTruthy( ); + expect( await screen.findByText( /Obscured/ ) ).toBeVisible( ); + } + ); + } ); } ); diff --git a/tests/jest.setup.js b/tests/jest.setup.js index 2e34f0a8c..4fe4dcbe4 100644 --- a/tests/jest.setup.js +++ b/tests/jest.setup.js @@ -21,7 +21,7 @@ import { } from "./vision-camera/vision-camera"; jest.mock( "vision-camera-plugin-inatvision", () => ( { - getPredictionsForImage: jest.fn( () => Promise.resolve( "Mocked cv prediction" ) ) + getPredictionsForImage: jest.fn( () => Promise.resolve( [] ) ) } ) ); jest.mock( "react-native-worklets-core", () => ( {