mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-18 20:50:54 -04:00
* Rename for consistency with obs photos * Refactor to be similar to obs photos * Include Sound as type * Revert "Include Sound as type" This reverts commit3db88ae031. * Revert "Refactor to be similar to obs photos" This reverts commit8ed7454be0. * Revert "Rename for consistency with obs photos" This reverts commit674b921bab. * Git mv without changes * Add types * Type api sound param * Type fields s const * Type fields as const * Type api param * Type new param * The only call site of this function passes in only defined strings * Adapted from Photo check for local path before deleting * Type fct params * Refactor constructor to only have a uri as param * Refactor Sound constructor to have string as param * Update Sound.ts * Remove unused param * Type fct params * Type realm * Also collect unsyncedSounds * Create operations for sound uploads based on Sound only * Add Evidence type * Change a sound upload operation to work with a RealmSound * Update realmSync to also just pass through Sounds * Use server ID for attaching obs_sound to obs * Also split photo pipeline for clarity This is not a functional change. Also previously, only RealmPhoto s are uploaded, only RealmObservationPhoto s are attached or modified. Same as for sounds now. * Update realmSync.ts * Remove log * Update realmSync.test.js * Update mocks * Update Sound map test * Update prepareMediaForUpload.test.js * Update prepareMediaForUpload.test.js * Update mediaUploader.test.js
335 lines
11 KiB
JavaScript
335 lines
11 KiB
JavaScript
import { createOrUpdateEvidence } from "api/observations";
|
|
import inatjs from "inaturalistjs";
|
|
import { prepareMediaForUpload } from "uploaders";
|
|
import { attachMediaToObservation, uploadObservationMedia } from "uploaders/mediaUploader";
|
|
import { trackEvidenceUpload } from "uploaders/utils/progressTracker";
|
|
|
|
jest.mock( "api/observations" );
|
|
jest.mock( "inaturalistjs" );
|
|
jest.mock( "uploaders" );
|
|
jest.mock( "uploaders/utils/progressTracker" );
|
|
|
|
const mockedCreateOrUpdateEvidence = jest.mocked( createOrUpdateEvidence );
|
|
const mockedPrepareMediaForUpload = jest.mocked( prepareMediaForUpload );
|
|
const mockedTrackEvidenceUpload = jest.mocked( trackEvidenceUpload );
|
|
|
|
describe( "mediaUploader", () => {
|
|
beforeEach( () => {
|
|
jest.resetAllMocks();
|
|
|
|
const mockProgress = {
|
|
attached: jest.fn(),
|
|
uploaded: jest.fn(),
|
|
};
|
|
mockedTrackEvidenceUpload.mockReturnValue( mockProgress );
|
|
mockedCreateOrUpdateEvidence.mockResolvedValue( { id: 123 } );
|
|
mockedPrepareMediaForUpload.mockImplementation( ( evidence, type, action, observationId ) => ( {
|
|
id: evidence.uuid,
|
|
type,
|
|
action,
|
|
observation_id: observationId,
|
|
file_url: evidence.url,
|
|
} ) );
|
|
} );
|
|
|
|
describe( "uploadObservationMedia", () => {
|
|
it( "should upload photos and sounds in parallel", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{
|
|
wasSynced: () => false,
|
|
photo: { uuid: "photo-uuid-1", url: "photo1.jpg" },
|
|
},
|
|
],
|
|
observationSounds: [
|
|
{
|
|
wasSynced: () => false,
|
|
uuid: "sound-uuid-1",
|
|
sound: { file_url: "file://soundUploads/sound1.mp3" },
|
|
},
|
|
],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).toHaveBeenCalledTimes( 2 );
|
|
expect( result ).toEqual( {
|
|
unsyncedObservationPhotos: observation.observationPhotos,
|
|
modifiedObservationPhotos: [],
|
|
unsyncedObservationSounds: observation.observationSounds,
|
|
} );
|
|
} );
|
|
|
|
it( "should handle empty media arrays", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).not.toHaveBeenCalled();
|
|
expect( result ).toEqual( {
|
|
unsyncedObservationPhotos: [],
|
|
modifiedObservationPhotos: [],
|
|
unsyncedObservationSounds: [],
|
|
} );
|
|
} );
|
|
|
|
it( "should handle photos that need updates", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{
|
|
wasSynced: () => true,
|
|
needsSync: () => true,
|
|
photo: { uuid: "photo-uuid-1", url: "photo1.jpg" },
|
|
},
|
|
],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).not.toHaveBeenCalled();
|
|
expect( result.modifiedObservationPhotos.length ).toBe( 1 );
|
|
} );
|
|
|
|
it( "should handle missing photo objects gracefully", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{
|
|
wasSynced: () => false,
|
|
get photo() {
|
|
throw new Error( "No object with key photo-uuid-missing" );
|
|
},
|
|
},
|
|
{
|
|
wasSynced: () => false,
|
|
photo: { uuid: "photo-uuid-1", url: "photo1.jpg" },
|
|
},
|
|
],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).toHaveBeenCalledTimes( 1 );
|
|
expect( result.unsyncedObservationPhotos.length ).toBe( 2 );
|
|
} );
|
|
} );
|
|
|
|
describe( "attachMediaToObservation", () => {
|
|
it( "should attach photos and sounds to an observation", async () => {
|
|
const observationUUID = "obs-uuid-123";
|
|
const mediaItems = {
|
|
unsyncedObservationPhotos: [
|
|
{ uuid: "photo-uuid-1", url: "photo1.jpg" },
|
|
],
|
|
modifiedObservationPhotos: [
|
|
{ uuid: "photo-uuid-2", url: "photo2.jpg" },
|
|
],
|
|
unsyncedObservationSounds: [
|
|
{ uuid: "sound-uuid-1", url: "sound1.mp3" },
|
|
],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
await attachMediaToObservation( observationUUID, mediaItems, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).toHaveBeenCalledTimes( 3 );
|
|
|
|
expect( mockedPrepareMediaForUpload ).toHaveBeenCalledWith(
|
|
mediaItems.unsyncedObservationPhotos[0],
|
|
"ObservationPhoto",
|
|
"attach",
|
|
observationUUID,
|
|
);
|
|
|
|
expect( mockedPrepareMediaForUpload ).toHaveBeenCalledWith(
|
|
mediaItems.unsyncedObservationSounds[0],
|
|
"ObservationSound",
|
|
"attach",
|
|
observationUUID,
|
|
);
|
|
|
|
expect( mockedPrepareMediaForUpload ).toHaveBeenCalledWith(
|
|
mediaItems.modifiedObservationPhotos[0],
|
|
"ObservationPhoto",
|
|
"update",
|
|
observationUUID,
|
|
);
|
|
|
|
expect( mockedCreateOrUpdateEvidence ).toHaveBeenCalledWith(
|
|
inatjs.observation_photos.create,
|
|
expect.any( Object ),
|
|
options,
|
|
);
|
|
expect( mockedCreateOrUpdateEvidence ).toHaveBeenCalledWith(
|
|
inatjs.observation_sounds.create,
|
|
expect.any( Object ),
|
|
options,
|
|
);
|
|
expect( mockedCreateOrUpdateEvidence ).toHaveBeenCalledWith(
|
|
inatjs.observation_photos.update,
|
|
expect.any( Object ),
|
|
options,
|
|
);
|
|
} );
|
|
|
|
it( "should handle empty media arrays", async () => {
|
|
const observationUUID = "obs-uuid-123";
|
|
const mediaItems = {
|
|
unsyncedObservationPhotos: [],
|
|
modifiedObservationPhotos: [],
|
|
unsyncedObservationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
await attachMediaToObservation( observationUUID, mediaItems, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).not.toHaveBeenCalled();
|
|
} );
|
|
} );
|
|
|
|
describe( "filterMediaForUpload", () => {
|
|
it( "should correctly filter media for upload", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{ wasSynced: () => false, photo: { uuid: "photo-uuid-1", url: "photo1.jpg" } },
|
|
{
|
|
wasSynced: () => true,
|
|
needsSync: () => true,
|
|
photo: { uuid: "photo-uuid-2", url: "photo2.jpg" },
|
|
},
|
|
{
|
|
wasSynced: () => true,
|
|
needsSync: () => false,
|
|
photo: { uuid: "photo-uuid-3", url: "photo3.jpg" },
|
|
},
|
|
],
|
|
observationSounds: [
|
|
{
|
|
wasSynced: () => false,
|
|
uuid: "sound-uuid-1",
|
|
sound: { file_url: "file://soundUploads/sound1.mp3" },
|
|
},
|
|
{
|
|
wasSynced: () => true,
|
|
uuid: "sound-uuid-2",
|
|
sound: { file_url: "file://soundUploads/sound2.mp3" },
|
|
},
|
|
],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( result.unsyncedObservationPhotos.length ).toBe( 1 );
|
|
expect( result.modifiedObservationPhotos.length ).toBe( 1 );
|
|
expect( result.unsyncedObservationSounds.length ).toBe( 1 );
|
|
} );
|
|
} );
|
|
|
|
describe( "progress tracking", () => {
|
|
it( "should track progress for upload evidence operations", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{ wasSynced: () => false, photo: { uuid: "photo-uuid-1", url: "photo1.jpg" } },
|
|
],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
const mockProgress = {
|
|
attached: jest.fn(),
|
|
uploaded: jest.fn(),
|
|
};
|
|
mockedTrackEvidenceUpload.mockReturnValue( mockProgress );
|
|
|
|
await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( trackEvidenceUpload ).toHaveBeenCalledWith( "obs-uuid-123" );
|
|
expect( mockProgress.uploaded ).toHaveBeenCalled();
|
|
expect( mockProgress.attached ).not.toHaveBeenCalled();
|
|
} );
|
|
|
|
it( "should track progress for attach operations", async () => {
|
|
const observationUUID = "obs-uuid-123";
|
|
const mediaItems = {
|
|
unsyncedObservationPhotos: [
|
|
{ uuid: "photo-uuid-1", url: "photo1.jpg" },
|
|
],
|
|
modifiedObservationPhotos: [],
|
|
unsyncedObservationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
const mockProgress = {
|
|
attached: jest.fn(),
|
|
uploaded: jest.fn(),
|
|
};
|
|
mockedTrackEvidenceUpload.mockReturnValue( mockProgress );
|
|
|
|
await attachMediaToObservation( observationUUID, mediaItems, options, realm );
|
|
|
|
expect( trackEvidenceUpload ).toHaveBeenCalledWith( "obs-uuid-123" );
|
|
expect( mockProgress.attached ).toHaveBeenCalled();
|
|
} );
|
|
} );
|
|
|
|
describe( "error handling", () => {
|
|
it( "should handle API errors gracefully", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{ wasSynced: () => false, photo: { uuid: "photo-uuid-1", url: "photo1.jpg" } },
|
|
],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
mockedCreateOrUpdateEvidence.mockRejectedValue( new Error( "API Error" ) );
|
|
|
|
await expect( uploadObservationMedia( observation, options, realm ) )
|
|
.rejects.toThrow( "API Error" );
|
|
} );
|
|
|
|
it( "should filter out null photos", async () => {
|
|
const observation = {
|
|
uuid: "obs-uuid-123",
|
|
observationPhotos: [
|
|
{
|
|
wasSynced: () => false,
|
|
photo: null,
|
|
},
|
|
],
|
|
observationSounds: [],
|
|
};
|
|
const options = { api_token: "test-token" };
|
|
const realm = {};
|
|
|
|
const result = await uploadObservationMedia( observation, options, realm );
|
|
|
|
expect( createOrUpdateEvidence ).not.toHaveBeenCalled();
|
|
expect( result.unsyncedObservationPhotos.length ).toBe( 1 );
|
|
} );
|
|
} );
|
|
} );
|