1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-23 02:04:34 +02:00
video.js/test/unit/component.test.js

1641 lines
53 KiB
JavaScript
Raw Permalink Normal View History

/* eslint-env qunit */
import window from 'global/window';
import Component from '../../src/js/component.js';
Broke up Lib and Util into smaller libraries of functions Broke out bind, guid, and element data functions from Lib Separated out more dom functions in to dom.js Broke out URL functions into url.js Removed setLocalStorage since it wasn't being used Moved browser tests out of lib Moved log functions into their own file Removed trim() since it wasn't being used Moved formatTime into its own file Moved round into its own file and renamed roundFloat() Moved capitalize into its own file and renamed as toTitleCase() Moved createTimeRange into its own file Removed Lib.arr.forEach infavor of the native forEach Removed Lib.obj.create in favor of native Object.create (ES6-sham) Removed obj.each in favor of native Object.getOwnPropertyNames().forEach() Removed obj.merge and copy. Using lodash.assign instead. Replaced Lib.obj.isPlain with lodash.isPlainObject Removed Lib.obj.isArray in favor of the native Array.isArray Also removed the lib.js tests file as all tests have been moved or removed. Removed Lib.isEmpty in favor of !Object.getOwnPropertyNames().length Switched Util.mergeOptions and deepMerge to use new mergeOptions() Moved Lib.TEST_VID to Html5.TEST_VID Removed Lib references everywhere. Woo! Attempting to fix sourcemap test errors by setting grunt-browserify version Switched to object.assign from lodash.assign Removed unused 'inherits' dependency Reorganzied test files and added '.test' to file names Combined js/core.js and js/video.js Moved events.js into the utils directory
2015-05-04 01:12:38 +02:00
import * as Dom from '../../src/js/utils/dom.js';
2019-08-01 20:26:59 +02:00
import DomData from '../../src/js/utils/dom-data';
Broke up Lib and Util into smaller libraries of functions Broke out bind, guid, and element data functions from Lib Separated out more dom functions in to dom.js Broke out URL functions into url.js Removed setLocalStorage since it wasn't being used Moved browser tests out of lib Moved log functions into their own file Removed trim() since it wasn't being used Moved formatTime into its own file Moved round into its own file and renamed roundFloat() Moved capitalize into its own file and renamed as toTitleCase() Moved createTimeRange into its own file Removed Lib.arr.forEach infavor of the native forEach Removed Lib.obj.create in favor of native Object.create (ES6-sham) Removed obj.each in favor of native Object.getOwnPropertyNames().forEach() Removed obj.merge and copy. Using lodash.assign instead. Replaced Lib.obj.isPlain with lodash.isPlainObject Removed Lib.obj.isArray in favor of the native Array.isArray Also removed the lib.js tests file as all tests have been moved or removed. Removed Lib.isEmpty in favor of !Object.getOwnPropertyNames().length Switched Util.mergeOptions and deepMerge to use new mergeOptions() Moved Lib.TEST_VID to Html5.TEST_VID Removed Lib references everywhere. Woo! Attempting to fix sourcemap test errors by setting grunt-browserify version Switched to object.assign from lodash.assign Removed unused 'inherits' dependency Reorganzied test files and added '.test' to file names Combined js/core.js and js/video.js Moved events.js into the utils directory
2015-05-04 01:12:38 +02:00
import * as Events from '../../src/js/utils/events.js';
import * as Obj from '../../src/js/utils/obj';
Broke up Lib and Util into smaller libraries of functions Broke out bind, guid, and element data functions from Lib Separated out more dom functions in to dom.js Broke out URL functions into url.js Removed setLocalStorage since it wasn't being used Moved browser tests out of lib Moved log functions into their own file Removed trim() since it wasn't being used Moved formatTime into its own file Moved round into its own file and renamed roundFloat() Moved capitalize into its own file and renamed as toTitleCase() Moved createTimeRange into its own file Removed Lib.arr.forEach infavor of the native forEach Removed Lib.obj.create in favor of native Object.create (ES6-sham) Removed obj.each in favor of native Object.getOwnPropertyNames().forEach() Removed obj.merge and copy. Using lodash.assign instead. Replaced Lib.obj.isPlain with lodash.isPlainObject Removed Lib.obj.isArray in favor of the native Array.isArray Also removed the lib.js tests file as all tests have been moved or removed. Removed Lib.isEmpty in favor of !Object.getOwnPropertyNames().length Switched Util.mergeOptions and deepMerge to use new mergeOptions() Moved Lib.TEST_VID to Html5.TEST_VID Removed Lib references everywhere. Woo! Attempting to fix sourcemap test errors by setting grunt-browserify version Switched to object.assign from lodash.assign Removed unused 'inherits' dependency Reorganzied test files and added '.test' to file names Combined js/core.js and js/video.js Moved events.js into the utils directory
2015-05-04 01:12:38 +02:00
import * as browser from '../../src/js/utils/browser.js';
import document from 'global/document';
import sinon from 'sinon';
import TestHelpers from './test-helpers.js';
class TestComponent1 extends Component {}
class TestComponent2 extends Component {}
class TestComponent3 extends Component {}
class TestComponent4 extends Component {}
TestComponent1.prototype.options_ = {
children: [
'testComponent2',
'testComponent3'
]
};
QUnit.module('Component', {
before() {
Component.registerComponent('TestComponent1', TestComponent1);
Component.registerComponent('TestComponent2', TestComponent2);
Component.registerComponent('TestComponent3', TestComponent3);
Component.registerComponent('TestComponent4', TestComponent4);
2023-06-12 20:31:06 +02:00
sinon.stub(window.DOMParser.prototype, 'parseFromString').returns({
querySelector: () => false,
documentElement: document.createElement('span')
});
},
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer();
},
afterEach() {
this.player.dispose();
this.clock.restore();
},
after() {
delete Component.components_.TestComponent1;
delete Component.components_.TestComponent2;
delete Component.components_.TestComponent3;
delete Component.components_.TestComponent4;
2023-06-12 20:31:06 +02:00
window.DOMParser.prototype.parseFromString.restore();
}
});
QUnit.test('registerComponent() throws with bad arguments', function(assert) {
assert.throws(
function() {
Component.registerComponent(null);
},
new Error('Illegal component name, "null"; must be a non-empty string.'),
'component names must be non-empty strings'
);
assert.throws(
function() {
Component.registerComponent('');
},
new Error('Illegal component name, ""; must be a non-empty string.'),
'component names must be non-empty strings'
);
assert.throws(
function() {
Component.registerComponent('TestComponent5', function() {});
},
new Error('Illegal component, "TestComponent5"; must be a Component subclass.'),
'components must be subclasses of Component'
);
assert.throws(
function() {
const Tech = Component.getComponent('Tech');
class DummyTech extends Tech {}
Component.registerComponent('TestComponent5', DummyTech);
},
new Error('Illegal component, "TestComponent5"; techs must be registered using Tech.registerTech().'),
'components must be subclasses of Component'
);
});
QUnit.test('should create an element', function(assert) {
const comp = new Component(this.player, {});
assert.ok(comp.el().nodeName);
comp.dispose();
});
QUnit.test('should add a child component', function(assert) {
const comp = new Component(this.player);
const child = comp.addChild('component');
assert.ok(comp.children().length === 1);
assert.ok(comp.children()[0] === child);
assert.ok(comp.el().childNodes[0] === child.el());
assert.ok(comp.getChild('component') === child);
assert.ok(comp.getChildById(child.id()) === child);
comp.dispose();
});
QUnit.test('should add a child component to an index', function(assert) {
const comp = new Component(this.player);
const child = comp.addChild('component');
assert.ok(comp.children().length === 1);
assert.ok(comp.children()[0] === child);
const child0 = comp.addChild('component', {}, 0);
assert.ok(comp.children().length === 2);
assert.ok(comp.children()[0] === child0);
assert.ok(comp.children()[1] === child);
const child1 = comp.addChild('component', {}, '2');
assert.ok(comp.children().length === 3);
assert.ok(comp.children()[2] === child1);
const child2 = comp.addChild('component', {}, undefined);
assert.ok(comp.children().length === 4);
assert.ok(comp.children()[3] === child2);
const child3 = comp.addChild('component', {}, -1);
assert.ok(comp.children().length === 5);
assert.ok(comp.children()[3] === child3);
assert.ok(comp.children()[4] === child2);
comp.dispose();
});
QUnit.test('should insert element relative to the element of the component to insert before', function(assert) {
// for legibility of the test itself:
/* eslint-disable no-unused-vars */
const comp = new Component(this.player);
const child0 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c0'})});
const child1 = comp.addChild('component', {createEl: false});
const child2 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c2'})});
const child3 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c3'})});
const child4 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c4'})}, comp.children_.indexOf(child2));
assert.ok(child2.el_.previousSibling === child4.el_, 'addChild should insert el before its next sibling\'s element');
/* eslint-enable no-unused-vars */
});
QUnit.test('should allow for children that are elements', function(assert) {
// for legibility of the test itself:
/* eslint-disable no-unused-vars */
const comp = new Component(this.player);
const testEl = Dom.createEl('div');
// Add element as video el gets added to player
comp.el().appendChild(testEl);
comp.children_.unshift(testEl);
const child1 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c1'})});
const child2 = comp.addChild('component', {el: Dom.createEl('div', {}, {class: 'c4'})}, 0);
assert.ok(child2.el_.nextSibling === testEl, 'addChild should insert el before a sibling that is an element');
/* eslint-enable no-unused-vars */
});
2023-06-12 20:31:06 +02:00
QUnit.test('setIcon should not do anything when experimentalSvgIcons is not set', function(assert) {
const comp = new Component(this.player);
const iconName = 'test';
assert.equal(comp.setIcon(iconName), null, 'we should not return anything');
comp.dispose();
});
QUnit.test('setIcon should return the correct SVG', function(assert) {
const player = TestHelpers.makePlayer({experimentalSvgIcons: true});
const comp = new Component(player);
const iconName = 'test';
// Elements and children of the icon.
const spanEl = comp.setIcon(iconName);
const svgEl = spanEl.childNodes[0];
const useEl = svgEl.childNodes[0];
// Ensure all elements are of the correct type.
assert.equal(spanEl.nodeName.toLowerCase(), 'span', 'parent element should be a <span>');
assert.equal(svgEl.nodeName.toLowerCase(), 'svg', 'first child element should be a <svg>');
assert.equal(useEl.nodeName.toLowerCase(), 'use', 'second child element should be a <use>');
// Ensure the classname and attributes are set correctly on the elements.
assert.equal(spanEl.className, 'vjs-icon-placeholder vjs-svg-icon', 'span should have icon class');
assert.equal(svgEl.getAttribute('viewBox'), '0 0 512 512', 'svg should have viewBox set');
assert.equal(useEl.getAttribute('href'), '#vjs-icon-test', 'use should have an href set with the correct icon url');
assert.equal(comp.iconIsSet_, true, 'the component iconIsSet_ property is set to true');
player.dispose();
comp.dispose();
});
QUnit.test('setIcon should call replaceChild if an icon already exists', function(assert) {
const player = TestHelpers.makePlayer({experimentalSvgIcons: true});
const comp = new Component(player);
const appendSpy = sinon.spy(comp.el(), 'appendChild');
const replaceSpy = sinon.spy(comp.el(), 'replaceChild');
// Elements and children of the icon.
let spanEl = comp.setIcon('test-1');
let svgEl = spanEl.childNodes[0];
let useEl = svgEl.childNodes[0];
// ensure first setIcon call works correctly
assert.equal(useEl.getAttribute('href'), '#vjs-icon-test-1', 'use should have an href set with the correct icon url');
assert.ok(appendSpy.calledOnce, '`appendChild` has been called');
spanEl = comp.setIcon('test-2');
svgEl = spanEl.childNodes[0];
useEl = svgEl.childNodes[0];
assert.equal(useEl.getAttribute('href'), '#vjs-icon-test-2', 'use should have an href set with the correct icon url');
assert.ok(replaceSpy.calledOnce, '`replaceChild` has been called');
appendSpy.restore();
replaceSpy.restore();
player.dispose();
comp.dispose();
});
QUnit.test('setIcon should append a child to the element passed into the method', function(assert) {
const player = TestHelpers.makePlayer({experimentalSvgIcons: true});
const comp = new Component(player);
const el = document.createElement('div');
comp.setIcon('test', el);
const spanEl = el.childNodes[0];
const svgEl = spanEl.childNodes[0];
const useEl = svgEl.childNodes[0];
assert.equal(useEl.getAttribute('href'), '#vjs-icon-test', 'href set on the element passed in');
player.dispose();
comp.dispose();
});
QUnit.test('addChild should throw if the child does not exist', function(assert) {
const comp = new Component(this.player);
assert.throws(function() {
comp.addChild('non-existent-child');
}, new Error('Component Non-existent-child does not exist'), 'addChild threw');
comp.dispose();
});
QUnit.test('addChild with instance should allow getting child correctly', function(assert) {
const comp = new Component(this.player);
const comp2 = new Component(this.player);
comp2.name = function() {
return 'foo';
};
comp.addChild(comp2);
assert.ok(comp.getChild('foo'), 'we can get child with camelCase');
assert.ok(comp.getChild('Foo'), 'we can get child with TitleCase');
comp.dispose();
});
QUnit.test('should add a child component with title case name', function(assert) {
const comp = new Component(this.player);
const child = comp.addChild('Component');
assert.ok(comp.children().length === 1);
assert.ok(comp.children()[0] === child);
assert.ok(comp.el().childNodes[0] === child.el());
assert.ok(comp.getChild('Component') === child);
assert.ok(comp.getChildById(child.id()) === child);
comp.dispose();
});
QUnit.test('should init child components from options', function(assert) {
const comp = new Component(this.player, {
children: {
component: {}
}
});
assert.ok(comp.children().length === 1);
assert.ok(comp.el().childNodes.length === 1);
comp.dispose();
});
QUnit.test('should init child components from simple children array', function(assert) {
const comp = new Component(this.player, {
children: [
'component',
'component',
'component'
]
});
assert.ok(comp.children().length === 3);
assert.ok(comp.el().childNodes.length === 3);
comp.dispose();
});
QUnit.test('should init child components from children array of objects', function(assert) {
const comp = new Component(this.player, {
children: [
{ name: 'component' },
{ name: 'component' },
{ name: 'component' }
]
});
assert.ok(comp.children().length === 3);
assert.ok(comp.el().childNodes.length === 3);
comp.dispose();
});
QUnit.test('should do a deep merge of child options', function(assert) {
// Create a default option for component
const oldOptions = Component.prototype.options_;
Component.prototype.options_ = {
example: {
childOne: { foo: 'bar', asdf: 'fdsa' },
childTwo: {},
childThree: {}
}
};
const comp = new Component(this.player, {
example: {
childOne: { foo: 'baz', abc: '123' },
childThree: false,
childFour: {}
}
});
const mergedOptions = comp.options_;
const children = mergedOptions.example;
assert.strictEqual(children.childOne.foo, 'baz', 'value three levels deep overridden');
assert.strictEqual(children.childOne.asdf, 'fdsa', 'value three levels deep maintained');
assert.strictEqual(children.childOne.abc, '123', 'value three levels deep added');
assert.ok(children.childTwo, 'object two levels deep maintained');
assert.strictEqual(children.childThree, false, 'object two levels deep removed');
assert.ok(children.childFour, 'object two levels deep added');
assert.strictEqual(
Component.prototype.options_.example.childOne.foo,
'bar',
'prototype options were not overridden'
);
// Reset default component options
Component.prototype.options_ = oldOptions;
comp.dispose();
});
QUnit.test('should init child components from component options', function(assert) {
const player = TestHelpers.makePlayer();
const testComp = new TestComponent1(player, {
testComponent2: false,
testComponent4: {}
});
assert.ok(!testComp.childNameIndex_.TestComponent2, 'we do not have testComponent2');
assert.ok(testComp.childNameIndex_.TestComponent4, 'we have a testComponent4');
player.dispose();
testComp.dispose();
});
QUnit.test('should allows setting child options at the parent options level', function(assert) {
let parent;
// using children array
let options = {
children: [
'component',
'nullComponent'
],
// parent-level option for child
component: {
foo: true
},
nullComponent: false
};
try {
parent = new Component(this.player, options);
} catch (err) {
assert.ok(false, 'Child with `false` option was initialized');
}
assert.equal(parent.children()[0].options_.foo, true, 'child options set when children array is used');
assert.equal(parent.children().length, 1, 'we should only have one child');
parent.dispose();
// using children object
options = {
children: {
component: {
foo: false
},
nullComponent: {}
},
// parent-level option for child
component: {
foo: true
},
nullComponent: false
};
try {
parent = new Component(this.player, options);
} catch (err) {
assert.ok(false, 'Child with `false` option was initialized');
}
assert.equal(parent.children()[0].options_.foo, true, 'child options set when children object is used');
assert.equal(parent.children().length, 1, 'we should only have one child');
parent.dispose();
});
QUnit.test('should dispose of component and children', function(assert) {
const comp = new Component(this.player);
// Add a child
const child = comp.addChild('Component');
assert.ok(comp.children().length === 1);
assert.notOk(comp.isDisposed(), 'the component reports that it is not disposed');
// Add a listener
comp.on('click', function() {
return true;
});
const el = comp.el();
2019-08-01 20:26:59 +02:00
const data = DomData.get(el);
let hasDisposed = false;
let bubbles = null;
comp.on('dispose', function(event) {
hasDisposed = true;
bubbles = event.bubbles;
});
comp.dispose();
child.dispose();
assert.ok(hasDisposed, 'component fired dispose event');
assert.ok(bubbles === false, 'dispose event does not bubble');
assert.ok(!comp.children(), 'component children were deleted');
assert.ok(!comp.el(), 'component element was deleted');
assert.ok(!child.children(), 'child children were deleted');
assert.ok(!child.el(), 'child element was deleted');
2019-08-01 20:26:59 +02:00
assert.ok(!DomData.has(el), 'listener data nulled');
assert.ok(
!Object.getOwnPropertyNames(data).length,
'original listener data object was emptied'
);
assert.ok(comp.isDisposed(), 'the component reports that it is disposed');
});
QUnit.test('should add and remove event listeners to element', function(assert) {
const comp = new Component(this.player, {});
// No need to make this async because we're triggering events inline.
// We're going to trigger the event after removing the listener,
// So if we get extra asserts that's a problem.
assert.expect(2);
const testListener = function() {
assert.ok(true, 'fired event once');
assert.ok(this === comp, 'listener has the component as context');
};
comp.on('test-event', testListener);
comp.trigger('test-event');
comp.off('test-event', testListener);
comp.trigger('test-event');
comp.dispose();
});
QUnit.test('should trigger a listener once using one()', function(assert) {
const comp = new Component(this.player, {});
assert.expect(1);
const testListener = function() {
assert.ok(true, 'fired event once');
};
comp.one('test-event', testListener);
comp.trigger('test-event');
comp.trigger('test-event');
comp.dispose();
});
QUnit.test('should be possible to pass data when you trigger an event', function(assert) {
const comp = new Component(this.player, {});
const data1 = 'Data1';
const data2 = {txt: 'Data2'};
assert.expect(3);
const testListener = function(evt, hash) {
assert.ok(true, 'fired event once');
assert.deepEqual(hash.d1, data1);
assert.deepEqual(hash.d2, data2);
};
comp.one('test-event', testListener);
comp.trigger('test-event', {d1: data1, d2: data2});
comp.trigger('test-event');
comp.dispose();
});
QUnit.test('should add listeners to other components and remove them', function(assert) {
const player = this.player;
const comp1 = new Component(player);
const comp2 = new Component(player);
let listenerFired = 0;
const testListener = function() {
assert.equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.on(comp2, 'test-event', testListener);
comp2.trigger('test-event');
assert.equal(listenerFired, 1, 'listener was fired once');
listenerFired = 0;
comp1.off(comp2, 'test-event', testListener);
comp2.trigger('test-event');
assert.equal(listenerFired, 0, 'listener was not fired after being removed');
// this component is disposed first
listenerFired = 0;
comp1.on(comp2, 'test-event', testListener);
comp1.dispose();
comp2.trigger('test-event');
assert.equal(listenerFired, 0, 'listener was removed when this component was disposed first');
comp1.off = function() {
throw new Error('Comp1 off called');
};
comp2.dispose();
assert.ok(true, 'this component removed dispose listeners from other component');
});
QUnit.test('should add listeners to other components and remove when them other component is disposed', function(assert) {
const player = this.player;
const comp1 = new Component(player);
const comp2 = new Component(player);
const testListener = function() {
assert.equal(this, comp1, 'listener has the first component as context');
};
comp1.on(comp2, 'test-event', testListener);
comp2.dispose();
comp2.off = function() {
throw new Error('Comp2 off called');
};
comp1.dispose();
assert.ok(true, 'this component removed dispose listener from this component that referenced other component');
});
QUnit.test('should add listeners to other components that are fired once', function(assert) {
const player = this.player;
const comp1 = new Component(player);
const comp2 = new Component(player);
let listenerFired = 0;
const testListener = function() {
assert.equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.one(comp2, 'test-event', testListener);
comp2.trigger('test-event');
assert.equal(listenerFired, 1, 'listener was executed once');
comp2.trigger('test-event');
assert.equal(listenerFired, 1, 'listener was executed only once');
comp1.dispose();
comp2.dispose();
});
QUnit.test('should add listeners to other element and remove them', function(assert) {
const player = this.player;
const comp1 = new Component(player);
const el = document.createElement('div');
let listenerFired = 0;
const testListener = function() {
assert.equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.on(el, 'test-event', testListener);
Events.trigger(el, 'test-event');
assert.equal(listenerFired, 1, 'listener was fired once');
listenerFired = 0;
comp1.off(el, 'test-event', testListener);
Events.trigger(el, 'test-event');
assert.equal(listenerFired, 0, 'listener was not fired after being removed from other element');
// this component is disposed first
listenerFired = 0;
comp1.on(el, 'test-event', testListener);
comp1.dispose();
Events.trigger(el, 'test-event');
assert.equal(listenerFired, 0, 'listener was removed when this component was disposed first');
comp1.off = function() {
throw new Error('Comp1 off called');
};
try {
Events.trigger(el, 'dispose');
} catch (e) {
assert.ok(false, 'listener was not removed from other element');
}
Events.trigger(el, 'dispose');
assert.ok(true, 'this component removed dispose listeners from other element');
comp1.dispose();
});
QUnit.test('should add listeners to other components that are fired once', function(assert) {
const player = this.player;
const comp1 = new Component(player);
const el = document.createElement('div');
let listenerFired = 0;
const testListener = function() {
assert.equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.one(el, 'test-event', testListener);
Events.trigger(el, 'test-event');
assert.equal(listenerFired, 1, 'listener was executed once');
Events.trigger(el, 'test-event');
assert.equal(listenerFired, 1, 'listener was executed only once');
comp1.dispose();
});
QUnit.test('should trigger a listener when ready', function(assert) {
let initListenerFired;
let methodListenerFired;
let syncListenerFired;
const comp = new Component(this.player, {}, function() {
initListenerFired = true;
});
comp.ready(function() {
methodListenerFired = true;
});
comp.triggerReady();
comp.ready(function() {
syncListenerFired = true;
}, true);
assert.ok(!initListenerFired, 'init listener should NOT fire synchronously');
assert.ok(!methodListenerFired, 'method listener should NOT fire synchronously');
assert.ok(syncListenerFired, 'sync listener SHOULD fire synchronously if after ready');
this.clock.tick(1);
assert.ok(initListenerFired, 'init listener should fire asynchronously');
assert.ok(methodListenerFired, 'method listener should fire asynchronously');
// Listeners should only be fired once and then removed
initListenerFired = false;
methodListenerFired = false;
syncListenerFired = false;
comp.triggerReady();
this.clock.tick(1);
assert.ok(!initListenerFired, 'init listener should be removed');
assert.ok(!methodListenerFired, 'method listener should be removed');
assert.ok(!syncListenerFired, 'sync listener should be removed');
comp.dispose();
});
QUnit.test('should not retrigger a listener when the listener calls triggerReady', function(assert) {
let timesCalled = 0;
let selfTriggered = false;
const comp = new Component(this.player, {});
const readyListener = function() {
timesCalled++;
// Don't bother calling again if we have
// already failed
if (!selfTriggered) {
selfTriggered = true;
comp.triggerReady();
}
};
comp.ready(readyListener);
comp.triggerReady();
this.clock.tick(100);
assert.equal(timesCalled, 1, 'triggerReady from inside a ready handler does not result in an infinite loop');
comp.dispose();
});
QUnit.test('should add and remove a CSS class', function(assert) {
const comp = new Component(this.player, {});
comp.addClass('test-class');
assert.ok(comp.el().className.indexOf('test-class') !== -1);
comp.removeClass('test-class');
assert.ok(comp.el().className.indexOf('test-class') === -1);
comp.toggleClass('test-class');
assert.ok(comp.el().className.indexOf('test-class') !== -1);
comp.toggleClass('test-class');
assert.ok(comp.el().className.indexOf('test-class') === -1);
comp.dispose();
});
QUnit.test('should add and remove CSS classes', function(assert) {
const comp = new Component(this.player, {});
comp.addClass('first-class', 'second-class');
assert.ok(comp.el().className.indexOf('first-class') !== -1);
assert.ok(comp.el().className.indexOf('second-class') !== -1);
comp.removeClass('first-class', 'second-class');
assert.ok(comp.el().className.indexOf('first-class') === -1);
assert.ok(comp.el().className.indexOf('second-class') === -1);
comp.addClass('first-class second-class');
assert.ok(comp.el().className.indexOf('first-class') !== -1);
assert.ok(comp.el().className.indexOf('second-class') !== -1);
comp.removeClass('first-class second-class');
assert.ok(comp.el().className.indexOf('first-class') === -1);
assert.ok(comp.el().className.indexOf('second-class') === -1);
comp.addClass('be cool', 'scooby', 'doo');
assert.ok(comp.el().className.indexOf('be cool scooby doo') !== -1);
comp.removeClass('be cool', 'scooby', 'doo');
assert.ok(comp.el().className.indexOf('be cool scooby doo') === -1);
comp.addClass('multiple spaces between words');
assert.ok(comp.el().className.indexOf('multiple spaces between words') !== -1);
comp.removeClass('multiple spaces between words');
assert.ok(comp.el().className.indexOf('multiple spaces between words') === -1);
comp.toggleClass('first-class second-class');
assert.ok(comp.el().className.indexOf('first-class second-class') !== -1);
comp.toggleClass('first-class second-class');
assert.ok(comp.el().className.indexOf('first-class second-class') === -1);
comp.dispose();
});
QUnit.test('should add CSS class passed in options', function(assert) {
const comp = new Component(this.player, {className: 'class1 class2'});
assert.ok(comp.el().className.indexOf('class1') !== -1, 'first of multiple classes added');
assert.ok(comp.el().className.indexOf('class2') !== -1, 'second of multiple classes added');
comp.dispose();
const comp2 = new Component(this.player, {className: 'class1'});
assert.ok(comp2.el().className.indexOf('class1') !== -1, 'singe class added');
comp2.dispose();
});
QUnit.test('should show and hide an element', function(assert) {
const comp = new Component(this.player, {});
comp.hide();
assert.ok(comp.hasClass('vjs-hidden') === true);
comp.show();
assert.ok(comp.hasClass('vjs-hidden') === false);
comp.dispose();
});
QUnit.test('dimension() should treat NaN and null as zero', function(assert) {
let newWidth;
const width = 300;
const height = 150;
const comp = new Component(this.player, {});
// set component dimension
comp.dimensions(width, height);
newWidth = comp.dimension('width', null);
assert.notEqual(newWidth, width, 'new width and old width are not the same');
assert.equal(newWidth, undefined, 'we set a value, so, return value is undefined');
assert.equal(comp.width(), 0, 'the new width is zero');
const newHeight = comp.dimension('height', NaN);
assert.notEqual(newHeight, height, 'new height and old height are not the same');
assert.equal(newHeight, undefined, 'we set a value, so, return value is undefined');
assert.equal(comp.height(), 0, 'the new height is zero');
comp.width(width);
newWidth = comp.dimension('width', undefined);
assert.equal(newWidth, width, 'we did not set the width with undefined');
comp.dispose();
});
QUnit.test('should change the width and height of a component', function(assert) {
const container = document.createElement('div');
const comp = new Component(this.player, {});
const el = comp.el();
const fixture = document.getElementById('qunit-fixture');
fixture.appendChild(container);
container.appendChild(el);
// Container of el needs dimensions or the component won't have dimensions
container.style.width = '1000px';
container.style.height = '1000px';
comp.width('50%');
comp.height('123px');
assert.ok(comp.width() === 500, 'percent values working');
const compStyle = TestHelpers.getComputedStyle(el, 'width');
assert.ok(compStyle === comp.width() + 'px', 'matches computed style');
assert.ok(comp.height() === 123, 'px values working');
comp.width(321);
assert.ok(comp.width() === 321, 'integer values working');
comp.width('auto');
comp.height('auto');
assert.ok(comp.width() === 1000, 'forced width was removed');
assert.ok(comp.height() === 0, 'forced height was removed');
comp.dispose();
});
QUnit.test('should get the computed dimensions', function(assert) {
const container = document.createElement('div');
const comp = new Component(this.player, {});
const el = comp.el();
const fixture = document.getElementById('qunit-fixture');
const computedWidth = '500px';
const computedHeight = '500px';
fixture.appendChild(container);
container.appendChild(el);
// Container of el needs dimensions or the component won't have dimensions
container.style.width = '1000px';
container.style.height = '1000px';
comp.width('50%');
comp.height('50%');
assert.equal(comp.currentWidth() + 'px', computedWidth, 'matches computed width');
assert.equal(comp.currentHeight() + 'px', computedHeight, 'matches computed height');
assert.equal(comp.currentDimension('width') + 'px', computedWidth, 'matches computed width');
assert.equal(comp.currentDimension('height') + 'px', computedHeight, 'matches computed height');
assert.equal(comp.currentDimensions().width + 'px', computedWidth, 'matches computed width');
assert.equal(comp.currentDimensions().height + 'px', computedHeight, 'matches computed width');
comp.dispose();
});
QUnit.test('should use a defined content el for appending children', function(assert) {
class CompWithContent extends Component {
createEl() {
// Create the main component element
const el = Dom.createEl('div');
// Create the element where children will be appended
this.contentEl_ = Dom.createEl('div', { id: 'contentEl' });
el.appendChild(this.contentEl_);
return el;
}
}
const comp = new CompWithContent(this.player);
const child = comp.addChild('component');
assert.ok(comp.children().length === 1);
assert.ok(comp.el().childNodes[0].id === 'contentEl');
assert.ok(comp.el().childNodes[0].childNodes[0] === child.el());
comp.removeChild(child);
assert.ok(comp.children().length === 0, 'Length should now be zero');
assert.ok(comp.el().childNodes[0].id === 'contentEl', 'Content El should still exist');
assert.ok(
comp.el().childNodes[0].childNodes[0] !== child.el(),
'Child el should be removed.'
);
child.dispose();
comp.dispose();
});
QUnit.test('should emit a tap event', function(assert) {
const comp = new Component(this.player);
let singleTouch = {};
const origTouch = browser.TOUCH_ENABLED;
assert.expect(3);
// Fake touch support. Real touch support isn't needed for this test.
2019-08-30 20:56:41 +02:00
browser.stub_TOUCH_ENABLED(true);
comp.emitTapEvents();
comp.on('tap', function() {
assert.ok(true, 'Tap event emitted');
});
// A touchstart followed by touchend should trigger a tap
Events.trigger(comp.el(), {type: 'touchstart', touches: [{}]});
comp.trigger('touchend');
// A touchmove with a lot of movement should not trigger a tap
Events.trigger(comp.el(), {type: 'touchstart', touches: [
{ pageX: 0, pageY: 0 }
]});
Events.trigger(comp.el(), {type: 'touchmove', touches: [
{ pageX: 100, pageY: 100 }
]});
comp.trigger('touchend');
// A touchmove with not much movement should still allow a tap
Events.trigger(comp.el(), {type: 'touchstart', touches: [
{ pageX: 0, pageY: 0 }
]});
Events.trigger(comp.el(), {type: 'touchmove', touches: [
{ pageX: 7, pageY: 7 }
]});
comp.trigger('touchend');
// A touchmove with a lot of movement by modifying the existing touch object
// should not trigger a tap
singleTouch = { pageX: 0, pageY: 0 };
Events.trigger(comp.el(), {type: 'touchstart', touches: [singleTouch]});
singleTouch.pageX = 100;
singleTouch.pageY = 100;
Events.trigger(comp.el(), {type: 'touchmove', touches: [singleTouch]});
comp.trigger('touchend');
// A touchmove with not much movement by modifying the existing touch object
// should still allow a tap
singleTouch = { pageX: 0, pageY: 0 };
Events.trigger(comp.el(), {type: 'touchstart', touches: [singleTouch]});
singleTouch.pageX = 7;
singleTouch.pageY = 7;
Events.trigger(comp.el(), {type: 'touchmove', touches: [singleTouch]});
comp.trigger('touchend');
// Reset to original value
2019-08-30 20:56:41 +02:00
browser.stub_TOUCH_ENABLED(origTouch);
comp.dispose();
});
QUnit.test('should provide timeout methods that automatically get cleared on component disposal', function(assert) {
const comp = new Component(this.player);
let timeoutsFired = 0;
const timeoutToClear = comp.setTimeout(function() {
timeoutsFired++;
assert.ok(false, 'Timeout should have been manually cleared');
}, 500);
assert.expect(4);
comp.setTimeout(function() {
timeoutsFired++;
assert.equal(this, comp, 'Timeout fn has the component as its context');
assert.ok(true, 'Timeout created and fired.');
}, 100);
comp.setTimeout(function() {
timeoutsFired++;
assert.ok(false, 'Timeout should have been disposed');
}, 1000);
this.clock.tick(100);
assert.ok(timeoutsFired === 1, 'One timeout should have fired by this point');
comp.clearTimeout(timeoutToClear);
this.clock.tick(500);
comp.dispose();
this.clock.tick(1000);
assert.ok(timeoutsFired === 1, 'One timeout should have fired overall');
});
QUnit.test('should provide interval methods that automatically get cleared on component disposal', function(assert) {
const comp = new Component(this.player);
let intervalsFired = 0;
const interval = comp.setInterval(function() {
intervalsFired++;
assert.equal(this, comp, 'Interval fn has the component as its context');
assert.ok(true, 'Interval created and fired.');
}, 100);
assert.expect(13);
comp.setInterval(function() {
intervalsFired++;
assert.ok(false, 'Interval should have been disposed');
}, 1200);
this.clock.tick(500);
assert.ok(intervalsFired === 5, 'Component interval fired 5 times');
comp.clearInterval(interval);
this.clock.tick(600);
assert.ok(intervalsFired === 5, 'Interval was manually cleared');
comp.dispose();
this.clock.tick(1200);
assert.ok(intervalsFired === 5, 'Interval was cleared when component was disposed');
});
QUnit.test('should provide a requestAnimationFrame method that is cleared on disposal', function(assert) {
const comp = new Component(this.player);
const oldRAF = window.requestAnimationFrame;
const oldCAF = window.cancelAnimationFrame;
// Stub the window.*AnimationFrame methods with window.setTimeout methods
// so we can control when the callbacks are called via sinon's timer stubs.
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
const spyRAF = sinon.spy();
comp.requestNamedAnimationFrame('testing', spyRAF);
assert.strictEqual(spyRAF.callCount, 0, 'rAF callback was not called immediately');
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was called after a "repaint"');
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was not called after a second "repaint"');
comp.cancelNamedAnimationFrame(comp.requestNamedAnimationFrame('testing', spyRAF));
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'second rAF callback was not called because it was cancelled');
comp.requestNamedAnimationFrame('testing', spyRAF);
comp.dispose();
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'third rAF callback was not called because the component was disposed');
window.requestAnimationFrame = oldRAF;
window.cancelAnimationFrame = oldCAF;
});
QUnit.test('should provide a requestNamedAnimationFrame method that is cleared on disposal', function(assert) {
const comp = new Component(this.player);
const oldRAF = window.requestAnimationFrame;
const oldCAF = window.cancelAnimationFrame;
// Stub the window.*AnimationFrame methods with window.setTimeout methods
// so we can control when the callbacks are called via sinon's timer stubs.
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
const spyRAF = sinon.spy();
comp.requestNamedAnimationFrame('testing', spyRAF);
assert.strictEqual(spyRAF.callCount, 0, 'rAF callback was not called immediately');
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was called after a "repaint"');
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was not called after a second "repaint"');
comp.cancelNamedAnimationFrame(comp.requestNamedAnimationFrame('testing', spyRAF));
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'second rAF callback was not called because it was cancelled');
comp.requestNamedAnimationFrame('testing', spyRAF);
comp.dispose();
this.clock.tick(1);
assert.strictEqual(spyRAF.callCount, 1, 'third rAF callback was not called because the component was disposed');
window.requestAnimationFrame = oldRAF;
window.cancelAnimationFrame = oldCAF;
});
QUnit.test('setTimeout should remove dispose handler on trigger', function(assert) {
const comp = new Component(this.player);
comp.setTimeout(() => {}, 1);
assert.equal(comp.setTimeoutIds_.size, 1, 'we removed our dispose handle');
this.clock.tick(1);
assert.equal(comp.setTimeoutIds_.size, 0, 'we removed our dispose handle');
comp.dispose();
});
QUnit.test('requestNamedAnimationFrame should remove dispose handler on trigger', function(assert) {
const comp = new Component(this.player);
const oldRAF = window.requestAnimationFrame;
const oldCAF = window.cancelAnimationFrame;
// Stub the window.*AnimationFrame methods with window.setTimeout methods
// so we can control when the callbacks are called via sinon's timer stubs.
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
const spyRAF = sinon.spy();
comp.requestNamedAnimationFrame('testFrame', spyRAF);
assert.equal(comp.rafIds_.size, 1, 'we got a new raf dispose handler');
assert.equal(comp.namedRafs_.size, 1, 'we got a new named raf dispose handler');
this.clock.tick(1);
assert.equal(comp.rafIds_.size, 0, 'we removed our raf dispose handle');
assert.equal(comp.namedRafs_.size, 0, 'we removed our named raf dispose handle');
comp.dispose();
window.requestAnimationFrame = oldRAF;
window.cancelAnimationFrame = oldCAF;
});
QUnit.test('requestAnimationFrame should remove dispose handler on trigger', function(assert) {
const comp = new Component(this.player);
const oldRAF = window.requestAnimationFrame;
const oldCAF = window.cancelAnimationFrame;
// Stub the window.*AnimationFrame methods with window.setTimeout methods
// so we can control when the callbacks are called via sinon's timer stubs.
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
const spyRAF = sinon.spy();
comp.requestAnimationFrame(spyRAF);
assert.equal(comp.rafIds_.size, 1, 'we got a new dispose handler');
this.clock.tick(1);
assert.equal(comp.rafIds_.size, 0, 'we removed our dispose handle');
comp.dispose();
window.requestAnimationFrame = oldRAF;
window.cancelAnimationFrame = oldCAF;
});
QUnit.test('setTimeout should be canceled on dispose', function(assert) {
const comp = new Component(this.player);
let called = false;
let clearId;
const setId = comp.setTimeout(() => {
called = true;
}, 1);
const clearTimeout = comp.clearTimeout;
comp.clearTimeout = (id) => {
clearId = id;
return clearTimeout.call(comp, id);
};
assert.equal(comp.setTimeoutIds_.size, 1, 'we added a timeout id');
comp.dispose();
assert.equal(comp.setTimeoutIds_.size, 0, 'we removed our timeout id');
assert.equal(clearId, setId, 'clearTimeout was called');
this.clock.tick(1);
assert.equal(called, false, 'setTimeout was never called');
});
QUnit.test('requestAnimationFrame should be canceled on dispose', function(assert) {
const comp = new Component(this.player);
let called = false;
let clearId;
const setId = comp.requestAnimationFrame(() => {
called = true;
});
const cancelAnimationFrame = comp.cancelAnimationFrame;
comp.cancelAnimationFrame = (id) => {
clearId = id;
return cancelAnimationFrame.call(comp, id);
};
assert.equal(comp.rafIds_.size, 1, 'we added a raf id');
comp.dispose();
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
assert.equal(clearId, setId, 'clearAnimationFrame was called');
this.clock.tick(1);
assert.equal(called, false, 'requestAnimationFrame was never called');
});
QUnit.test('setInterval should be canceled on dispose', function(assert) {
const comp = new Component(this.player);
let called = false;
let clearId;
const setId = comp.setInterval(() => {
called = true;
});
const clearInterval = comp.clearInterval;
comp.clearInterval = (id) => {
clearId = id;
return clearInterval.call(comp, id);
};
assert.equal(comp.setIntervalIds_.size, 1, 'we added an interval id');
comp.dispose();
assert.equal(comp.setIntervalIds_.size, 0, 'we removed a raf id');
assert.equal(clearId, setId, 'clearInterval was called');
this.clock.tick(1);
assert.equal(called, false, 'setInterval was never called');
});
QUnit.test('requestNamedAnimationFrame should be canceled on dispose', function(assert) {
const comp = new Component(this.player);
let called = false;
let clearName;
const setName = comp.requestNamedAnimationFrame('testing', () => {
called = true;
});
const cancelNamedAnimationFrame = comp.cancelNamedAnimationFrame;
comp.cancelNamedAnimationFrame = (name) => {
clearName = name;
return cancelNamedAnimationFrame.call(comp, name);
};
assert.equal(comp.namedRafs_.size, 1, 'we added a named raf');
assert.equal(comp.rafIds_.size, 1, 'we added a raf id');
comp.dispose();
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
assert.equal(clearName, setName, 'cancelNamedAnimationFrame was called');
this.clock.tick(1);
assert.equal(called, false, 'requestNamedAnimationFrame was never called');
});
QUnit.test('requestNamedAnimationFrame should only allow one raf of a specific name at a time', function(assert) {
const comp = new Component(this.player);
const calls = {
one: 0,
two: 0,
three: 0
};
const cancelNames = [];
const name = 'testing';
const handlerOne = () => {
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
calls.one++;
};
const handlerTwo = () => {
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
calls.two++;
};
const handlerThree = () => {
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
calls.three++;
};
const oldRAF = window.requestAnimationFrame;
const oldCAF = window.cancelAnimationFrame;
// Stub the window.*AnimationFrame methods with window.setTimeout methods
// so we can control when the callbacks are called via sinon's timer stubs.
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
const cancelNamedAnimationFrame = comp.cancelNamedAnimationFrame;
comp.cancelNamedAnimationFrame = (_name) => {
cancelNames.push(_name);
return cancelNamedAnimationFrame.call(comp, _name);
};
comp.requestNamedAnimationFrame(name, handlerOne);
assert.equal(comp.namedRafs_.size, 1, 'we added a named raf');
assert.equal(comp.rafIds_.size, 1, 'we added a raf id');
comp.requestNamedAnimationFrame(name, handlerTwo);
assert.deepEqual(cancelNames, [], 'no named cancels');
assert.equal(comp.namedRafs_.size, 1, 'still only one named raf');
assert.equal(comp.rafIds_.size, 1, 'still only one raf id');
this.clock.tick(1);
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
assert.deepEqual(calls, {
one: 1,
two: 0,
three: 0
}, 'only handlerOne was called');
comp.requestNamedAnimationFrame(name, handlerOne);
comp.requestNamedAnimationFrame(name, handlerTwo);
comp.requestNamedAnimationFrame(name, handlerThree);
assert.deepEqual(cancelNames, [], 'no named cancels for testing');
assert.equal(comp.namedRafs_.size, 1, 'only added one named raf');
assert.equal(comp.rafIds_.size, 1, 'only added one named raf');
this.clock.tick(1);
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
assert.deepEqual(calls, {
one: 2,
two: 0,
three: 0
}, 'only the handlerOne called');
window.requestAnimationFrame = oldRAF;
window.cancelAnimationFrame = oldCAF;
});
QUnit.test('$ and $$ functions', function(assert) {
const comp = new Component(this.player);
const contentEl = document.createElement('div');
const children = [
document.createElement('div'),
document.createElement('div')
];
comp.contentEl_ = contentEl;
children.forEach(child => contentEl.appendChild(child));
assert.strictEqual(comp.$('div'), children[0], '$ defaults to contentEl as scope');
assert.strictEqual(comp.$$('div').length, children.length, '$$ defaults to contentEl as scope');
comp.dispose();
});
QUnit.test('should use the stateful mixin', function(assert) {
const comp = new Component(this.player, {});
assert.ok(Obj.isPlain(comp.state), '`state` is a plain object');
assert.strictEqual(Object.prototype.toString.call(comp.setState), '[object Function]', '`setState` is a function');
comp.setState({foo: 'bar'});
assert.strictEqual(comp.state.foo, 'bar', 'the component passes a basic stateful test');
comp.dispose();
});
QUnit.test('should remove child when the child moves to the other parent', function(assert) {
const parentComponent1 = new Component(this.player, {});
const parentComponent2 = new Component(this.player, {});
const childComponent = new Component(this.player, {});
parentComponent1.addChild(childComponent);
assert.strictEqual(parentComponent1.children().length, 1, 'the children number of `parentComponent1` is 1');
assert.strictEqual(parentComponent1.children()[0], childComponent, 'the first child of `parentComponent1` is `childComponent`');
assert.strictEqual(parentComponent1.el().childNodes[0], childComponent.el(), '`parentComponent1` contains the DOM element of `childComponent`');
parentComponent2.addChild(childComponent);
assert.strictEqual(parentComponent1.children().length, 0, 'the children number of `parentComponent1` is 0');
assert.strictEqual(parentComponent1.el().childNodes.length, 0, 'the length of `childNodes` of `parentComponent1` is 0');
assert.strictEqual(parentComponent2.children().length, 1, 'the children number of `parentComponent2` is 1');
assert.strictEqual(parentComponent2.children()[0], childComponent, 'the first child of `parentComponent2` is `childComponent`');
assert.strictEqual(parentComponent2.el().childNodes.length, 1, 'the length of `childNodes` of `parentComponent2` is 1');
assert.strictEqual(parentComponent2.el().childNodes[0], childComponent.el(), '`parentComponent2` contains the DOM element of `childComponent`');
parentComponent1.dispose();
parentComponent2.dispose();
childComponent.dispose();
});
QUnit.test('getDescendant should work as expected', function(assert) {
const comp = new Component(this.player, {name: 'component'});
const descendant1 = new Component(this.player, {name: 'descendant1'});
const descendant2 = new Component(this.player, {name: 'descendant2'});
const descendant3 = new Component(this.player, {name: 'descendant3'});
comp.addChild(descendant1);
descendant1.addChild(descendant2);
descendant2.addChild(descendant3);
assert.equal(comp.getDescendant('descendant1', 'descendant2', 'descendant3'), descendant3, 'can pass as args');
assert.equal(comp.getDescendant(['descendant1', 'descendant2', 'descendant3']), descendant3, 'can pass as array');
assert.equal(comp.getDescendant('descendant1'), descendant1, 'can pass as single string');
assert.equal(comp.getDescendant(), comp, 'no args returns base component');
assert.notOk(comp.getDescendant('descendant5'), 'undefined descendant returned');
assert.notOk(comp.getDescendant('descendant1', 'descendant5'), 'undefined descendant returned');
assert.notOk(comp.getDescendant(['descendant1', 'descendant5']), 'undefined descendant returned');
comp.dispose();
});
QUnit.test('ready queue should not run after dispose', function(assert) {
let option = false;
let callback = false;
const comp = new Component(this.player, {name: 'component'}, () => {
option = true;
});
comp.ready(() => {
callback = true;
});
comp.dispose();
comp.triggerReady();
// TODO: improve this error. It is a variant of:
// "Cannot read property 'parentNode' of null"
//
// but on some browsers such as IE 11 and safari 9 other errors are thrown,
// I think any error at all works for our purposes here.
assert.throws(() => this.clock.tick(1), /.*/, 'throws trigger error');
assert.notOk(option, 'ready option not run');
assert.notOk(callback, 'ready callback not run');
});
QUnit.test('a component\'s el can be replaced on dispose', function(assert) {
const comp = this.player.addChild('Component', {}, {}, 2);
const prevIndex = Array.from(this.player.el_.childNodes).indexOf(comp.el_);
const replacementEl = document.createElement('div');
comp.dispose({restoreEl: replacementEl});
assert.strictEqual(replacementEl.parentNode, this.player.el_, 'replacement was inserted');
assert.strictEqual(Array.from(this.player.el_.childNodes).indexOf(replacementEl), prevIndex, 'replacement was inserted at same position');
});
feat: implement spatial navigation (#8570) * feat(player): add spatialNavigation feature Adds spatialNavigation feature to enhance user experience - Implemented spatial navigation in slider component - Enhanced player functionality for improved navigation * feat(player): add spatialNavigation class Adds spatialNavigation class to manage spatial-navigation-polyfill - Set class SpatialNavigation on its own file - Imported SpatialNavigation class on component class * feat(player): update spatialNavigation class Adds 3 methods to spatialNavigation class to manage spatial-navigation-polyfill - Added start() to: Start listen of keydown events - Added stop() to: Stop listen key down events - Added getComponents() to: Get current focusable components * feat(player): modify spatialNavigation class & modify component class Modify spatialNavigation class: -Remove unrequired version of function ‘getComponents’ Modify component class: -Add function ‘getIsFocusable’ * Added methods getPositions, handleFocus and handleBLur for spatial navigation needs * feat(player): modify Component class, BigPlayButton class & ClickableComponent class Modify Component class: -Add method getIsAvailableToBeFocused -Modify method getIsFocusable to only focus on finding focusable candidates Modify spatialNavigation class: -Remove unrequired method ‘getIsFocusable’ Modify component class: -Remove unrequired method ‘getIsFocusable’ * Added import in player.js, Created base methods inside spatial-navigation.js * feat(player): modify Component class & SpatialNavigation class Modify Component class: -Modify method getIsAvailableToBeFocused to be more strict on candidates Modify spatialNavigation class: -Modify method getComponents to get all focusable components * feat(player): modify Component class Modify Component class: -Add documentation to ‘isVisible’ function * added keydown event logic for spatial-navigation * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Modify documentation of functions * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add ‘clear’ & ‘remove’ methods * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add documentation of functions * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add function ‘getCurretComponent’‘’ * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add documentation for ‘findBestCandidate’ method * Added logic for moving focus to the best candidate * Implemented move, findBestCandidate, isInDirection, and calculateDistance methods for spatial navigation logic * Added a new player option enableKeydownListener, Added gap: 1px to control-bar for spatial-navigation-polyfill needs * feat(player): modify SpatialNavigation class & Component class Modify SpatialNavigation class: -Add function ‘handlePlayerBlur’ -Add function ‘handlePlayerFocus’ Modify Component class: -Modify ‘handleBlur’ -Modify ‘handleFocus’ * Removed enableKeydownListener flag, as user should start the SpatialNavigation manually * Added functionality to track changes in the focusableComponents list (custom event focusableComponentsChanged) * feat(player): modify SpatialNavigation class, ModalDialog & Component class Modify SpatialNavigation class: -Add ‘lastFocusedComponent’ -Add function ‘refocusComponent’ Modify ModalDialog class: -Add condition on ‘close’ function Modify Component class: -Modify ‘handleBlur’ to store blurred component * feat(player): modify ModalDialog Modify ModalDialog: -Add condition to close Modal on Backspace * Refactor SpatialNavigation to use player.spatialNavigation * Added a new custom event endOfFocusableComponents * Added new styles for focused elements in case spatial navigation is enabled * feat(player): modify SpatialNavigation class: -Add condition so getComponents can get as candidates the UI elements from the playlist-ui * Changed to window.SpatialNabigation to this.player_.spatialNavigation * feat(player): modify text-track-settings, created test-track-settings-colors.js, text-track-settings-font.js,text-track-fieldset.js & text-track-select.js: Modify text-track-settings class: - Add changes so newly created components can work as content of the modal. - Create new components as a refactor of the contents of text-track-settings * changed handleKeyDown inside component.js, getComponents method is now iterating player.children * feat(player): create TrackSettingsControls Component & Modify TextTrackSettings Create TrackSettingsControls Component: -Create Component to show buttons reset & done as components. Modify TextTrackSettings: -Add Component TrackSettingsControls in TextTrackSettings * feat(player): Modify ModalDialog Modify ModalDialog: -Add condition for stop propagation of event inside of ModalDialog when spatialNavigation is enabled * getIsFocusable and getIsAvailableToBeFocused methods are now accepting el as a parameter, added a new methods findSuitableDOMChild and focus for spatialNavigation class * feat(player): Modify TextTrackSettings: Modify TextTrackSettings: -Remove unrequired methods to create DOM elements since now those are created by Components. * feat(player): Modify CaptionSettingsMenuItem: Modify CaptionSettingsMenuItem: -Add condition to focus component of TextTrackSelect when modal is open * feat(player): Modify TextTrackSelect & TextTrackFieldset: Modify TextTrackSelect : Modify TextTrackFieldset: -Add comments to certain functions to explain the code * feat(player): Modify TrackSettingsControls: Modify TrackSettingsControls: -Remove unrequired comments & add comments to certain functions to explain the code * feat(player): Modify SpatialNavigation, Component & ModalDialog: Modify SpatialNavigation: Modify Component: Modify ModalDialog: -Add & update comments of documentation. * Handle ENTER keydown in Modals when spatial navigation is enabled * feat(player): Modify ModalDialog, spatialNavigation, TrackSettingsControls, TextTrackFieldset, TextTrackSelect, TrackSettingsColors, TrackSettingsFont: Modify ModalDialog: Modify spatialNavigation: Modify TrackSettingsControls: Modify TextTrackFieldset: Modify TextTrackSelect: Modify TrackSettingsColors: Modify TrackSettingsFont: -Add & update comments of documentation. * Implement additional RCU controls * feat(player): Modify Component class: Modify Component : -Remove unrequired condition inside of handleFocus method. * feat(player): Modify ModalDialog & CaptionSettingsMenuItem Modify ModalDialog: Modify CaptionSettingsMenuItem: -Modify spatialNavigation condition to be more specific regarding spatialNavigation implementation. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation : -Fix bug where ‘enter’ press was not working properly on select component inside of the ‘vjs-text-track-settings’ modal. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation : -Minor improvements on the loops of certain functions to stop when they have found the element they are looking for. -Implement minor spacing formatting on switch statement. * Update src/js/component.js More understandable documentation. Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> * Update src/js/component.js More understandable documentation. Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> * feat(player): Modify SpatialNavigation & Component class: Modify Component class : Modify SpatialNavigation class : -Modify ‘getIsFocusable’ function to use ‘this.el_’ instead of ‘el’ parameter * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation class : -Refactor onKeyDown function to use static data & return when pause is true. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation class : -Refactor to use ‘.el()’ instead of ‘.el_’ * Update src/js/spatial-navigation.js Co-authored-by: Walter Seymour <walterseymour15@gmail.com> * feat(player): Modify ModalDialog class & MenuItem class: Modify ModalDialog class : Modify MenuItem class : -Correct typo of ‘isSpatialNavlistening’ to ‘isSpatialNavListening’. * removed unused property, remove this.focus, which was added for testing purposes * Changed parameters to private, removed redundant code, removed initialFocusedComponent parameter, change STEP_SECONDS to static * feat(player): solve remaining conflict: Modify Spatial Navigation class : - Solve conflict * feat(player): Rename TrackSettingsColors & TrackSettingsFont * feat(player): Remove unrequired functions calls from components TextTrackSettingsColors & TextTrackSettingsFont. * feat(player): Update spatial-navigation.js's keypress return keyword. * bind focus and blur just if spatial navigation is enabled, add 1px gap if spatial navigation is enabled * feat(player): Modify calls on 'isListening' & 'isPaused' for ModalDialog & TextTrackMenuItem * feat(player): remove unrequired object on component 'TrackSettingsControls' * Removed 1px gap * feat(player): Rename function ‘getComponents’ to ‘updateFocusableComponents’ * Changed SpatialNavigation class to extend EventTarget, removed redundant methods for events * fix(player): fix call of 'getIsAvailableToBeFocused' that was throwing an error. * removed Static maps for key presses and extended keycode with the missing keys * refactor(player): Modify functions of 'getIsDisabled', 'getIsExpresslyInert' & 'getIsFocusable' to be more in pair when stablished code of the player. * Conditional assignment for keycode.codes.back based on platform, changed Backspace to Back key for Modal closing * Extend the object for reverse lookup, prenet Up/down keys to open a menu if spatial navigation is anabled * refactor(player): Refactor 'SpatialNavKeycodes' file to not patch 'keycode' dependency * fix(pllayer): fix issue related to 'back' not being used properly in function 'isEventKey' * feat(player): Rename imports of 'spatial-navigation-keycode' to have their extension * feat(player): Add example of use of 'Client app uses a global spatial-navigation solution' * feat(player): rename 'spatial-navigation-keycode.js' filename * Fix on src chnage issue, ESC button closing modal, expand vjs-modal-dialog * change file name and object name * fix: Update ids of labels to use 'guid' so unit test works properly * fix: update localized text in text-track-settings-font & text-track-settings * Mark some methods as private * fix: modify content of modal 'text-track-settings' to change language properly * fix: add missing '.' in jsdoc of text-track components * feature: add unit test for 'text-track-select' component * Add test for Spatial Navigation * test(player): Add minor test related to 'handleBlur' & 'handleFocus' * feat(player): Remove unrequired files from 'react-video-nav-app' * test(player): Add small test to check if 'getPositions' returns required properties * test(player): add test to verify 'getPositions()' properties are not empty * Add missing tests for performMediaAction_ and move * test(player): add test to for 'component.js' related to 'handleBlur' * test(player): add minor test in component related to test keypress propagation event * test(player): add test for component related to 'getIsAvailableToBeFocused' function * test(player): add test for Modal Dialog related to call function of spatial navigation * test(player): add tests for 'spatial-navigation-key-codes' * test(player): add tests for keycodes related to 'should return event name if keyCode is not available' * test(player): add minor test for case when not required parametters are passed * test(player): add test for 'caption-settings-menu-item' * feat(player): remove 'react-video-nav-app' * Move handleFocus and handleBlur from components.js to spatial-navigation.js * refactor(player): refactor 'searchForTrackSelect' to be handled in the spatial navigation * remove unrequired code in function 'searchForTrackSelect' * update documentation comment to be in pair to its current use * remove spatial navigation keydown from modal dialog and move it to spatial navigation class, modify the modal-dialog test accordingly * remove useless tests * Remove caption-settings-menu-item.test.js * Add minor test to 'searchForTrackSelect' in spatial-navigation.test.js * Add unit test for back key and listening to events --------- Co-authored-by: CarlosVillasenor <carlosdeveloper9@gmail.com> Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> Co-authored-by: Walter Seymour <walterseymour15@gmail.com> Co-authored-by: Carlos Villasenor Castillo <cvillasenor@Carloss-MacBook-Pro.local>
2024-04-18 03:34:52 +02:00
QUnit.test('should be able to call `getPositions()` from a component', function(assert) {
const player = TestHelpers.makePlayer({});
const appendSpy = sinon.spy(player.controlBar, 'getPositions');
player.controlBar.getPositions();
assert.expect(1);
assert.ok(appendSpy.calledOnce, '`handleBlur` has been called');
player.dispose();
});
QUnit.test('getPositions() returns properties of `boundingClientRect` & `center` from elements that support it', function(assert) {
const player = TestHelpers.makePlayer({
spatialNavigation: {
enabled: true
}
});
assert.expect(4);
assert.ok(player.controlBar.getPositions().boundingClientRect, '`boundingClientRect` present in `controlBar`');
assert.ok(player.controlBar.getPositions().center, '`center` present in `controlBar`');
assert.ok(typeof player.controlBar.getPositions().boundingClientRect === 'object', '`boundingClientRect` is an object');
assert.ok(typeof player.controlBar.getPositions().center === 'object', '`center` is an object`');
player.dispose();
});
QUnit.test('getPositions() properties should not be empty', function(assert) {
const player = TestHelpers.makePlayer({
controls: true,
bigPlayButton: true,
spatialNavigation: { enabled: true }
});
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
let hasEmptyProperties = false;
const getPositionsProps = player.bigPlayButton.getPositions();
for (const property in getPositionsProps) {
const getPositionsProp = getPositionsProps[property];
for (const innerProperty in getPositionsProp) {
if (isEmpty(innerProperty)) {
hasEmptyProperties = true;
}
}
}
assert.expect(1);
assert.ok(!hasEmptyProperties, '`getPositions()` properties are not empty');
player.dispose();
});
QUnit.test('component keydown event propagation does not stop if spatial navigation is active', function(assert) {
// Ensure each test starts with a player that has spatial navigation enabled
this.player = TestHelpers.makePlayer({
controls: true,
bigPlayButton: true,
spatialNavigation: { enabled: true }
});
// Directly reference the instantiated SpatialNavigation from the player
this.spatialNav = this.player.spatialNavigation;
this.spatialNav.start();
const handlerSpy = sinon.spy(this.player, 'handleKeyDown');
// Create and dispatch a mock keydown event.
const event = new KeyboardEvent('keydown', { // eslint-disable-line no-undef
key: 'ArrowRight',
code: 'ArrowRight',
keyCode: 39,
location: 2,
repeat: true
});
this.player.bigPlayButton.handleKeyDown(event);
assert.ok(handlerSpy.calledOnce);
handlerSpy.restore();
this.player.dispose();
});
QUnit.test('Should be able to call `getIsAvailableToBeFocused()` even without passing an HTML element', function(assert) {
// Ensure each test starts with a player that has spatial navigation enabled
this.player = TestHelpers.makePlayer({
controls: true,
bigPlayButton: true,
spatialNavigation: { enabled: true }
});
// Directly reference the instantiated SpatialNavigation from the player
this.spatialNav = this.player.spatialNavigation;
const component = this.player.getChild('bigPlayButton');
const focusSpy = sinon.spy(component, 'getIsAvailableToBeFocused');
component.getIsAvailableToBeFocused(component.el());
component.getIsAvailableToBeFocused();
assert.ok(focusSpy.getCalls().length === 2, 'focus method called on component');
// Clean up
focusSpy.restore();
this.player.dispose();
});