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