diff options
Diffstat (limited to 'javascript/videojs/test/unit/tech/html5.test.js')
| -rw-r--r-- | javascript/videojs/test/unit/tech/html5.test.js | 983 |
1 files changed, 983 insertions, 0 deletions
diff --git a/javascript/videojs/test/unit/tech/html5.test.js b/javascript/videojs/test/unit/tech/html5.test.js new file mode 100644 index 0000000..d5f02b1 --- /dev/null +++ b/javascript/videojs/test/unit/tech/html5.test.js @@ -0,0 +1,983 @@ +/* eslint-env qunit, browser */ +let player; +let tech; + +import Html5 from '../../../src/js/tech/html5.js'; +import * as browser from '../../../src/js/utils/browser.js'; +import document from 'global/document'; +import sinon from 'sinon'; + +QUnit.module('HTML5', { + beforeEach(assert) { + const el = document.createElement('div'); + + el.innerHTML = '<div />'; + player = { + id() { + return 'id'; + }, + el() { + return el; + }, + options_: {}, + options() { + return this.options_; + }, + bufferedPercent() { + return 0; + }, + controls() { + return false; + }, + usingNativeControls() { + return false; + }, + on() { + return this; + }, + off() { + return this; + }, + ready() {}, + addChild() {}, + trigger() {} + }; + tech = new Html5({}); + }, + afterEach(assert) { + tech.dispose(); + player = null; + tech = null; + } +}); + +QUnit[browser.IS_ANY_SAFARI ? 'test' : 'skip']('if setScrubbing is true and fastSeek is available, use it', function(assert) { + Object.defineProperty(tech.el(), 'currentTime', { + get: () => {}, + set: () => {}, + + writeable: true, + enumerable: false, + configurable: true + }); + + const currentTimeSpy = sinon.spy(tech.el(), 'currentTime', ['set']); + + tech.setCurrentTime(5); + assert.ok(currentTimeSpy.set.called, 'currentTime setter was called'); + assert.ok(currentTimeSpy.set.calledWith(5), 'currentTime setter was called with 5'); + + tech.setScrubbing(true); + + // when scrubbing is set but fastSeek isn't available, currentTime should still be called + tech.el().fastSeek = null; + tech.setCurrentTime(10); + assert.ok(currentTimeSpy.set.called, 'currentTime setter was called'); + assert.ok(currentTimeSpy.set.calledWith(10), 'currentTime setter was called with 10'); + + const fastSeekSpy = tech.el().fastSeek = sinon.spy(); + + tech.setCurrentTime(15); + assert.ok(currentTimeSpy.set.calledTwice, 'currentTime setter was only called twice and not a 3rd time for fastSeek'); + assert.ok(fastSeekSpy.called, 'fastSeek called'); + assert.ok(fastSeekSpy.calledWith(15), 'fastSeek called with 15'); +}); + +QUnit.test('should be able to set playsinline attribute', function(assert) { + assert.expect(2); + + tech.createEl(); + tech.setPlaysinline(true); + + assert.ok(tech.el().hasAttribute('playsinline'), 'playsinline attribute was added'); + + tech.setPlaysinline(false); + + assert.ok(!tech.el().hasAttribute('playsinline'), 'playsinline attribute was removed'); +}); + +QUnit.test('should detect whether the volume can be changed', function(assert) { + + if (!{}.__defineSetter__) { + assert.ok(true, 'your browser does not support this test, skipping it'); + return; + } + const testVid = Html5.TEST_VID; + const ConstVolumeVideo = function() { + this.volume = 1; + this.__defineSetter__('volume', function() {}); + }; + + Html5.TEST_VID = new ConstVolumeVideo(); + + assert.ok(!Html5.canControlVolume()); + Html5.TEST_VID = testVid; +}); + +QUnit.test('test playbackRate', function(assert) { + // Android 2.3 always returns 0 for playback rate + if (!Html5.canControlPlaybackRate()) { + assert.ok(true, 'Playback rate is not supported'); + return; + } + + tech.createEl(); + + tech.el().playbackRate = 1.25; + assert.strictEqual(tech.playbackRate(), 1.25, 'can be changed from the element'); + + tech.setPlaybackRate(0.75); + assert.strictEqual(tech.playbackRate(), 0.75, 'can be changed from the API'); +}); + +QUnit.test('test defaultPlaybackRate', function(assert) { + // Android 2.3 always returns 0 for playback rate + if (!Html5.canControlPlaybackRate()) { + assert.ok(true, 'Playback rate is not supported'); + return; + } + + tech.createEl(); + + tech.el().defaultPlaybackRate = 1.25; + assert.strictEqual(tech.defaultPlaybackRate(), 1.25, 'can be changed from the element'); + + tech.setDefaultPlaybackRate(0.75); + assert.strictEqual(tech.defaultPlaybackRate(), 0.75, 'can be changed from the API'); +}); + +QUnit.test('blacklist playbackRate support on older versions of Chrome on Android', function(assert) { + if (!Html5.canControlPlaybackRate()) { + assert.ok(true, 'playbackRate is not supported'); + return; + } + + // Reset playbackrate - Firefox's rounding of playbackRate causes the rate not to change in canControlPlaybackRate() after a few instances + Html5.TEST_VID.playbackRate = 1; + + const oldIsAndroid = browser.IS_ANDROID; + const oldIsChrome = browser.IS_CHROME; + const oldChromeVersion = browser.CHROME_VERSION; + + browser.stub_IS_ANDROID(true); + browser.stub_IS_CHROME(true); + browser.stub_CHROME_VERSION(50); + assert.strictEqual(Html5.canControlPlaybackRate(), false, 'canControlPlaybackRate should return false on older Chrome'); + + browser.stub_CHROME_VERSION(58); + assert.strictEqual(Html5.canControlPlaybackRate(), true, 'canControlPlaybackRate should return true on newer Chrome'); + + browser.stub_IS_ANDROID(oldIsAndroid); + browser.stub_IS_CHROME(oldIsChrome); + browser.stub_CHROME_VERSION(oldChromeVersion); +}); + +QUnit.test('test volume', function(assert) { + if (!Html5.canControlVolume()) { + assert.ok(true, 'Volume is not supported'); + return; + } + + tech.createEl(); + + tech.el().volume = 0.5; + assert.strictEqual(tech.volume(), 0.5, 'can be changed from the element'); + + tech.setVolume(1); + assert.strictEqual(tech.volume(), 1, 'can be changed from the API'); +}); + +QUnit.test('test defaultMuted', function(assert) { + tech.createEl(); + + tech.el().defaultMuted = true; + assert.strictEqual(tech.defaultMuted(), true, 'can be changed from the element'); + + tech.setDefaultMuted(false); + assert.strictEqual(tech.defaultMuted(), false, 'can be changed from the API'); +}); + +QUnit.test('should export played', function(assert) { + tech.createEl(); + assert.deepEqual(tech.played(), tech.el().played, 'returns the played attribute'); +}); + +QUnit.test('should remove the controls attribute when recreating the element', function(assert) { + player.tagAttributes = { + controls: true + }; + // force custom controls so the test environment is equivalent on iOS + player.options_.nativeControlsForTouch = false; + const el = tech.createEl(); + + // On the iPhone controls are always true + if (!browser.IS_IPHONE) { + assert.ok(!el.controls, 'controls attribute is absent'); + } + + assert.ok(player.tagAttributes.controls, 'tag attribute is still present'); +}); + +QUnit.test('error events may not set the errors property', function(assert) { + assert.equal(tech.error(), undefined, 'no tech-level error'); + tech.trigger('error'); + assert.ok(true, 'no error was thrown'); +}); + +QUnit.test('should have the source handler interface', function(assert) { + assert.ok(Html5.registerSourceHandler, 'has the registerSourceHandler function'); +}); + +QUnit.test('native source handler canPlayType', function(assert) { + // Stub the test video canPlayType (used in canPlayType) to control results + const origCPT = Html5.TEST_VID.canPlayType; + + Html5.TEST_VID.canPlayType = function(type) { + if (type === 'video/mp4') { + return 'maybe'; + } + return ''; + }; + + const canPlayType = Html5.nativeSourceHandler.canPlayType; + + assert.equal( + canPlayType('video/mp4'), + 'maybe', + 'Native source handler reported type support' + ); + assert.equal(canPlayType('foo'), '', 'Native source handler handled bad type'); + + // Reset test video canPlayType + Html5.TEST_VID.canPlayType = origCPT; +}); + +QUnit.test('native source handler canHandleSource', function(assert) { + // Stub the test video canPlayType (used in canHandleSource) to control results + const origCPT = Html5.TEST_VID.canPlayType; + + Html5.TEST_VID.canPlayType = function(type) { + if (type === 'video/mp4') { + return 'maybe'; + } + return ''; + }; + + const canHandleSource = Html5.nativeSourceHandler.canHandleSource; + + assert.equal( + canHandleSource({ type: 'video/mp4', src: 'video.flv' }, {}), + 'maybe', + 'Native source handler reported type support' + ); + assert.equal( + canHandleSource({ src: 'http://www.example.com/video.mp4' }, {}), + 'maybe', + 'Native source handler reported extension support' + ); + assert.equal( + canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo&token=bar' }, {}), + 'maybe', + 'Native source handler reported extension support' + ); + assert.equal( + canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo' }, {}), + 'maybe', + 'Native source handler reported extension support' + ); + + // Test for issue videojs/video.js#1785 and other potential failures + assert.equal( + canHandleSource({ src: '' }, {}), + '', + 'Native source handler handled empty src' + ); + assert.equal( + canHandleSource({}, {}), + '', + 'Native source handler handled empty object' + ); + assert.equal( + canHandleSource({ src: 'foo' }, {}), + '', + 'Native source handler handled bad src' + ); + assert.equal( + canHandleSource({ type: 'foo' }, {}), + '', + 'Native source handler handled bad type' + ); + + // Reset test video canPlayType + Html5.TEST_VID.canPlayType = origCPT; +}); + +if (Html5.supportsNativeTextTracks()) { + QUnit.test('add native textTrack listeners on startup', function(assert) { + const adds = []; + const rems = []; + const tt = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.textTracks = tt; + + const htmlTech = new Html5({el}); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + + htmlTech.dispose(); + }); + + QUnit.test('does not add native textTrack listeners when disabled', function(assert) { + const events = []; + const tt = { + length: 0, + addEventListener: (type, fn) => events.push([type, fn]), + removeEventListener: (type, fn) => events.push([type, fn]) + }; + const el = document.createElement('video'); + + Object.defineProperty(el, 'textTracks', { + get: () => tt + }); + + const htmlTech = new Html5({el, nativeTextTracks: false}); + + assert.equal(events.length, 0, 'no listeners added'); + + const htmlTechAlternate = new Html5({el, nativeCaptions: false}); + + assert.equal(events.length, 0, 'no listeners added'); + + htmlTech.dispose(); + htmlTechAlternate.dispose(); + }); + + QUnit.test('remove all tracks from emulated list on dispose', function(assert) { + const adds = []; + const rems = []; + const tt = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.textTracks = tt; + + const htmlTech = new Html5({el}); + + htmlTech.dispose(); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + assert.equal(rems[0][0], 'change', 'change event handler removed'); + assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed'); + assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed'); + assert.equal(adds[0][0], rems[0][0], 'change event handler removed'); + assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed'); + assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed'); + }); +} + +if (Html5.supportsNativeAudioTracks()) { + QUnit.test('add native audioTrack listeners on startup', function(assert) { + const adds = []; + const rems = []; + const at = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.audioTracks = at; + + const htmlTech = new Html5({el}); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + + htmlTech.dispose(); + }); + + QUnit.test('does not add native audioTrack listeners when disabled', function(assert) { + const events = []; + const at = { + length: 0, + addEventListener: (type, fn) => events.push([type, fn]), + removeEventListener: (type, fn) => events.push([type, fn]) + }; + const el = document.createElement('div'); + + el.audioTracks = at; + + const htmlTech = new Html5({el, nativeAudioTracks: false}); + + assert.equal(events.length, 0, 'no listeners added'); + + htmlTech.dispose(); + }); + + QUnit.test('remove all tracks from emulated list on dispose', function(assert) { + const adds = []; + const rems = []; + const at = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.audioTracks = at; + + const htmlTech = new Html5({el}); + + htmlTech.dispose(); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + assert.equal(rems[0][0], 'change', 'change event handler removed'); + assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed'); + assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed'); + assert.equal(adds[0][0], rems[0][0], 'change event handler removed'); + assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed'); + assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed'); + }); + + QUnit.test('should use overrideNativeTracks on audio correctly', function(assert) { + assert.expect(8); + + const adds = []; + const rems = []; + const at = { + length: 0, + addEventListener: (type, fn) => { + adds.push({ type, fn }); + }, + removeEventListener: (type, fn) => { + rems.push({ type, fn }); + } + }; + + const el = document.createElement('div'); + + el.audioTracks = at; + + const htmlTech = new Html5({el}); + + assert.equal( + adds.length, 3, + 'should have added change, remove, add listeners' + ); + assert.equal( + rems.length, 0, + 'no listeners should be removed' + ); + + htmlTech.overrideNativeAudioTracks(true); + + assert.equal( + adds.length, 3, + 'should not have added additional listeners' + ); + assert.equal( + rems.length, 3, + 'should have removed previous three listeners' + ); + + htmlTech.overrideNativeAudioTracks(true); + + assert.equal( + adds.length, 3, + 'no state change so do not add listeners' + ); + assert.equal( + rems.length, 3, + 'no state change so do not remove listeners' + ); + + htmlTech.overrideNativeAudioTracks(false); + + assert.equal( + adds.length, 6, + 'should add listeners because native tracks should be proxied' + ); + assert.equal( + rems.length, 3, + 'should not remove listeners because there where none added on previous state' + ); + + htmlTech.dispose(); + }); +} + +if (Html5.supportsNativeVideoTracks()) { + QUnit.test('add native videoTrack listeners on startup', function(assert) { + const adds = []; + const rems = []; + const vt = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.videoTracks = vt; + + const htmlTech = new Html5({el}); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + + htmlTech.dispose(); + }); + + QUnit.test('does not add native audioTrack listeners when disabled', function(assert) { + const events = []; + const vt = { + length: 0, + addEventListener: (type, fn) => events.push([type, fn]), + removeEventListener: (type, fn) => events.push([type, fn]) + }; + const el = document.createElement('div'); + + el.videoTracks = vt; + + const htmlTech = new Html5({el, nativeVideoTracks: false}); + + assert.equal(events.length, 0, 'no listeners added'); + + htmlTech.dispose(); + }); + + QUnit.test('remove all tracks from emulated list on dispose', function(assert) { + const adds = []; + const rems = []; + const vt = { + length: 0, + addEventListener: (type, fn) => adds.push([type, fn]), + removeEventListener: (type, fn) => rems.push([type, fn]) + }; + const el = document.createElement('div'); + + el.videoTracks = vt; + + const htmlTech = new Html5({el}); + + htmlTech.dispose(); + + assert.equal(adds[0][0], 'change', 'change event handler added'); + assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added'); + assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added'); + assert.equal(rems[0][0], 'change', 'change event handler removed'); + assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed'); + assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed'); + assert.equal(adds[0][0], rems[0][0], 'change event handler removed'); + assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed'); + assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed'); + }); + + QUnit.test('should use overrideNativeTracks on video correctly', function(assert) { + assert.expect(8); + + const adds = []; + const rems = []; + const vt = { + length: 0, + addEventListener: (type, fn) => { + adds.push({ type, fn }); + }, + removeEventListener: (type, fn) => { + rems.push({ type, fn }); + } + }; + const el = document.createElement('div'); + + el.videoTracks = vt; + + const htmlTech = new Html5({el}); + + assert.equal( + adds.length, 3, + 'should have added change, remove, add listeners' + ); + assert.equal( + rems.length, 0, + 'no listeners should be removed' + ); + + htmlTech.overrideNativeVideoTracks(true); + + assert.equal( + adds.length, 3, + 'should not have added additional listeners' + ); + assert.equal( + rems.length, 3, + 'should have removed previous three listeners' + ); + + htmlTech.overrideNativeVideoTracks(true); + + assert.equal( + adds.length, 3, + 'no state change so do not add listeners' + ); + assert.equal( + rems.length, 3, + 'no state change so do not remove listeners' + ); + + htmlTech.overrideNativeVideoTracks(false); + + assert.equal( + adds.length, 6, + 'should add listeners because native tracks should be proxied' + ); + assert.equal( + rems.length, 3, + 'should not remove listeners because there where none added on previous state' + ); + + htmlTech.dispose(); + }); +} + +QUnit.test('should always return currentSource_ if set', function(assert) { + const currentSrc = Html5.prototype.currentSrc; + + assert.equal( + currentSrc.call({el_: {currentSrc: 'test1'}}), + 'test1', + 'sould return source from element if nothing else set' + ); + assert.equal( + currentSrc.call({currentSource_: {src: 'test2'}}), + 'test2', + 'sould return source from currentSource_, if nothing else set' + ); + assert.equal( + currentSrc.call({currentSource_: {src: 'test2'}, + el_: {currentSrc: 'test1'}}), + 'test2', + 'sould return source from source set, not from element' + ); +}); + +QUnit.test('should fire makeup events when a video tag is initialized late', function(assert) { + const lateInit = Html5.prototype.handleLateInit_; + let triggeredEvents = []; + const mockHtml5 = { + readyListeners: [], + ready(listener) { + this.readyListeners.push(listener); + }, + triggerReady() { + this.readyListeners.forEach(function(listener) { + listener.call(this); + }, this); + }, + trigger(type) { + triggeredEvents.push(type); + }, + on() {}, + off() {} + }; + + function testStates(statesObject, expectedEvents) { + lateInit.call(mockHtml5, statesObject); + mockHtml5.triggerReady(); + assert.deepEqual( + triggeredEvents, + expectedEvents, + 'wrong events triggered for ' + + `networkState:${statesObject.networkState} ` + + `and readyState:${statesObject.readyState || 'no readyState'}` + ); + + // reset mock + triggeredEvents = []; + mockHtml5.readyListeners = []; + } + + // Network States + testStates({ networkState: 0, readyState: 0 }, []); + testStates({ networkState: 1, readyState: 0 }, ['loadstart']); + testStates({ networkState: 2, readyState: 0 }, ['loadstart']); + testStates({ networkState: 3, readyState: 0 }, []); + + // Ready States + testStates({ networkState: 1, readyState: 0 }, ['loadstart']); + testStates({ networkState: 1, readyState: 1 }, ['loadstart', 'loadedmetadata']); + testStates( + { networkState: 1, readyState: 2 }, + ['loadstart', 'loadedmetadata', 'loadeddata'] + ); + testStates( + { networkState: 1, readyState: 3 }, + ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay'] + ); + testStates( + { networkState: 1, readyState: 4 }, + ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough'] + ); +}); + +QUnit.test('Html5.resetMediaElement should remove sources and call load', function(assert) { + let selector; + const removedChildren = []; + let removedAttribute; + let loaded; + const children = ['source1', 'source2', 'source3']; + const testEl = { + querySelectorAll(input) { + selector = input; + return children; + }, + + removeChild(child) { + removedChildren.push(child); + }, + + removeAttribute(attr) { + removedAttribute = attr; + }, + + load() { + loaded = true; + } + }; + + Html5.resetMediaElement(testEl); + assert.equal(selector, 'source', 'we got the source elements from the test el'); + assert.deepEqual( + removedChildren, + children.reverse(), + 'we removed the children that were present' + ); + assert.equal(removedAttribute, 'src', 'we removed the src attribute'); + assert.ok(loaded, 'we called load on the element'); +}); + +QUnit.test('Html5#reset calls Html5.resetMediaElement when called', function(assert) { + const oldResetMedia = Html5.resetMediaElement; + let resetEl; + + Html5.resetMediaElement = (el) => { + resetEl = el; + }; + + const el = {}; + + Html5.prototype.reset.call({el_: el}); + + assert.equal(resetEl, el, 'we called resetMediaElement with the tech\'s el'); + + Html5.resetMediaElement = oldResetMedia; +}); + +QUnit.test('When Android Chrome reports Infinity duration with currentTime 0, return NaN', function(assert) { + const oldIsAndroid = browser.IS_ANDROID; + const oldIsChrome = browser.IS_CHROME; + const oldEl = tech.el_; + + browser.stub_IS_ANDROID(true); + browser.stub_IS_CHROME(true); + + tech.el_ = { + duration: Infinity, + currentTime: 0 + }; + assert.ok(Number.isNaN(tech.duration()), 'returned NaN with currentTime 0'); + + browser.stub_IS_ANDROID(oldIsAndroid); + browser.stub_IS_CHROME(oldIsChrome); + tech.el_ = oldEl; +}); + +QUnit.test('supports getting available media playback quality metrics', function(assert) { + const origPerformance = window.performance; + const oldEl = tech.el_; + const videoPlaybackQuality = { + creationTime: 1, + corruptedVideoFrames: 2, + droppedVideoFrames: 3, + totalVideoFrames: 5 + }; + + tech.el_ = { + getVideoPlaybackQuality: () => videoPlaybackQuality + }; + assert.deepEqual( + tech.getVideoPlaybackQuality(), + videoPlaybackQuality, + 'uses native implementation when supported' + ); + tech.el_ = { + webkitDroppedFrameCount: 1, + webkitDecodedFrameCount: 2 + }; + window.performance = { + now: () => 4 + }; + assert.deepEqual( + tech.getVideoPlaybackQuality(), + { droppedVideoFrames: 1, totalVideoFrames: 2, creationTime: 4 }, + 'uses webkit prefixed metrics and performance.now when supported' + ); + + tech.el_ = {}; + window.performance = void 0; + assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'empty object when not supported'); + + window.performance = { + now: () => 7 + }; + assert.deepEqual( + tech.getVideoPlaybackQuality(), + { creationTime: 7 }, + 'only creation time when it\'s the only piece available' + ); + + tech.el_ = { + getVideoPlaybackQuality: () => videoPlaybackQuality, + webkitDroppedFrameCount: 1, + webkitDecodedFrameCount: 2 + }; + assert.deepEqual( + tech.getVideoPlaybackQuality(), + videoPlaybackQuality, + 'prefers native implementation when supported' + ); + + tech.el_ = oldEl; + window.performance = origPerformance; +}); + +QUnit.test('featuresVideoFrameCallback is false for audio elements', function(assert) { + const el = document.createElement('audio'); + const audioTech = new Html5({ + el, + source: [{src: 'https://example.org/stream.m3u8'}] + }); + + assert.strictEqual(audioTech.featuresVideoFrameCallback, false, 'Html5 with audio element should not support rvf'); + + audioTech.dispose(); +}); + +QUnit.test('featuresVideoFrameCallback is false for Safari DRM', function(assert) { + // Looking for `super.requestVideoFrameCallback()` being called + const spy = sinon.spy(Object.getPrototypeOf(Object.getPrototypeOf(tech)), 'requestVideoFrameCallback'); + + tech.featuresVideoFrameCallback = true; + + try { + tech.el_.webkitKeys = {}; + tech.requestVideoFrameCallback(function() {}); + + assert.ok(spy.calledOnce, false, 'rvf fallback used'); + } catch (e) { + // video.webkitKeys isn't writable on Safari, so relying on the mocked property on other browsers + assert.ok(true, 'skipped because webkitKeys not writable'); + } +}); + +QUnit.test('supportsFullScreen is always with `webkitEnterFullScreen`', function(assert) { + const oldEl = tech.el_; + + tech.el_ = { + webkitEnterFullScreen: () => {} + }; + + assert.ok(tech.supportsFullScreen(), 'supportsFullScreen() true with webkitEnterFullScreen'); + + tech.el_ = oldEl; +}); + +QUnit.test('addSourceElement adds a valid source element with srcUrl and mimeType', function(assert) { + const videoEl = document.createElement('video'); + + tech.el_ = videoEl; + + const srcUrl = 'http://example.com/video.mp4'; + const mimeType = 'video/mp4'; + + const added = tech.addSourceElement(srcUrl, mimeType); + const sourceElement = videoEl.querySelector('source'); + + assert.ok(added, 'Returned true'); + assert.ok(sourceElement, 'A source element was added'); + assert.equal(sourceElement.src, srcUrl, 'Source element has correct src'); + assert.equal(sourceElement.type, mimeType, 'Source element has correct type'); +}); + +QUnit.test('addSourceElement adds a valid source element without a mimeType', function(assert) { + const videoEl = document.createElement('video'); + + tech.el_ = videoEl; + + const srcUrl = 'http://example.com/video2.mp4'; + + const added = tech.addSourceElement(srcUrl); + const sourceElement = videoEl.querySelector('source'); + + assert.ok(added, 'Returned true'); + assert.ok(sourceElement, 'A source element was added even without a type'); + assert.equal(sourceElement.src, srcUrl, 'Source element has correct src'); + assert.notOk(sourceElement.type, 'Source element does not have a type attribute'); +}); + +QUnit.test('addSourceElement does not add a source element for invalid source URL', function(assert) { + const videoEl = document.createElement('video'); + + tech.el_ = videoEl; + + const added = tech.addSourceElement(''); + const sourceElement = videoEl.querySelector('source'); + + assert.notOk(added, 'Returned false'); + assert.notOk(sourceElement, 'No source element was added for missing src'); +}); + +QUnit.test('removeSourceElement removes a source element by its URL', function(assert) { + const videoEl = document.createElement('video'); + + tech.el_ = videoEl; + + tech.addSourceElement('http://example.com/video1.mp4'); + tech.addSourceElement('http://example.com/video2.mp4'); + + let sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]'); + + assert.ok(sourceElement, 'Source element for video1.mp4 was added'); + + const removed = tech.removeSourceElement('http://example.com/video1.mp4'); + + assert.ok(removed, 'Source element was successfully removed'); + sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]'); + assert.notOk(sourceElement, 'Source element for video1.mp4 was removed'); +}); + +QUnit.test('removeSourceElement does not remove a source element if URL does not match', function(assert) { + const videoEl = document.createElement('video'); + + tech.el_ = videoEl; + + tech.addSourceElement('http://example.com/video.mp4'); + + const removed = tech.removeSourceElement('http://example.com/invalid.mp4'); + + assert.notOk(removed, 'No source element was removed for non-matching URL'); +}); |
