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