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