xref: /expo/apps/test-suite/tests/MediaLibrary.js (revision cd4bd26b)
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;
22const 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 (e) {
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 {
288              assets,
289              endCursor,
290              hasNextPage,
291              totalCount,
292            } = await MediaLibrary.getAssetsAsync(options);
293            t.expect(assets.length).toBe(2);
294            t.expect(totalCount).toBe(IMG_NUMBER);
295            t.expect(hasNextPage).toBeTruthy();
296            assets.forEach(asset => t.expect(DEFAULT_MEDIA_TYPES).toContain(asset.mediaType));
297            options.after = endCursor;
298          }
299          {
300            const { assets, hasNextPage, totalCount } = await MediaLibrary.getAssetsAsync(options);
301            t.expect(assets.length).toBe(IMG_NUMBER - 2);
302            t.expect(totalCount).toBe(IMG_NUMBER);
303            t.expect(hasNextPage).toBeFalsy();
304          }
305        });
306
307        t.it('mediaType: video', async () => {
308          const mediaType = MediaLibrary.MediaType.video;
309          const options = { mediaType, album };
310          const { assets } = await MediaLibrary.getAssetsAsync(options);
311          assets.forEach(asset => t.expect(asset.mediaType).toBe(mediaType));
312          t.expect(assets.length).toBe(1);
313        });
314
315        t.it('mediaType: photo', async () => {
316          const mediaType = MediaLibrary.MediaType.photo;
317          const options = { mediaType, album };
318          const { assets } = await MediaLibrary.getAssetsAsync(options);
319          t.expect(assets.length).toBe(IMG_NUMBER);
320          assets.forEach(asset => t.expect(asset.mediaType).toBe(mediaType));
321        });
322
323        t.it('check size - photo', async () => {
324          const mediaType = MediaLibrary.MediaType.photo;
325          const options = { mediaType, album };
326          const { assets } = await MediaLibrary.getAssetsAsync(options);
327          t.expect(assets.length).toBe(IMG_NUMBER);
328          assets.forEach(asset => {
329            t.expect(asset.width).not.toEqual(0);
330            t.expect(asset.height).not.toEqual(0);
331          });
332        });
333
334        t.it('check size - video', async () => {
335          const mediaType = MediaLibrary.MediaType.video;
336          const options = { mediaType, album };
337          const { assets } = await MediaLibrary.getAssetsAsync(options);
338          t.expect(assets.length).toBe(VIDEO_NUMBER);
339          assets.forEach(asset => {
340            t.expect(asset.width).not.toEqual(0);
341            t.expect(asset.height).not.toEqual(0);
342          });
343        });
344
345        t.it('supports getting assets from specified time range', async () => {
346          const assetsToCheck = 7;
347
348          // Get some assets with the biggest creation time.
349          const { assets } = await MediaLibrary.getAssetsAsync({
350            first: assetsToCheck,
351            sortBy: MediaLibrary.SortBy.creationTime,
352          });
353
354          // Set time range based on the newest and oldest creation times.
355          const createdAfter = assets[assets.length - 1].creationTime;
356          const createdBefore = assets[0].creationTime;
357
358          // Repeat assets request but with the time range.
359          const { assets: filteredAssets } = await MediaLibrary.getAssetsAsync({
360            first: assetsToCheck,
361            sortBy: MediaLibrary.SortBy.creationTime,
362            createdAfter,
363            createdBefore,
364          });
365
366          // We can't get more assets than previously, but they could be equal if there are multiple assets with the same timestamp.
367          t.expect(filteredAssets.length).toBeLessThanOrEqual(assets.length);
368
369          // Check if every asset was created within the time range.
370          for (const asset of filteredAssets) {
371            t.expect(asset.creationTime).toBeLessThanOrEqual(createdBefore);
372            t.expect(asset.creationTime).toBeGreaterThanOrEqual(createdAfter);
373          }
374        });
375      });
376
377      t.describe('getAssetInfoAsync', async () => {
378        t.it('shouldDownloadFromNetwork: false, for photos', async () => {
379          const mediaType = MediaLibrary.MediaType.photo;
380          const options = { mediaType, album };
381          const { assets } = await MediaLibrary.getAssetsAsync(options);
382          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
383            shouldDownloadFromNetwork: false,
384          });
385          const keys = Object.keys(value);
386
387          const expectedExtraKeys = Platform.select({
388            ios: ['isNetworkAsset'],
389            default: [],
390          });
391          expectedExtraKeys.forEach(key => t.expect(keys).toContain(key));
392          if (Platform.OS === 'ios') {
393            t.expect(value['isNetworkAsset']).toBe(false);
394          }
395        });
396
397        t.it('shouldDownloadFromNetwork: true, for photos', async () => {
398          const mediaType = MediaLibrary.MediaType.photo;
399          const options = { mediaType, album };
400          const { assets } = await MediaLibrary.getAssetsAsync(options);
401          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
402            shouldDownloadFromNetwork: true,
403          });
404          const keys = Object.keys(value);
405
406          const expectedExtraKeys = Platform.select({
407            ios: ['isNetworkAsset'],
408            default: [],
409          });
410          expectedExtraKeys.forEach(key => t.expect(keys).not.toContain(key));
411        });
412
413        t.it('shouldDownloadFromNetwork: false, for videos', async () => {
414          const mediaType = MediaLibrary.MediaType.video;
415          const options = { mediaType, album };
416          const { assets } = await MediaLibrary.getAssetsAsync(options);
417          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
418            shouldDownloadFromNetwork: false,
419          });
420          const keys = Object.keys(value);
421
422          const expectedExtraKeys = Platform.select({
423            ios: ['isNetworkAsset'],
424            default: [],
425          });
426          expectedExtraKeys.forEach(key => t.expect(keys).toContain(key));
427          if (Platform.OS === 'ios') {
428            t.expect(value['isNetworkAsset']).toBe(false);
429          }
430        });
431
432        t.it('shouldDownloadFromNetwork: true, for videos', async () => {
433          const mediaType = MediaLibrary.MediaType.video;
434          const options = { mediaType, album };
435          const { assets } = await MediaLibrary.getAssetsAsync(options);
436          const value = await MediaLibrary.getAssetInfoAsync(assets[0], {
437            shouldDownloadFromNetwork: true,
438          });
439          const keys = Object.keys(value);
440
441          const expectedExtraKeys = Platform.select({
442            ios: ['isNetworkAsset'],
443            default: [],
444          });
445          expectedExtraKeys.forEach(key => t.expect(keys).not.toContain(key));
446        });
447      });
448    });
449
450    t.describe('Delete tests', async () => {
451      t.it(
452        'deleteAssetsAsync',
453        async () => {
454          const assets = await getAssets(files);
455          const result = await MediaLibrary.deleteAssetsAsync(assets);
456          const deletedAssets = await Promise.all(
457            assets.map(async asset => await MediaLibrary.getAssetInfoAsync(asset))
458          );
459          t.expect(result).toEqual(true);
460          t.expect(assets.length).not.toEqual(0);
461          t.expect(deletedAssets.length).toEqual(assets.length);
462          deletedAssets.forEach(deletedAsset => t.expect(deletedAsset).toBeNull);
463        },
464        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
465      );
466
467      t.it(
468        'deleteAlbumsAsync',
469        async () => {
470          const assets = await getAssets([files[0]]);
471          const album = await createAlbum(assets, ALBUM_NAME);
472
473          const result = await MediaLibrary.deleteAlbumsAsync(album, true);
474          t.expect(result).toEqual(true);
475          const deletedAlbum = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
476          t.expect(deletedAlbum).toBeNull();
477
478          if (shouldCopyAssets) {
479            await MediaLibrary.deleteAssetsAsync(assets);
480          }
481        },
482        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
483      );
484
485      t.it(
486        'deleteManyAlbums',
487        async () => {
488          const assets = await getAssets(files.slice(0, 2));
489          let firstAlbum = await MediaLibrary.createAlbumAsync(
490            ALBUM_NAME,
491            assets[0],
492            shouldCopyAssets
493          );
494
495          let secondAlbum = await MediaLibrary.createAlbumAsync(
496            SECOND_ALBUM_NAME,
497            assets[1],
498            shouldCopyAssets
499          );
500
501          await MediaLibrary.deleteAlbumsAsync([firstAlbum, secondAlbum], true);
502          firstAlbum = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
503          secondAlbum = await MediaLibrary.getAlbumAsync(SECOND_ALBUM_NAME);
504          t.expect(firstAlbum).toBeNull();
505          t.expect(secondAlbum).toBeNull();
506
507          if (!shouldCopyAssets) {
508            const firstAsset = await MediaLibrary.getAssetInfoAsync(assets[0]);
509            const secondAsset = await MediaLibrary.getAssetInfoAsync(assets[1]);
510            t.expect(firstAsset).toBeNull();
511            t.expect(secondAsset).toBeNull();
512          } else {
513            await MediaLibrary.deleteAssetsAsync(assets);
514          }
515        },
516        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
517      );
518    });
519
520    t.describe('Listeners', async () => {
521      const createdAssets = [];
522
523      t.afterAll(async () => {
524        if (createdAssets) {
525          await MediaLibrary.deleteAssetsAsync(createdAssets);
526        }
527      }, TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT);
528
529      t.it(
530        'addAsset calls listener',
531        async () => {
532          const spy = t.jasmine.createSpy('addAsset spy', () => {});
533          const remove = MediaLibrary.addListener(spy);
534          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
535
536          t.expect(asset).not.toBeNull();
537          await timeoutWrapper(() => t.expect(spy).toHaveBeenCalled(), WAIT_TIME);
538
539          remove.remove();
540          createdAssets.push(asset);
541        },
542        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
543      );
544
545      t.it(
546        'remove listener',
547        async () => {
548          const spy = t.jasmine.createSpy('remove spy', () => {});
549          const subscription = MediaLibrary.addListener(spy);
550          subscription.remove();
551          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
552
553          t.expect(asset).not.toBeNull();
554          await timeoutWrapper(() => t.expect(spy).not.toHaveBeenCalled(), WAIT_TIME);
555
556          createdAssets.push(asset);
557        },
558        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
559      );
560
561      t.it(
562        'deleteListener calls listener',
563        async () => {
564          const spy = t.jasmine.createSpy('deleteAsset spy', () => {});
565          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
566          const subscription = MediaLibrary.addListener(spy);
567
568          t.expect(asset).not.toBeNull();
569          await MediaLibrary.deleteAssetsAsync(asset);
570          await timeoutWrapper(() => t.expect(spy).toHaveBeenCalled(), WAIT_TIME);
571          subscription.remove();
572        },
573        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
574      );
575
576      t.it(
577        'removeAllListeners',
578        async () => {
579          const spy = t.jasmine.createSpy('removeAll', () => {});
580          MediaLibrary.addListener(spy);
581          MediaLibrary.removeAllListeners();
582
583          const asset = await MediaLibrary.createAssetAsync(files[0].localUri);
584          t.expect(asset).not.toBeNull();
585          await timeoutWrapper(() => t.expect(spy).not.toHaveBeenCalled(), WAIT_TIME);
586
587          createdAssets.push(asset);
588        },
589        TIMEOUT_WHEN_USER_NEEDS_TO_INTERACT
590      );
591    });
592  });
593}
594