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