xref: /expo/apps/test-suite/tests/Video.js (revision 30572fa4)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import { Video, VideoFullscreenUpdate } from 'expo-av';
5import { forEach } from 'lodash';
6import React from 'react';
7import { Platform } from 'react-native';
8
9import { waitFor, retryForStatus, mountAndWaitFor as originalMountAndWaitFor } from './helpers';
10
11export const name = 'Video';
12const imageRemoteSource = { uri: 'http://via.placeholder.com/350x150' };
13const videoRemoteSource = { uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4' };
14const redirectingVideoRemoteSource = { uri: 'http://bit.ly/2mcW40Q' };
15const mp4Source = require('../assets/big_buck_bunny.mp4');
16const hlsStreamUri = 'http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8';
17const hlsStreamUriWithRedirect = 'http://bit.ly/1iy90bn';
18let source = null; // Local URI of the downloaded default source is set in a beforeAll callback.
19let portraitVideoSource = null;
20let imageSource = null;
21let webmSource = null;
22
23const style = { width: 200, height: 200 };
24
25export function test(t, { setPortalChild, cleanupPortal }) {
26  t.describe('Video', () => {
27    t.beforeAll(async () => {
28      const mp4Asset = Asset.fromModule(mp4Source);
29      await mp4Asset.downloadAsync();
30      source = { uri: mp4Asset.localUri };
31
32      const portraitAsset = Asset.fromModule(require('../assets/portrait_video.mp4'));
33      await portraitAsset.downloadAsync();
34      portraitVideoSource = { uri: portraitAsset.localUri };
35
36      const imageAsset = Asset.fromModule(require('../assets/black-128x256.png'));
37      await imageAsset.downloadAsync();
38      imageSource = { uri: imageAsset.localUri };
39
40      const webmAsset = Asset.fromModule(require('../assets/unsupported_bunny.webm'));
41      await webmAsset.downloadAsync();
42      webmSource = { uri: webmAsset.localUri };
43    });
44
45    let instance = null;
46    const refSetter = (ref) => {
47      instance = ref;
48    };
49
50    t.afterEach(async () => {
51      instance = null;
52      await cleanupPortal();
53    });
54
55    const mountAndWaitFor = (child, propName = 'onLoad') =>
56      originalMountAndWaitFor(child, propName, setPortalChild);
57
58    const testPropValues = (propName, values, moreTests) =>
59      t.describe(`Video.props.${propName}`, () => {
60        forEach(values, (value) =>
61          t.it(`sets it to \`${value}\``, async () => {
62            let instance = null;
63            const refSetter = (ref) => {
64              instance = ref;
65            };
66            const element = React.createElement(Video, {
67              style,
68              source,
69              ref: refSetter,
70              [propName]: value,
71            });
72            await mountAndWaitFor(element, 'onLoad');
73            await retryForStatus(instance, { [propName]: value });
74          })
75        );
76
77        if (moreTests) {
78          moreTests();
79        }
80      });
81
82    const testNoCrash = (propName, values) =>
83      t.describe(`Video.props.${propName}`, () => {
84        forEach(values, (value) =>
85          t.it(`setting to \`${value}\` doesn't crash`, async () => {
86            const element = React.createElement(Video, { style, source, [propName]: value });
87            await mountAndWaitFor(element, 'onLoad');
88          })
89        );
90      });
91
92    const testPropSetter = (propName, propSetter, values, moreTests) =>
93      t.describe(`Video.${propSetter}`, () => {
94        forEach(values, (value) =>
95          t.it(`sets it to \`${value}\``, async () => {
96            let instance = null;
97            const refSetter = (ref) => {
98              instance = ref;
99            };
100            const element = React.createElement(Video, {
101              style,
102              source,
103              ref: refSetter,
104              [propName]: value,
105            });
106            await mountAndWaitFor(element);
107            await instance[propSetter](value);
108            const status = await instance.getStatusAsync();
109            t.expect(status).toEqual(t.jasmine.objectContaining({ [propName]: value }));
110          })
111        );
112
113        if (moreTests) {
114          moreTests();
115        }
116      });
117
118    t.describe('Video.props.onLoadStart', () => {
119      t.it('gets called when the source starts loading', async () => {
120        await mountAndWaitFor(<Video style={style} source={source} />, 'onLoadStart');
121      });
122    });
123
124    t.describe('Video.props.onLoad', () => {
125      t.it('gets called when the source loads', async () => {
126        await mountAndWaitFor(<Video style={style} source={source} />, 'onLoad');
127      });
128
129      t.it('gets called right when the video starts to play if it should autoplay', async () => {
130        const status = await mountAndWaitFor(
131          <Video style={style} source={videoRemoteSource} shouldPlay />,
132          'onLoad'
133        );
134        t.expect(status.positionMillis).toEqual(0);
135      });
136    });
137
138    t.describe('Video.props.source', () => {
139      t.it('mounts even when the source is undefined', async () => {
140        await mountAndWaitFor(<Video style={style} />, 'ref');
141      });
142
143      t.it('loads `require` source', async () => {
144        const status = await mountAndWaitFor(<Video style={style} source={mp4Source} />);
145        t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
146      });
147
148      t.it('loads `Asset` source', async () => {
149        const status = await mountAndWaitFor(
150          <Video style={style} source={Asset.fromModule(mp4Source)} />
151        );
152        t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
153      });
154
155      t.it('loads `uri` source', async () => {
156        const status = await mountAndWaitFor(<Video style={style} source={videoRemoteSource} />);
157        t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
158      });
159
160      if (Platform.OS === 'android') {
161        t.it(
162          'calls onError when the file from the Internet redirects to a non-standard content',
163          async () => {
164            const error = await mountAndWaitFor(
165              <Video
166                style={style}
167                source={{
168                  uri: hlsStreamUriWithRedirect,
169                }}
170              />,
171              'onError'
172            );
173            t.expect(error.toLowerCase()).toContain('none');
174          }
175        );
176        t.it(
177          'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided',
178          async () => {
179            let hasBeenRejected = false;
180            try {
181              const status = await mountAndWaitFor(
182                <Video
183                  style={style}
184                  source={{ uri: hlsStreamUriWithRedirect, overrideFileExtensionAndroid: 'm3u8' }}
185                />
186              );
187              t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
188            } catch {
189              hasBeenRejected = true;
190            }
191            t.expect(hasBeenRejected).toBe(false);
192          }
193        );
194      } else {
195        t.it(
196          'loads the file from the Internet that redirects to non-standard content',
197          async () => {
198            let hasBeenRejected = false;
199            try {
200              const status = await mountAndWaitFor(
201                <Video style={style} source={{ uri: hlsStreamUriWithRedirect }} />
202              );
203              t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
204            } catch {
205              hasBeenRejected = true;
206            }
207            t.expect(hasBeenRejected).toBe(false);
208          }
209        );
210      }
211
212      t.it('loads HLS stream', async () => {
213        const status = await mountAndWaitFor(
214          <Video style={style} source={{ uri: hlsStreamUri }} />
215        );
216        t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
217      });
218
219      t.it('loads redirecting `uri` source', async () => {
220        const status = await mountAndWaitFor(
221          <Video style={style} source={redirectingVideoRemoteSource} />
222        );
223        t.expect(status).toEqual(t.jasmine.objectContaining({ isLoaded: true }));
224      });
225
226      t.it('changes the source', async () => {
227        await mountAndWaitFor(<Video style={style} source={videoRemoteSource} />);
228        await mountAndWaitFor(<Video style={style} source={redirectingVideoRemoteSource} />);
229      });
230
231      t.it('changes the source and enables native-controls', async () => {
232        await mountAndWaitFor(<Video style={style} source={videoRemoteSource} />);
233        await mountAndWaitFor(
234          <Video style={style} source={redirectingVideoRemoteSource} useNativeControls />
235        );
236      });
237
238      t.it('changes the source and disables native-controls', async () => {
239        await mountAndWaitFor(<Video style={style} source={videoRemoteSource} useNativeControls />);
240        await mountAndWaitFor(<Video style={style} source={redirectingVideoRemoteSource} />);
241      });
242
243      // These two are flaky on iOS, sometimes they pass, sometimes they timeout.
244      t.it(
245        'calls onError when given image source',
246        async () => {
247          const error = await mountAndWaitFor(
248            <Video style={style} source={imageSource} shouldPlay />,
249            'onError'
250          );
251          t.expect(error).toBeDefined();
252        },
253        30000
254      );
255
256      if (Platform.OS === 'ios') {
257        t.it(
258          'calls onError with a reason when unsupported format given (WebM)',
259          async () => {
260            const error = await mountAndWaitFor(
261              <Video style={style} source={webmSource} shouldPlay />,
262              'onError'
263            );
264            // We cannot check for the specific reason,
265            // as sometimes it isn't what we would expect it to be
266            // (we'd expect "This media format is not supported."),
267            // so let's check whether there is a reason-description separator,
268            // which is included only if `localizedFailureReason` is not nil.
269            t.expect(error).toContain(' - ');
270          },
271          30000
272        );
273      }
274    });
275
276    testNoCrash('useNativeControls', [true, false]);
277    testNoCrash('usePoster', [true, false]);
278    testNoCrash('resizeMode', [
279      Video.RESIZE_MODE_COVER,
280      Video.RESIZE_MODE_CONTAIN,
281      Video.RESIZE_MODE_STRETCH,
282    ]);
283
284    t.describe(`Video.props.posterSource`, () => {
285      t.it("doesn't crash if is set to required image", async () => {
286        const props = {
287          style,
288          source,
289          posterSource: imageSource,
290        };
291        await mountAndWaitFor(<Video {...props} />);
292      });
293
294      t.it("doesn't crash if is set to uri", async () => {
295        const props = {
296          style,
297          source,
298          posterSource: imageRemoteSource,
299        };
300        await mountAndWaitFor(<Video {...props} />);
301      });
302    });
303
304    t.describe(`Video.props.onReadyForDisplay`, () => {
305      t.it('gets called with the `naturalSize` object', async () => {
306        const props = {
307          style,
308          source,
309        };
310        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
311        t.expect(status.naturalSize).toBeDefined();
312        t.expect(status.naturalSize.width).toBeDefined();
313        t.expect(status.naturalSize.height).toBeDefined();
314        t.expect(status.naturalSize.orientation).toBe('landscape');
315      });
316
317      t.it('gets called with the `status` object', async () => {
318        const props = {
319          style,
320          source,
321        };
322        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
323        t.expect(status.status).toBeDefined();
324        t.expect(status.status.isLoaded).toBe(true);
325      });
326
327      t.it('gets called when the component uses native controls', async () => {
328        const props = {
329          style,
330          source,
331          useNativeControls: true,
332        };
333        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
334        t.expect(status.status).toBeDefined();
335        t.expect(status.status.isLoaded).toBe(true);
336      });
337
338      t.it("gets called when the component doesn't use native controls", async () => {
339        const props = {
340          style,
341          source,
342          useNativeControls: false,
343        };
344        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
345        t.expect(status.status).toBeDefined();
346        t.expect(status.status.isLoaded).toBe(true);
347      });
348
349      t.it('gets called for HLS streams', async () => {
350        const props = {
351          style,
352          source: { uri: hlsStreamUri },
353        };
354        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
355        t.expect(status.naturalSize).toBeDefined();
356        t.expect(status.naturalSize.width).toBeDefined();
357        t.expect(status.naturalSize.height).toBeDefined();
358        t.expect(status.naturalSize.orientation).toBeDefined();
359      });
360
361      t.it('correctly detects portrait video', async () => {
362        const props = {
363          style,
364          source: portraitVideoSource,
365        };
366        const status = await mountAndWaitFor(<Video {...props} />, 'onReadyForDisplay');
367        t.expect(status.naturalSize).toBeDefined();
368        t.expect(status.naturalSize.width).toBeDefined();
369        t.expect(status.naturalSize.height).toBeDefined();
370        t.expect(status.naturalSize.orientation).toBe('portrait');
371      });
372    });
373
374    t.describe('Video fullscreen player', () => {
375      t.it('presents the player and calls callback func', async () => {
376        const fullscreenUpdates = [];
377        const onFullscreenUpdate = (event) => fullscreenUpdates.push(event.fullscreenUpdate);
378
379        await mountAndWaitFor(
380          <Video
381            style={style}
382            source={source}
383            ref={refSetter}
384            onFullscreenUpdate={onFullscreenUpdate}
385          />,
386          'onReadyForDisplay'
387        );
388
389        await instance.presentFullscreenPlayer();
390        await waitFor(1000);
391
392        t.expect(fullscreenUpdates).toEqual([
393          VideoFullscreenUpdate.PLAYER_WILL_PRESENT,
394          VideoFullscreenUpdate.PLAYER_DID_PRESENT,
395        ]);
396
397        await instance.dismissFullscreenPlayer();
398        await waitFor(1000);
399
400        t.expect(fullscreenUpdates).toEqual([
401          VideoFullscreenUpdate.PLAYER_WILL_PRESENT,
402          VideoFullscreenUpdate.PLAYER_DID_PRESENT,
403          VideoFullscreenUpdate.PLAYER_WILL_DISMISS,
404          VideoFullscreenUpdate.PLAYER_DID_DISMISS,
405        ]);
406      });
407
408      if (Platform.OS === 'android') {
409        t.it("raises an error if the code didn't wait for completion", async () => {
410          let presentationError = null;
411          let dismissalError = null;
412          try {
413            await mountAndWaitFor(
414              <Video style={style} source={source} ref={refSetter} />,
415              'onReadyForDisplay'
416            );
417            instance.presentFullscreenPlayer().catch((error) => {
418              presentationError = error;
419            });
420            await waitFor(1000);
421            await instance.dismissFullscreenPlayer();
422            await waitFor(1000);
423          } catch (error) {
424            dismissalError = error;
425          }
426
427          t.expect(presentationError).toBeDefined();
428          t.expect(dismissalError).toBeDefined();
429        });
430      }
431
432      t.it('rejects dismissal request if present request is being handled', async () => {
433        await mountAndWaitFor(
434          <Video style={style} source={source} ref={refSetter} />,
435          'onReadyForDisplay'
436        );
437        let error = null;
438        const presentationPromise = instance.presentFullscreenPlayer();
439        await waitFor(1000);
440        try {
441          await instance.dismissFullscreenPlayer();
442          await waitFor(1000);
443        } catch (err) {
444          error = err;
445        }
446        t.expect(error).toBeDefined();
447        await presentationPromise;
448        await instance.dismissFullscreenPlayer();
449      });
450
451      t.it('rejects presentation request if present request is already being handled', async () => {
452        await mountAndWaitFor(
453          <Video style={style} source={source} ref={refSetter} />,
454          'onReadyForDisplay'
455        );
456        let error = null;
457        const presentationPromise = instance.presentFullscreenPlayer();
458        await waitFor(1000);
459        try {
460          await instance.presentFullscreenPlayer();
461          await waitFor(1000);
462        } catch (err) {
463          error = err;
464        }
465        t.expect(error).toBeDefined();
466        await presentationPromise;
467        await instance.dismissFullscreenPlayer();
468      });
469    });
470
471    // Actually values 2.0 and -0.5 shouldn't be allowed, however at the moment
472    // it is possible to set them through props successfully.
473    testPropValues('volume', [0.5, 1.0, 2.0, -0.5]);
474    testPropSetter('volume', 'setVolumeAsync', [0, 0.5, 1], () => {
475      t.it('errors when trying to set it to 2', async () => {
476        let error = null;
477        try {
478          const props = { style, source, ref: refSetter };
479          await mountAndWaitFor(<Video {...props} />);
480          await instance.setVolumeAsync(2);
481        } catch (err) {
482          error = err;
483        }
484        t.expect(error).toBeDefined();
485        t.expect(error.toString()).toMatch(/value .+ between/);
486      });
487
488      t.it('errors when trying to set it to -0.5', async () => {
489        let error = null;
490        try {
491          const props = { style, source, ref: refSetter };
492          await mountAndWaitFor(<Video {...props} />);
493          await instance.setVolumeAsync(-0.5);
494        } catch (err) {
495          error = err;
496        }
497        t.expect(error).toBeDefined();
498        t.expect(error.toString()).toMatch(/value .+ between/);
499      });
500    });
501
502    testPropValues('isMuted', [true, false]);
503    testPropSetter('isMuted', 'setIsMutedAsync', [true, false]);
504
505    testPropValues('isLooping', [true, false]);
506    testPropSetter('isLooping', 'setIsLoopingAsync', [true, false]);
507
508    // Actually values 34 and -0.5 shouldn't be allowed, however at the moment
509    // it is possible to set them through props successfully.
510    testPropValues('rate', [0.5, 1.0, 2, 34, -0.5]);
511    testPropSetter('rate', 'setRateAsync', [0, 0.5, 1], () => {
512      t.it('errors when trying to set it above 32', async () => {
513        let error = null;
514        try {
515          const props = { style, source, ref: refSetter };
516          await mountAndWaitFor(<Video {...props} />);
517          await instance.setRateAsync(34);
518        } catch (err) {
519          error = err;
520        }
521        t.expect(error).toBeDefined();
522        t.expect(error.toString()).toMatch(/value .+ between/);
523      });
524
525      t.it('errors when trying to set it under 0', async () => {
526        let error = null;
527        try {
528          const props = { style, source, ref: refSetter };
529          await mountAndWaitFor(<Video {...props} />);
530          await instance.setRateAsync(-0.5);
531        } catch (err) {
532          error = err;
533        }
534        t.expect(error).toBeDefined();
535        t.expect(error.toString()).toMatch(/value .+ between/);
536      });
537    });
538
539    testPropValues('shouldPlay', [true, false]);
540    testPropValues('shouldCorrectPitch', [true, false]);
541
542    t.describe('Video.onPlaybackStatusUpdate', () => {
543      t.it('gets called with `didJustFinish = true` when video is done playing', async () => {
544        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
545        const props = {
546          onPlaybackStatusUpdate,
547          source,
548          style,
549          ref: refSetter,
550        };
551        await mountAndWaitFor(<Video {...props} />);
552        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
553        const status = await instance.getStatusAsync();
554        await instance.setStatusAsync({
555          shouldPlay: true,
556          positionMillis: status.durationMillis - 500,
557        });
558        await retryForStatus(instance, { isPlaying: true });
559        await new Promise((resolve) => {
560          setTimeout(() => {
561            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
562              t.jasmine.objectContaining({ didJustFinish: true })
563            );
564            resolve();
565          }, 1000);
566        });
567      });
568
569      t.it('gets called periodically when playing', async () => {
570        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
571        const props = {
572          onPlaybackStatusUpdate,
573          source,
574          style,
575          ref: refSetter,
576          progressUpdateIntervalMillis: 10,
577        };
578        await mountAndWaitFor(<Video {...props} />);
579        await new Promise((resolve) => setTimeout(resolve, 100));
580        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
581        // Verify that status-update doesn't get called periodically when not started
582        const beforeCount = onPlaybackStatusUpdate.calls.count();
583        t.expect(beforeCount).toBeLessThan(6);
584
585        const status = await instance.getStatusAsync();
586        await instance.setStatusAsync({
587          shouldPlay: true,
588          positionMillis: status.durationMillis - 500,
589        });
590        await retryForStatus(instance, { isPlaying: true });
591        await new Promise((resolve) => setTimeout(resolve, 500));
592        await retryForStatus(instance, { isPlaying: false });
593        const duringCount = onPlaybackStatusUpdate.calls.count() - beforeCount;
594        t.expect(duringCount).toBeGreaterThan(50);
595
596        // Wait a bit longer and verify it doesn't get called anymore
597        await new Promise((resolve) => setTimeout(resolve, 100));
598        const afterCount = onPlaybackStatusUpdate.calls.count() - beforeCount - duringCount;
599        t.expect(afterCount).toBeLessThan(3);
600      });
601    });
602
603    /*t.describe('Video.setProgressUpdateIntervalAsync', () => {
604      t.it('sets frequence of the progress updates', async () => {
605        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
606        const props = {
607          style,
608          source,
609          ref: refSetter,
610          shouldPlay: true,
611          onPlaybackStatusUpdate,
612        };
613        await mountAndWaitFor(<Video {...props} />);
614        const updateInterval = 100;
615        await instance.setProgressUpdateIntervalAsync(updateInterval);
616        await new Promise(resolve => {
617          setTimeout(() => {
618            const expectedArgsCount = Platform.OS === 'android' ? 5 : 9;
619            t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(expectedArgsCount);
620
621            const realMillis = map(
622              takeRight(filter(flatten(onPlaybackStatusUpdate.calls.allArgs()), 'isPlaying'), 4),
623              'positionMillis'
624            );
625
626            for (let i = 3; i > 0; i--) {
627              const difference = Math.abs(realMillis[i] - realMillis[i - 1] - updateInterval);
628              t.expect(difference).toBeLessThan(updateInterval / 2 + 1);
629            }
630
631            resolve();
632          }, 1500);
633        });
634      });
635    });*/
636
637    t.describe('Video.setPositionAsync', () => {
638      t.it('sets position of the video', async () => {
639        const props = { style, source, ref: refSetter };
640        await mountAndWaitFor(<Video {...props} />);
641        await retryForStatus(instance, { isBuffering: false });
642        const status = await instance.getStatusAsync();
643        await retryForStatus(instance, { playableDurationMillis: status.durationMillis });
644        const positionMillis = 500;
645        await instance.setPositionAsync(positionMillis);
646        await retryForStatus(instance, { positionMillis });
647      });
648    });
649
650    t.describe('Video.loadAsync', () => {
651      // NOTE(2018-03-08): Some of these tests are failing on iOS
652      const unreliablyIt = Platform.OS === 'ios' ? t.xit : t.it;
653      unreliablyIt('loads the video', async () => {
654        const props = { style };
655        const instance = await mountAndWaitFor(<Video {...props} />, 'ref');
656        await instance.loadAsync(source);
657        await retryForStatus(instance, { isLoaded: true });
658      });
659
660      // better positionmillis check
661      unreliablyIt('sets the initial status', async () => {
662        const props = { style };
663        const instance = await mountAndWaitFor(<Video {...props} />, 'ref');
664        const initialStatus = { volume: 0.5, isLooping: true, rate: 0.5 };
665        await instance.loadAsync(source, { ...initialStatus, positionMillis: 1000 });
666        await retryForStatus(instance, { isLoaded: true, ...initialStatus });
667        const status = await instance.getStatusAsync();
668        t.expect(status.positionMillis).toBeLessThan(1100);
669        t.expect(status.positionMillis).toBeGreaterThan(900);
670      });
671
672      unreliablyIt('keeps the video instance after load when using poster', async () => {
673        const instance = await mountAndWaitFor(<Video style={style} usePoster />, 'ref');
674        await instance.loadAsync(source, { shouldPlay: true });
675        await waitFor(500);
676        await retryForStatus(instance, { isPlaying: true });
677      });
678    });
679
680    t.describe('Video.unloadAsync', () => {
681      t.it('unloads the video', async () => {
682        const props = { style, source, ref: refSetter };
683        await mountAndWaitFor(<Video {...props} />);
684        await retryForStatus(instance, { isLoaded: true });
685        await instance.unloadAsync();
686        await retryForStatus(instance, { isLoaded: false });
687      });
688    });
689
690    t.describe('Video.pauseAsync', () => {
691      t.it('pauses the video', async () => {
692        const props = { style, source, shouldPlay: true, ref: refSetter };
693        await mountAndWaitFor(<Video {...props} />);
694        await retryForStatus(instance, { isPlaying: true });
695        await new Promise((r) => setTimeout(r, 500));
696        await instance.pauseAsync();
697        await retryForStatus(instance, { isPlaying: false });
698        const { positionMillis } = await instance.getStatusAsync();
699        t.expect(positionMillis).toBeGreaterThan(0);
700      });
701    });
702
703    t.describe('Video.playAsync', () => {
704      // NOTE(2018-03-08): Some of these tests are failing on iOS
705      const unreliablyIt = Platform.OS === 'ios' ? t.xit : t.it;
706
707      t.it('plays the stopped video', async () => {
708        const props = { style, source, ref: refSetter };
709        await mountAndWaitFor(<Video {...props} />);
710        await retryForStatus(instance, { isLoaded: true });
711        await instance.playAsync();
712        await retryForStatus(instance, { isPlaying: true });
713      });
714
715      t.it('plays the paused video', async () => {
716        const props = { style, source, ref: refSetter };
717        await mountAndWaitFor(<Video {...props} />);
718        await retryForStatus(instance, { isLoaded: true });
719        await instance.playAsync();
720        await retryForStatus(instance, { isPlaying: true });
721        await instance.pauseAsync();
722        await retryForStatus(instance, { isPlaying: false });
723        await instance.playAsync();
724        await retryForStatus(instance, { isPlaying: true });
725      });
726
727      unreliablyIt('does not play video that played to an end', async () => {
728        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
729        const props = {
730          onPlaybackStatusUpdate,
731          source,
732          style,
733          ref: refSetter,
734        };
735        await mountAndWaitFor(<Video {...props} />);
736        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
737        const status = await instance.getStatusAsync();
738        await instance.setStatusAsync({
739          shouldPlay: true,
740          positionMillis: status.durationMillis - 500,
741        });
742        await new Promise((resolve) => {
743          setTimeout(() => {
744            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
745              t.jasmine.objectContaining({ didJustFinish: true })
746            );
747            resolve();
748          }, 1000);
749        });
750        await instance.playAsync();
751        t.expect((await instance.getStatusAsync()).isPlaying).toBe(false);
752      });
753    });
754
755    t.describe('Video.playFromPositionAsync', () => {
756      t.it('plays a video that played to an end', async () => {
757        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
758        const props = { onPlaybackStatusUpdate, source, style, ref: refSetter };
759        await mountAndWaitFor(<Video {...props} />);
760        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
761        const status = await instance.getStatusAsync();
762        await instance.setStatusAsync({
763          shouldPlay: true,
764          positionMillis: status.durationMillis - 500,
765        });
766        await new Promise((resolve) => {
767          setTimeout(() => {
768            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
769              t.jasmine.objectContaining({ didJustFinish: true })
770            );
771            resolve();
772          }, 1000);
773        });
774        await instance.playFromPositionAsync(0);
775        await retryForStatus(instance, { isPlaying: true });
776      });
777    });
778
779    t.describe('Video.replayAsync', () => {
780      t.it('replays the video', async () => {
781        await mountAndWaitFor(<Video source={source} ref={refSetter} style={style} shouldPlay />);
782        await retryForStatus(instance, { isPlaying: true });
783        await waitFor(500);
784        const statusBefore = await instance.getStatusAsync();
785        await instance.replayAsync();
786        await retryForStatus(instance, { isPlaying: true });
787        const statusAfter = await instance.getStatusAsync();
788        t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis);
789      });
790
791      t.it('plays a video that played to an end', async () => {
792        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
793        const props = { onPlaybackStatusUpdate, source, style, ref: refSetter };
794        await mountAndWaitFor(<Video {...props} />);
795        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
796        const status = await instance.getStatusAsync();
797        await instance.setStatusAsync({
798          shouldPlay: true,
799          positionMillis: status.durationMillis - 500,
800        });
801        await new Promise((resolve) => {
802          setTimeout(() => {
803            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
804              t.jasmine.objectContaining({ didJustFinish: true })
805            );
806            resolve();
807          }, 1000);
808        });
809        await instance.replayAsync();
810        await retryForStatus(instance, { isPlaying: true });
811      });
812
813      /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => {
814        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
815        const props = {
816          style,
817          source,
818          ref: refSetter,
819          shouldPlay: true,
820          onPlaybackStatusUpdate,
821        };
822        await mountAndWaitFor(<Video {...props} />);
823        await retryForStatus(instance, { isPlaying: true });
824        await waitFor(500);
825        await instance.replayAsync();
826        t
827          .expect(onPlaybackStatusUpdate)
828          .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true }));
829      });*/
830    });
831
832    t.describe('Video.stopAsync', () => {
833      let originalTimeout;
834
835      t.beforeAll(async () => {
836        originalTimeout = t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
837        t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout * 6;
838      });
839
840      t.afterAll(() => {
841        t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
842      });
843
844      t.it('stops a playing video', async () => {
845        const props = { style, source, shouldPlay: true, ref: refSetter };
846        await mountAndWaitFor(<Video {...props} />);
847        await retryForStatus(instance, { isPlaying: true });
848        await instance.stopAsync();
849        await retryForStatus(instance, { isPlaying: false, positionMillis: 0 });
850      });
851
852      t.it('stops a paused video', async () => {
853        const props = { style, source, shouldPlay: true, ref: refSetter };
854        await mountAndWaitFor(<Video {...props} />);
855        await retryForStatus(instance, { isPlaying: true });
856        await waitFor(500);
857        await instance.pauseAsync();
858        await retryForStatus(instance, { isPlaying: false });
859        await instance.stopAsync();
860        await retryForStatus(instance, { isPlaying: false, positionMillis: 0 });
861      });
862    });
863  });
864}
865