'use strict'; import { Asset } from 'expo-asset'; import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; import { Platform } from 'react-native'; import { retryForStatus, waitFor } from './helpers'; export const name = 'Audio'; const mainTestingSource = require('../assets/LLizard.mp3'); const soundUri = 'http://www.noiseaddicts.com/samples_1w72b820/280.mp3'; const hlsStreamUri = 'http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8'; const hlsStreamUriWithRedirect = 'http://bit.ly/1iy90bn'; const redirectingSoundUri = 'http://bit.ly/2qBMx80'; const authenticatedStaticFilesBackend = 'https://authenticated-static-files.vercel.app'; export function test(t) { t.describe('Audio class', () => { t.describe('Audio.setAudioModeAsync', () => { // These tests should work according to the documentation, // but the implementation doesn't return anything from the Promise. // t.it('sets one set of the options', async () => { // const mode = { // playsInSilentModeIOS: true, // allowsRecordingIOS: true, // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS, // shouldDuckAndroid: true, // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS, // playThroughEarpieceAndroid: false, // }; // try { // const receivedMode = await Audio.setAudioModeAsync(mode); // t.expect(receivedMode).toBeDefined(); // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); // } catch (error) { // t.fail(error); // } // }); // t.it('sets another set of the options', async () => { // const mode = { // playsInSilentModeIOS: false, // allowsRecordingIOS: false, // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, // shouldDuckAndroid: false, // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX, // playThroughEarpieceAndroid: false, // }; // try { // const receivedMode = await Audio.setAudioModeAsync(mode); // t.expect(receivedMode).toBeDefined(); // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); // } catch (error) { // t.fail(error); // } // }); if (Platform.OS === 'ios') { t.it('rejects an invalid promise', async () => { const mode = { playsInSilentModeIOS: false, allowsRecordingIOS: true, interruptionModeIOS: InterruptionModeIOS.DoNotMix, shouldDuckAndroid: false, interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, playThroughEarpieceAndroid: false, staysActiveInBackground: false, }; let error = null; try { await Audio.setAudioModeAsync(mode); } catch (err) { error = err; } t.expect(error).not.toBeNull(); error && t.expect(error.message).toMatch('Impossible audio mode'); }); } }); }); t.describe('Audio instances', () => { let soundObject = null; t.beforeAll(async () => { await Audio.setIsEnabledAsync(true); }); t.beforeEach(() => { soundObject = new Audio.Sound(); }); t.afterEach(async () => { await soundObject.unloadAsync(); soundObject = null; }); t.describe('Audio.loadAsync', () => { t.it('loads the file with `require`', async () => { await soundObject.loadAsync(require('../assets/LLizard.mp3')); await retryForStatus(soundObject, { isLoaded: true }); }); t.it('loads the file from `Asset`', async () => { await soundObject.loadAsync(Asset.fromModule(require('../assets/LLizard.mp3'))); await retryForStatus(soundObject, { isLoaded: true }); }); t.it('loads the file from the Internet', async () => { await soundObject.loadAsync({ uri: soundUri }); await retryForStatus(soundObject, { isLoaded: true }); }); t.describe('cookie session', () => { t.afterEach(async () => { try { await fetch(`${authenticatedStaticFilesBackend}/sign_out`, { method: 'DELETE', credentials: true, }); } catch (error) { console.warn(`Could not sign out of cookie session test backend, error: ${error}.`); } }); t.it( 'is shared with fetch session', async () => { let error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, }); } catch (err) { error = err; } t.expect(error).toBeDefined(); if (Platform.OS === 'android') { t.expect(error.message).toMatch('Response code: 401'); } else { t.expect(error.message).toMatch('error code -1013'); } const signInResponse = await ( await fetch(`${authenticatedStaticFilesBackend}/sign_in`, { method: 'POST', credentials: true, }) ).text(); t.expect(signInResponse).toMatch('Signed in successfully!'); error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, }); } catch (err) { error = err; } t.expect(error).toBeNull(); }, 30000 ); }); t.it( 'supports adding custom headers to media request', async () => { let error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, }); } catch (err) { error = err; } if (!error) { throw new Error('Backend unexpectedly allowed unauthenticated request.'); } error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, headers: { authorization: 'mellon', }, }); } catch (err) { error = err; } t.expect(error).toBeNull(); }, 30000 ); if (Platform.OS === 'android') { t.it( 'supports adding custom headers to media request (MediaPlayer implementation)', async () => { let error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, androidImplementation: 'MediaPlayer', }); } catch (err) { error = err; } if (!error) { throw new Error('Backend unexpectedly allowed unauthenticated request.'); } error = null; try { await soundObject.loadAsync({ uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, androidImplementation: 'MediaPlayer', headers: { authorization: 'mellon', }, }); } catch (err) { error = err; } t.expect(error).toBeNull(); } ); } t.it('redirects from HTTPS URL to HTTPS URL (302)', async () => { // Redirects link shortened URL to GitHub raw audio MP3 URL for LLizard.mp3 asset. let error = null; try { await soundObject.loadAsync({ uri: 'https://rb.gy/eodxez', }); } catch (err) { error = err; } t.expect(error).toBeNull(); }); if (Platform.OS === 'android') { t.it( 'rejects the file from the Internet that redirects to non-standard content', async () => { let hasBeenRejected = false; try { await soundObject.loadAsync({ uri: hlsStreamUriWithRedirect, }); await retryForStatus(soundObject, { isLoaded: true }); } catch { hasBeenRejected = true; } t.expect(hasBeenRejected).toBe(true); } ); t.it( 'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided', async () => { let hasBeenRejected = false; try { await soundObject.loadAsync({ uri: hlsStreamUriWithRedirect, overrideFileExtensionAndroid: 'm3u8', }); await retryForStatus(soundObject, { isLoaded: true }); } catch { hasBeenRejected = true; } t.expect(hasBeenRejected).toBe(false); } ); } else { t.it( 'loads the file from the Internet that redirects to non-standard content', async () => { let hasBeenRejected = false; try { await soundObject.loadAsync({ uri: hlsStreamUriWithRedirect, }); await retryForStatus(soundObject, { isLoaded: true }); } catch { hasBeenRejected = true; } t.expect(hasBeenRejected).toBe(false); } ); } t.it('loads HLS stream', async () => { await soundObject.loadAsync({ uri: hlsStreamUri, }); await retryForStatus(soundObject, { isLoaded: true }); }); t.it('loads the file from the Internet (with redirecting URL)', async () => { await soundObject.loadAsync({ uri: redirectingSoundUri, }); await retryForStatus(soundObject, { isLoaded: true }); }); t.it('rejects if a file is already loaded', async () => { await soundObject.loadAsync({ uri: soundUri }); await retryForStatus(soundObject, { isLoaded: true }); let hasBeenRejected = false; try { await soundObject.loadAsync(mainTestingSource); } catch (error) { hasBeenRejected = true; error && t.expect(error.message).toMatch('already loaded'); } t.expect(hasBeenRejected).toBe(true); }); }); t.describe('Audio.loadAsync(require, initialStatus)', () => { t.it('sets an initial status', async () => { const options = { shouldPlay: true, isLooping: true, isMuted: false, volume: 0.5, audioPan: -0.5, rate: 1.5, }; await soundObject.loadAsync(mainTestingSource, options); await retryForStatus(soundObject, options); }); }); t.describe('Audio.setStatusAsync', () => { t.it('sets a status', async () => { const options = { shouldPlay: true, isLooping: true, isMuted: false, volume: 0.5, audioPan: 0.5, rate: 1.5, }; await soundObject.loadAsync(mainTestingSource, options); await retryForStatus(soundObject, options); }); }); t.describe('Audio.unloadAsync(require, initialStatus)', () => { t.it('unloads the object when it is loaded', async () => { await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { isLoaded: true }); await soundObject.unloadAsync(); await retryForStatus(soundObject, { isLoaded: false }); }); t.it("rejects if the object isn't loaded", async () => { let hasBeenRejected = false; try { await soundObject.unloadAsync(); } catch { hasBeenRejected = true; } t.expect(hasBeenRejected).toBe(false); }); }); /*t.describe('Audio.setOnPlaybackStatusUpdate', () => { t.it('sets callbacks that gets called when playing and stopping', async () => { const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { isLoaded: true }); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); await soundObject.stopAsync(); await retryForStatus(soundObject, { isPlaying: false }); t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false }); t .expect(onPlaybackStatusUpdate) .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true })); t .expect(onPlaybackStatusUpdate) .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true })); t .expect(onPlaybackStatusUpdate) .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false })); }); t.it( 'sets callbacks that gets called with didJustFinish when playback finishes', async () => { const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { isLoaded: true }); await retryForStatus(soundObject, { isBuffering: false }); const status = await soundObject.getStatusAsync(); await soundObject.setStatusAsync({ positionMillis: status.playableDurationMillis - 300, shouldPlay: true, }); await new Promise(resolve => { setTimeout(() => { t .expect(onPlaybackStatusUpdate) .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true })); resolve(); }, 1000); }); } ); });*/ t.describe('Audio.playAsync', () => { t.it('plays the sound', async () => { await soundObject.loadAsync(mainTestingSource); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); }); }); t.describe('Audio.replayAsync', () => { t.it('replays the sound', async () => { await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { isLoaded: true }); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); await waitFor(500); const statusBefore = await soundObject.getStatusAsync(); soundObject.replayAsync(); await retryForStatus(soundObject, { isPlaying: true }); const statusAfter = await soundObject.getStatusAsync(); t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis); }); /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => { const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); await soundObject.loadAsync(mainTestingSource); soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); await retryForStatus(soundObject, { isLoaded: true }); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); await soundObject.replayAsync(); t .expect(onPlaybackStatusUpdate) .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true })); });*/ }); t.describe('Audio.pauseAsync', () => { t.it('pauses the sound', async () => { await soundObject.loadAsync(mainTestingSource); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); await soundObject.pauseAsync(); await retryForStatus(soundObject, { isPlaying: false }); await soundObject.playAsync(); await retryForStatus(soundObject, { isPlaying: true }); }); }); t.describe('Audio.stopAsync', () => { t.it('stops the sound', async () => { await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); await retryForStatus(soundObject, { isPlaying: true }); await soundObject.stopAsync(); await retryForStatus(soundObject, { isPlaying: false }); }); }); t.describe('Audio.setPositionAsync', () => { t.it('sets the position', async () => { await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { positionMillis: 0 }); await soundObject.setPositionAsync(1000); await retryForStatus(soundObject, { positionMillis: 1000 }); }); }); t.describe('Audio.setPositionAsync', () => { t.it('sets the position', async () => { await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { positionMillis: 0 }); await soundObject.setPositionAsync(1000); await retryForStatus(soundObject, { positionMillis: 1000 }); }); t.it('sets the position with tolerance', async () => { await soundObject.loadAsync(mainTestingSource); await retryForStatus(soundObject, { positionMillis: 0 }); await soundObject.setPositionAsync(999, { toleranceMillisBefore: 0, toleranceMillisAfter: 0, }); await retryForStatus(soundObject, { positionMillis: 999 }); }); }); t.describe('Audio.setVolumeAsync', () => { t.beforeEach(async () => { await soundObject.loadAsync(mainTestingSource, { volume: 1 }); await retryForStatus(soundObject, { volume: 1 }); }); t.it('sets the volume', async () => { await soundObject.setVolumeAsync(0.5); await retryForStatus(soundObject, { volume: 0.5 }); }); t.it('sets the audio panning', async () => { await soundObject.setVolumeAsync(0.5, 1); await retryForStatus(soundObject, { volume: 0.5, audioPan: 1 }); }); const testVolumeFailure = (valueDescription, values) => t.it( `rejects if volume ${values.audioPan ? 'panning' : 'value'} is ${valueDescription}`, async () => { let hasBeenRejected = false; try { await soundObject.setVolumeAsync(values.volume, values.audioPan); } catch (error) { hasBeenRejected = true; error && t.expect(error.message).toMatch(/value .+ between/); } t.expect(hasBeenRejected).toBe(true); } ); testVolumeFailure('too big', { volume: 2 }); testVolumeFailure('negative', { volume: -0.5 }); testVolumeFailure('too small', { volume: 1, audioPan: -1.1 }); testVolumeFailure('too big', { volume: 1, audioPan: 1.1 }); }); t.describe('Audio.setIsMutedAsync', () => { t.it('sets whether the audio is muted', async () => { await soundObject.loadAsync(mainTestingSource, { isMuted: true }); await retryForStatus(soundObject, { isMuted: true }); await soundObject.setIsMutedAsync(false); await retryForStatus(soundObject, { isMuted: false }); }); }); t.describe('Audio.setIsLoopingAsync', () => { t.it('sets whether the audio is looped', async () => { await soundObject.loadAsync(mainTestingSource, { isLooping: false }); await retryForStatus(soundObject, { isLooping: false }); await soundObject.setIsLoopingAsync(true); await retryForStatus(soundObject, { isLooping: true }); }); }); /*t.describe('Audio.setProgressUpdateIntervalAsync', () => { t.it('sets update interval', async () => { const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); await retryForStatus(soundObject, { isPlaying: true }); soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); await soundObject.setProgressUpdateIntervalAsync(100); await new Promise(resolve => { setTimeout(() => { t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5); resolve(); }, 800); }); }); });*/ t.describe('Audio.setRateAsync', () => { let rate = 0; let shouldError = false; let shouldCorrectPitch = false; let pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; t.beforeEach(async () => { const rate = 0.9; const status = await soundObject.loadAsync(mainTestingSource, { rate }); t.expect(status.rate).toBeCloseTo(rate, 2); }); t.afterEach(async () => { let hasBeenRejected = false; try { const status = await soundObject.setRateAsync( rate, shouldCorrectPitch, pitchCorrectionQuality ); t.expect(status.rate).toBeCloseTo(rate, 2); t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch); t.expect(status.pitchCorrectionQuality).toBe(pitchCorrectionQuality); } catch { hasBeenRejected = true; } t.expect(hasBeenRejected).toEqual(shouldError); rate = 0; shouldError = false; shouldCorrectPitch = false; }); t.it('sets rate with shouldCorrectPitch = true', async () => { rate = 1.5; shouldCorrectPitch = true; }); t.it('sets rate with shouldCorrectPitch = false', async () => { rate = 0.75; shouldCorrectPitch = false; }); t.it('sets pitchCorrectionQuality to Low', async () => { rate = 0.5; shouldCorrectPitch = true; pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; }); t.it('sets pitchCorrectionQuality to Medium', async () => { pitchCorrectionQuality = Audio.PitchCorrectionQuality.Medium; }); t.it('sets pitchCorrectionQuality to High', async () => { pitchCorrectionQuality = Audio.PitchCorrectionQuality.High; }); t.it('rejects too high rate', async () => { rate = 40; shouldError = true; }); t.it('rejects negative rate', async () => { rate = -10; shouldError = true; }); }); }); }