mirror of
https://github.com/videojs/video.js.git
synced 2024-12-23 02:04:34 +02:00
e715145d86
## Description The current implementation of `requestNamedAnimationFrame` prevents multiple updates on a frame but by disregarding all but the first request per frame. This throttling behaviour is apparent when playing a very short video - if a `timeupdate` occurs just before the `ended` event, the progress bar position on the `timeupdate` is set at say 98% and 100% from the `ended` is discarded. Although #8633 removed the throttle from the `ended` handler itself, the throttle and non-throttled update can still both execute between frames. ## Specific Changes proposed Changes the implementation to apply only the last callback instead. If any exist they will be cancelled. There will still be only one update, but now it's the last. Updates tests to reflect the changed behaviour. Fixes #8782 ## Requirements Checklist - [x] Feature implemented / Bug fixed - [ ] If necessary, more likely in a feature request than a bug fix - [x] Change has been verified in an actual browser (Chrome, Firefox, IE) - [x] Unit Tests updated or fixed - [ ] Docs/guides updated - [ ] Example created ([starter template on JSBin](https://codepen.io/gkatsev/pen/GwZegv?editors=1000#0)) - [x] Has no DOM changes which impact accessiblilty or trigger warnings (e.g. Chrome issues tab) - [x] Has no changes to JSDoc which cause `npm run docs:api` to error - [ ] Reviewed by Two Core Contributors
1641 lines
53 KiB
JavaScript
1641 lines
53 KiB
JavaScript
/* eslint-env qunit */
|
|
import window from 'global/window';
|
|
import Component from '../../src/js/component.js';
|
|
import * as Dom from '../../src/js/utils/dom.js';
|
|
import DomData from '../../src/js/utils/dom-data';
|
|
import * as Events from '../../src/js/utils/events.js';
|
|
import * as Obj from '../../src/js/utils/obj';
|
|
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);
|
|
|
|
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;
|
|
|
|
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 */
|
|
});
|
|
|
|
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();
|
|
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');
|
|
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.
|
|
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
|
|
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, ['testing'], 'one handler was cancelled');
|
|
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: 0,
|
|
two: 1,
|
|
three: 0
|
|
}, 'only handlerTwo was called');
|
|
|
|
comp.requestNamedAnimationFrame(name, handlerOne);
|
|
comp.requestNamedAnimationFrame(name, handlerTwo);
|
|
comp.requestNamedAnimationFrame(name, handlerThree);
|
|
|
|
assert.deepEqual(cancelNames, ['testing', 'testing', 'testing'], 'two more cancels');
|
|
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: 0,
|
|
two: 1,
|
|
three: 1
|
|
}, 'now handlerThree has also been 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');
|
|
|
|
});
|
|
|
|
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();
|
|
});
|