xref: /expo/apps/test-suite/tests/Audio.js (revision bb8f4f99)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import { Audio } from 'expo-av';
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.vercel.app';
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 (
145              await fetch(`${authenticatedStaticFilesBackend}/sign_in`, {
146                method: 'POST',
147                credentials: true,
148              })
149            ).text();
150            t.expect(signInResponse).toMatch('Signed in successfully!');
151            error = null;
152            try {
153              await soundObject.loadAsync({
154                uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`,
155              });
156            } catch (err) {
157              error = err;
158            }
159            t.expect(error).toBeNull();
160          },
161          30000
162        );
163      });
164
165      t.it(
166        'supports adding custom headers to media request',
167        async () => {
168          let error = null;
169          try {
170            await soundObject.loadAsync({
171              uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`,
172            });
173          } catch (err) {
174            error = err;
175          }
176          if (!error) {
177            throw new Error('Backend unexpectedly allowed unauthenticated request.');
178          }
179          error = null;
180          try {
181            await soundObject.loadAsync({
182              uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`,
183              headers: {
184                authorization: 'mellon',
185              },
186            });
187          } catch (err) {
188            error = err;
189          }
190          t.expect(error).toBeNull();
191        },
192        30000
193      );
194
195      if (Platform.OS === 'android') {
196        t.it(
197          'supports adding custom headers to media request (MediaPlayer implementation)',
198          async () => {
199            let error = null;
200            try {
201              await soundObject.loadAsync({
202                uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`,
203                androidImplementation: 'MediaPlayer',
204              });
205            } catch (err) {
206              error = err;
207            }
208            if (!error) {
209              throw new Error('Backend unexpectedly allowed unauthenticated request.');
210            }
211            error = null;
212            try {
213              await soundObject.loadAsync({
214                uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`,
215                androidImplementation: 'MediaPlayer',
216                headers: {
217                  authorization: 'mellon',
218                },
219              });
220            } catch (err) {
221              error = err;
222            }
223            t.expect(error).toBeNull();
224          }
225        );
226      }
227
228      t.it('redirects from HTTPS URL to HTTPS URL (302)', async () => {
229        // Redirects link shortened URL to GitHub raw audio MP3 URL for LLizard.mp3 asset.
230        let error = null;
231        try {
232          await soundObject.loadAsync({
233            uri: 'https://rb.gy/eodxez',
234          });
235        } catch (err) {
236          error = err;
237        }
238        t.expect(error).toBeNull();
239      });
240
241      if (Platform.OS === 'android') {
242        t.it(
243          'rejects the file from the Internet that redirects to non-standard content',
244          async () => {
245            let hasBeenRejected = false;
246            try {
247              await soundObject.loadAsync({
248                uri: hlsStreamUriWithRedirect,
249              });
250              await retryForStatus(soundObject, { isLoaded: true });
251            } catch (error) {
252              hasBeenRejected = true;
253            }
254            t.expect(hasBeenRejected).toBe(true);
255          }
256        );
257        t.it(
258          'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided',
259          async () => {
260            let hasBeenRejected = false;
261            try {
262              await soundObject.loadAsync({
263                uri: hlsStreamUriWithRedirect,
264                overrideFileExtensionAndroid: 'm3u8',
265              });
266              await retryForStatus(soundObject, { isLoaded: true });
267            } catch (error) {
268              hasBeenRejected = true;
269            }
270            t.expect(hasBeenRejected).toBe(false);
271          }
272        );
273      } else {
274        t.it(
275          'loads the file from the Internet that redirects to non-standard content',
276          async () => {
277            let hasBeenRejected = false;
278            try {
279              await soundObject.loadAsync({
280                uri: hlsStreamUriWithRedirect,
281              });
282              await retryForStatus(soundObject, { isLoaded: true });
283            } catch (error) {
284              hasBeenRejected = true;
285            }
286            t.expect(hasBeenRejected).toBe(false);
287          }
288        );
289      }
290
291      t.it('loads HLS stream', async () => {
292        await soundObject.loadAsync({
293          uri: hlsStreamUri,
294        });
295        await retryForStatus(soundObject, { isLoaded: true });
296      });
297
298      t.it('loads the file from the Internet (with redirecting URL)', async () => {
299        await soundObject.loadAsync({
300          uri: redirectingSoundUri,
301        });
302        await retryForStatus(soundObject, { isLoaded: true });
303      });
304
305      t.it('rejects if a file is already loaded', async () => {
306        await soundObject.loadAsync({ uri: soundUri });
307        await retryForStatus(soundObject, { isLoaded: true });
308        let hasBeenRejected = false;
309        try {
310          await soundObject.loadAsync(mainTestingSource);
311        } catch (error) {
312          hasBeenRejected = true;
313          error && t.expect(error.toString()).toMatch('already loaded');
314        }
315        t.expect(hasBeenRejected).toBe(true);
316      });
317    });
318
319    t.describe('Audio.loadAsync(require, initialStatus)', () => {
320      t.it('sets an initial status', async () => {
321        const options = {
322          shouldPlay: true,
323          isLooping: true,
324          isMuted: false,
325          volume: 0.5,
326          rate: 1.5,
327        };
328        await soundObject.loadAsync(mainTestingSource, options);
329        await retryForStatus(soundObject, options);
330      });
331    });
332
333    t.describe('Audio.setStatusAsync', () => {
334      t.it('sets a status', async () => {
335        const options = {
336          shouldPlay: true,
337          isLooping: true,
338          isMuted: false,
339          volume: 0.5,
340          rate: 1.5,
341        };
342        await soundObject.loadAsync(mainTestingSource, options);
343        await retryForStatus(soundObject, options);
344      });
345    });
346
347    t.describe('Audio.unloadAsync(require, initialStatus)', () => {
348      t.it('unloads the object when it is loaded', async () => {
349        await soundObject.loadAsync(mainTestingSource);
350        await retryForStatus(soundObject, { isLoaded: true });
351        await soundObject.unloadAsync();
352        await retryForStatus(soundObject, { isLoaded: false });
353      });
354
355      t.it("rejects if the object isn't loaded", async () => {
356        let hasBeenRejected = false;
357        try {
358          await soundObject.unloadAsync();
359        } catch (error) {
360          hasBeenRejected = true;
361        }
362        t.expect(hasBeenRejected).toBe(false);
363      });
364    });
365
366    /*t.describe('Audio.setOnPlaybackStatusUpdate', () => {
367      t.it('sets callbacks that gets called when playing and stopping', async () => {
368        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
369        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
370        await soundObject.loadAsync(mainTestingSource);
371        await retryForStatus(soundObject, { isLoaded: true });
372        await soundObject.playAsync();
373        await retryForStatus(soundObject, { isPlaying: true });
374        await soundObject.stopAsync();
375        await retryForStatus(soundObject, { isPlaying: false });
376        t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false });
377        t
378          .expect(onPlaybackStatusUpdate)
379          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true }));
380        t
381          .expect(onPlaybackStatusUpdate)
382          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true }));
383        t
384          .expect(onPlaybackStatusUpdate)
385          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false }));
386      });
387
388      t.it(
389        'sets callbacks that gets called with didJustFinish when playback finishes',
390        async () => {
391          const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
392          soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
393          await soundObject.loadAsync(mainTestingSource);
394          await retryForStatus(soundObject, { isLoaded: true });
395          await retryForStatus(soundObject, { isBuffering: false });
396          const status = await soundObject.getStatusAsync();
397          await soundObject.setStatusAsync({
398            positionMillis: status.playableDurationMillis - 300,
399            shouldPlay: true,
400          });
401          await new Promise(resolve => {
402            setTimeout(() => {
403              t
404                .expect(onPlaybackStatusUpdate)
405                .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true }));
406              resolve();
407            }, 1000);
408          });
409        }
410      );
411    });*/
412
413    t.describe('Audio.playAsync', () => {
414      t.it('plays the sound', async () => {
415        await soundObject.loadAsync(mainTestingSource);
416        await soundObject.playAsync();
417        await retryForStatus(soundObject, { isPlaying: true });
418      });
419    });
420
421    t.describe('Audio.replayAsync', () => {
422      t.it('replays the sound', async () => {
423        await soundObject.loadAsync(mainTestingSource);
424        await retryForStatus(soundObject, { isLoaded: true });
425        await soundObject.playAsync();
426        await retryForStatus(soundObject, { isPlaying: true });
427        await waitFor(500);
428        const statusBefore = await soundObject.getStatusAsync();
429        soundObject.replayAsync();
430        await retryForStatus(soundObject, { isPlaying: true });
431        const statusAfter = await soundObject.getStatusAsync();
432        t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis);
433      });
434
435      /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => {
436        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
437        await soundObject.loadAsync(mainTestingSource);
438        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
439        await retryForStatus(soundObject, { isLoaded: true });
440        await soundObject.playAsync();
441        await retryForStatus(soundObject, { isPlaying: true });
442        await soundObject.replayAsync();
443        t
444          .expect(onPlaybackStatusUpdate)
445          .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true }));
446      });*/
447    });
448
449    t.describe('Audio.pauseAsync', () => {
450      t.it('pauses the sound', async () => {
451        await soundObject.loadAsync(mainTestingSource);
452        await soundObject.playAsync();
453        await retryForStatus(soundObject, { isPlaying: true });
454        await soundObject.pauseAsync();
455        await retryForStatus(soundObject, { isPlaying: false });
456        await soundObject.playAsync();
457        await retryForStatus(soundObject, { isPlaying: true });
458      });
459    });
460
461    t.describe('Audio.stopAsync', () => {
462      t.it('stops the sound', async () => {
463        await soundObject.loadAsync(mainTestingSource, { shouldPlay: true });
464        await retryForStatus(soundObject, { isPlaying: true });
465        await soundObject.stopAsync();
466        await retryForStatus(soundObject, { isPlaying: false });
467      });
468    });
469
470    t.describe('Audio.setPositionAsync', () => {
471      t.it('sets the position', async () => {
472        await soundObject.loadAsync(mainTestingSource);
473        await retryForStatus(soundObject, { positionMillis: 0 });
474        await soundObject.setPositionAsync(1000);
475        await retryForStatus(soundObject, { positionMillis: 1000 });
476      });
477    });
478
479    t.describe('Audio.setPositionAsync', () => {
480      t.it('sets the position', async () => {
481        await soundObject.loadAsync(mainTestingSource);
482        await retryForStatus(soundObject, { positionMillis: 0 });
483        await soundObject.setPositionAsync(1000);
484        await retryForStatus(soundObject, { positionMillis: 1000 });
485      });
486
487      t.it('sets the position with tolerance', async () => {
488        await soundObject.loadAsync(mainTestingSource);
489        await retryForStatus(soundObject, { positionMillis: 0 });
490        await soundObject.setPositionAsync(999, {
491          toleranceMillisBefore: 0,
492          toleranceMillisAfter: 0,
493        });
494        await retryForStatus(soundObject, { positionMillis: 999 });
495      });
496    });
497
498    t.describe('Audio.setVolumeAsync', () => {
499      t.beforeEach(async () => {
500        await soundObject.loadAsync(mainTestingSource, { volume: 1 });
501        await retryForStatus(soundObject, { volume: 1 });
502      });
503
504      t.it('sets the volume', async () => {
505        await soundObject.setVolumeAsync(0.5);
506        await retryForStatus(soundObject, { volume: 0.5 });
507      });
508
509      const testVolumeFailure = (valueDescription, value) =>
510        t.it(`rejects if volume value is ${valueDescription}`, async () => {
511          let hasBeenRejected = false;
512          try {
513            await soundObject.setVolumeAsync(value);
514          } catch (error) {
515            hasBeenRejected = true;
516            error && t.expect(error.toString()).toMatch(/value .+ between/);
517          }
518          t.expect(hasBeenRejected).toBe(true);
519        });
520
521      testVolumeFailure('too big', 2);
522      testVolumeFailure('negative', -0.5);
523    });
524
525    t.describe('Audio.setIsMutedAsync', () => {
526      t.it('sets whether the audio is muted', async () => {
527        await soundObject.loadAsync(mainTestingSource, { isMuted: true });
528        await retryForStatus(soundObject, { isMuted: true });
529        await soundObject.setIsMutedAsync(false);
530        await retryForStatus(soundObject, { isMuted: false });
531      });
532    });
533
534    t.describe('Audio.setIsLoopingAsync', () => {
535      t.it('sets whether the audio is looped', async () => {
536        await soundObject.loadAsync(mainTestingSource, { isLooping: false });
537        await retryForStatus(soundObject, { isLooping: false });
538        await soundObject.setIsLoopingAsync(true);
539        await retryForStatus(soundObject, { isLooping: true });
540      });
541    });
542
543    /*t.describe('Audio.setProgressUpdateIntervalAsync', () => {
544      t.it('sets update interval', async () => {
545        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
546        await soundObject.loadAsync(mainTestingSource, { shouldPlay: true });
547        await retryForStatus(soundObject, { isPlaying: true });
548        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
549        await soundObject.setProgressUpdateIntervalAsync(100);
550        await new Promise(resolve => {
551          setTimeout(() => {
552            t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5);
553            resolve();
554          }, 800);
555        });
556      });
557    });*/
558
559    t.describe('Audio.setRateAsync', () => {
560      let rate = 0;
561      let shouldError = false;
562      let shouldCorrectPitch = false;
563      let pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low;
564
565      t.beforeEach(async () => {
566        const rate = 0.9;
567
568        const status = await soundObject.loadAsync(mainTestingSource, { rate });
569        t.expect(status.rate).toBeCloseTo(rate, 2);
570      });
571
572      t.afterEach(async () => {
573        let hasBeenRejected = false;
574
575        try {
576          const status = await soundObject.setRateAsync(
577            rate,
578            shouldCorrectPitch,
579            pitchCorrectionQuality
580          );
581          t.expect(status.rate).toBeCloseTo(rate, 2);
582          t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch);
583          t.expect(status.pitchCorrectionQuality).toBe(pitchCorrectionQuality);
584        } catch (error) {
585          hasBeenRejected = true;
586        }
587
588        t.expect(hasBeenRejected).toEqual(shouldError);
589
590        rate = 0;
591        shouldError = false;
592        shouldCorrectPitch = false;
593      });
594
595      t.it('sets rate with shouldCorrectPitch = true', async () => {
596        rate = 1.5;
597        shouldCorrectPitch = true;
598      });
599
600      t.it('sets rate with shouldCorrectPitch = false', async () => {
601        rate = 0.75;
602        shouldCorrectPitch = false;
603      });
604
605      t.it('sets pitchCorrectionQuality to Low', async () => {
606        rate = 0.5;
607        shouldCorrectPitch = true;
608        pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low;
609      });
610
611      t.it('sets pitchCorrectionQuality to Medium', async () => {
612        pitchCorrectionQuality = Audio.PitchCorrectionQuality.Medium;
613      });
614
615      t.it('sets pitchCorrectionQuality to High', async () => {
616        pitchCorrectionQuality = Audio.PitchCorrectionQuality.High;
617      });
618
619      t.it('rejects too high rate', async () => {
620        rate = 40;
621        shouldError = true;
622      });
623
624      t.it('rejects negative rate', async () => {
625        rate = -10;
626        shouldError = true;
627      });
628    });
629  });
630}
631