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