summaryrefslogtreecommitdiff
path: root/javascript/videojs/test/unit/component.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/test/unit/component.test.js')
-rw-r--r--javascript/videojs/test/unit/component.test.js1640
1 files changed, 1640 insertions, 0 deletions
diff --git a/javascript/videojs/test/unit/component.test.js b/javascript/videojs/test/unit/component.test.js
new file mode 100644
index 0000000..2b384bc
--- /dev/null
+++ b/javascript/videojs/test/unit/component.test.js
@@ -0,0 +1,1640 @@
+/* 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();
+});