xref: /expo/apps/test-suite/tests/Video.js (revision bb8f4f99)
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' };
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 (error) {
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 (error) {
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          Video.FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT,
394          Video.FULLSCREEN_UPDATE_PLAYER_DID_PRESENT,
395        ]);
396
397        await instance.dismissFullscreenPlayer();
398        await waitFor(1000);
399
400        t.expect(fullscreenUpdates).toEqual([
401          Video.FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT,
402          Video.FULLSCREEN_UPDATE_PLAYER_DID_PRESENT,
403          Video.FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS,
404          Video.FULLSCREEN_UPDATE_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 instance.dismissFullscreenPlayer();
421          } catch (error) {
422            dismissalError = error;
423          }
424
425          t.expect(presentationError).toBeDefined();
426          t.expect(dismissalError).toBeDefined();
427        });
428      }
429
430      t.it('rejects dismissal request if present request is being handled', async () => {
431        await mountAndWaitFor(
432          <Video style={style} source={source} ref={refSetter} />,
433          'onReadyForDisplay'
434        );
435        let error = null;
436        const presentationPromise = instance.presentFullscreenPlayer();
437        try {
438          await instance.dismissFullscreenPlayer();
439        } catch (err) {
440          error = err;
441        }
442        t.expect(error).toBeDefined();
443        await presentationPromise;
444        await instance.dismissFullscreenPlayer();
445      });
446
447      t.it('rejects presentation request if present request is already being handled', async () => {
448        await mountAndWaitFor(
449          <Video style={style} source={source} ref={refSetter} />,
450          'onReadyForDisplay'
451        );
452        let error = null;
453        const presentationPromise = instance.presentFullscreenPlayer();
454        try {
455          await instance.presentFullscreenPlayer();
456        } catch (err) {
457          error = err;
458        }
459        t.expect(error).toBeDefined();
460        await presentationPromise;
461        await instance.dismissFullscreenPlayer();
462      });
463
464      // NOTE(2018-10-17): Some of these tests are failing on iOS
465      const unreliablyIt = Platform.OS === 'ios' ? t.xit : t.it;
466
467      unreliablyIt(
468        'rejects all but the last request to change fullscreen mode before the video loads',
469        async () => {
470          // Adding second clause sometimes crashes the application,
471          // because by the time we call `present` second time,
472          // the video loads, so it handles the first request properly,
473          // rejects the second and it may reject the third request
474          // if it tries to be handled while the first presentation request
475          // is being handled.
476          let firstErrored = false;
477          // let secondErrored = false;
478          let thirdErrored = false;
479          // We're using remote source as this gives us time to request changes
480          // before the video loads.
481          const instance = await mountAndWaitFor(
482            <Video style={style} source={videoRemoteSource} />,
483            'ref'
484          );
485          instance.dismissFullscreenPlayer().catch(() => {
486            firstErrored = true;
487          });
488          // instance.presentFullscreenPlayer().catch(() => (secondErrored = true));
489          try {
490            await instance.dismissFullscreenPlayer();
491          } catch (_error) {
492            thirdErrored = true;
493          }
494
495          if (!firstErrored) {
496            // First present request finished too early so we cannot
497            // test this behavior at all. Normally I would put
498            // `t.pending` here, but as for the end of 2017 it doesn't work.
499          } else {
500            t.expect(firstErrored).toBe(true);
501            // t.expect(secondErrored).toBe(true);
502            t.expect(thirdErrored).toBe(false);
503          }
504          const pleaseDismiss = async () => {
505            try {
506              await instance.dismissFullscreenPlayer();
507            } catch (error) {
508              pleaseDismiss();
509            }
510          };
511          await pleaseDismiss();
512        }
513      );
514    });
515
516    // Actually values 2.0 and -0.5 shouldn't be allowed, however at the moment
517    // it is possible to set them through props successfully.
518    testPropValues('volume', [0.5, 1.0, 2.0, -0.5]);
519    testPropSetter('volume', 'setVolumeAsync', [0, 0.5, 1], () => {
520      t.it('errors when trying to set it to 2', async () => {
521        let error = null;
522        try {
523          const props = { style, source, ref: refSetter };
524          await mountAndWaitFor(<Video {...props} />);
525          await instance.setVolumeAsync(2);
526        } catch (err) {
527          error = err;
528        }
529        t.expect(error).toBeDefined();
530        t.expect(error.toString()).toMatch(/value .+ between/);
531      });
532
533      t.it('errors when trying to set it to -0.5', async () => {
534        let error = null;
535        try {
536          const props = { style, source, ref: refSetter };
537          await mountAndWaitFor(<Video {...props} />);
538          await instance.setVolumeAsync(-0.5);
539        } catch (err) {
540          error = err;
541        }
542        t.expect(error).toBeDefined();
543        t.expect(error.toString()).toMatch(/value .+ between/);
544      });
545    });
546
547    testPropValues('isMuted', [true, false]);
548    testPropSetter('isMuted', 'setIsMutedAsync', [true, false]);
549
550    testPropValues('isLooping', [true, false]);
551    testPropSetter('isLooping', 'setIsLoopingAsync', [true, false]);
552
553    // Actually values 34 and -0.5 shouldn't be allowed, however at the moment
554    // it is possible to set them through props successfully.
555    testPropValues('rate', [0.5, 1.0, 2, 34, -0.5]);
556    testPropSetter('rate', 'setRateAsync', [0, 0.5, 1], () => {
557      t.it('errors when trying to set it above 32', async () => {
558        let error = null;
559        try {
560          const props = { style, source, ref: refSetter };
561          await mountAndWaitFor(<Video {...props} />);
562          await instance.setRateAsync(34);
563        } catch (err) {
564          error = err;
565        }
566        t.expect(error).toBeDefined();
567        t.expect(error.toString()).toMatch(/value .+ between/);
568      });
569
570      t.it('errors when trying to set it under 0', async () => {
571        let error = null;
572        try {
573          const props = { style, source, ref: refSetter };
574          await mountAndWaitFor(<Video {...props} />);
575          await instance.setRateAsync(-0.5);
576        } catch (err) {
577          error = err;
578        }
579        t.expect(error).toBeDefined();
580        t.expect(error.toString()).toMatch(/value .+ between/);
581      });
582    });
583
584    testPropValues('shouldPlay', [true, false]);
585    testPropValues('shouldCorrectPitch', [true, false]);
586
587    t.describe('Video.onPlaybackStatusUpdate', () => {
588      t.it('gets called with `didJustFinish = true` when video is done playing', async () => {
589        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
590        const props = {
591          onPlaybackStatusUpdate,
592          source,
593          style,
594          ref: refSetter,
595        };
596        await mountAndWaitFor(<Video {...props} />);
597        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
598        const status = await instance.getStatusAsync();
599        await instance.setStatusAsync({
600          shouldPlay: true,
601          positionMillis: status.durationMillis - 500,
602        });
603        await retryForStatus(instance, { isPlaying: true });
604        await new Promise(resolve => {
605          setTimeout(() => {
606            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
607              t.jasmine.objectContaining({ didJustFinish: true })
608            );
609            resolve();
610          }, 1000);
611        });
612      });
613
614      t.it('gets called periodically when playing', async () => {
615        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
616        const props = {
617          onPlaybackStatusUpdate,
618          source,
619          style,
620          ref: refSetter,
621          progressUpdateIntervalMillis: 10,
622        };
623        await mountAndWaitFor(<Video {...props} />);
624        await new Promise(resolve => setTimeout(resolve, 100));
625        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
626        // Verify that status-update doesn't get called periodically when not started
627        const beforeCount = onPlaybackStatusUpdate.calls.count();
628        t.expect(beforeCount).toBeLessThan(6);
629
630        const status = await instance.getStatusAsync();
631        await instance.setStatusAsync({
632          shouldPlay: true,
633          positionMillis: status.durationMillis - 500,
634        });
635        await retryForStatus(instance, { isPlaying: true });
636        await new Promise(resolve => setTimeout(resolve, 500));
637        await retryForStatus(instance, { isPlaying: false });
638        const duringCount = onPlaybackStatusUpdate.calls.count() - beforeCount;
639        t.expect(duringCount).toBeGreaterThan(50);
640
641        // Wait a bit longer and verify it doesn't get called anymore
642        await new Promise(resolve => setTimeout(resolve, 100));
643        const afterCount = onPlaybackStatusUpdate.calls.count() - beforeCount - duringCount;
644        t.expect(afterCount).toBeLessThan(3);
645      });
646    });
647
648    /*t.describe('Video.setProgressUpdateIntervalAsync', () => {
649      t.it('sets frequence of the progress updates', async () => {
650        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
651        const props = {
652          style,
653          source,
654          ref: refSetter,
655          shouldPlay: true,
656          onPlaybackStatusUpdate,
657        };
658        await mountAndWaitFor(<Video {...props} />);
659        const updateInterval = 100;
660        await instance.setProgressUpdateIntervalAsync(updateInterval);
661        await new Promise(resolve => {
662          setTimeout(() => {
663            const expectedArgsCount = Platform.OS === 'android' ? 5 : 9;
664            t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(expectedArgsCount);
665
666            const realMillis = map(
667              takeRight(filter(flatten(onPlaybackStatusUpdate.calls.allArgs()), 'isPlaying'), 4),
668              'positionMillis'
669            );
670
671            for (let i = 3; i > 0; i--) {
672              const difference = Math.abs(realMillis[i] - realMillis[i - 1] - updateInterval);
673              t.expect(difference).toBeLessThan(updateInterval / 2 + 1);
674            }
675
676            resolve();
677          }, 1500);
678        });
679      });
680    });*/
681
682    t.describe('Video.setPositionAsync', () => {
683      t.it('sets position of the video', async () => {
684        const props = { style, source, ref: refSetter };
685        await mountAndWaitFor(<Video {...props} />);
686        await retryForStatus(instance, { isBuffering: false });
687        const status = await instance.getStatusAsync();
688        await retryForStatus(instance, { playableDurationMillis: status.durationMillis });
689        const positionMillis = 500;
690        await instance.setPositionAsync(positionMillis);
691        await retryForStatus(instance, { positionMillis });
692      });
693    });
694
695    t.describe('Video.loadAsync', () => {
696      // NOTE(2018-03-08): Some of these tests are failing on iOS
697      const unreliablyIt = Platform.OS === 'ios' ? t.xit : t.it;
698      unreliablyIt('loads the video', async () => {
699        const props = { style };
700        const instance = await mountAndWaitFor(<Video {...props} />, 'ref');
701        await instance.loadAsync(source);
702        await retryForStatus(instance, { isLoaded: true });
703      });
704
705      // better positionmillis check
706      unreliablyIt('sets the initial status', async () => {
707        const props = { style };
708        const instance = await mountAndWaitFor(<Video {...props} />, 'ref');
709        const initialStatus = { volume: 0.5, isLooping: true, rate: 0.5 };
710        await instance.loadAsync(source, { ...initialStatus, positionMillis: 1000 });
711        await retryForStatus(instance, { isLoaded: true, ...initialStatus });
712        const status = await instance.getStatusAsync();
713        t.expect(status.positionMillis).toBeLessThan(1100);
714        t.expect(status.positionMillis).toBeGreaterThan(900);
715      });
716
717      unreliablyIt('keeps the video instance after load when using poster', async () => {
718        const instance = await mountAndWaitFor(<Video style={style} usePoster />, 'ref');
719        await instance.loadAsync(source, { shouldPlay: true });
720        await waitFor(500);
721        await retryForStatus(instance, { isPlaying: true });
722      });
723    });
724
725    t.describe('Video.unloadAsync', () => {
726      t.it('unloads the video', async () => {
727        const props = { style, source, ref: refSetter };
728        await mountAndWaitFor(<Video {...props} />);
729        await retryForStatus(instance, { isLoaded: true });
730        await instance.unloadAsync();
731        await retryForStatus(instance, { isLoaded: false });
732      });
733    });
734
735    t.describe('Video.pauseAsync', () => {
736      t.it('pauses the video', async () => {
737        const props = { style, source, shouldPlay: true, ref: refSetter };
738        await mountAndWaitFor(<Video {...props} />);
739        await retryForStatus(instance, { isPlaying: true });
740        await new Promise(r => setTimeout(r, 500));
741        await instance.pauseAsync();
742        await retryForStatus(instance, { isPlaying: false });
743        const { positionMillis } = await instance.getStatusAsync();
744        t.expect(positionMillis).toBeGreaterThan(0);
745      });
746    });
747
748    t.describe('Video.playAsync', () => {
749      // NOTE(2018-03-08): Some of these tests are failing on iOS
750      const unreliablyIt = Platform.OS === 'ios' ? t.xit : t.it;
751
752      t.it('plays the stopped video', async () => {
753        const props = { style, source, ref: refSetter };
754        await mountAndWaitFor(<Video {...props} />);
755        await retryForStatus(instance, { isLoaded: true });
756        await instance.playAsync();
757        await retryForStatus(instance, { isPlaying: true });
758      });
759
760      t.it('plays the paused video', async () => {
761        const props = { style, source, ref: refSetter };
762        await mountAndWaitFor(<Video {...props} />);
763        await retryForStatus(instance, { isLoaded: true });
764        await instance.playAsync();
765        await retryForStatus(instance, { isPlaying: true });
766        await instance.pauseAsync();
767        await retryForStatus(instance, { isPlaying: false });
768        await instance.playAsync();
769        await retryForStatus(instance, { isPlaying: true });
770      });
771
772      unreliablyIt('does not play video that played to an end', async () => {
773        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
774        const props = {
775          onPlaybackStatusUpdate,
776          source,
777          style,
778          ref: refSetter,
779        };
780        await mountAndWaitFor(<Video {...props} />);
781        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
782        const status = await instance.getStatusAsync();
783        await instance.setStatusAsync({
784          shouldPlay: true,
785          positionMillis: status.durationMillis - 500,
786        });
787        await new Promise(resolve => {
788          setTimeout(() => {
789            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
790              t.jasmine.objectContaining({ didJustFinish: true })
791            );
792            resolve();
793          }, 1000);
794        });
795        await instance.playAsync();
796        t.expect((await instance.getStatusAsync()).isPlaying).toBe(false);
797      });
798    });
799
800    t.describe('Video.playFromPositionAsync', () => {
801      t.it('plays a video that played to an end', async () => {
802        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
803        const props = { onPlaybackStatusUpdate, source, style, ref: refSetter };
804        await mountAndWaitFor(<Video {...props} />);
805        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
806        const status = await instance.getStatusAsync();
807        await instance.setStatusAsync({
808          shouldPlay: true,
809          positionMillis: status.durationMillis - 500,
810        });
811        await new Promise(resolve => {
812          setTimeout(() => {
813            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
814              t.jasmine.objectContaining({ didJustFinish: true })
815            );
816            resolve();
817          }, 1000);
818        });
819        await instance.playFromPositionAsync(0);
820        await retryForStatus(instance, { isPlaying: true });
821      });
822    });
823
824    t.describe('Video.replayAsync', () => {
825      t.it('replays the video', async () => {
826        await mountAndWaitFor(<Video source={source} ref={refSetter} style={style} shouldPlay />);
827        await retryForStatus(instance, { isPlaying: true });
828        await waitFor(500);
829        const statusBefore = await instance.getStatusAsync();
830        await instance.replayAsync();
831        await retryForStatus(instance, { isPlaying: true });
832        const statusAfter = await instance.getStatusAsync();
833        t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis);
834      });
835
836      t.it('plays a video that played to an end', async () => {
837        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
838        const props = { onPlaybackStatusUpdate, source, style, ref: refSetter };
839        await mountAndWaitFor(<Video {...props} />);
840        await retryForStatus(instance, { isBuffering: false, isLoaded: true });
841        const status = await instance.getStatusAsync();
842        await instance.setStatusAsync({
843          shouldPlay: true,
844          positionMillis: status.durationMillis - 500,
845        });
846        await new Promise(resolve => {
847          setTimeout(() => {
848            t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith(
849              t.jasmine.objectContaining({ didJustFinish: true })
850            );
851            resolve();
852          }, 1000);
853        });
854        await instance.replayAsync();
855        await retryForStatus(instance, { isPlaying: true });
856      });
857
858      /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => {
859        const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate');
860        const props = {
861          style,
862          source,
863          ref: refSetter,
864          shouldPlay: true,
865          onPlaybackStatusUpdate,
866        };
867        await mountAndWaitFor(<Video {...props} />);
868        await retryForStatus(instance, { isPlaying: true });
869        await waitFor(500);
870        await instance.replayAsync();
871        t
872          .expect(onPlaybackStatusUpdate)
873          .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true }));
874      });*/
875    });
876
877    t.describe('Video.stopAsync', () => {
878      t.it('stops a playing video', async () => {
879        const props = { style, source, shouldPlay: true, ref: refSetter };
880        await mountAndWaitFor(<Video {...props} />);
881        await retryForStatus(instance, { isPlaying: true });
882        await instance.stopAsync();
883        await retryForStatus(instance, { isPlaying: false, positionMillis: 0 });
884      });
885
886      t.it('stops a paused video', async () => {
887        const props = { style, source, shouldPlay: true, ref: refSetter };
888        await mountAndWaitFor(<Video {...props} />);
889        await retryForStatus(instance, { isPlaying: true });
890        await waitFor(500);
891        await instance.pauseAsync();
892        await retryForStatus(instance, { isPlaying: false });
893        await instance.stopAsync();
894        await retryForStatus(instance, { isPlaying: false, positionMillis: 0 });
895      });
896    });
897  });
898}
899