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