mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
test AI camera in e2e (#2188)
* Add a cameraZoomRange default * Small detox version bump * Add a mocked camera with a take photo button * An e2e mock for the computer vision plugin predicting a frame * Mock taking a photo by loading the first photo from the CameraRoll Closes #1981
This commit is contained in:
@@ -2,15 +2,30 @@ import mockFaker from "tests/helpers/faker";
|
||||
|
||||
export const nativeInterface = jest.fn( );
|
||||
export const CameraRoll = {
|
||||
getPhotos: jest.fn( ( ) => ( {
|
||||
page_info: {
|
||||
end_cursor: jest.fn( ),
|
||||
has_next_page: false
|
||||
},
|
||||
edges: [
|
||||
// This expexcts something like
|
||||
// { node: photo }
|
||||
]
|
||||
getPhotos: jest.fn( ( ) => new Promise( resolve => {
|
||||
resolve( {
|
||||
page_info: {
|
||||
end_cursor: jest.fn( ),
|
||||
has_next_page: false
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
image: {
|
||||
filename: "IMG_20210901_123456.jpg",
|
||||
filepath: "/path/to/IMG_20210901_123456.jpg",
|
||||
extension: "jpg",
|
||||
uri: "file:///path/to/IMG_20210901_123456.jpg",
|
||||
height: 1920,
|
||||
width: 1080,
|
||||
fileSize: 123456,
|
||||
playableDuration: NaN,
|
||||
orientation: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
} );
|
||||
} ) ),
|
||||
getAlbums: jest.fn( ( ) => ( {
|
||||
// Expecting album titles as keys and photo counts as values
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = {
|
||||
exists: jest.fn( async ( ) => true ),
|
||||
moveFile: async ( ) => "testdata",
|
||||
copyFile: async ( ) => "testdata",
|
||||
copyAssetsFileIOS: async ( ) => "testdata",
|
||||
stat: jest.fn( ( ) => ( {
|
||||
mtime: new Date()
|
||||
} ) ),
|
||||
|
||||
80
e2e/aiCamera.e2e.js
Normal file
80
e2e/aiCamera.e2e.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
by, device, element, waitFor
|
||||
} from "detox";
|
||||
|
||||
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
|
||||
import deleteObservation from "./sharedFlows/deleteObservation";
|
||||
import signIn from "./sharedFlows/signIn";
|
||||
import switchPowerMode from "./sharedFlows/switchPowerMode";
|
||||
import uploadObservation from "./sharedFlows/uploadObservation";
|
||||
|
||||
describe( "AICamera", () => {
|
||||
beforeAll( async () => iNatE2eBeforeAll( device ) );
|
||||
beforeEach( async () => iNatE2eBeforeEach( device ) );
|
||||
|
||||
it(
|
||||
"should open the ai camera, take photo, select a suggestion, upload and delete observation",
|
||||
async () => {
|
||||
/*
|
||||
/ 1. Sign in
|
||||
*/
|
||||
const username = await signIn();
|
||||
|
||||
/*
|
||||
/ 2. Switch UI to power user mode
|
||||
*/
|
||||
await switchPowerMode();
|
||||
|
||||
/*
|
||||
/ 3. Take photo with AI Camera, select a suggestion, upload and delete observation
|
||||
*/
|
||||
// Tap to open AICamera
|
||||
const addObsButton = element( by.id( "add-obs-button" ) );
|
||||
await waitFor( addObsButton ).toBeVisible().withTimeout( 10000 );
|
||||
await addObsButton.tap();
|
||||
const aiCameraButton = element( by.id( "aicamera-button" ) );
|
||||
await waitFor( aiCameraButton ).toBeVisible().withTimeout( 10000 );
|
||||
await aiCameraButton.tap();
|
||||
|
||||
// Check that the camera screen is visible
|
||||
const cameraContainer = element( by.id( "CameraWithDevice" ) );
|
||||
await waitFor( cameraContainer ).toBeVisible().withTimeout( 10000 );
|
||||
// Check that the mocked cv suggestion is visible
|
||||
const taxonResult = element( by.id( "AICamera.taxa.51779" ) );
|
||||
await waitFor( taxonResult ).toBeVisible().withTimeout( 10000 );
|
||||
// Tap the take photo button
|
||||
const takePhotoButton = element( by.id( "take-photo-button" ) );
|
||||
await waitFor( takePhotoButton ).toBeVisible().withTimeout( 10000 );
|
||||
await takePhotoButton.tap();
|
||||
// On suggestions find the first element in the suggestions list
|
||||
const firstSuggestion = element( by.id( /SuggestionsList\.taxa\..*/ ) ).atIndex(
|
||||
0
|
||||
);
|
||||
await waitFor( firstSuggestion ).toBeVisible().withTimeout( 10000 );
|
||||
const suggestionAttributes = await firstSuggestion.getAttributes();
|
||||
const taxonID = suggestionAttributes.elements
|
||||
? suggestionAttributes.elements[0].identifier.split( "." ).pop()
|
||||
: suggestionAttributes.identifier.split( "." ).pop();
|
||||
await firstSuggestion.tap();
|
||||
// On Taxon Detail
|
||||
const selectTaxonButon = element( by.id( "TaxonDetails.SelectButton" ) );
|
||||
await waitFor( selectTaxonButon ).toBeVisible().withTimeout( 10000 );
|
||||
await selectTaxonButon.tap();
|
||||
|
||||
await uploadObservation( { upload: true } );
|
||||
|
||||
// Check that the display taxon name is visible
|
||||
const displayTaxonName = element( by.id( `display-taxon-name.${taxonID}` ) ).atIndex(
|
||||
0
|
||||
);
|
||||
await waitFor( displayTaxonName ).toBeVisible().withTimeout( 10000 );
|
||||
await displayTaxonName.tap();
|
||||
|
||||
// Delete the observation
|
||||
await deleteObservation( { uploaded: true } );
|
||||
|
||||
// Make sure we're back on MyObservations
|
||||
await waitFor( username ).toBeVisible().withTimeout( 10000 );
|
||||
}
|
||||
);
|
||||
} );
|
||||
@@ -4,7 +4,12 @@ export async function iNatE2eBeforeAll( device ) {
|
||||
if ( device.getPlatform() === "android" ) {
|
||||
await device.launchApp( {
|
||||
newInstance: true,
|
||||
permissions: { location: "always" }
|
||||
permissions: {
|
||||
location: "always",
|
||||
camera: "YES",
|
||||
medialibrary: "YES",
|
||||
photos: "YES"
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -17,7 +22,12 @@ export async function iNatE2eBeforeEach( device ) {
|
||||
} else {
|
||||
await device.launchApp( {
|
||||
newInstance: true,
|
||||
permissions: { location: "always" }
|
||||
permissions: {
|
||||
location: "always",
|
||||
camera: "YES",
|
||||
medialibrary: "YES",
|
||||
photos: "YES"
|
||||
}
|
||||
} );
|
||||
// disable password autofill
|
||||
execSync(
|
||||
|
||||
30
e2e/sharedFlows/deleteObservation.js
Normal file
30
e2e/sharedFlows/deleteObservation.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
by, element, expect, waitFor
|
||||
} from "detox";
|
||||
|
||||
// Start this on ObsEdit or ObsDetails via uploaded = false / true
|
||||
export default async function deleteObservation( options = { uploaded: false } ) {
|
||||
if ( options.uploaded ) {
|
||||
const editButton = element( by.id( "ObsDetail.editButton" ) );
|
||||
await waitFor( editButton ).toBeVisible().withTimeout( 10000 );
|
||||
// Navigate to the edit screen
|
||||
await editButton.tap();
|
||||
}
|
||||
// Check that the edit screen is visible
|
||||
await waitFor( element( by.text( "EVIDENCE" ) ) )
|
||||
.toBeVisible()
|
||||
.withTimeout( 10000 );
|
||||
// Press header kebab menu
|
||||
const headerKebabMenu = element( by.id( "KebabMenu.Button" ) );
|
||||
await expect( headerKebabMenu ).toBeVisible();
|
||||
await headerKebabMenu.tap();
|
||||
// Press delete observation
|
||||
const deleteObservationMenuItem = element( by.id( "Header.delete-observation" ) );
|
||||
await waitFor( deleteObservationMenuItem ).toBeVisible().withTimeout( 10000 );
|
||||
await deleteObservationMenuItem.tap();
|
||||
// Check that the delete button is visible
|
||||
const deleteObservationButton = element( by.text( "DELETE" ) );
|
||||
await waitFor( deleteObservationButton ).toBeVisible().withTimeout( 10000 );
|
||||
// Press delete observation
|
||||
await deleteObservationButton.tap();
|
||||
}
|
||||
29
e2e/sharedFlows/signIn.js
Normal file
29
e2e/sharedFlows/signIn.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
by, element, expect, waitFor
|
||||
} from "detox";
|
||||
import Config from "react-native-config-node";
|
||||
|
||||
export default async function signIn() {
|
||||
const loginText = element( by.id( "log-in-to-iNaturalist-button.text" ) );
|
||||
// 10000 timeout is for github actions, which was failing with a
|
||||
// shorter timeout period
|
||||
await waitFor( loginText ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( loginText ).toBeVisible();
|
||||
await element( by.id( "log-in-to-iNaturalist-button.text" ) ).tap();
|
||||
const usernameInput = element( by.id( "Login.email" ) );
|
||||
await waitFor( usernameInput ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( usernameInput ).toBeVisible();
|
||||
await element( by.id( "Login.email" ) ).tap();
|
||||
await element( by.id( "Login.email" ) ).typeText( Config.E2E_TEST_USERNAME );
|
||||
const passwordInput = element( by.id( "Login.password" ) );
|
||||
await expect( passwordInput ).toBeVisible();
|
||||
await element( by.id( "Login.password" ) ).tap();
|
||||
await element( by.id( "Login.password" ) ).typeText( Config.E2E_TEST_PASSWORD );
|
||||
const loginButton = element( by.id( "Login.loginButton" ) );
|
||||
await expect( loginButton ).toBeVisible();
|
||||
await element( by.id( "Login.loginButton" ) ).tap();
|
||||
const username = element( by.text( `@${Config.E2E_TEST_USERNAME}` ) ).atIndex( 1 );
|
||||
await waitFor( username ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( username ).toBeVisible();
|
||||
return username;
|
||||
}
|
||||
17
e2e/sharedFlows/switchPowerMode.js
Normal file
17
e2e/sharedFlows/switchPowerMode.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
by, element, waitFor
|
||||
} from "detox";
|
||||
|
||||
export default async function switchPowerMode() {
|
||||
const drawerButton = element( by.id( "OPEN_DRAWER" ) );
|
||||
await waitFor( drawerButton ).toBeVisible().withTimeout( 10000 );
|
||||
await drawerButton.tap();
|
||||
// Tap the settings drawer menu item
|
||||
const settingsDrawerMenuItem = element( by.id( "settings" ) );
|
||||
await waitFor( settingsDrawerMenuItem ).toBeVisible().withTimeout( 10000 );
|
||||
await settingsDrawerMenuItem.tap();
|
||||
// Tap the settings radio button for power user mode
|
||||
const powerUserRadioButton = element( by.id( "all-observation-option" ) );
|
||||
await waitFor( powerUserRadioButton ).toBeVisible().withTimeout( 10000 );
|
||||
await powerUserRadioButton.tap();
|
||||
}
|
||||
30
e2e/sharedFlows/uploadObservation.js
Normal file
30
e2e/sharedFlows/uploadObservation.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
by, element, expect, waitFor
|
||||
} from "detox";
|
||||
|
||||
// This needs to be a relative path for the e2e-mock version to be used
|
||||
import { CHUCKS_PAD } from "../../src/appConstants/e2e";
|
||||
|
||||
// Upload or save an observation
|
||||
export default async function uploadObservation( options = { upload: false } ) {
|
||||
// Start this on ObsEdit
|
||||
// Check that the new observation screen is visible
|
||||
const newObservationText = element( by.id( "new-observation-text" ) );
|
||||
await waitFor( newObservationText ).toBeVisible().withTimeout( 10000 );
|
||||
// Ensure the location from the e2e-mock is being used so we don't end up
|
||||
// with tests flaking out due to time zone issues
|
||||
const pattern = new RegExp( `.*${CHUCKS_PAD.latitude.toFixed( 4 )}.*` );
|
||||
const locationText = element( by.text( pattern ) );
|
||||
await waitFor( locationText ).toBeVisible().withTimeout( 10000 );
|
||||
if ( options.upload ) {
|
||||
// Press Upload now button
|
||||
const uploadNowButton = element( by.id( "ObsEdit.uploadButton" ) );
|
||||
await expect( uploadNowButton ).toBeVisible();
|
||||
await uploadNowButton.tap();
|
||||
} else {
|
||||
// Press Save button
|
||||
const saveButton = element( by.id( "ObsEdit.saveButton" ) );
|
||||
await expect( saveButton ).toBeVisible();
|
||||
await saveButton.tap();
|
||||
}
|
||||
}
|
||||
13
e2e/signIn.e2e.js
Normal file
13
e2e/signIn.e2e.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { device } from "detox";
|
||||
|
||||
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
|
||||
import signIn from "./sharedFlows/signIn";
|
||||
|
||||
describe( "Shared flow", () => {
|
||||
beforeAll( async () => iNatE2eBeforeAll( device ) );
|
||||
beforeEach( async () => iNatE2eBeforeEach( device ) );
|
||||
|
||||
it( "should sign in the test user", async () => {
|
||||
await signIn( );
|
||||
} );
|
||||
} );
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
by, device, element, expect, waitFor
|
||||
} from "detox";
|
||||
import Config from "react-native-config-node";
|
||||
|
||||
// This needs to be a relative path for the e2e-mock version to be used
|
||||
import { CHUCKS_PAD } from "../src/appConstants/e2e";
|
||||
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
|
||||
import deleteObservation from "./sharedFlows/deleteObservation";
|
||||
import signIn from "./sharedFlows/signIn";
|
||||
import switchPowerMode from "./sharedFlows/switchPowerMode";
|
||||
import uploadObservation from "./sharedFlows/uploadObservation";
|
||||
|
||||
describe( "Signed in user", () => {
|
||||
beforeAll( async ( ) => iNatE2eBeforeAll( device ) );
|
||||
@@ -22,27 +23,8 @@ describe( "Signed in user", () => {
|
||||
);
|
||||
await expect( obsWithoutEvidenceButton ).toBeVisible();
|
||||
await obsWithoutEvidenceButton.tap();
|
||||
// Check that the new observation screen is visible
|
||||
await waitFor( element( by.id( "new-observation-text" ) ) )
|
||||
.toBeVisible()
|
||||
.withTimeout( 10000 );
|
||||
// Ensure the location from the e2e-mock is being used so we don't end up
|
||||
// with tests flaking out due to time zone issues
|
||||
const pattern = new RegExp( `.*${CHUCKS_PAD.latitude.toFixed( 4 )}.*` );
|
||||
await waitFor( element( by.text( pattern ) ) )
|
||||
.toBeVisible()
|
||||
.withTimeout( 10000 );
|
||||
if ( options.upload ) {
|
||||
// Press Upload now button
|
||||
const uploadNowButton = element( by.id( "ObsEdit.uploadButton" ) );
|
||||
await expect( uploadNowButton ).toBeVisible();
|
||||
await uploadNowButton.tap();
|
||||
} else {
|
||||
// Press Save button
|
||||
const saveButton = element( by.id( "ObsEdit.saveButton" ) );
|
||||
await expect( saveButton ).toBeVisible();
|
||||
await saveButton.tap();
|
||||
}
|
||||
|
||||
await uploadObservation( options );
|
||||
|
||||
// Check that the comments count component for the obs we just created is
|
||||
// visible. Since it just saved and there's an animation the runs before
|
||||
@@ -69,29 +51,7 @@ describe( "Signed in user", () => {
|
||||
async function deleteObservationByUUID( uuid, username, options = { uploaded: false } ) {
|
||||
const obsListItem = element( by.id( `MyObservations.obsListItem.${uuid}` ) );
|
||||
await obsListItem.tap();
|
||||
if ( options.uploaded ) {
|
||||
const editButton = element( by.id( "ObsDetail.editButton" ) );
|
||||
await waitFor( editButton ).toBeVisible().withTimeout( 10000 );
|
||||
// Navigate to the edit screen
|
||||
await editButton.tap();
|
||||
}
|
||||
// Check that the edit screen is visible
|
||||
await waitFor( element( by.text( "EVIDENCE" ) ) )
|
||||
.toBeVisible()
|
||||
.withTimeout( 10000 );
|
||||
// Press header kebab menu
|
||||
const headerKebabMenu = element( by.id( "KebabMenu.Button" ) );
|
||||
await expect( headerKebabMenu ).toBeVisible();
|
||||
await headerKebabMenu.tap();
|
||||
// Press delete observation
|
||||
const deleteObservation = element( by.id( "Header.delete-observation" ) );
|
||||
await waitFor( deleteObservation ).toBeVisible().withTimeout( 10000 );
|
||||
await deleteObservation.tap();
|
||||
// Check that the delete button is visible
|
||||
const deleteObservationButton = element( by.text( "DELETE" ) );
|
||||
await waitFor( deleteObservationButton ).toBeVisible().withTimeout( 10000 );
|
||||
// Press delete observation
|
||||
await deleteObservationButton.tap();
|
||||
await deleteObservation( options );
|
||||
// Make sure we're back on MyObservations
|
||||
await waitFor( username ).toBeVisible().withTimeout( 10000 );
|
||||
}
|
||||
@@ -100,47 +60,15 @@ describe( "Signed in user", () => {
|
||||
/*
|
||||
/ 1. Sign in
|
||||
*/
|
||||
const loginText = element( by.id( "log-in-to-iNaturalist-button.text" ) );
|
||||
// 10000 timeout is for github actions, which was failing with a
|
||||
// shorter timeout period
|
||||
await waitFor( loginText ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( loginText ).toBeVisible();
|
||||
await element( by.id( "log-in-to-iNaturalist-button.text" ) ).tap();
|
||||
const usernameInput = element( by.id( "Login.email" ) );
|
||||
await waitFor( usernameInput ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( usernameInput ).toBeVisible();
|
||||
await element( by.id( "Login.email" ) ).tap();
|
||||
await element( by.id( "Login.email" ) ).typeText( Config.E2E_TEST_USERNAME );
|
||||
const passwordInput = element( by.id( "Login.password" ) );
|
||||
await expect( passwordInput ).toBeVisible();
|
||||
await element( by.id( "Login.password" ) ).tap();
|
||||
await element( by.id( "Login.password" ) ).typeText( Config.E2E_TEST_PASSWORD );
|
||||
const loginButton = element( by.id( "Login.loginButton" ) );
|
||||
await expect( loginButton ).toBeVisible();
|
||||
await element( by.id( "Login.loginButton" ) ).tap();
|
||||
const username = element( by.text( `@${Config.E2E_TEST_USERNAME}` ) ).atIndex(
|
||||
1
|
||||
);
|
||||
await waitFor( username ).toBeVisible().withTimeout( 10000 );
|
||||
await expect( username ).toBeVisible();
|
||||
const username = await signIn( );
|
||||
|
||||
/*
|
||||
/ 2. Switch UI to power user mode
|
||||
*/
|
||||
const drawerButton = element( by.id( "OPEN_DRAWER" ) );
|
||||
await waitFor( drawerButton ).toBeVisible().withTimeout( 10000 );
|
||||
await drawerButton.tap();
|
||||
// Tap the settings drawer menu item
|
||||
const settingsDrawerMenuItem = element( by.id( "settings" ) );
|
||||
await waitFor( settingsDrawerMenuItem ).toBeVisible().withTimeout( 10000 );
|
||||
await settingsDrawerMenuItem.tap();
|
||||
// Tap the settings radio button for power user mode
|
||||
const powerUserRadioButton = element( by.id( "all-observation-option" ) );
|
||||
await waitFor( powerUserRadioButton ).toBeVisible().withTimeout( 10000 );
|
||||
await powerUserRadioButton.tap();
|
||||
await switchPowerMode( );
|
||||
|
||||
/*
|
||||
/ 3. Create two observations
|
||||
/ 3. Create two observations without evidence
|
||||
*/
|
||||
const uuid = await createAndUploadObservation( { upload: true } );
|
||||
// Create a second b/c later we want to test that the deleted status text
|
||||
@@ -176,7 +104,7 @@ describe( "Signed in user", () => {
|
||||
await waitFor( username ).toBeVisible( ).withTimeout( 10000 );
|
||||
|
||||
/*
|
||||
/ 5. Delete the observations
|
||||
/ 5. Delete the two observations without evidence
|
||||
*/
|
||||
await deleteObservationByUUID( uuid, username, { uploaded: true } );
|
||||
// It would be nice to test for the "1 observation deleted" status text in
|
||||
|
||||
13
e2e/switchPowerMode.e2e.js.js
Normal file
13
e2e/switchPowerMode.e2e.js.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { device } from "detox";
|
||||
|
||||
import { iNatE2eBeforeAll, iNatE2eBeforeEach } from "./helpers";
|
||||
import switchPowerMode from "./sharedFlows/switchPowerMode";
|
||||
|
||||
describe( "Shared flow", () => {
|
||||
beforeAll( async () => iNatE2eBeforeAll( device ) );
|
||||
beforeEach( async () => iNatE2eBeforeEach( device ) );
|
||||
|
||||
it( "should switch to power mode", async () => {
|
||||
await switchPowerMode( );
|
||||
} );
|
||||
} );
|
||||
234
package-lock.json
generated
234
package-lock.json
generated
@@ -129,7 +129,7 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"chalk": "^5.3.0",
|
||||
"decompress": "^4.2.1",
|
||||
"detox": "^20.19.4",
|
||||
"detox": "^20.27.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -2807,9 +2807,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@flatten-js/interval-tree": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.2.tgz",
|
||||
"integrity": "sha512-OwLoV9E/XM6b7bes2rSFnGNjyRy7vcoIHFTnmBR2WAaZTf0Fe4EX4GdA65vU1KgFAasti7iRSg2dZfYd1Zt00Q==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@fluent/bundle": {
|
||||
@@ -7645,6 +7645,33 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"node_modules/bunyamin": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/bunyamin/-/bunyamin-1.6.3.tgz",
|
||||
"integrity": "sha512-m1hAijFhu8pFiidsVc0XEDic46uxPK+mKNLqkb5mluNx0nTolNzx/DjwMqHChQWCgfOLMjKYJJ2uPTQLE6t4Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@flatten-js/interval-tree": "^1.1.2",
|
||||
"multi-sort-stream": "^1.0.4",
|
||||
"stream-json": "^1.7.5",
|
||||
"trace-event-lib": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/bunyan": "^1.8.8",
|
||||
"bunyan": "^1.8.15 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/bunyan": {
|
||||
"optional": true
|
||||
},
|
||||
"bunyan": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bunyan": {
|
||||
"version": "1.8.15",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz",
|
||||
@@ -9118,9 +9145,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/detox": {
|
||||
"version": "20.19.4",
|
||||
"resolved": "https://registry.npmjs.org/detox/-/detox-20.19.4.tgz",
|
||||
"integrity": "sha512-frlWR+6oSrVhyr2xXqnSUbOwxBl1Yl7cdayIErVcHYIxfWIMYOH9o4csuIso3HRCUpJUwZkQrg0IsZUzWe08IA==",
|
||||
"version": "20.27.2",
|
||||
"resolved": "https://registry.npmjs.org/detox/-/detox-20.27.2.tgz",
|
||||
"integrity": "sha512-cC6S40v7ix+uA5jYzG8eazSs7YtOWgc2aCwWLZIIzfE5Kvo0gfHgtqeRhrYWCMZaj/irKKs39h2B070oNQOIrA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -9130,13 +9157,14 @@
|
||||
"caf": "^15.0.1",
|
||||
"chalk": "^4.0.0",
|
||||
"child-process-promise": "^2.2.0",
|
||||
"detox-copilot": "^0.0.9",
|
||||
"execa": "^5.1.1",
|
||||
"find-up": "^5.0.0",
|
||||
"fs-extra": "^11.0.0",
|
||||
"funpermaproxy": "^1.1.0",
|
||||
"glob": "^8.0.3",
|
||||
"ini": "^1.3.4",
|
||||
"jest-environment-emit": "^1.0.5",
|
||||
"jest-environment-emit": "^1.0.8",
|
||||
"json-cycle": "^1.3.0",
|
||||
"lodash": "^4.17.11",
|
||||
"multi-sort-stream": "^1.0.3",
|
||||
@@ -9175,6 +9203,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detox-copilot": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.9.tgz",
|
||||
"integrity": "sha512-Wk2fuisD8EH+349b0ysNWvZ7UEsThAChbYFlLqOR1jWkDaonEvgf6IOUlmxjvyTl9ENtl8ckd1U7k94yCBYwqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/detox/node_modules/ajv": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
|
||||
@@ -9307,97 +9341,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detox/node_modules/jest-environment-emit": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-emit/-/jest-environment-emit-1.0.7.tgz",
|
||||
"integrity": "sha512-/0AYqbL3zrfRTtGyzTZwgRxQZiDXEM8ZUfY7Uscla/XGs9vszx4f0XTSZqAk3CQaiwYAoKvFZkB2vSKm1Q08fQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bunyamin": "^1.5.2",
|
||||
"bunyan": "^2.0.5",
|
||||
"bunyan-debug-stream": "^3.1.0",
|
||||
"funpermaproxy": "^1.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"node-ipc": "9.2.1",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tslib": "^2.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jest/environment": ">=27.2.5",
|
||||
"@jest/types": ">=27.2.5",
|
||||
"jest": ">=27.2.5",
|
||||
"jest-environment-jsdom": ">=27.2.5",
|
||||
"jest-environment-node": ">=27.2.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@jest/environment": {
|
||||
"optional": true
|
||||
},
|
||||
"@jest/types": {
|
||||
"optional": true
|
||||
},
|
||||
"jest": {
|
||||
"optional": true
|
||||
},
|
||||
"jest-environment-jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"jest-environment-node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detox/node_modules/jest-environment-emit/node_modules/bunyamin": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/bunyamin/-/bunyamin-1.5.2.tgz",
|
||||
"integrity": "sha512-Xp2nfqk33zt3nX90OSTkLVOc5N+1zdR3MWvfLHoIrm3cGRkdxPTPYB9CCgrDV8oum5rbghJjAbmXFXOrRXvMtg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@flatten-js/interval-tree": "^1.1.2",
|
||||
"multi-sort-stream": "^1.0.4",
|
||||
"stream-json": "^1.7.5",
|
||||
"trace-event-lib": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/bunyan": "^1.8.8",
|
||||
"bunyan": "^1.8.15 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/bunyan": {
|
||||
"optional": true
|
||||
},
|
||||
"bunyan": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detox/node_modules/jest-environment-emit/node_modules/bunyan": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-2.0.5.tgz",
|
||||
"integrity": "sha512-Jvl74TdxCN6rSP9W1I6+UOUtwslTDqsSFkDqZlFb/ilaSvQ+bZAnXT/GT97IZ5L+Vph0joPZPhxUyn6FLNmFAA==",
|
||||
"dev": true,
|
||||
"engines": [
|
||||
"node >=0.10.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"exeunt": "1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"bunyan": "bin/bunyan"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dtrace-provider": "~0.8",
|
||||
"moment": "^2.19.3",
|
||||
"mv": "~2",
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"node_modules/detox/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -13282,6 +13225,70 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-emit": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-emit/-/jest-environment-emit-1.0.8.tgz",
|
||||
"integrity": "sha512-WNqvxBLH0yNojHJQ99Y21963aT7UTavxV3PgiBQFi8zwrlnKU6HvkB6LOvQrbk5I8mI8JEKvcoOrQOvBVMLIXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bunyamin": "^1.5.2",
|
||||
"bunyan": "^2.0.5",
|
||||
"bunyan-debug-stream": "^3.1.0",
|
||||
"funpermaproxy": "^1.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"node-ipc": "9.2.1",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tslib": "^2.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@jest/environment": ">=27.2.5",
|
||||
"@jest/types": ">=27.2.5",
|
||||
"jest": ">=27.2.5",
|
||||
"jest-environment-jsdom": ">=27.2.5",
|
||||
"jest-environment-node": ">=27.2.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@jest/environment": {
|
||||
"optional": true
|
||||
},
|
||||
"@jest/types": {
|
||||
"optional": true
|
||||
},
|
||||
"jest": {
|
||||
"optional": true
|
||||
},
|
||||
"jest-environment-jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"jest-environment-node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-emit/node_modules/bunyan": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-2.0.5.tgz",
|
||||
"integrity": "sha512-Jvl74TdxCN6rSP9W1I6+UOUtwslTDqsSFkDqZlFb/ilaSvQ+bZAnXT/GT97IZ5L+Vph0joPZPhxUyn6FLNmFAA==",
|
||||
"dev": true,
|
||||
"engines": [
|
||||
"node >=0.10.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"exeunt": "1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"bunyan": "bin/bunyan"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dtrace-provider": "~0.8",
|
||||
"moment": "^2.19.3",
|
||||
"mv": "~2",
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||
@@ -15597,9 +15604,9 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -15646,6 +15653,7 @@
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -15663,6 +15671,7 @@
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||
"integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -15683,9 +15692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
|
||||
"integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==",
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
|
||||
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -19194,9 +19203,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stream-json": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.5.tgz",
|
||||
"integrity": "sha512-NSkoVduGakxZ8a+pTPUlcGEeAGQpWL9rKJhOFCV+J/QtdQUEU5vtBgVg6eJXn8JB8RZvpbJWZGvXkhz70MLWoA==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.8.0.tgz",
|
||||
"integrity": "sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"stream-chain": "^2.2.5"
|
||||
@@ -19967,13 +19976,12 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/trace-event-lib": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/trace-event-lib/-/trace-event-lib-1.3.1.tgz",
|
||||
"integrity": "sha512-RO/TD5E9RNqU6MhOfi/njFWKYhrzOJCpRXlEQHgXwM+6boLSrQnOZ9xbHwOXzC+Luyixc7LNNSiTsqTVeF7I1g==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/trace-event-lib/-/trace-event-lib-1.4.1.tgz",
|
||||
"integrity": "sha512-TOgFolKG8JFY+9d5EohGWMvwvteRafcyfPWWNIqcuD1W/FUvxWcy2MSCZ/beYHM63oYPHYHCd3tkbgCctHVP7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"browser-process-hrtime": "^1.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
"browser-process-hrtime": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"chalk": "^5.3.0",
|
||||
"decompress": "^4.2.1",
|
||||
"detox": "^20.19.4",
|
||||
"detox": "^20.27.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
|
||||
@@ -45,11 +45,11 @@ const AddObsModal = ( { closeModal, navAndCloseModal }: Props ) => {
|
||||
const prepareObsEdit = useStore( state => state.prepareObsEdit );
|
||||
|
||||
const obsCreateItems = useMemo( ( ) => ( {
|
||||
arCamera: {
|
||||
aiCamera: {
|
||||
text: t( "Use-iNaturalists-AI-Camera" ),
|
||||
icon: "arcamera",
|
||||
onPress: ( ) => navAndCloseModal( "Camera", { camera: "AI" } ),
|
||||
testID: "arcamera-button",
|
||||
testID: "aicamera-button",
|
||||
className: classnames( GREEN_CIRCLE_CLASS, "absolute bottom-[26px]" ),
|
||||
accessibilityLabel: t( "AI-Camera" ),
|
||||
accessibilityHint: t( "Navigates-to-AI-camera" )
|
||||
@@ -138,7 +138,7 @@ const AddObsModal = ( { closeModal, navAndCloseModal }: Props ) => {
|
||||
} )}
|
||||
>
|
||||
{renderAddObsIcon( obsCreateItems.standardCamera )}
|
||||
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.arCamera )}
|
||||
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
|
||||
{renderAddObsIcon( obsCreateItems.photoLibrary )}
|
||||
</View>
|
||||
<View className={classnames( ROW_CLASS, "items-center" )}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { View } from "components/styledComponents";
|
||||
import React from "react";
|
||||
import { GestureResponderEvent, ViewStyle } from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import { TakePhotoOptions } from "react-native-vision-camera";
|
||||
import type { TakePhotoOptions } from "react-native-vision-camera";
|
||||
|
||||
import AIDebugButton from "./AIDebugButton";
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// @flow
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import CameraView from "components/Camera/CameraView.tsx";
|
||||
import {
|
||||
useFrameProcessor
|
||||
} from "components/Camera/helpers/visionCameraWrapper";
|
||||
import InatVision from "components/Camera/helpers/visionPluginWrapper";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
useFrameProcessor
|
||||
} from "react-native-vision-camera";
|
||||
import { Worklets } from "react-native-worklets-core";
|
||||
import { modelPath, modelVersion, taxonomyPath } from "sharedHelpers/cvModel.ts";
|
||||
import {
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
usePatchedRunAsync
|
||||
} from "sharedHelpers/visionCameraPatches";
|
||||
import { useDeviceOrientation } from "sharedHooks";
|
||||
import * as InatVision from "vision-camera-plugin-inatvision";
|
||||
|
||||
type Props = {
|
||||
// $FlowIgnore
|
||||
@@ -68,7 +68,7 @@ const FrameProcessorCamera = ( {
|
||||
inactive
|
||||
}: Props ): Node => {
|
||||
const { deviceOrientation } = useDeviceOrientation();
|
||||
const [lastTimestamp, setLastTimestamp] = useState( Date.now() );
|
||||
const [lastTimestamp, setLastTimestamp] = useState( undefined );
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
@@ -128,9 +128,12 @@ const FrameProcessorCamera = ( {
|
||||
return;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const timeSinceLastFrame = timestamp - lastTimestamp;
|
||||
if ( timeSinceLastFrame < ( 1000 / fps ) ) {
|
||||
return;
|
||||
// If there is no lastTimestamp, i.e. the first time this runs do not compare
|
||||
if ( lastTimestamp ) {
|
||||
const timeSinceLastFrame = timestamp - lastTimestamp;
|
||||
if ( timeSinceLastFrame < 1000 / fps ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
patchedRunAsync( frame, () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
import { GestureResponderEvent, ViewStyle } from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { TakePhotoOptions } from "react-native-vision-camera";
|
||||
import type { TakePhotoOptions } from "react-native-vision-camera";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
const isTablet = DeviceInfo.isTablet();
|
||||
|
||||
@@ -46,6 +46,7 @@ const TakePhoto = ( {
|
||||
accessibilityState={{ disabled }}
|
||||
disabled={disabled}
|
||||
style={DROP_SHADOW}
|
||||
testID="take-photo-button"
|
||||
>
|
||||
{showPrediction
|
||||
? (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import {
|
||||
useCameraDevice
|
||||
} from "components/Camera/helpers/visionCameraWrapper";
|
||||
import React, {
|
||||
useState
|
||||
} from "react";
|
||||
import { Alert } from "react-native";
|
||||
import {
|
||||
useCameraDevice
|
||||
} from "react-native-vision-camera";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
import CameraWithDevice from "./CameraWithDevice";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useAppState } from "@react-native-community/hooks";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import {
|
||||
Camera,
|
||||
useCameraFormat
|
||||
} from "components/Camera/helpers/visionCameraWrapper";
|
||||
import useFocusTap from "components/Camera/hooks/useFocusTap.ts";
|
||||
import VeryBadIpadRotator from "components/SharedComponents/VeryBadIpadRotator";
|
||||
import React, {
|
||||
@@ -11,7 +15,6 @@ import {
|
||||
} from "react-native-gesture-handler";
|
||||
import Reanimated from "react-native-reanimated";
|
||||
import type { CameraDevice, CameraProps, CameraRuntimeError } from "react-native-vision-camera";
|
||||
import { Camera, useCameraFormat } from "react-native-vision-camera";
|
||||
import { orientationPatch } from "sharedHelpers/visionCameraPatches";
|
||||
import useDeviceOrientation from "sharedHooks/useDeviceOrientation.ts";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useIsFocused, useNavigation } from "@react-navigation/native";
|
||||
import { Camera } from "components/Camera/helpers/visionCameraWrapper";
|
||||
import PermissionGateContainer, {
|
||||
WRITE_MEDIA_PERMISSIONS
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
@@ -9,7 +10,7 @@ import React, {
|
||||
import { StatusBar } from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import Orientation from "react-native-orientation-locker";
|
||||
import { Camera, CameraDevice } from "react-native-vision-camera";
|
||||
import type { CameraDevice } from "react-native-vision-camera";
|
||||
// import { log } from "sharedHelpers/logger";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import useDeviceOrientation, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from "react";
|
||||
import { GestureResponderEvent, ViewStyle } from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { TakePhotoOptions } from "react-native-vision-camera";
|
||||
import type { TakePhotoOptions } from "react-native-vision-camera";
|
||||
|
||||
import CameraFlip from "./Buttons/CameraFlip";
|
||||
import Flash from "./Buttons/Flash";
|
||||
|
||||
25
src/components/Camera/helpers/visionCameraWrapper.e2e-mock
Normal file
25
src/components/Camera/helpers/visionCameraWrapper.e2e-mock
Normal file
@@ -0,0 +1,25 @@
|
||||
// This wraps the react-native-vision-camera component and methods we use,
|
||||
// so we can mock them for e2e tests in simulators without camera.
|
||||
/*
|
||||
Note that we are not mocking the useFrameProcessor hook. So, in the e2e test
|
||||
a real frame processor in the sense of react-native-vision-camera is built.
|
||||
As you can see in the next wrapper file our plugin is not used though in this
|
||||
frame processor and only a mocked prediction is immediately returned.
|
||||
*/
|
||||
import { useFrameProcessor } from "react-native-vision-camera";
|
||||
import {
|
||||
mockCamera,
|
||||
mockUseCameraDevice,
|
||||
mockUseCameraFormat
|
||||
} from "tests/vision-camera/vision-camera";
|
||||
|
||||
const Camera = mockCamera;
|
||||
const useCameraDevice = mockUseCameraDevice;
|
||||
const useCameraFormat = mockUseCameraFormat;
|
||||
|
||||
export {
|
||||
Camera,
|
||||
useCameraDevice,
|
||||
useCameraFormat,
|
||||
useFrameProcessor
|
||||
};
|
||||
12
src/components/Camera/helpers/visionCameraWrapper.js
Normal file
12
src/components/Camera/helpers/visionCameraWrapper.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// This wraps the react-native-vision-camera component and methods we use,
|
||||
// so we can mock them for e2e tests in simulators without camera.
|
||||
import {
|
||||
Camera,
|
||||
useCameraDevice,
|
||||
useCameraFormat,
|
||||
useFrameProcessor
|
||||
} from "react-native-vision-camera";
|
||||
|
||||
export {
|
||||
Camera, useCameraDevice, useCameraFormat, useFrameProcessor
|
||||
};
|
||||
34
src/components/Camera/helpers/visionPluginWrapper.e2e-mock
Normal file
34
src/components/Camera/helpers/visionPluginWrapper.e2e-mock
Normal file
@@ -0,0 +1,34 @@
|
||||
// This wraps our vision camera plugin,
|
||||
// so we can mock it for e2e tests in simulators without camera.
|
||||
import * as InatVision from "vision-camera-plugin-inatvision";
|
||||
|
||||
const mockModelResult = {
|
||||
timestamp: Date.now(),
|
||||
predictions: [
|
||||
{
|
||||
name: "Sempervivum tectorum",
|
||||
rank_level: 10,
|
||||
rank: "species",
|
||||
score: 0.96,
|
||||
taxon_id: 51779
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockVision = ( ) => {
|
||||
"worklet";
|
||||
|
||||
return mockModelResult;
|
||||
};
|
||||
|
||||
/*
|
||||
We are mocking the frame processor plugin to return a defined mocked prediction.
|
||||
Note that we are not mocking the getPredictionsForImage function of the plugin,
|
||||
so in the e2e test when the mocked camera "saves" the photo and the app navigates
|
||||
to the suggestions screen, the real example cv model is run on the still image and
|
||||
the e2e test checks for a real prediction from the model.
|
||||
*/
|
||||
export default {
|
||||
...InatVision,
|
||||
inatVision: mockVision
|
||||
};
|
||||
5
src/components/Camera/helpers/visionPluginWrapper.js
Normal file
5
src/components/Camera/helpers/visionPluginWrapper.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// This wraps our vision camera plugin,
|
||||
// so we can mock it for e2e tests in simulators without camera.
|
||||
import * as InatVision from "vision-camera-plugin-inatvision";
|
||||
|
||||
export default InatVision;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Camera } from "components/Camera/helpers/visionCameraWrapper";
|
||||
import React, {
|
||||
useCallback, useMemo, useRef, useState
|
||||
} from "react";
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
GestureStateChangeEvent,
|
||||
TapGestureHandlerEventPayload
|
||||
} from "react-native-gesture-handler";
|
||||
import { Camera } from "react-native-vision-camera";
|
||||
|
||||
const HALF_SIZE_FOCUS_BOX = 33;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Camera } from "components/Camera/helpers/visionCameraWrapper";
|
||||
import { RealmContext } from "providers/contexts.ts";
|
||||
import React, {
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
Camera, CameraDevice, PhotoFile, TakePhotoOptions
|
||||
import type {
|
||||
CameraDevice, PhotoFile, TakePhotoOptions
|
||||
} from "react-native-vision-camera";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import {
|
||||
|
||||
@@ -71,14 +71,15 @@ const PhotoContainer = ( { photo, onPress, style }: Props ): Node => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const loadingIndicator = (
|
||||
<ActivityIndicator
|
||||
className={classnames(
|
||||
"absolute",
|
||||
loadSuccess !== null && "hidden"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderLoadingIndicator = ( ) => {
|
||||
if ( loadSuccess === null ) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute" />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -88,7 +89,7 @@ const PhotoContainer = ( { photo, onPress, style }: Props ): Node => {
|
||||
className="justify-center items-center"
|
||||
accessibilityLabel={photo.attribution}
|
||||
>
|
||||
{loadingIndicator}
|
||||
{renderLoadingIndicator( )}
|
||||
{image}
|
||||
{loadSuccess === false && (
|
||||
<OfflineNotice
|
||||
|
||||
@@ -188,7 +188,7 @@ const DisplayTaxonName = ( {
|
||||
// so in these cases we want to return text only
|
||||
if ( removeStyling ) {
|
||||
return (
|
||||
<Text testID="display-taxon-name-no-styling">
|
||||
<Text testID={`display-taxon-name-no-styling.${taxon.id}`}>
|
||||
{topTextComponent}
|
||||
{bottomTextComponent && (
|
||||
<>
|
||||
@@ -204,7 +204,7 @@ const DisplayTaxonName = ( {
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="display-taxon-name"
|
||||
testID={`display-taxon-name.${taxon.id}`}
|
||||
className={classnames( "flex", {
|
||||
"flex-row items-end flex-wrap w-11/12": isHorizontal
|
||||
} )}
|
||||
|
||||
@@ -346,6 +346,7 @@ const TaxonDetails = ( ): Node => {
|
||||
{showSelectButton && (
|
||||
<ButtonBar containerClass="items-center z-50">
|
||||
<Button
|
||||
testID="TaxonDetails.SelectButton"
|
||||
className="max-w-[500px] w-full"
|
||||
level="focus"
|
||||
text={t( "SELECT-THIS-TAXON" )}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import CameraContainer from "components/Camera/CameraContainer.tsx";
|
||||
// Please don't change this to an aliased path or the e2e mock will not get
|
||||
// used in our e2e tests on Github Actions
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import CameraContainer from "components/Camera/CameraContainer";
|
||||
import GroupPhotosContainer from "components/PhotoImporter/GroupPhotosContainer";
|
||||
import PhotoGallery from "components/PhotoImporter/PhotoGallery";
|
||||
import { Heading4 } from "components/SharedComponents";
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import Geolocation, {
|
||||
GeolocationError,
|
||||
import {
|
||||
GeolocationResponse
|
||||
} from "@react-native-community/geolocation";
|
||||
import { CHUCKS_PAD } from "appConstants/e2e.ts";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
function watchPosition(
|
||||
success: ( position: GeolocationResponse ) => void,
|
||||
error?: ( error: GeolocationError ) => void,
|
||||
options?: {
|
||||
interval?: number;
|
||||
fastestInterval?: number;
|
||||
timeout?: number;
|
||||
maximumAge?: number;
|
||||
enableHighAccuracy?: boolean;
|
||||
distanceFilter?: number;
|
||||
useSignificantChanges?: boolean;
|
||||
}
|
||||
success: ( position: GeolocationResponse ) => void
|
||||
) {
|
||||
console.log( "[DEBUG geolocationWrapper.e2e-mock] watchPosition" );
|
||||
const watchID = Date.now();
|
||||
setTimeout( ( ) => {
|
||||
console.log( "[DEBUG geolocationWrapper.e2e-mock] watchPosition success" );
|
||||
success( {
|
||||
coords: CHUCKS_PAD,
|
||||
timestamp: Date.now()
|
||||
} );
|
||||
}, 1000 );
|
||||
/*
|
||||
We have to limit this here to not run forever otherwise the e2e
|
||||
test never idles and times out.
|
||||
*/
|
||||
if ( counter < 5 ) {
|
||||
setTimeout( ( ) => {
|
||||
console.log( "[DEBUG geolocationWrapper.e2e-mock] watchPosition success" );
|
||||
counter += 1;
|
||||
success( {
|
||||
coords: CHUCKS_PAD,
|
||||
timestamp: Date.now()
|
||||
} );
|
||||
}, 1000 );
|
||||
}
|
||||
return watchID;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
|
||||
describe( "AddObsModal", ( ) => {
|
||||
it( "shows the AI camera button", async ( ) => {
|
||||
render( <AddObsModal closeModal={jest.fn( )} /> );
|
||||
const arCameraButton = screen.getByLabelText(
|
||||
const aiCameraButton = screen.getByLabelText(
|
||||
i18next.t( "AI-Camera" )
|
||||
);
|
||||
expect( arCameraButton ).toBeOnTheScreen();
|
||||
expect( aiCameraButton ).toBeOnTheScreen();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -47,7 +47,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon for species", ( ) => {
|
||||
render( <DisplayTaxonName taxon={speciesTaxon} /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${speciesTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${speciesTaxon.preferred_common_name}${speciesTaxon.name}`
|
||||
);
|
||||
} );
|
||||
@@ -55,15 +57,17 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon w/o common name", ( ) => {
|
||||
render( <DisplayTaxonName taxon={noCommonNameTaxon} /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
noCommonNameTaxon.name
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${noCommonNameTaxon.id}` )
|
||||
).toHaveTextContent( noCommonNameTaxon.name );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon w/o common name and no species", ( ) => {
|
||||
render( <DisplayTaxonName taxon={highRankTaxon} /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${highRankTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${capitalizeFirstLetter( highRankTaxon.rank )} ${highRankTaxon.name}`
|
||||
);
|
||||
} );
|
||||
@@ -71,7 +75,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon for a subfamily", ( ) => {
|
||||
render( <DisplayTaxonName taxon={highRankTaxon} /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${highRankTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${capitalizeFirstLetter( highRankTaxon.rank )} ${highRankTaxon.name}`
|
||||
);
|
||||
} );
|
||||
@@ -79,24 +85,24 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon for subspecies", ( ) => {
|
||||
render( <DisplayTaxonName taxon={subspeciesTaxon} layout="horizontal" /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Silver Lupine Lupinus albifrons var. collinus"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Silver Lupine Lupinus albifrons var. collinus" );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon for improperly capitalized common name", ( ) => {
|
||||
render( <DisplayTaxonName taxon={uncapitalizedTaxon} layout="horizontal" /> );
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Crown-of-thorns Blue Sea-Stars Acanthaster planci"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${uncapitalizedTaxon.id}` )
|
||||
).toHaveTextContent( "Crown-of-thorns Blue Sea-Stars Acanthaster planci" );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon for species in grid view", ( ) => {
|
||||
render( <DisplayTaxonName layout="vertical" taxon={subspeciesTaxon} /> );
|
||||
// Grid view should not have a space between text
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Silver LupineLupinus albifrons var. collinus"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Silver LupineLupinus albifrons var. collinus" );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -104,7 +110,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon for species", ( ) => {
|
||||
render( <DisplayTaxonName taxon={speciesTaxon} scientificNameFirst layout="horizontal" /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${speciesTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${speciesTaxon.name} ${speciesTaxon.preferred_common_name}`
|
||||
);
|
||||
} );
|
||||
@@ -112,15 +120,17 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
test( "renders correct taxon w/o common name", ( ) => {
|
||||
render( <DisplayTaxonName taxon={noCommonNameTaxon} scientificNameFirst /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
noCommonNameTaxon.name
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${noCommonNameTaxon.id}` )
|
||||
).toHaveTextContent( noCommonNameTaxon.name );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon w/o common name and no species", ( ) => {
|
||||
render( <DisplayTaxonName taxon={highRankTaxon} scientificNameFirst /> );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${highRankTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${capitalizeFirstLetter( highRankTaxon.rank )} ${highRankTaxon.name}`
|
||||
);
|
||||
} );
|
||||
@@ -130,9 +140,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
<DisplayTaxonName taxon={subspeciesTaxon} scientificNameFirst layout="horizontal" />
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinus Silver Lupine"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinus Silver Lupine" );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon for species in grid view", ( ) => {
|
||||
@@ -145,9 +155,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
);
|
||||
|
||||
// Grid view should not have a space between text
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinusSilver Lupine"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinusSilver Lupine" );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -162,9 +172,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
speciesTaxon.name
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${speciesTaxon.id}` )
|
||||
).toHaveTextContent( speciesTaxon.name );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon w/o common name", ( ) => {
|
||||
@@ -176,9 +186,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
noCommonNameTaxon.name
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${noCommonNameTaxon.id}` )
|
||||
).toHaveTextContent( noCommonNameTaxon.name );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon w/o common name and no species", ( ) => {
|
||||
@@ -190,7 +200,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${highRankTaxon.id}` )
|
||||
).toHaveTextContent(
|
||||
`${capitalizeFirstLetter( highRankTaxon.rank )} ${highRankTaxon.name}`
|
||||
);
|
||||
} );
|
||||
@@ -205,9 +217,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinus"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinus" );
|
||||
} );
|
||||
|
||||
test( "renders correct taxon for species in grid view", ( ) => {
|
||||
@@ -220,9 +232,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinus"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinus" );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -248,9 +260,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
describe( "when displayed as plain text within a Trans component", ( ) => {
|
||||
it( "it displays common name followed by scientific name", async ( ) => {
|
||||
render( <DisplayTaxonName taxon={subspeciesTaxon} removeStyling layout="horizontal" /> );
|
||||
expect( screen.getByTestId( "display-taxon-name-no-styling" ) ).toHaveTextContent(
|
||||
"Silver Lupine (Lupinus albifrons var. collinus)"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name-no-styling.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Silver Lupine (Lupinus albifrons var. collinus)" );
|
||||
} );
|
||||
|
||||
it( "it displays scientific name followed by common name", async ( ) => {
|
||||
@@ -262,9 +274,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
scientificNameFirst
|
||||
/>
|
||||
);
|
||||
expect( screen.getByTestId( "display-taxon-name-no-styling" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinus (Silver Lupine)"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name-no-styling.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinus (Silver Lupine)" );
|
||||
} );
|
||||
|
||||
it( "it displays scientific name only", async ( ) => {
|
||||
@@ -277,9 +289,9 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
prefersCommonNames={false}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByTestId( "display-taxon-name-no-styling" ) ).toHaveTextContent(
|
||||
"Lupinus albifrons var. collinus"
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId( `display-taxon-name-no-styling.${subspeciesTaxon.id}` )
|
||||
).toHaveTextContent( "Lupinus albifrons var. collinus" );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -291,7 +291,7 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
],
|
||||
]
|
||||
}
|
||||
testID="display-taxon-name"
|
||||
testID="display-taxon-name.1"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import RNFS, { writeFile } from "react-native-fs";
|
||||
import RNFS from "react-native-fs";
|
||||
|
||||
const mockFrame = {
|
||||
isValid: true,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bytesPerRow: 1920,
|
||||
planesCount: 1,
|
||||
isMirrored: false,
|
||||
timestamp: 0,
|
||||
orientation: "portrait",
|
||||
pixelFormat: "yuv",
|
||||
// Returns a fake ArrayBuffer
|
||||
toArrayBuffer: () => new ArrayBuffer( 1920 * 1080 ),
|
||||
toString: () => "Frame",
|
||||
getNativeBuffer: () => ( {
|
||||
// Returns a fake pointer
|
||||
pointer: 0,
|
||||
delete: () => null
|
||||
} ),
|
||||
incrementRefCount: () => null,
|
||||
decrementRefCount: () => null
|
||||
};
|
||||
|
||||
const style = { flex: 1, backgroundColor: "red" };
|
||||
export class mockCamera extends React.PureComponent {
|
||||
static async getAvailableCameraDevices() {
|
||||
return [
|
||||
@@ -11,18 +35,70 @@ export class mockCamera extends React.PureComponent {
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
Every time the component updates we are running the frame processor that is a prop
|
||||
to the camera component. We are running the frame processor with a mocked frame that
|
||||
does not include any kind of image data at all.
|
||||
Running it only on component update means it only is called a few times and not
|
||||
every second (or so - depending on fps). This is enough to satisfy the e2e test
|
||||
though because the mocked prediction needs to appear only once to be found by the
|
||||
test matcher. I tried running it with a timer every second but since it never idles
|
||||
the test never finishes.
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
const { frameProcessor } = this.props;
|
||||
frameProcessor?.frameProcessor( mockFrame );
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this, react/no-unused-class-component-methods
|
||||
async takePhoto( ) {
|
||||
const writePath = `${RNFS.DocumentDirectoryPath}/simulated_camera_photo.png`;
|
||||
|
||||
const imageDataBase64 = "some_large_base_64_encoded_simulated_camera_photo";
|
||||
await writeFile( writePath, imageDataBase64, "base64" );
|
||||
|
||||
return { path: writePath };
|
||||
async takePhoto() {
|
||||
// TODO: this only works on iOS
|
||||
return CameraRoll.getPhotos( {
|
||||
first: 20,
|
||||
assetType: "Photos"
|
||||
} )
|
||||
.then( async r => {
|
||||
/*
|
||||
Basically, here, we are reading the newest twenty photos from the simulators gallery
|
||||
and return the oldest one of those. Copy it to a new path and treat it as a new photo.
|
||||
*/
|
||||
const testPhoto = r.edges[r.edges.length - 1].node.image;
|
||||
let oldUri = testPhoto.uri;
|
||||
if ( testPhoto.uri.includes( "ph://" ) ) {
|
||||
let id = testPhoto.uri.replace( "ph://", "" );
|
||||
id = id.substring( 0, id.indexOf( "/" ) );
|
||||
oldUri = `assets-library://asset/asset.jpg?id=${id}&ext=jpg`;
|
||||
console.log( `Converted file uri to ${oldUri}` );
|
||||
}
|
||||
const encodedUri = encodeURI( oldUri );
|
||||
const destPath = `${RNFS.TemporaryDirectoryPath}temp.jpg`;
|
||||
const newPath = await RNFS.copyAssetsFileIOS(
|
||||
encodedUri,
|
||||
destPath,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const photo = { uri: newPath, predictions: [] };
|
||||
if ( typeof photo !== "object" ) {
|
||||
console.log( "photo is not an object", typeof photo );
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...testPhoto,
|
||||
path: newPath,
|
||||
metadata: {
|
||||
Orientation: testPhoto.orientation
|
||||
}
|
||||
};
|
||||
} )
|
||||
.catch( err => {
|
||||
console.log( "Error getting photos", err );
|
||||
return null;
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
return <View />;
|
||||
return <View style={style} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user