1
0
mirror of https://github.com/videojs/video.js.git synced 2024-11-24 08:42:25 +02:00
video.js/test/unit/player.test.js
heff 1bfe0b4fed Clean up and documentation of src/js/video.js and DOM functions
Preparing to export utility functions on the videojs object

closes #2182

Change el() to getEl() for consistency

Cleaned up DOM functions library

Clean up and document videojs object API

Fixed mergeOptions to modify the first object instead of a copy

More cleanup of the main video.js file and documentation

Fixed issues with mergeOptions

Cleaned up the addLanguage function

Removed unnecessary underscores in private module vars
2015-05-21 11:33:12 -07:00

794 lines
25 KiB
JavaScript

import Player from '../../src/js/player.js';
import videojs from '../../src/js/video.js';
import globalOptions from '../../src/js/global-options.js';
import * as Dom from '../../src/js/utils/dom.js';
import * as browser from '../../src/js/utils/browser.js';
import log from '../../src/js/utils/log.js';
import MediaError from '../../src/js/media-error.js';
import Html5 from '../../src/js/tech/html5.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
import css from 'css';
q.module('Player', {
'setup': function() {
this.clock = sinon.useFakeTimers();
},
'teardown': function() {
this.clock.restore();
}
});
// Compiler doesn't like using 'this' in setup/teardown.
// module("Player", {
// /**
// * @this {*}
// */
// setup: function(){
// window.player1 = true; // using window works
// },
// /**
// * @this {*}
// */
// teardown: function(){
// // if (this.player && this.player.el() !== null) {
// // this.player.dispose();
// // this.player = null;
// // }
// }
// });
// Object.size = function(obj) {
// var size = 0, key;
// for (key in obj) {
// console.log('key', key)
// if (obj.hasOwnProperty(key)) size++;
// }
// return size;
// };
test('should create player instance that inherits from component and dispose it', function(){
var player = TestHelpers.makePlayer();
ok(player.el().nodeName === 'DIV');
ok(player.on, 'component function exists');
player.dispose();
ok(player.el() === null, 'element disposed');
});
test('should accept options from multiple sources and override in correct order', function(){
// For closure compiler to work, all reference to the prop have to be the same type
// As in options['attr'] or options.attr. Compiler will minimize each separately.
// Since we're using setAttribute which requires a string, we have to use the string
// version of the key for all version.
// Set a global option
globalOptions['attr'] = 1;
var tag0 = TestHelpers.makeTag();
var player0 = new Player(tag0);
ok(player0.options_['attr'] === 1, 'global option was set');
player0.dispose();
// Set a tag level option
var tag1 = TestHelpers.makeTag();
tag1.setAttribute('attr', 'asdf'); // Attributes must be set as strings
var player1 = new Player(tag1);
ok(player1.options_['attr'] === 'asdf', 'Tag options overrode global options');
player1.dispose();
// Set a tag level option
var tag2 = TestHelpers.makeTag();
tag2.setAttribute('attr', 'asdf');
var player2 = new Player(tag2, { 'attr': 'fdsa' });
ok(player2.options_['attr'] === 'fdsa', 'Init options overrode tag and global options');
player2.dispose();
});
test('should get tag, source, and track settings', function(){
// Partially tested in lib->getElAttributes
var fixture = document.getElementById('qunit-fixture');
var html = '<video id="example_1" class="video-js" autoplay preload="none">';
html += '<source src="http://google.com" type="video/mp4">';
html += '<source src="http://google.com" type="video/webm">';
html += '<track kind="captions" attrtest>';
html += '</video>';
fixture.innerHTML += html;
var tag = document.getElementById('example_1');
var player = TestHelpers.makePlayer({}, tag);
ok(player.options_['autoplay'] === true);
ok(player.options_['preload'] === 'none'); // No extern. Use string.
ok(player.options_['id'] === 'example_1');
ok(player.options_['sources'].length === 2);
ok(player.options_['sources'][0].src === 'http://google.com');
ok(player.options_['sources'][0].type === 'video/mp4');
ok(player.options_['sources'][1].type === 'video/webm');
ok(player.options_['tracks'].length === 1);
ok(player.options_['tracks'][0]['kind'] === 'captions'); // No extern
ok(player.options_['tracks'][0]['attrtest'] === '');
ok(player.el().className.indexOf('video-js') !== -1, 'transferred class from tag to player div');
ok(player.el().id === 'example_1', 'transferred id from tag to player div');
ok(Player.players[player.id()] === player, 'player referenceable from global list');
ok(tag.id !== player.id, 'tag ID no longer is the same as player ID');
ok(tag.className !== player.el().className, 'tag classname updated');
player.dispose();
ok(tag['player'] !== player, 'tag player ref killed');
ok(!Player.players['example_1'], 'global player ref killed');
ok(player.el() === null, 'player el killed');
});
test('should asynchronously fire error events during source selection', function() {
expect(2);
sinon.stub(log, 'error');
var player = TestHelpers.makePlayer({
'techOrder': ['foo'],
'sources': [
{ 'src': 'http://vjs.zencdn.net/v/oceans.mp4', 'type': 'video/mp4' }
]
});
ok(player.options_['techOrder'][0] === 'foo', 'Foo listed as the only tech');
player.on('error', function(e) {
ok(player.error().code === 4, 'Source could not be played error thrown');
});
this.clock.tick(1);
player.dispose();
log.error.restore();
});
test('should set the width, height, and aspect ratio via a css class', function(){
let player = TestHelpers.makePlayer();
let getStyleText = function(styleEl){
return (styleEl.styleSheet && styleEl.styleSheet.cssText) || styleEl.innerHTML;
};
ok(player.styleEl_.parentNode === player.el(), 'player has a style element');
ok(!getStyleText(player.styleEl_), 'style element should be empty when the player is given no dimensions');
let rules;
function getStyleRules(){
const styleText = getStyleText(player.styleEl_);
const cssAST = css.parse(styleText);
const styleRules = {};
cssAST.stylesheet.rules.forEach(function(ruleAST){
let selector = ruleAST.selectors.join(' ');
styleRules[selector] = {};
let rule = styleRules[selector];
ruleAST.declarations.forEach(function(dec){
rule[dec.property] = dec.value;
});
});
return styleRules;
}
// Set only the width
player.width(100);
rules = getStyleRules();
equal(rules['.example_1-dimensions'].width, '100px', 'style width should equal the supplied width in pixels');
equal(rules['.example_1-dimensions'].height, '56.25px', 'style height should match the default aspect ratio of the width');
// Set the height
player.height(200);
rules = getStyleRules();
equal(rules['.example_1-dimensions'].height, '200px', 'style height should match the supplied height in pixels');
// Reset the width and height to defaults
player.width('');
player.height('');
rules = getStyleRules();
equal(rules['.example_1-dimensions'].width, '300px', 'supplying an empty string should reset the width');
equal(rules['.example_1-dimensions'].height, '168.75px', 'supplying an empty string should reset the height');
// Switch to fluid mode
player.fluid(true);
rules = getStyleRules();
ok(player.hasClass('vjs-fluid'), 'the vjs-fluid class should be added to the player');
equal(rules['.example_1-dimensions.vjs-fluid']['padding-top'], '56.25%', 'fluid aspect ratio should match the default aspect ratio');
// Change the aspect ratio
player.aspectRatio('4:1');
rules = getStyleRules();
equal(rules['.example_1-dimensions.vjs-fluid']['padding-top'], '25%', 'aspect ratio percent should match the newly set aspect ratio');
});
test('should wrap the original tag in the player div', function(){
var tag = TestHelpers.makeTag();
var container = document.createElement('div');
var fixture = document.getElementById('qunit-fixture');
container.appendChild(tag);
fixture.appendChild(container);
var player = new Player(tag);
var el = player.el();
ok(el.parentNode === container, 'player placed at same level as tag');
// Tag may be placed inside the player element or it may be removed from the DOM
ok(tag.parentNode !== container, 'tag removed from original place');
player.dispose();
});
test('should set and update the poster value', function(){
var tag, poster, updatedPoster, player;
poster = 'http://example.com/poster.jpg';
updatedPoster = 'http://example.com/updated-poster.jpg';
tag = TestHelpers.makeTag();
tag.setAttribute('poster', poster);
player = TestHelpers.makePlayer({}, tag);
equal(player.poster(), poster, 'the poster property should equal the tag attribute');
var pcEmitted = false;
player.on('posterchange', function(){
pcEmitted = true;
});
player.poster(updatedPoster);
ok(pcEmitted, 'posterchange event was emitted');
equal(player.poster(), updatedPoster, 'the updated poster is returned');
player.dispose();
});
// hasStarted() is equivalent to the "show poster flag" in the
// standard, for the purpose of displaying the poster image
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play
test('should hide the poster when play is called', function() {
var player = TestHelpers.makePlayer({
poster: 'https://example.com/poster.jpg'
});
equal(player.hasStarted(), false, 'the show poster flag is true before play');
player.play();
equal(player.hasStarted(), true, 'the show poster flag is false after play');
player.tech.trigger('loadstart');
equal(player.hasStarted(),
false,
'the resource selection algorithm sets the show poster flag to true');
player.play();
equal(player.hasStarted(), true, 'the show poster flag is false after play');
});
test('should load a media controller', function(){
var player = TestHelpers.makePlayer({
preload: 'none',
sources: [
{ src: 'http://google.com', type: 'video/mp4' },
{ src: 'http://google.com', type: 'video/webm' }
]
});
ok(player.el().children[0].className.indexOf('vjs-tech') !== -1, 'media controller loaded');
player.dispose();
});
test('should be able to initialize player twice on the same tag using string reference', function() {
var videoTag = TestHelpers.makeTag();
var id = videoTag.id;
var fixture = document.getElementById('qunit-fixture');
fixture.appendChild(videoTag);
var player = videojs(videoTag.id);
ok(player, 'player is created');
player.dispose();
ok(!document.getElementById(id), 'element is removed');
videoTag = TestHelpers.makeTag();
fixture.appendChild(videoTag);
//here we receive cached version instead of real
player = videojs(videoTag.id);
//here it triggers error, because player was destroyed already after first dispose
player.dispose();
});
test('should set controls and trigger events', function() {
//expect(6);
var player = TestHelpers.makePlayer({ 'controls': false });
ok(player.controls() === false, 'controls set through options');
var hasDisabledClass = player.el().className.indexOf('vjs-controls-disabled');
ok(hasDisabledClass !== -1, 'Disabled class added to player');
player.controls(true);
ok(player.controls() === true, 'controls updated');
var hasEnabledClass = player.el().className.indexOf('vjs-controls-enabled');
ok(hasEnabledClass !== -1, 'Disabled class added to player');
player.on('controlsenabled', function(){
ok(true, 'enabled fired once');
});
player.on('controlsdisabled', function(){
ok(true, 'disabled fired once');
});
player.controls(false);
//player.controls(true);
// Check for unnecessary events
//player.controls(true);
player.dispose();
});
// Can't figure out how to test fullscreen events with tests
// Browsers aren't triggering the events at least
// asyncTest('should trigger the fullscreenchange event', function() {
// expect(3);
// var player = TestHelpers.makePlayer();
// player.on('fullscreenchange', function(){
// ok(true, 'fullscreenchange event fired');
// ok(this.isFullscreen() === true, 'isFullscreen is true');
// ok(this.el().className.indexOf('vjs-fullscreen') !== -1, 'vjs-fullscreen class added');
// player.dispose();
// start();
// });
// player.requestFullscreen();
// });
test('should toggle user the user state between active and inactive', function(){
var player = TestHelpers.makePlayer({});
expect(9);
ok(player.userActive(), 'User should be active at player init');
player.on('userinactive', function(){
ok(true, 'userinactive event triggered');
});
player.on('useractive', function(){
ok(true, 'useractive event triggered');
});
player.userActive(false);
ok(player.userActive() === false, 'Player state changed to inactive');
ok(player.el().className.indexOf('vjs-user-active') === -1, 'Active class removed');
ok(player.el().className.indexOf('vjs-user-inactive') !== -1, 'Inactive class added');
player.userActive(true);
ok(player.userActive() === true, 'Player state changed to active');
ok(player.el().className.indexOf('vjs-user-inactive') === -1, 'Inactive class removed');
ok(player.el().className.indexOf('vjs-user-active') !== -1, 'Active class added');
player.dispose();
});
test('should add a touch-enabled classname when touch is supported', function(){
var player;
expect(1);
// Fake touch support. Real touch support isn't needed for this test.
var origTouch = browser.TOUCH_ENABLED;
browser.TOUCH_ENABLED = true;
player = TestHelpers.makePlayer({});
ok(player.el().className.indexOf('vjs-touch-enabled'), 'touch-enabled classname added');
browser.TOUCH_ENABLED = origTouch;
player.dispose();
});
test('should allow for tracking when native controls are used', function(){
var player = TestHelpers.makePlayer({});
expect(6);
// Make sure native controls is false before starting test
player.usingNativeControls(false);
player.on('usingnativecontrols', function(){
ok(true, 'usingnativecontrols event triggered');
});
player.on('usingcustomcontrols', function(){
ok(true, 'usingcustomcontrols event triggered');
});
player.usingNativeControls(true);
ok(player.usingNativeControls() === true, 'Using native controls is true');
ok(player.el().className.indexOf('vjs-using-native-controls') !== -1, 'Native controls class added');
player.usingNativeControls(false);
ok(player.usingNativeControls() === false, 'Using native controls is false');
ok(player.el().className.indexOf('vjs-using-native-controls') === -1, 'Native controls class removed');
player.dispose();
});
// test('should use custom message when encountering an unsupported video type',
// function() {
// videojs.options['notSupportedMessage'] = 'Video no go <a href="">link</a>';
// var fixture = document.getElementById('qunit-fixture');
// var html =
// '<video id="example_1">' +
// '<source src="fake.foo" type="video/foo">' +
// '</video>';
// fixture.innerHTML += html;
// var tag = document.getElementById('example_1');
// var player = new Player(tag);
// var incompatibilityMessage = player.el().getElementsByTagName('p')[0];
// // ie8 capitalizes tag names
// equal(incompatibilityMessage.innerHTML.toLowerCase(), 'video no go <a href="">link</a>');
// player.dispose();
// });
test('should register players with generated ids', function(){
var fixture, video, player, id;
fixture = document.getElementById('qunit-fixture');
video = document.createElement('video');
video.className = 'vjs-default-skin video-js';
fixture.appendChild(video);
player = new Player(video);
id = player.el().id;
equal(player.el().id, player.id(), 'the player and element ids are equal');
ok(Player.players[id], 'the generated id is registered');
});
test('should not add multiple first play events despite subsequent loads', function() {
expect(1);
var player = TestHelpers.makePlayer({});
player.on('firstplay', function(){
ok(true, 'First play should fire once.');
});
// Checking to make sure onLoadStart removes first play listener before adding a new one.
player.tech.trigger('loadstart');
player.tech.trigger('loadstart');
player.tech.trigger('play');
});
test('should fire firstplay after resetting the player', function() {
var player = TestHelpers.makePlayer({});
var fpFired = false;
player.on('firstplay', function(){
fpFired = true;
});
// init firstplay listeners
player.tech.trigger('loadstart');
player.tech.trigger('play');
ok(fpFired, 'First firstplay fired');
// reset the player
player.tech.trigger('loadstart');
fpFired = false;
player.tech.trigger('play');
ok(fpFired, 'Second firstplay fired');
// the play event can fire before the loadstart event.
// in that case we still want the firstplay even to fire.
player.tech.paused = function(){ return false; };
fpFired = false;
// reset the player
player.tech.trigger('loadstart');
// player.tech.trigger('play');
ok(fpFired, 'Third firstplay fired');
});
test('should remove vjs-has-started class', function(){
expect(3);
var player = TestHelpers.makePlayer({});
player.tech.trigger('loadstart');
player.tech.trigger('play');
ok(player.el().className.indexOf('vjs-has-started') !== -1, 'vjs-has-started class added');
player.tech.trigger('loadstart');
ok(player.el().className.indexOf('vjs-has-started') === -1, 'vjs-has-started class removed');
player.tech.trigger('play');
ok(player.el().className.indexOf('vjs-has-started') !== -1, 'vjs-has-started class added again');
});
test('should add and remove vjs-ended class', function() {
expect(4);
var player = TestHelpers.makePlayer({});
player.tech.trigger('loadstart');
player.tech.trigger('play');
player.tech.trigger('ended');
ok(player.el().className.indexOf('vjs-ended') !== -1, 'vjs-ended class added');
player.tech.trigger('play');
ok(player.el().className.indexOf('vjs-ended') === -1, 'vjs-ended class removed');
player.tech.trigger('ended');
ok(player.el().className.indexOf('vjs-ended') !== -1, 'vjs-ended class re-added');
player.tech.trigger('loadstart');
ok(player.el().className.indexOf('vjs-ended') === -1, 'vjs-ended class removed');
});
test('player should handle different error types', function(){
expect(8);
var player = TestHelpers.makePlayer({});
var testMsg = 'test message';
// prevent error log messages in the console
sinon.stub(log, 'error');
// error code supplied
function errCode(){
equal(player.error().code, 1, 'error code is correct');
}
player.on('error', errCode);
player.error(1);
player.off('error', errCode);
// error instance supplied
function errInst(){
equal(player.error().code, 2, 'MediaError code is correct');
equal(player.error().message, testMsg, 'MediaError message is correct');
}
player.on('error', errInst);
player.error(new MediaError({ code: 2, message: testMsg }));
player.off('error', errInst);
// error message supplied
function errMsg(){
equal(player.error().code, 0, 'error message code is correct');
equal(player.error().message, testMsg, 'error message is correct');
}
player.on('error', errMsg);
player.error(testMsg);
player.off('error', errMsg);
// error config supplied
function errConfig(){
equal(player.error().code, 3, 'error config code is correct');
equal(player.error().message, testMsg, 'error config message is correct');
}
player.on('error', errConfig);
player.error({ code: 3, message: testMsg });
player.off('error', errConfig);
// check for vjs-error classname
ok(player.el().className.indexOf('vjs-error') >= 0, 'player does not have vjs-error classname');
// restore error logging
log.error.restore();
});
test('Data attributes on the video element should persist in the new wrapper element', function() {
var dataId, tag, player;
dataId = 123;
tag = TestHelpers.makeTag();
tag.setAttribute('data-id', dataId);
player = TestHelpers.makePlayer({}, tag);
equal(player.el().getAttribute('data-id'), dataId, 'data-id should be available on the new player element after creation');
});
test('should restore attributes from the original video tag when creating a new element', function(){
var tag, html5Mock, el;
// simulate attributes stored from the original tag
tag = Dom.createEl('video');
tag.setAttribute('preload', 'auto');
tag.setAttribute('autoplay', '');
tag.setAttribute('webkit-playsinline', '');
html5Mock = { options_: { tag: tag } };
// set options that should override tag attributes
html5Mock.options_.preload = 'none';
// create the element
el = Html5.prototype.createEl.call(html5Mock);
equal(el.getAttribute('preload'), 'none', 'attribute was successful overridden by an option');
equal(el.getAttribute('autoplay'), '', 'autoplay attribute was set properly');
equal(el.getAttribute('webkit-playsinline'), '', 'webkit-playsinline attribute was set properly');
});
test('should honor default inactivity timeout', function() {
var player;
var clock = sinon.useFakeTimers();
// default timeout is 2000ms
player = TestHelpers.makePlayer({});
equal(player.userActive(), true, 'User is active on creation');
clock.tick(1800);
equal(player.userActive(), true, 'User is still active');
clock.tick(500);
equal(player.userActive(), false, 'User is inactive after timeout expired');
clock.restore();
});
test('should honor configured inactivity timeout', function() {
var player;
var clock = sinon.useFakeTimers();
// default timeout is 2000ms, set to shorter 200ms
player = TestHelpers.makePlayer({
'inactivityTimeout': 200
});
equal(player.userActive(), true, 'User is active on creation');
clock.tick(150);
equal(player.userActive(), true, 'User is still active');
clock.tick(350);
// make sure user is now inactive after 500ms
equal(player.userActive(), false, 'User is inactive after timeout expired');
clock.restore();
});
test('should honor disabled inactivity timeout', function() {
var player;
var clock = sinon.useFakeTimers();
// default timeout is 2000ms, disable by setting to zero
player = TestHelpers.makePlayer({
'inactivityTimeout': 0
});
equal(player.userActive(), true, 'User is active on creation');
clock.tick(5000);
equal(player.userActive(), true, 'User is still active');
clock.restore();
});
test('should clear pending errors on disposal', function() {
var clock = sinon.useFakeTimers(), player;
player = TestHelpers.makePlayer();
player.src({
src: 'http://example.com/movie.unsupported-format',
type: 'video/unsupported-format'
});
player.dispose();
try {
clock.tick(5000);
} catch (e) {
return ok(!e, 'threw an error: ' + e.message);
}
ok(true, 'did not throw an error after disposal');
});
test('pause is called when player ended event is fired and player is not paused', function() {
var video = document.createElement('video'),
player = TestHelpers.makePlayer({}, video),
pauses = 0;
player.paused = function() {
return false;
};
player.pause = function() {
pauses++;
};
player.tech.trigger('ended');
equal(pauses, 1, 'pause was called');
});
test('pause is not called if the player is paused and ended is fired', function() {
var video = document.createElement('video'),
player = TestHelpers.makePlayer({}, video),
pauses = 0;
player.paused = function() {
return true;
};
player.pause = function() {
pauses++;
};
player.tech.trigger('ended');
equal(pauses, 0, 'pause was not called when ended fired');
});
test('should add an audio class if an audio el is used', function() {
var audio = document.createElement('audio'),
player = TestHelpers.makePlayer({}, audio),
audioClass = 'vjs-audio';
ok(player.el().className.indexOf(audioClass) !== -1, 'added '+ audioClass +' css class');
});
test('should not be scrubbing while not seeking', function(){
var player = TestHelpers.makePlayer();
equal(player.scrubbing(), false, 'player is not scrubbing');
ok(player.el().className.indexOf('scrubbing') === -1, 'scrubbing class is not present');
player.scrubbing(false);
equal(player.scrubbing(), false, 'player is not scrubbing');
});
test('should be scrubbing while seeking', function(){
var player = TestHelpers.makePlayer();
player.scrubbing(true);
equal(player.scrubbing(), true, 'player is scrubbing');
ok(player.el().className.indexOf('scrubbing') !== -1, 'scrubbing class is present');
});
test('should throw on startup no techs are specified', function() {
const techOrder = globalOptions.techOrder;
globalOptions.techOrder = null;
q.throws(function() {
videojs(TestHelpers.makeTag());
}, 'a falsey techOrder should throw');
globalOptions.techOrder = techOrder;
});
test('should have a sensible toJSON that is equivalent to player.options', function() {
const playerOptions = {
html5: {
nativeTextTracks: false
}
};
const player = TestHelpers.makePlayer(playerOptions);
deepEqual(player.toJSON(), player.options(), 'simple player options toJSON produces output equivalent to player.options()');
const playerOptions2 = {
tracks: [{
label: 'English',
srclang: 'en',
src: '../docs/examples/shared/example-captions.vtt',
kind: 'captions'
}]
};
const player2 = TestHelpers.makePlayer(playerOptions2);
playerOptions2.tracks[0].player = player2;
const popts = player2.options();
popts.tracks[0].player = undefined;
deepEqual(player2.toJSON(), popts, 'no circular references');
});