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