diff options
Diffstat (limited to 'javascript/videojs/test/unit/component.test.js')
| -rw-r--r-- | javascript/videojs/test/unit/component.test.js | 1640 |
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(); +}); |
