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