1'use strict'; 2 3import { Asset, Audio } from 'expo'; 4import { Platform } from 'react-native'; 5 6import { retryForStatus, waitFor } from './helpers'; 7 8export const name = 'Audio'; 9const mainTestingSource = require('../assets/LLizard.mp3'); 10const soundUri = 'http://www.noiseaddicts.com/samples_1w72b820/280.mp3'; 11const hlsStreamUri = 'http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8'; 12const hlsStreamUriWithRedirect = 'http://bit.ly/1iy90bn'; 13const redirectingSoundUri = 'http://bit.ly/2qBMx80'; 14const authenticatedStaticFilesBackend = 'https://authenticated-static-files-hagckpsbra.now.sh'; 15 16export function test(t) { 17 t.describe('Audio class', () => { 18 t.describe('Audio.setAudioModeAsync', () => { 19 // These tests should work according to the documentation, 20 // but the implementation doesn't return anything from the Promise. 21 22 // t.it('sets one set of the options', async () => { 23 // const mode = { 24 // playsInSilentModeIOS: true, 25 // allowsRecordingIOS: true, 26 // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS, 27 // shouldDuckAndroid: true, 28 // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS, 29 // playThroughEarpieceAndroid: false, 30 // }; 31 // try { 32 // const receivedMode = await Audio.setAudioModeAsync(mode); 33 // t.expect(receivedMode).toBeDefined(); 34 // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); 35 // } catch (error) { 36 // t.fail(error); 37 // } 38 // }); 39 40 // t.it('sets another set of the options', async () => { 41 // const mode = { 42 // playsInSilentModeIOS: false, 43 // allowsRecordingIOS: false, 44 // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, 45 // shouldDuckAndroid: false, 46 // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX, 47 // playThroughEarpieceAndroid: false, 48 // }; 49 // try { 50 // const receivedMode = await Audio.setAudioModeAsync(mode); 51 // t.expect(receivedMode).toBeDefined(); 52 // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); 53 // } catch (error) { 54 // t.fail(error); 55 // } 56 // }); 57 58 if (Platform.OS === 'ios') { 59 t.it('rejects an invalid promise', async () => { 60 const mode = { 61 playsInSilentModeIOS: false, 62 allowsRecordingIOS: true, 63 interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, 64 shouldDuckAndroid: false, 65 interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX, 66 playThroughEarpieceAndroid: false, 67 }; 68 let error = null; 69 try { 70 await Audio.setAudioModeAsync(mode); 71 } catch (err) { 72 error = err; 73 } 74 t.expect(error).not.toBeNull(); 75 error && t.expect(error.toString()).toMatch('Impossible audio mode'); 76 }); 77 } 78 }); 79 }); 80 81 t.describe('Audio instances', () => { 82 let soundObject = null; 83 84 t.beforeAll(async () => { 85 await Audio.setIsEnabledAsync(true); 86 }); 87 88 t.beforeEach(() => { 89 soundObject = new Audio.Sound(); 90 }); 91 92 t.afterEach(async () => { 93 await soundObject.unloadAsync(); 94 soundObject = null; 95 }); 96 97 t.describe('Audio.loadAsync', () => { 98 t.it('loads the file with `require`', async () => { 99 await soundObject.loadAsync(require('../assets/LLizard.mp3')); 100 await retryForStatus(soundObject, { isLoaded: true }); 101 }); 102 103 t.it('loads the file from `Asset`', async () => { 104 await soundObject.loadAsync(Asset.fromModule(require('../assets/LLizard.mp3'))); 105 await retryForStatus(soundObject, { isLoaded: true }); 106 }); 107 108 t.it('loads the file from the Internet', async () => { 109 await soundObject.loadAsync({ uri: soundUri }); 110 await retryForStatus(soundObject, { isLoaded: true }); 111 }); 112 113 t.describe('cookie session', () => { 114 t.afterEach(async () => { 115 try { 116 await fetch(`${authenticatedStaticFilesBackend}/sign_out`, { 117 method: 'DELETE', 118 credentials: true, 119 }); 120 } catch (error) { 121 console.warn(`Could not sign out of cookie session test backend, error: ${error}.`); 122 } 123 }); 124 125 t.it( 126 'is shared with fetch session', 127 async () => { 128 let error = null; 129 try { 130 await soundObject.loadAsync({ 131 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 132 }); 133 } catch (err) { 134 error = err; 135 } 136 t.expect(error).toBeDefined(); 137 if (Platform.OS === 'android') { 138 t.expect(error.toString()).toMatch('Response code: 401'); 139 } else { 140 t.expect(error.toString()).toMatch('error code -1013'); 141 } 142 const signInResponse = await (await fetch( 143 `${authenticatedStaticFilesBackend}/sign_in`, 144 { 145 method: 'POST', 146 credentials: true, 147 } 148 )).text(); 149 t.expect(signInResponse).toMatch('Signed in successfully!'); 150 error = null; 151 try { 152 await soundObject.loadAsync({ 153 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 154 }); 155 } catch (err) { 156 error = err; 157 } 158 t.expect(error).toBeNull(); 159 }, 160 30000 161 ); 162 }); 163 164 t.it( 165 'supports adding custom headers to media request', 166 async () => { 167 let error = null; 168 try { 169 await soundObject.loadAsync({ 170 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 171 }); 172 } catch (err) { 173 error = err; 174 } 175 if (!error) { 176 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 177 } 178 error = null; 179 try { 180 await soundObject.loadAsync({ 181 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 182 headers: { 183 authorization: 'mellon', 184 }, 185 }); 186 } catch (err) { 187 error = err; 188 } 189 t.expect(error).toBeNull(); 190 }, 191 30000 192 ); 193 194 if (Platform.OS === 'android') { 195 t.it( 196 'supports adding custom headers to media request (MediaPlayer implementation)', 197 async () => { 198 let error = null; 199 try { 200 await soundObject.loadAsync({ 201 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 202 androidImplementation: 'MediaPlayer', 203 }); 204 } catch (err) { 205 error = err; 206 } 207 if (!error) { 208 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 209 } 210 error = null; 211 try { 212 await soundObject.loadAsync({ 213 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 214 androidImplementation: 'MediaPlayer', 215 headers: { 216 authorization: 'mellon', 217 }, 218 }); 219 } catch (err) { 220 error = err; 221 } 222 t.expect(error).toBeNull(); 223 } 224 ); 225 } 226 227 t.it('redirects from HTTPS URL to HTTPS URL (301)', async () => { 228 let error = null; 229 try { 230 await soundObject.loadAsync({ 231 uri: 232 'https://player.vimeo.com/external/181375362.sd.mp4?s=cf573e9cf7d747f4eaf7e57eeec88e9b22e3933f&profile_id=165', 233 }); 234 } catch (err) { 235 error = err; 236 } 237 t.expect(error).toBeNull(); 238 }); 239 240 if (Platform.OS === 'android') { 241 t.it( 242 'rejects the file from the Internet that redirects to non-standard content', 243 async () => { 244 let hasBeenRejected = false; 245 try { 246 await soundObject.loadAsync({ 247 uri: hlsStreamUriWithRedirect, 248 }); 249 await retryForStatus(soundObject, { isLoaded: true }); 250 } catch (error) { 251 hasBeenRejected = true; 252 } 253 t.expect(hasBeenRejected).toBe(true); 254 } 255 ); 256 t.it( 257 'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided', 258 async () => { 259 let hasBeenRejected = false; 260 try { 261 await soundObject.loadAsync({ 262 uri: hlsStreamUriWithRedirect, 263 overrideFileExtensionAndroid: 'm3u8', 264 }); 265 await retryForStatus(soundObject, { isLoaded: true }); 266 } catch (error) { 267 hasBeenRejected = true; 268 } 269 t.expect(hasBeenRejected).toBe(false); 270 } 271 ); 272 } else { 273 t.it( 274 'loads the file from the Internet that redirects to non-standard content', 275 async () => { 276 let hasBeenRejected = false; 277 try { 278 await soundObject.loadAsync({ 279 uri: hlsStreamUriWithRedirect, 280 }); 281 await retryForStatus(soundObject, { isLoaded: true }); 282 } catch (error) { 283 hasBeenRejected = true; 284 } 285 t.expect(hasBeenRejected).toBe(false); 286 } 287 ); 288 } 289 290 t.it('loads HLS stream', async () => { 291 await soundObject.loadAsync({ 292 uri: hlsStreamUri, 293 }); 294 await retryForStatus(soundObject, { isLoaded: true }); 295 }); 296 297 t.it('loads the file from the Internet (with redirecting URL)', async () => { 298 await soundObject.loadAsync({ 299 uri: redirectingSoundUri, 300 }); 301 await retryForStatus(soundObject, { isLoaded: true }); 302 }); 303 304 t.it('rejects if a file is already loaded', async () => { 305 await soundObject.loadAsync({ uri: soundUri }); 306 await retryForStatus(soundObject, { isLoaded: true }); 307 let hasBeenRejected = false; 308 try { 309 await soundObject.loadAsync(mainTestingSource); 310 } catch (error) { 311 hasBeenRejected = true; 312 error && t.expect(error.toString()).toMatch('already loaded'); 313 } 314 t.expect(hasBeenRejected).toBe(true); 315 }); 316 }); 317 318 t.describe('Audio.loadAsync(require, initialStatus)', () => { 319 t.it('sets an initial status', async () => { 320 const options = { 321 shouldPlay: true, 322 isLooping: true, 323 isMuted: false, 324 volume: 0.5, 325 rate: 1.5, 326 }; 327 await soundObject.loadAsync(mainTestingSource, options); 328 await retryForStatus(soundObject, options); 329 }); 330 }); 331 332 t.describe('Audio.setStatusAsync', () => { 333 t.it('sets a status', async () => { 334 const options = { 335 shouldPlay: true, 336 isLooping: true, 337 isMuted: false, 338 volume: 0.5, 339 rate: 1.5, 340 }; 341 await soundObject.loadAsync(mainTestingSource, options); 342 await retryForStatus(soundObject, options); 343 }); 344 }); 345 346 t.describe('Audio.unloadAsync(require, initialStatus)', () => { 347 t.it('unloads the object when it is loaded', async () => { 348 await soundObject.loadAsync(mainTestingSource); 349 await retryForStatus(soundObject, { isLoaded: true }); 350 await soundObject.unloadAsync(); 351 await retryForStatus(soundObject, { isLoaded: false }); 352 }); 353 354 t.it("rejects if the object isn't loaded", async () => { 355 let hasBeenRejected = false; 356 try { 357 await soundObject.unloadAsync(); 358 } catch (error) { 359 hasBeenRejected = true; 360 } 361 t.expect(hasBeenRejected).toBe(false); 362 }); 363 }); 364 365 /*t.describe('Audio.setOnPlaybackStatusUpdate', () => { 366 t.it('sets callbacks that gets called when playing and stopping', async () => { 367 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 368 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 369 await soundObject.loadAsync(mainTestingSource); 370 await retryForStatus(soundObject, { isLoaded: true }); 371 await soundObject.playAsync(); 372 await retryForStatus(soundObject, { isPlaying: true }); 373 await soundObject.stopAsync(); 374 await retryForStatus(soundObject, { isPlaying: false }); 375 t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false }); 376 t 377 .expect(onPlaybackStatusUpdate) 378 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true })); 379 t 380 .expect(onPlaybackStatusUpdate) 381 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true })); 382 t 383 .expect(onPlaybackStatusUpdate) 384 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false })); 385 }); 386 387 t.it( 388 'sets callbacks that gets called with didJustFinish when playback finishes', 389 async () => { 390 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 391 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 392 await soundObject.loadAsync(mainTestingSource); 393 await retryForStatus(soundObject, { isLoaded: true }); 394 await retryForStatus(soundObject, { isBuffering: false }); 395 const status = await soundObject.getStatusAsync(); 396 await soundObject.setStatusAsync({ 397 positionMillis: status.playableDurationMillis - 300, 398 shouldPlay: true, 399 }); 400 await new Promise(resolve => { 401 setTimeout(() => { 402 t 403 .expect(onPlaybackStatusUpdate) 404 .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true })); 405 resolve(); 406 }, 1000); 407 }); 408 } 409 ); 410 });*/ 411 412 t.describe('Audio.playAsync', () => { 413 t.it('plays the sound', async () => { 414 await soundObject.loadAsync(mainTestingSource); 415 await soundObject.playAsync(); 416 await retryForStatus(soundObject, { isPlaying: true }); 417 }); 418 }); 419 420 t.describe('Audio.replayAsync', () => { 421 t.it('replays the sound', async () => { 422 await soundObject.loadAsync(mainTestingSource); 423 await retryForStatus(soundObject, { isLoaded: true }); 424 await soundObject.playAsync(); 425 await retryForStatus(soundObject, { isPlaying: true }); 426 await waitFor(500); 427 const statusBefore = await soundObject.getStatusAsync(); 428 soundObject.replayAsync(); 429 await retryForStatus(soundObject, { isPlaying: true }); 430 const statusAfter = await soundObject.getStatusAsync(); 431 t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis); 432 }); 433 434 /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => { 435 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 436 await soundObject.loadAsync(mainTestingSource); 437 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 438 await retryForStatus(soundObject, { isLoaded: true }); 439 await soundObject.playAsync(); 440 await retryForStatus(soundObject, { isPlaying: true }); 441 await soundObject.replayAsync(); 442 t 443 .expect(onPlaybackStatusUpdate) 444 .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true })); 445 });*/ 446 }); 447 448 t.describe('Audio.pauseAsync', () => { 449 t.it('pauses the sound', async () => { 450 await soundObject.loadAsync(mainTestingSource); 451 await soundObject.playAsync(); 452 await retryForStatus(soundObject, { isPlaying: true }); 453 await soundObject.pauseAsync(); 454 await retryForStatus(soundObject, { isPlaying: false }); 455 await soundObject.playAsync(); 456 await retryForStatus(soundObject, { isPlaying: true }); 457 }); 458 }); 459 460 t.describe('Audio.stopAsync', () => { 461 t.it('stops the sound', async () => { 462 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 463 await retryForStatus(soundObject, { isPlaying: true }); 464 await soundObject.stopAsync(); 465 await retryForStatus(soundObject, { isPlaying: false }); 466 }); 467 }); 468 469 t.describe('Audio.setPositionAsync', () => { 470 t.it('sets the position', async () => { 471 await soundObject.loadAsync(mainTestingSource); 472 await retryForStatus(soundObject, { positionMillis: 0 }); 473 await soundObject.setPositionAsync(1000); 474 await retryForStatus(soundObject, { positionMillis: 1000 }); 475 }); 476 }); 477 478 t.describe('Audio.setPositionAsync', () => { 479 t.it('sets the position', async () => { 480 await soundObject.loadAsync(mainTestingSource); 481 await retryForStatus(soundObject, { positionMillis: 0 }); 482 await soundObject.setPositionAsync(1000); 483 await retryForStatus(soundObject, { positionMillis: 1000 }); 484 }); 485 486 t.it('sets the position with tolerance', async () => { 487 await soundObject.loadAsync(mainTestingSource); 488 await retryForStatus(soundObject, { positionMillis: 0 }); 489 await soundObject.setPositionAsync(999, { 490 toleranceMillisBefore: 0, 491 toleranceMillisAfter: 0, 492 }); 493 await retryForStatus(soundObject, { positionMillis: 999 }); 494 }); 495 }); 496 497 t.describe('Audio.setVolumeAsync', () => { 498 t.beforeEach(async () => { 499 await soundObject.loadAsync(mainTestingSource, { volume: 1 }); 500 await retryForStatus(soundObject, { volume: 1 }); 501 }); 502 503 t.it('sets the volume', async () => { 504 await soundObject.setVolumeAsync(0.5); 505 await retryForStatus(soundObject, { volume: 0.5 }); 506 }); 507 508 const testVolumeFailure = (valueDescription, value) => 509 t.it(`rejects if volume value is ${valueDescription}`, async () => { 510 let hasBeenRejected = false; 511 try { 512 await soundObject.setVolumeAsync(value); 513 } catch (error) { 514 hasBeenRejected = true; 515 error && t.expect(error.toString()).toMatch(/value .+ between/); 516 } 517 t.expect(hasBeenRejected).toBe(true); 518 }); 519 520 testVolumeFailure('too big', 2); 521 testVolumeFailure('negative', -0.5); 522 }); 523 524 t.describe('Audio.setIsMutedAsync', () => { 525 t.it('sets whether the audio is muted', async () => { 526 await soundObject.loadAsync(mainTestingSource, { isMuted: true }); 527 await retryForStatus(soundObject, { isMuted: true }); 528 await soundObject.setIsMutedAsync(false); 529 await retryForStatus(soundObject, { isMuted: false }); 530 }); 531 }); 532 533 t.describe('Audio.setIsLoopingAsync', () => { 534 t.it('sets whether the audio is looped', async () => { 535 await soundObject.loadAsync(mainTestingSource, { isLooping: false }); 536 await retryForStatus(soundObject, { isLooping: false }); 537 await soundObject.setIsLoopingAsync(true); 538 await retryForStatus(soundObject, { isLooping: true }); 539 }); 540 }); 541 542 /*t.describe('Audio.setProgressUpdateIntervalAsync', () => { 543 t.it('sets update interval', async () => { 544 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 545 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 546 await retryForStatus(soundObject, { isPlaying: true }); 547 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 548 await soundObject.setProgressUpdateIntervalAsync(100); 549 await new Promise(resolve => { 550 setTimeout(() => { 551 t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5); 552 resolve(); 553 }, 800); 554 }); 555 }); 556 });*/ 557 558 t.describe('Audio.setRateAsync', () => { 559 let rate = 0; 560 let shouldError = false; 561 let shouldCorrectPitch = false; 562 563 t.beforeEach(async () => { 564 const rate = 0.9; 565 566 const status = await soundObject.loadAsync(mainTestingSource, { rate }); 567 t.expect(status.rate).toBeCloseTo(rate, 2); 568 }); 569 570 t.afterEach(async () => { 571 let hasBeenRejected = false; 572 573 try { 574 const status = await soundObject.setRateAsync(rate, shouldCorrectPitch); 575 t.expect(status.rate).toBeCloseTo(rate, 2); 576 t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch); 577 } catch (error) { 578 hasBeenRejected = true; 579 } 580 581 t.expect(hasBeenRejected).toEqual(shouldError); 582 583 rate = 0; 584 shouldError = false; 585 shouldCorrectPitch = false; 586 }); 587 588 t.it('sets rate with shouldCorrectPitch = true', async () => { 589 rate = 1.5; 590 shouldCorrectPitch = true; 591 }); 592 593 t.it('sets rate with shouldCorrectPitch = false', async () => { 594 rate = 0.75; 595 shouldCorrectPitch = false; 596 }); 597 598 t.it('rejects too high rate', async () => { 599 rate = 40; 600 shouldError = true; 601 }); 602 603 t.it('rejects negative rate', async () => { 604 rate = -10; 605 shouldError = true; 606 }); 607 }); 608 }); 609} 610