xref: /expo/apps/test-suite/tests/MediaLibrary.js (revision 4e758f24)
1import { Asset } from 'expo-asset';
2import * as MediaLibrary from 'expo-media-library';
3import { Platform } from 'react-native';
4
5import * as TestUtils from '../TestUtils';
6import { isDeviceFarm } from '../utils/Environment';
7
8export const name = 'MediaLibrary';
9
10const FILES = [
11  require('../assets/icons/app.png'),
12  require('../assets/icons/loading.png'),
13  require('../assets/black-128x256.png'),
14  require('../assets/big_buck_bunny.mp4'),
15];
16
17const WAIT_TIME = 1000;
18const IMG_NUMBER = 3;
19const VIDEO_NUMBER = 1;
20const F_SIZE = IMG_NUMBER + VIDEO_NUMBER;
21const MEDIA_TYPES = [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video];
22const DEFAULT_MEDIA_TYPES = [MediaLibrary.MediaType.photo];
23const DEFAULT_PAGE_SIZE = 20;
24const ASSET_KEYS = [
25  'id',
26  'filename',
27  'uri',
28  'mediaType',
29  'width',
30  'height',
31  'creationTime',
32  'modificationTime',
33  'duration',
34  Platform.OS === 'ios' ? 'mediaSubtypes' : 'albumId',
35];
36
37const INFO_KEYS = [
38  'localUri',
39  'location',
40  'exif',
41  ...(Platform !== 'ios' ? [] : ['orientation', 'isFavorite']),
42];
43
44const ALBUM_KEYS = [
45  'id',
46  'title',
47  'assetCount',
48  ...(Platform !== 'ios'
49    ? []
50    : ['type', 'startTime', 'endTime', 'approximateLocation', 'locationNames']),
51];
52
53const GET_ASSETS_KEYS = ['assets', 'endCursor', 'hasNextPage', 'totalCount'];
54const ALBUM_NAME = 'Expo Test-Suite Album #1';
55const SECOND_ALBUM_NAME = 'Expo Test-Suite Album #2';
56const WRONG_NAME = 'wertyuiopdfghjklvbnhjnftyujn';
57const WRONG_ID = '1234567890';
58
59// We don't want to move files to the albums on Android R or higher, because it requires a user confirmation.
60const shouldCopyAssets = Platform.OS === 'android' && Platform.Version >= 30;
61
62async function getFiles() {
63  return await Asset.loadAsync(FILES);
64}
65
66async function getAssets(files) {
67  return await Promise.all(
68    files.map(({ localUri }) => {
69      return MediaLibrary.createAssetAsync(localUri);
70    })
71  );
72}
73
74async function createAlbum(assets, name) {
75  const album = await MediaLibrary.createAlbumAsync(name, assets[0], shouldCopyAssets);
76  if (assets.length > 1) {
77    await MediaLibrary.addAssetsToAlbumAsync(assets.slice(1), album, shouldCopyAssets);
78  }
79  return album;
80}
81
82async function checkIfThrows(f) {
83  try {
84    await f();
85  } catch (e) {
86    return true;
87  }
88
89  return false;
90}
91
92function timeoutWrapper(fun, time) {
93  return new Promise(resolve => {
94    setTimeout(() => {
95      fun();
96      resolve(null);
97    }, time);
98  });
99}
100
101export async function test(t) {
102  const shouldSkipTestsRequiringPermissions =
103    (await TestUtils.shouldSkipTestsRequiringPermissionsAsync()) && isDeviceFarm();
104  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe;
105  // On iOS and on android R or higher, some actions require user confirmation. For those actions, we set the timeout to 30 seconds.
106  const TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT =
107    (Platform.OS === 'android' && Platform.Version >= 30) || Platform.OS === 'ios'
108      ? 30 * 1000
109      : t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
110
111  describeWithPermissions('MediaLibrary', async () => {
112    let files;
113    let permissions;
114
115    const checkIfAllPermissionsWereGranted = () => {
116      if (Platform.OS === 'ios') {
117        return permissions.accessPrivileges === 'all';
118      }
119      return permissions.granted;
120    };
121
122    const oldIt = t.it;
123    t.it = (name, fn, timeout) =>
124      oldIt(
125        name,
126        async () => {
127          if (checkIfAllPermissionsWereGranted()) {
128            await fn();
129          }
130        },
131        timeout
132      );
133
134    t.beforeAll(async () => {
135      files = await getFiles();
136      permissions = await MediaLibrary.requestPermissionsAsync();
137      if (!checkIfAllPermissionsWereGranted()) {
138        console.warn('Tests were skipped - not enough permissions to run them.');
139      }
140    });
141
142    t.describe('With default assets', async () => {
143      let testAssets;
144      let album;
145
146      async function initializeDefaultAssetsAsync() {
147        testAssets = await getAssets(files);
148        album = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
149        if (album == null) {
150          album = await createAlbum(testAssets, ALBUM_NAME);
151        } else {
152          await MediaLibrary.addAssetsToAlbumAsync(testAssets, album, shouldCopyAssets);
153        }
154      }
155
156      async function cleanupAsync() {
157        if (checkIfAllPermissionsWereGranted()) {
158          await MediaLibrary.deleteAssetsAsync(testAssets);
159          await MediaLibrary.deleteAlbumsAsync(album);
160        }
161      }
162
163      t.beforeAll(async () => {
164        // NOTE(2020-06-03): The `initializeAsync` function is flaky on Android; often the
165        // `addAssetsToAlbumAsync` method call inside of `createAlbum` will fail with the error
166        // "Could not get all of the requested assets". Usually retrying a few times works, so we do
167        // that programmatically here.
168        let error;
169        for (let i = 0; i < 3; i++) {
170          try {
171            await initializeDefaultAssetsAsync();
172            return;
173          } catch (e) {
174            error = e;
175            console.log('Error initializing MediaLibrary tests, trying again', e.message);
176            await cleanupAsync();
177            await waitFor(1000);
178          }
179        }
180        // if we get here, just throw
181        throw error;
182      }, TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT);
183
184      t.afterAll(async () => {
185        await cleanupAsync();
186      }, TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT);
187
188      t.describe('Every return value has proper shape', async () => {
189        t.it('createAssetAsync', () => {
190          const keys = Object.keys(testAssets[0]);
191          ASSET_KEYS.forEach(key => t.expect(keys).toContain(key));
192        });
193
194        t.it('getAssetInfoAsync', async () => {
195          const { assets } = await MediaLibrary.getAssetsAsync();
196          const value = await MediaLibrary.getAssetInfoAsync(assets[0]);
197          const keys = Object.keys(value);
198          INFO_KEYS.forEach(key => t.expect(keys).toContain(key));
199        });
200
201        t.it('getAlbumAsync', async () => {
202          const value = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
203          const keys = Object.keys(value);
204          ALBUM_KEYS.forEach(key => t.expect(keys).toContain(key));
205        });
206
207        t.it('getAssetsAsync', async () => {
208          const value = await MediaLibrary.getAssetsAsync();
209          const keys = Object.keys(value);
210          GET_ASSETS_KEYS.forEach(key => t.expect(keys).toContain(key));
211        });
212      });
213
214      t.describe('Small tests', async () => {
215        t.it('Function getAlbums returns test album', async () => {
216          const albums = await MediaLibrary.getAlbumsAsync();
217          t.expect(albums.filter(elem => elem.id === album.id).length).toBe(1);
218        });
219
220        t.it('getAlbum returns test album', async () => {
221          const otherAlbum = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
222          t.expect(otherAlbum.title).toBe(album.title);
223          t.expect(otherAlbum.id).toBe(album.id);
224          t.expect(otherAlbum.assetCount).toBe(F_SIZE);
225        });
226
227        t.it('getAlbum with not existing album', async () => {
228          const album = await MediaLibrary.getAlbumAsync(WRONG_NAME);
229          t.expect(album).toBeNull();
230        });
231
232        t.it('getAssetInfo with not existing id', async () => {
233          const asset = await MediaLibrary.getAssetInfoAsync(WRONG_ID);
234          t.expect(asset).toBeNull();
235        });
236
237        t.it(
238          'saveToLibraryAsync should throw when the provided path does not contain an extension',
239          async () => {
240            t.expect(
241              await checkIfThrows(() => MediaLibrary.saveToLibraryAsync('/test/file'))
242            ).toBeTruthy();
243          }
244        );
245
246        t.it(
247          'createAssetAsync should throw when the provided path does not contain an extension',
248          async () => {
249            t.expect(
250              await checkIfThrows(() => MediaLibrary.createAssetAsync('/test/file'))
251            ).toBeTruthy();
252          }
253        );
254
255        // On both platforms assets should perserve their id. On iOS it's native behaviour,
256        // but on Android it should be implemented (but it isn't)
257        // t.it("After createAlbum and addAssetsTo album all assets have the same id", async () => {
258        //   await Promise.all(testAssets.map(async asset => {
259        //     const info = await MediaLibrary.getAssetInfoAsync(asset);
260        //     t.expect(info.id).toBe(asset.id);
261        //   }));
262        // });
263      });
264
265      t.describe('getAssetsAsync', async () => {
266        t.it('No arguments', async () => {
267          const options = {};
268          const { assets } = await MediaLibrary.getAssetsAsync(options);
269          t.expect(assets.length).toBeLessThanOrEqual(DEFAULT_PAGE_SIZE);
270          t.expect(assets.length).toBeGreaterThanOrEqual(IMG_NUMBER);
271          assets.forEach(asset => t.expect(DEFAULT_MEDIA_TYPES).toContain(asset.mediaType));
272        });
273
274        t.it('album', async () => {
275          const options = { album };
276          const { assets } = await MediaLibrary.getAssetsAsync(options);
277          t.expect(assets.length).toBe(IMG_NUMBER);
278          assets.forEach(asset => t.expect(DEFAULT_MEDIA_TYPES).toContain(asset.mediaType));
279          if (Platform.OS === 'android')
280            assets.forEach(asset => t.expect(asset.albumId).toBe(album.id));
281        });
282
283        t.it('first, after', async () => {
284          const options = { first: 2, album };
285          {
286            const {
287              assets,
288              endCursor,
289              hasNextPage,
290              totalCount,
291            } = await MediaLibrary.getAssetsAsync(options);
292            t.expect(assets.length).toBe(2);
293            t.expect(totalCount).toBe(IMG_NUMBER);
294            t.expect(hasNextPage).toBeTruthy();
295            assets.forEach(asset => t.expect(DEFAULT_MEDIA_TYPES).toContain(asset.mediaType));
296            options.after = endCursor;
297          }
298          {
299            const { assets, hasNextPage, totalCount } = await MediaLibrary.getAssetsAsync(options);
300            t.expect(assets.length).toBe(IMG_NUMBER - 2);
301            t.expect(totalCount).toBe(IMG_NUMBER);
302            t.expect(hasNextPage).toBeFalsy();
303          }
304        });
305
306        t.it('mediaType: video', async () => {
307          const mediaType = MediaLibrary.MediaType.video;
308          const options = { mediaType, album };
309          const { assets } = await MediaLibrary.getAssetsAsync(options);
310          assets.forEach(asset => t.expect(asset.mediaType).toBe(mediaType));
311          t.expect(assets.length).toBe(1);
312        });
313
314        t.it('mediaType: photo', async () => {
315          const mediaType = MediaLibrary.MediaType.photo;
316          const options = { mediaType, album };
317          const { assets } = await MediaLibrary.getAssetsAsync(options);
318          t.expect(assets.length).toBe(IMG_NUMBER);
319          assets.forEach(asset => t.expect(asset.mediaType).toBe(mediaType));
320        });
321
322        t.it('check size - photo', async () => {
323          const mediaType = MediaLibrary.MediaType.photo;
324          const options = { mediaType, album };
325          const { assets } = await MediaLibrary.getAssetsAsync(options);
326          t.expect(assets.length).toBe(IMG_NUMBER);
327          assets.forEach(asset => {
328            t.expect(asset.width).not.toEqual(0);
329            t.expect(asset.height).not.toEqual(0);
330          });
331        });
332
333        t.it('check size - video', async () => {
334          const mediaType = MediaLibrary.MediaType.video;
335          const options = { mediaType, album };
336          const { assets } = await MediaLibrary.getAssetsAsync(options);
337          t.expect(assets.length).toBe(VIDEO_NUMBER);
338          assets.forEach(asset => {
339            t.expect(asset.width).not.toEqual(0);
340            t.expect(asset.height).not.toEqual(0);
341          });
342        });
343
344        t.it('supports getting assets from specified time range', async () => {
345          const assetsToCheck = 7;
346
347          // Get some assets with the biggest creation time.
348          const { assets } = await MediaLibrary.getAssetsAsync({
349            first: assetsToCheck,
350            sortBy: MediaLibrary.SortBy.creationTime,
351          });
352
353          // Set time range based on the newest and oldest creation times.
354          const createdAfter = assets[assets.length - 1].creationTime;
355          const createdBefore = assets[0].creationTime;
356
357          // Repeat assets request but with the time range.
358          const { assets: filteredAssets } = await MediaLibrary.getAssetsAsync({
359            first: assetsToCheck,
360            sortBy: MediaLibrary.SortBy.creationTime,
361            createdAfter,
362            createdBefore,
363          });
364
365          // We can't get more assets than previously, but they could be equal if there are multiple assets with the same timestamp.
366          t.expect(filteredAssets.length).toBeLessThanOrEqual(assets.length);
367
368          // Check if every asset was created within the time range.
369          for (const asset of filteredAssets) {
370            t.expect(asset.creationTime).toBeLessThanOrEqual(createdBefore);
371            t.expect(asset.creationTime).toBeGreaterThanOrEqual(createdAfter);
372          }
373        });
374      });
375
376      t.describe('getAssetInfoAsync', async () => {
377        t.it('shouldDownloadFromNetwork: false, for photos', async () => {
378          const mediaType = MediaLibrary.MediaType.photo;
379          const options = { mediaType, album };
380          const { assets } = await MediaLibrary.getAssetsAsync(options);
381          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
382            shouldDownloadFromNetwork: false,
383          });
384          const keys = Object.keys(value);
385
386          const expectedExtraKeys = Platform.select({
387            ios: ['isNetworkAsset'],
388            default: [],
389          });
390          expectedExtraKeys.forEach(key => t.expect(keys).toContain(key));
391          if (Platform.OS === 'ios') {
392            t.expect(value['isNetworkAsset']).toBe(false);
393          }
394        });
395
396        t.it('shouldDownloadFromNetwork: true, for photos', async () => {
397          const mediaType = MediaLibrary.MediaType.photo;
398          const options = { mediaType, album };
399          const { assets } = await MediaLibrary.getAssetsAsync(options);
400          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
401            shouldDownloadFromNetwork: true,
402          });
403          const keys = Object.keys(value);
404
405          const expectedExtraKeys = Platform.select({
406            ios: ['isNetworkAsset'],
407            default: [],
408          });
409          expectedExtraKeys.forEach(key => t.expect(keys).not.toContain(key));
410        });
411
412        t.it('shouldDownloadFromNetwork: false, for videos', async () => {
413          const mediaType = MediaLibrary.MediaType.video;
414          const options = { mediaType, album };
415          const { assets } = await MediaLibrary.getAssetsAsync(options);
416          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
417            shouldDownloadFromNetwork: false,
418          });
419          const keys = Object.keys(value);
420
421          const expectedExtraKeys = Platform.select({
422            ios: ['isNetworkAsset'],
423            default: [],
424          });
425          expectedExtraKeys.forEach(key => t.expect(keys).toContain(key));
426          if (Platform.OS === 'ios') {
427            t.expect(value['isNetworkAsset']).toBe(false);
428          }
429        });
430
431        t.it('shouldDownloadFromNetwork: true, for videos', async () => {
432          const mediaType = MediaLibrary.MediaType.video;
433          const options = { mediaType, album };
434          const { assets } = await MediaLibrary.getAssetsAsync(options);
435          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
436            shouldDownloadFromNetwork: true,
437          });
438          const keys = Object.keys(value);
439
440          const expectedExtraKeys = Platform.select({
441            ios: ['isNetworkAsset'],
442            default: [],
443          });
444          expectedExtraKeys.forEach(key => t.expect(keys).not.toContain(key));
445        });
446      });
447    });
448
449    t.describe('Delete tests', async () => {
450      t.it(
451        'deleteAssetsAsync',
452        async () => {
453          const assets = await getAssets(files);
454          const result = await MediaLibrary.deleteAssetsAsync(assets);
455          const deletedAssets = await Promise.all(
456            assets.map(async asset => await MediaLibrary.getAssetInfoAsync(asset))
457          );
458          t.expect(result).toEqual(true);
459          t.expect(assets.length).not.toEqual(0);
460          t.expect(deletedAssets.length).toEqual(assets.length);
461          deletedAssets.forEach(deletedAsset => t.expect(deletedAsset).toBeNull);
462        },
463        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
464      );
465
466      t.it(
467        'deleteAlbumsAsync',
468        async () => {
469          const assets = await getAssets([files[0]]);
470          const album = await createAlbum(assets, ALBUM_NAME);
471
472          const result = await MediaLibrary.deleteAlbumsAsync(album, true);
473          t.expect(result).toEqual(true);
474          const deletedAlbum = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
475          t.expect(deletedAlbum).toBeNull();
476
477          if (shouldCopyAssets) {
478            await MediaLibrary.deleteAssetsAsync(assets);
479          }
480        },
481        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
482      );
483
484      t.it(
485        'deleteManyAlbums',
486        async () => {
487          const assets = await getAssets(files.slice(0, 2));
488          let firstAlbum = await MediaLibrary.createAlbumAsync(
489            ALBUM_NAME,
490            assets[0],
491            shouldCopyAssets
492          );
493
494          let secondAlbum = await MediaLibrary.createAlbumAsync(
495            SECOND_ALBUM_NAME,
496            assets[1],
497            shouldCopyAssets
498          );
499
500          await MediaLibrary.deleteAlbumsAsync([firstAlbum, secondAlbum], true);
501          firstAlbum = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
502          secondAlbum = await MediaLibrary.getAlbumAsync(SECOND_ALBUM_NAME);
503          t.expect(firstAlbum).toBeNull();
504          t.expect(secondAlbum).toBeNull();
505
506          if (!shouldCopyAssets) {
507            const firstAsset = await MediaLibrary.getAssetInfoAsync(assets[0]);
508            const secondAsset = await MediaLibrary.getAssetInfoAsync(assets[1]);
509            t.expect(firstAsset).toBeNull();
510            t.expect(secondAsset).toBeNull();
511          } else {
512            await MediaLibrary.deleteAssetsAsync(assets);
513          }
514        },
515        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
516      );
517    });
518
519    t.describe('Listeners', async () => {
520      const createdAssets = [];
521
522      t.afterAll(async () => {
523        if (createdAssets) {
524          await MediaLibrary.deleteAssetsAsync(createdAssets);
525        }
526      }, TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT);
527
528      t.it(
529        'addAsset calls listener',
530        async () => {
531          const spy = t.jasmine.createSpy('addAsset spy', () => {});
532          const remove = MediaLibrary.addListener(spy);
533          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
534
535          t.expect(asset).not.toBeNull();
536          await timeoutWrapper(() => t.expect(spy).toHaveBeenCalled(), WAIT_TIME);
537
538          remove.remove();
539          createdAssets.push(asset);
540        },
541        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
542      );
543
544      t.it(
545        'remove listener',
546        async () => {
547          const spy = t.jasmine.createSpy('remove spy', () => {});
548          const subscription = MediaLibrary.addListener(spy);
549          subscription.remove();
550          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
551
552          t.expect(asset).not.toBeNull();
553          await timeoutWrapper(() => t.expect(spy).not.toHaveBeenCalled(), WAIT_TIME);
554
555          createdAssets.push(asset);
556        },
557        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
558      );
559
560      t.it(
561        'deleteListener calls listener',
562        async () => {
563          const spy = t.jasmine.createSpy('deleteAsset spy', () => {});
564          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
565          const subscription = MediaLibrary.addListener(spy);
566
567          t.expect(asset).not.toBeNull();
568          await MediaLibrary.deleteAssetsAsync(asset);
569          await timeoutWrapper(() => t.expect(spy).toHaveBeenCalled(), WAIT_TIME);
570          subscription.remove();
571        },
572        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
573      );
574
575      t.it(
576        'removeAllListeners',
577        async () => {
578          const spy = t.jasmine.createSpy('removeAll', () => {});
579          MediaLibrary.addListener(spy);
580          MediaLibrary.removeAllListeners();
581
582          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
583          t.expect(asset).not.toBeNull();
584          await timeoutWrapper(() => t.expect(spy).not.toHaveBeenCalled(), WAIT_TIME);
585
586          createdAssets.push(asset);
587        },
588        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
589      );
590    });
591  });
592}
593