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