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