xref: /expo/apps/test-suite/tests/Audio.js (revision 72b9cd04)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import { Audio, InterruptionModeAndroid, InterruptionModeIOS } 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: InterruptionModeIOS.DoNotMix,
65            shouldDuckAndroid: false,
66            interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
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.message).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.message).toMatch('Response code: 401');
141            } else {
142              t.expect(error.message).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 {
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 {
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 {
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.message).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          audioPan: -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          audioPan: 0.5,
342          rate: 1.5,
343        };
344        await soundObject.loadAsync(mainTestingSource, options);
345        await retryForStatus(soundObject, options);
346      });
347    });
348
349    t.describe('Audio.unloadAsync(require, initialStatus)', () => {
350      t.it('unloads the object when it is loaded', async () => {
351        await soundObject.loadAsync(mainTestingSource);
352        await retryForStatus(soundObject, { isLoaded: true });
353        await soundObject.unloadAsync();
354        await retryForStatus(soundObject, { isLoaded: false });
355      });
356
357      t.it("rejects if the object isn't loaded", async () => {
358        let hasBeenRejected = false;
359        try {
360          await soundObject.unloadAsync();
361        } catch {
362          hasBeenRejected = true;
363        }
364        t.expect(hasBeenRejected).toBe(false);
365      });
366    });
367
368    /*t.describe('Audio.setOnPlaybackStatusUpdate', () => {
369      t.it('sets callbacks that gets called when playing and stopping', async () => {
370        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
371        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
372        await soundObject.loadAsync(mainTestingSource);
373        await retryForStatus(soundObject, { isLoaded: true });
374        await soundObject.playAsync();
375        await retryForStatus(soundObject, { isPlaying: true });
376        await soundObject.stopAsync();
377        await retryForStatus(soundObject, { isPlaying: false });
378        t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false });
379        t
380          .expect(onPlaybackStatusUpdate)
381          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true }));
382        t
383          .expect(onPlaybackStatusUpdate)
384          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true }));
385        t
386          .expect(onPlaybackStatusUpdate)
387          .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false }));
388      });
389
390      t.it(
391        'sets callbacks that gets called with didJustFinish when playback finishes',
392        async () => {
393          const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
394          soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
395          await soundObject.loadAsync(mainTestingSource);
396          await retryForStatus(soundObject, { isLoaded: true });
397          await retryForStatus(soundObject, { isBuffering: false });
398          const status = await soundObject.getStatusAsync();
399          await soundObject.setStatusAsync({
400            positionMillis: status.playableDurationMillis - 300,
401            shouldPlay: true,
402          });
403          await new Promise(resolve => {
404            setTimeout(() => {
405              t
406                .expect(onPlaybackStatusUpdate)
407                .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true }));
408              resolve();
409            }, 1000);
410          });
411        }
412      );
413    });*/
414
415    t.describe('Audio.playAsync', () => {
416      t.it('plays the sound', async () => {
417        await soundObject.loadAsync(mainTestingSource);
418        await soundObject.playAsync();
419        await retryForStatus(soundObject, { isPlaying: true });
420      });
421    });
422
423    t.describe('Audio.replayAsync', () => {
424      t.it('replays the sound', async () => {
425        await soundObject.loadAsync(mainTestingSource);
426        await retryForStatus(soundObject, { isLoaded: true });
427        await soundObject.playAsync();
428        await retryForStatus(soundObject, { isPlaying: true });
429        await waitFor(500);
430        const statusBefore = await soundObject.getStatusAsync();
431        soundObject.replayAsync();
432        await retryForStatus(soundObject, { isPlaying: true });
433        const statusAfter = await soundObject.getStatusAsync();
434        t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis);
435      });
436
437      /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => {
438        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
439        await soundObject.loadAsync(mainTestingSource);
440        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
441        await retryForStatus(soundObject, { isLoaded: true });
442        await soundObject.playAsync();
443        await retryForStatus(soundObject, { isPlaying: true });
444        await soundObject.replayAsync();
445        t
446          .expect(onPlaybackStatusUpdate)
447          .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true }));
448      });*/
449    });
450
451    t.describe('Audio.pauseAsync', () => {
452      t.it('pauses the sound', async () => {
453        await soundObject.loadAsync(mainTestingSource);
454        await soundObject.playAsync();
455        await retryForStatus(soundObject, { isPlaying: true });
456        await soundObject.pauseAsync();
457        await retryForStatus(soundObject, { isPlaying: false });
458        await soundObject.playAsync();
459        await retryForStatus(soundObject, { isPlaying: true });
460      });
461    });
462
463    t.describe('Audio.stopAsync', () => {
464      t.it('stops the sound', async () => {
465        await soundObject.loadAsync(mainTestingSource, { shouldPlay: true });
466        await retryForStatus(soundObject, { isPlaying: true });
467        await soundObject.stopAsync();
468        await retryForStatus(soundObject, { isPlaying: false });
469      });
470    });
471
472    t.describe('Audio.setPositionAsync', () => {
473      t.it('sets the position', async () => {
474        await soundObject.loadAsync(mainTestingSource);
475        await retryForStatus(soundObject, { positionMillis: 0 });
476        await soundObject.setPositionAsync(1000);
477        await retryForStatus(soundObject, { positionMillis: 1000 });
478      });
479    });
480
481    t.describe('Audio.setPositionAsync', () => {
482      t.it('sets the position', async () => {
483        await soundObject.loadAsync(mainTestingSource);
484        await retryForStatus(soundObject, { positionMillis: 0 });
485        await soundObject.setPositionAsync(1000);
486        await retryForStatus(soundObject, { positionMillis: 1000 });
487      });
488
489      t.it('sets the position with tolerance', async () => {
490        await soundObject.loadAsync(mainTestingSource);
491        await retryForStatus(soundObject, { positionMillis: 0 });
492        await soundObject.setPositionAsync(999, {
493          toleranceMillisBefore: 0,
494          toleranceMillisAfter: 0,
495        });
496        await retryForStatus(soundObject, { positionMillis: 999 });
497      });
498    });
499
500    t.describe('Audio.setVolumeAsync', () => {
501      t.beforeEach(async () => {
502        await soundObject.loadAsync(mainTestingSource, { volume: 1 });
503        await retryForStatus(soundObject, { volume: 1 });
504      });
505
506      t.it('sets the volume', async () => {
507        await soundObject.setVolumeAsync(0.5);
508        await retryForStatus(soundObject, { volume: 0.5 });
509      });
510
511      t.it('sets the audio panning', async () => {
512        await soundObject.setVolumeAsync(0.5, 1);
513        await retryForStatus(soundObject, { volume: 0.5, audioPan: 1 });
514      });
515
516      const testVolumeFailure = (valueDescription, values) =>
517        t.it(
518          `rejects if volume ${values.audioPan ? 'panning' : 'value'} is ${valueDescription}`,
519          async () => {
520            let hasBeenRejected = false;
521            try {
522              await soundObject.setVolumeAsync(values.volume, values.audioPan);
523            } catch (error) {
524              hasBeenRejected = true;
525              error && t.expect(error.message).toMatch(/value .+ between/);
526            }
527            t.expect(hasBeenRejected).toBe(true);
528          }
529        );
530
531      testVolumeFailure('too big', { volume: 2 });
532      testVolumeFailure('negative', { volume: -0.5 });
533
534      testVolumeFailure('too small', { volume: 1, audioPan: -1.1 });
535      testVolumeFailure('too big', { volume: 1, audioPan: 1.1 });
536    });
537
538    t.describe('Audio.setIsMutedAsync', () => {
539      t.it('sets whether the audio is muted', async () => {
540        await soundObject.loadAsync(mainTestingSource, { isMuted: true });
541        await retryForStatus(soundObject, { isMuted: true });
542        await soundObject.setIsMutedAsync(false);
543        await retryForStatus(soundObject, { isMuted: false });
544      });
545    });
546
547    t.describe('Audio.setIsLoopingAsync', () => {
548      t.it('sets whether the audio is looped', async () => {
549        await soundObject.loadAsync(mainTestingSource, { isLooping: false });
550        await retryForStatus(soundObject, { isLooping: false });
551        await soundObject.setIsLoopingAsync(true);
552        await retryForStatus(soundObject, { isLooping: true });
553      });
554    });
555
556    /*t.describe('Audio.setProgressUpdateIntervalAsync', () => {
557      t.it('sets update interval', async () => {
558        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
559        await soundObject.loadAsync(mainTestingSource, { shouldPlay: true });
560        await retryForStatus(soundObject, { isPlaying: true });
561        soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
562        await soundObject.setProgressUpdateIntervalAsync(100);
563        await new Promise(resolve => {
564          setTimeout(() => {
565            t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5);
566            resolve();
567          }, 800);
568        });
569      });
570    });*/
571
572    t.describe('Audio.setRateAsync', () => {
573      let rate = 0;
574      let shouldError = false;
575      let shouldCorrectPitch = false;
576      let pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low;
577
578      t.beforeEach(async () => {
579        const rate = 0.9;
580
581        const status = await soundObject.loadAsync(mainTestingSource, { rate });
582        t.expect(status.rate).toBeCloseTo(rate, 2);
583      });
584
585      t.afterEach(async () => {
586        let hasBeenRejected = false;
587
588        try {
589          const status = await soundObject.setRateAsync(
590            rate,
591            shouldCorrectPitch,
592            pitchCorrectionQuality
593          );
594          t.expect(status.rate).toBeCloseTo(rate, 2);
595          t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch);
596          t.expect(status.pitchCorrectionQuality).toBe(pitchCorrectionQuality);
597        } catch {
598          hasBeenRejected = true;
599        }
600
601        t.expect(hasBeenRejected).toEqual(shouldError);
602
603        rate = 0;
604        shouldError = false;
605        shouldCorrectPitch = false;
606      });
607
608      t.it('sets rate with shouldCorrectPitch = true', async () => {
609        rate = 1.5;
610        shouldCorrectPitch = true;
611      });
612
613      t.it('sets rate with shouldCorrectPitch = false', async () => {
614        rate = 0.75;
615        shouldCorrectPitch = false;
616      });
617
618      t.it('sets pitchCorrectionQuality to Low', async () => {
619        rate = 0.5;
620        shouldCorrectPitch = true;
621        pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low;
622      });
623
624      t.it('sets pitchCorrectionQuality to Medium', async () => {
625        pitchCorrectionQuality = Audio.PitchCorrectionQuality.Medium;
626      });
627
628      t.it('sets pitchCorrectionQuality to High', async () => {
629        pitchCorrectionQuality = Audio.PitchCorrectionQuality.High;
630      });
631
632      t.it('rejects too high rate', async () => {
633        rate = 40;
634        shouldError = true;
635      });
636
637      t.it('rejects negative rate', async () => {
638        rate = -10;
639        shouldError = true;
640      });
641    });
642  });
643}
644