xref: /expo/apps/test-suite/tests/Audio.js (revision f10e66fc)
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