summaryrefslogtreecommitdiff
path: root/js/videojs/test/unit/tracks
diff options
context:
space:
mode:
Diffstat (limited to 'js/videojs/test/unit/tracks')
-rw-r--r--js/videojs/test/unit/tracks/audio-track-list.test.js120
-rw-r--r--js/videojs/test/unit/tracks/audio-track.test.js124
-rw-r--r--js/videojs/test/unit/tracks/audio-tracks.test.js110
-rw-r--r--js/videojs/test/unit/tracks/html-track-element-list.test.js87
-rw-r--r--js/videojs/test/unit/tracks/html-track-element.test.js81
-rw-r--r--js/videojs/test/unit/tracks/text-track-controls.test.js572
-rw-r--r--js/videojs/test/unit/tracks/text-track-cue-list.test.js99
-rw-r--r--js/videojs/test/unit/tracks/text-track-display.test.js622
-rw-r--r--js/videojs/test/unit/tracks/text-track-list-converter.test.js336
-rw-r--r--js/videojs/test/unit/tracks/text-track-list.test.js105
-rw-r--r--js/videojs/test/unit/tracks/text-track-select.test.js43
-rw-r--r--js/videojs/test/unit/tracks/text-track-settings.test.js416
-rw-r--r--js/videojs/test/unit/tracks/text-track.test.js806
-rw-r--r--js/videojs/test/unit/tracks/text-tracks.test.js690
-rw-r--r--js/videojs/test/unit/tracks/track-baseline.js49
-rw-r--r--js/videojs/test/unit/tracks/track-list.test.js177
-rw-r--r--js/videojs/test/unit/tracks/track.test.js58
-rw-r--r--js/videojs/test/unit/tracks/video-track-list.test.js117
-rw-r--r--js/videojs/test/unit/tracks/video-track.test.js120
-rw-r--r--js/videojs/test/unit/tracks/video-tracks.test.js112
20 files changed, 4844 insertions, 0 deletions
diff --git a/js/videojs/test/unit/tracks/audio-track-list.test.js b/js/videojs/test/unit/tracks/audio-track-list.test.js
new file mode 100644
index 0000000..d559cb2
--- /dev/null
+++ b/js/videojs/test/unit/tracks/audio-track-list.test.js
@@ -0,0 +1,120 @@
+/* eslint-env qunit */
+import AudioTrackList from '../../../src/js/tracks/audio-track-list.js';
+import AudioTrack from '../../../src/js/tracks/audio-track.js';
+import EventTarget from '../../../src/js/event-target.js';
+
+QUnit.module('Audio Track List');
+
+QUnit.test('trigger "change" when "enabledchange" is fired on a track', function(assert) {
+ const track = new EventTarget();
+
+ track.loaded_ = true;
+ const audioTrackList = new AudioTrackList([track]);
+ let changes = 0;
+ const changeHandler = function() {
+ changes++;
+ };
+
+ audioTrackList.on('change', changeHandler);
+ track.trigger('enabledchange');
+ assert.equal(changes, 1, 'one change events for trigger');
+
+ audioTrackList.off('change', changeHandler);
+ audioTrackList.onchange = changeHandler;
+
+ track.trigger('enabledchange');
+ assert.equal(changes, 2, 'one change events for another trigger');
+
+ audioTrackList.removeTrack(track);
+ audioTrackList.off('change');
+});
+
+QUnit.test('only one track is ever enabled', function(assert) {
+ const track = new AudioTrack({enabled: true});
+ const track2 = new AudioTrack({enabled: true});
+ const track3 = new AudioTrack({enabled: true});
+ const track4 = new AudioTrack();
+ const list = new AudioTrackList([track, track2]);
+
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, true, 'track2 is enabled');
+
+ track.enabled = true;
+ assert.equal(track.enabled, true, 'track is enabled');
+ assert.equal(track2.enabled, false, 'track2 is disabled');
+
+ list.addTrack(track3);
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, false, 'track2 is disabled');
+ assert.equal(track3.enabled, true, 'track3 is enabled');
+
+ track2.enabled = true;
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, true, 'track2 is enabled');
+ assert.equal(track3.enabled, false, 'track3 is disabled');
+
+ list.addTrack(track4);
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, true, 'track2 is enabled');
+ assert.equal(track3.enabled, false, 'track3 is disabled');
+ assert.equal(track4.enabled, false, 'track4 is disabled');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+ list.removeTrack(track3);
+ list.removeTrack(track4);
+});
+
+QUnit.test('all tracks can be disabled', function(assert) {
+ const track = new AudioTrack();
+ const track2 = new AudioTrack();
+
+ // we need audiotracklist here to verify that it does not
+ // re-enable a track
+ const list = new AudioTrackList([track, track2]);
+
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, false, 'track2 is disabled');
+
+ track.enabled = true;
+ assert.equal(track.enabled, true, 'track is enabled');
+ assert.equal(track2.enabled, false, 'track2 is disabled');
+
+ track.enabled = false;
+ assert.equal(track.enabled, false, 'track is disabled');
+ assert.equal(track2.enabled, false, 'track2 is disabled');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+});
+
+QUnit.test('trigger a change event per enabled change', function(assert) {
+ const track = new AudioTrack({enabled: true});
+ const track2 = new AudioTrack({enabled: true});
+ const track3 = new AudioTrack({enabled: true});
+ const track4 = new AudioTrack();
+ const list = new AudioTrackList([track, track2]);
+ let change = 0;
+
+ list.on('change', () => change++);
+ track.enabled = true;
+ assert.equal(change, 1, 'one change triggered');
+
+ list.addTrack(track3);
+ assert.equal(change, 2, 'another change triggered by adding an enabled track');
+
+ track.enabled = true;
+ assert.equal(change, 3, 'another change trigger by changing enabled');
+
+ track.enabled = false;
+ assert.equal(change, 4, 'another change trigger by changing enabled');
+
+ list.addTrack(track4);
+ assert.equal(change, 4, 'no change triggered by adding a disabled track');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+ list.removeTrack(track3);
+ list.removeTrack(track4);
+ list.off();
+});
diff --git a/js/videojs/test/unit/tracks/audio-track.test.js b/js/videojs/test/unit/tracks/audio-track.test.js
new file mode 100644
index 0000000..e8f3f2f
--- /dev/null
+++ b/js/videojs/test/unit/tracks/audio-track.test.js
@@ -0,0 +1,124 @@
+/* eslint-env qunit */
+import AudioTrack from '../../../src/js/tracks/audio-track.js';
+import {AudioTrackKind} from '../../../src/js/tracks/track-enums.js';
+import TrackBaseline from './track-baseline';
+
+QUnit.module('Audio Track');
+
+// do baseline track testing
+TrackBaseline(AudioTrack, {
+ id: '1',
+ language: 'en',
+ label: 'English',
+ kind: 'main'
+});
+
+QUnit.test('can create an enabled property on an AudioTrack', function(assert) {
+ const enabled = true;
+ const track = new AudioTrack({
+ enabled
+ });
+
+ assert.equal(track.enabled, enabled, 'enabled value matches what we passed in');
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const track = new AudioTrack();
+
+ assert.equal(track.kind, '', 'kind defaulted to empty string');
+ assert.equal(track.enabled, false, 'enabled defaulted to true since there is one track');
+ assert.equal(track.label, '', 'label defaults to empty string');
+ assert.equal(track.language, '', 'language defaults to empty string');
+ assert.ok(track.id.match(/vjs_track_\d+/), 'id defaults to vjs_track_GUID');
+});
+
+QUnit.test('kind can only be one of several options, defaults to empty string', function(assert) {
+ const track1 = new AudioTrack({
+ kind: 'foo'
+ });
+
+ assert.equal(track1.kind, '', 'the kind is set to empty string, not foo');
+ assert.notEqual(track1.kind, 'foo', 'the kind is set to empty string, not foo');
+
+ // loop through all possible kinds to verify
+ for (const key in AudioTrackKind) {
+ const currentKind = AudioTrackKind[key];
+ const track = new AudioTrack({
+ kind: currentKind
+ });
+
+ assert.equal(track.kind, currentKind, 'the kind is set to ' + currentKind);
+ }
+});
+
+QUnit.test('enabled can only be instantiated to true or false, defaults to false', function(assert) {
+ let track = new AudioTrack({
+ enabled: 'foo'
+ });
+
+ assert.equal(track.enabled, false, 'the enabled value is set to false, not foo');
+ assert.notEqual(track.enabled, 'foo', 'the enabled value is not set to foo');
+
+ track = new AudioTrack({
+ enabled: true
+ });
+
+ assert.equal(track.enabled, true, 'the enabled value is set to true');
+
+ track = new AudioTrack({
+ enabled: false
+ });
+
+ assert.equal(track.enabled, false, 'the enabled value is set to false');
+});
+
+QUnit.test('enabled can only be changed to true or false', function(assert) {
+ const track = new AudioTrack();
+
+ track.enabled = 'foo';
+ assert.notEqual(track.enabled, 'foo', 'enabled not set to invalid value, foo');
+ assert.equal(track.enabled, false, 'enabled remains on the old value, false');
+
+ track.enabled = true;
+ assert.equal(track.enabled, true, 'enabled was set to true');
+
+ track.enabled = 'baz';
+ assert.notEqual(track.enabled, 'baz', 'enabled not set to invalid value, baz');
+ assert.equal(track.enabled, true, 'enabled remains on the old value, true');
+
+ track.enabled = false;
+ assert.equal(track.enabled, false, 'enabled was set to false');
+});
+
+QUnit.test('when enabled is changed enabledchange event is fired', function(assert) {
+ const track = new AudioTrack({
+ tech: this.tech,
+ enabled: false
+ });
+ let eventsTriggered = 0;
+
+ track.addEventListener('enabledchange', () => {
+ eventsTriggered++;
+ });
+
+ // two events
+ track.enabled = true;
+ track.enabled = false;
+ assert.equal(eventsTriggered, 2, 'two enabled changes');
+
+ // no event here
+ track.enabled = false;
+ track.enabled = false;
+ assert.equal(eventsTriggered, 2, 'still two enabled changes');
+
+ // one event
+ track.enabled = true;
+ assert.equal(eventsTriggered, 3, 'three enabled changes');
+
+ // no events
+ track.enabled = true;
+ track.enabled = true;
+ assert.equal(eventsTriggered, 3, 'still three enabled changes');
+
+ track.off();
+});
diff --git a/js/videojs/test/unit/tracks/audio-tracks.test.js b/js/videojs/test/unit/tracks/audio-tracks.test.js
new file mode 100644
index 0000000..70327e0
--- /dev/null
+++ b/js/videojs/test/unit/tracks/audio-tracks.test.js
@@ -0,0 +1,110 @@
+/* eslint-env qunit */
+import Html5 from '../../../src/js/tech/html5.js';
+import TestHelpers from '../test-helpers.js';
+import sinon from 'sinon';
+
+QUnit.module('Audio Tracks', {
+ beforeEach(assert) {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ }
+});
+
+QUnit.test('Player track methods call the tech', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let calls = 0;
+
+ player.tech_.audioTracks = function() {
+ calls++;
+ };
+
+ player.audioTracks();
+
+ assert.equal(calls, 1, 'audioTrack defers to the tech');
+ player.dispose();
+});
+
+QUnit.test('listen to remove and add track events in native audio tracks', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+ const oldAudioTracks = Html5.prototype.audioTracks;
+ const events = {};
+
+ Html5.prototype.audioTracks = function() {
+ return {
+ removeEventListener() {},
+ addEventListener(type, handler) {
+ events[type] = true;
+ }
+ };
+ };
+
+ Html5.TEST_VID = {
+ audioTracks: []
+ };
+
+ const player = {
+ // Function.prototype is a built-in no-op function.
+ controls() {},
+ ready() {},
+ options() {
+ return {};
+ },
+ addChild() {},
+ id() {},
+ el() {
+ return {
+ insertBefore() {},
+ appendChild() {}
+ };
+ }
+ };
+
+ player.player_ = player;
+ player.options_ = {};
+
+ const html = new Html5({});
+
+ assert.ok(events.removetrack, 'removetrack listener was added');
+ assert.ok(events.addtrack, 'addtrack listener was added');
+
+ Html5.TEST_VID = oldTestVid;
+ Html5.prototype.audioTracks = oldAudioTracks;
+ html.dispose();
+});
+
+QUnit.test('html5 tech supports native audio tracks if the video supports it', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+
+ Html5.TEST_VID = {
+ audioTracks: []
+ };
+
+ assert.ok(Html5.supportsNativeAudioTracks(), 'native audio tracks are supported');
+
+ Html5.TEST_VID = oldTestVid;
+});
+
+QUnit.test('html5 tech does not support native audio tracks if the video does not supports it', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+
+ Html5.TEST_VID = {};
+
+ assert.ok(!Html5.supportsNativeAudioTracks(), 'native audio tracks are not supported');
+
+ Html5.TEST_VID = oldTestVid;
+});
+
+QUnit.test('when switching techs, we should not get a new audio track', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ player.loadTech_('TechFaker');
+ const firstTracks = player.audioTracks();
+
+ player.loadTech_('TechFaker');
+ const secondTracks = player.audioTracks();
+
+ assert.ok(firstTracks === secondTracks, 'the tracks are equal');
+ player.dispose();
+});
diff --git a/js/videojs/test/unit/tracks/html-track-element-list.test.js b/js/videojs/test/unit/tracks/html-track-element-list.test.js
new file mode 100644
index 0000000..da25d36
--- /dev/null
+++ b/js/videojs/test/unit/tracks/html-track-element-list.test.js
@@ -0,0 +1,87 @@
+/* eslint-env qunit */
+import HTMLTrackElementList from '../../../src/js/tracks/html-track-element-list.js';
+import TextTrack from '../../../src/js/tracks/text-track.js';
+
+const defaultTech = {
+ textTracks() {},
+ on() {},
+ off() {},
+ one() {},
+ currentTime() {}
+};
+
+const track1 = new TextTrack({
+ id: 1,
+ tech: defaultTech
+});
+const track2 = new TextTrack({
+ id: 2,
+ tech: defaultTech
+});
+
+const genericHtmlTrackElements = [{
+ tech() {},
+ kind: 'captions',
+ track: track1
+}, {
+ tech() {},
+ kind: 'chapters',
+ track: track2
+}];
+
+QUnit.module('HTML Track Element List');
+
+QUnit.test('HTMLTrackElementList\'s length is set correctly', function(assert) {
+ const htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements);
+
+ assert.equal(
+ htmlTrackElementList.length,
+ genericHtmlTrackElements.length,
+ `the length is ${genericHtmlTrackElements.length}`
+ );
+});
+
+QUnit.test('can get html track element by track', function(assert) {
+ const htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements);
+
+ assert.equal(
+ htmlTrackElementList.getTrackElementByTrack_(track1).kind,
+ 'captions',
+ 'track1 has kind of captions'
+ );
+ assert.equal(
+ htmlTrackElementList.getTrackElementByTrack_(track2).kind,
+ 'chapters',
+ 'track2 has kind of captions'
+ );
+});
+
+QUnit.test('length is updated when new tracks are added or removed', function(assert) {
+ const htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements);
+
+ htmlTrackElementList.addTrackElement_({tech() {}});
+ assert.equal(
+ htmlTrackElementList.length,
+ genericHtmlTrackElements.length + 1,
+ `the length is ${genericHtmlTrackElements.length + 1}`
+ );
+ htmlTrackElementList.addTrackElement_({tech() {}});
+ assert.equal(
+ htmlTrackElementList.length,
+ genericHtmlTrackElements.length + 2,
+ `the length is ${genericHtmlTrackElements.length + 2}`
+ );
+
+ htmlTrackElementList.removeTrackElement_(htmlTrackElementList.getTrackElementByTrack_(track1));
+ assert.equal(
+ htmlTrackElementList.length,
+ genericHtmlTrackElements.length + 1,
+ `the length is ${genericHtmlTrackElements.length + 1}`
+ );
+ htmlTrackElementList.removeTrackElement_(htmlTrackElementList.getTrackElementByTrack_(track2));
+ assert.equal(
+ htmlTrackElementList.length,
+ genericHtmlTrackElements.length,
+ `the length is ${genericHtmlTrackElements.length}`
+ );
+});
diff --git a/js/videojs/test/unit/tracks/html-track-element.test.js b/js/videojs/test/unit/tracks/html-track-element.test.js
new file mode 100644
index 0000000..b46b072
--- /dev/null
+++ b/js/videojs/test/unit/tracks/html-track-element.test.js
@@ -0,0 +1,81 @@
+/* eslint-env qunit */
+import HTMLTrackElement from '../../../src/js/tracks/html-track-element.js';
+import TechFaker from '../tech/tech-faker';
+
+QUnit.module('HTML Track Element', {
+ beforeEach() {
+ this.tech = new TechFaker();
+ },
+ afterEach() {
+ this.tech.dispose();
+ this.tech = null;
+ }
+});
+
+QUnit.test('html track element requires a tech', function(assert) {
+ assert.throws(
+ function() {
+ return new HTMLTrackElement();
+ },
+ new Error('A tech was not provided.'),
+ 'a tech is required for html track element'
+ );
+});
+
+QUnit.test('can create a html track element with various properties', function(assert) {
+ const kind = 'chapters';
+ const label = 'English';
+ const language = 'en';
+
+ const htmlTrackElement = new HTMLTrackElement({
+ kind,
+ label,
+ language,
+ tech: this.tech
+ });
+
+ assert.equal(typeof htmlTrackElement.default, 'undefined', 'we have a default');
+ assert.equal(htmlTrackElement.kind, kind, 'we have a kind');
+ assert.equal(htmlTrackElement.label, label, 'we have a label');
+ assert.equal(htmlTrackElement.readyState, 0, 'we have a readyState');
+ assert.equal(htmlTrackElement.srclang, language, 'we have a srclang');
+
+ htmlTrackElement.track.off();
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const htmlTrackElement = new HTMLTrackElement({
+ tech: this.tech
+ });
+
+ assert.equal(typeof htmlTrackElement.default, 'undefined', 'we have a default');
+ assert.equal(htmlTrackElement.kind, 'subtitles', 'we have a kind');
+ assert.equal(htmlTrackElement.label, '', 'we have a label');
+ assert.equal(htmlTrackElement.readyState, 0, 'we have a readyState');
+ assert.equal(typeof htmlTrackElement.src, 'undefined', 'we have a src');
+ assert.equal(htmlTrackElement.srclang, '', 'we have a srclang');
+ assert.equal(htmlTrackElement.track.cues.length, 0, 'we have a track');
+
+ htmlTrackElement.track.off();
+});
+
+QUnit.test('fires loadeddata when track cues become populated', function(assert) {
+ let changes = 0;
+ const loadHandler = function() {
+ changes++;
+ };
+ const htmlTrackElement = new HTMLTrackElement({
+ tech: this.tech
+ });
+
+ htmlTrackElement.addEventListener('load', loadHandler);
+
+ // trigger loaded cues event
+ htmlTrackElement.track.trigger('loadeddata');
+
+ assert.equal(changes, 1, 'a loadeddata event trigger addEventListener');
+ assert.equal(htmlTrackElement.readyState, 2, 'readyState is loaded');
+
+ htmlTrackElement.track.off();
+ htmlTrackElement.off('load');
+});
diff --git a/js/videojs/test/unit/tracks/text-track-controls.test.js b/js/videojs/test/unit/tracks/text-track-controls.test.js
new file mode 100644
index 0000000..e96ff94
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-controls.test.js
@@ -0,0 +1,572 @@
+/* eslint-env qunit */
+import TextTrackMenuItem from '../../../src/js/control-bar/text-track-controls/text-track-menu-item.js';
+import TestHelpers from '../test-helpers.js';
+import sinon from 'sinon';
+
+QUnit.module('Text Track Controls', {
+ beforeEach(assert) {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ }
+});
+
+const track = {
+ kind: 'captions',
+ label: 'test'
+};
+
+QUnit.test('should be displayed when text tracks list is not empty', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [track]
+ });
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
+ 'control is displayed'
+ );
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+QUnit.test('should be displayed when a text track is added to an empty track list', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ player.addRemoteTextTrack(track, true);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
+ 'control is displayed'
+ );
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+QUnit.test('should not be displayed when text tracks list is empty', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ assert.ok(
+ player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
+ 'control is not displayed'
+ );
+ assert.equal(player.textTracks().length, 0, 'textTracks is empty');
+
+ player.dispose();
+});
+
+QUnit.test('should not be displayed when last text track is removed', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [track]
+ });
+
+ player.removeRemoteTextTrack(player.textTracks()[0]);
+
+ assert.ok(
+ player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
+ 'control is not displayed'
+ );
+ assert.equal(player.textTracks().length, 0, 'textTracks is empty');
+
+ player.dispose();
+});
+
+QUnit.test('menu should contain "Settings", "Off" and one track', function(assert) {
+ const player = TestHelpers.makePlayer({
+ language: 'en',
+ tracks: [track]
+ });
+
+ this.clock.tick(1000);
+
+ const menuItems = player.controlBar.subsCapsButton.items;
+
+ assert.equal(menuItems.length, 3, 'menu contains three items');
+ assert.equal(
+ menuItems[0].track.label,
+ 'captions settings',
+ 'menu contains "captions settings"'
+ );
+ assert.equal(menuItems[1].track.label, 'captions off', 'menu contains "captions off"');
+ assert.equal(menuItems[2].track.label, 'test', 'menu contains "test" track');
+
+ player.dispose();
+});
+
+QUnit.test('menu should contain "Settings", "Off", one captions and one subtitles track', function(assert) {
+ const player = TestHelpers.makePlayer({
+ language: 'en',
+ tracks: [track, {
+ kind: 'subtitles',
+ label: 'test subs'
+ }]
+ });
+
+ this.clock.tick(1000);
+
+ const menuItems = player.controlBar.subsCapsButton.items;
+
+ assert.equal(menuItems.length, 4, 'menu contains three items');
+ assert.equal(
+ menuItems[0].track.label,
+ 'captions settings',
+ 'menu contains "captions settings"'
+ );
+ assert.equal(menuItems[1].track.label, 'captions off', 'menu contains "captions off"');
+ assert.equal(menuItems[2].track.label, 'test', 'menu contains "test" track');
+
+ player.dispose();
+});
+
+QUnit.test('menu should update with addRemoteTextTrack', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [track]
+ });
+
+ this.clock.tick(1000);
+
+ player.addRemoteTextTrack(track, true);
+
+ assert.equal(
+ player.controlBar.subsCapsButton.items.length,
+ 4,
+ 'menu does contain added track'
+ );
+ assert.equal(player.textTracks().length, 2, 'textTracks contains two items');
+
+ player.dispose();
+});
+
+QUnit.test('menu should update with removeRemoteTextTrack', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [track, track]
+ });
+
+ this.clock.tick(1000);
+
+ player.removeRemoteTextTrack(player.textTracks()[0]);
+
+ assert.equal(
+ player.controlBar.subsCapsButton.items.length,
+ 3,
+ 'menu does not contain removed track'
+ );
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+const descriptionstrack = {
+ kind: 'descriptions',
+ label: 'desc'
+};
+
+QUnit.test('descriptions should be displayed when text tracks list is not empty', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [descriptionstrack]
+ });
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
+ 'descriptions control is displayed'
+ );
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+QUnit.test('descriptions should be displayed when a text track is added to an empty track list', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ player.addRemoteTextTrack(descriptionstrack, true);
+
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
+ 'control is displayed'
+ );
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+QUnit.test('descriptions should not be displayed when text tracks list is empty', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ assert.ok(
+ player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
+ 'control is not displayed'
+ );
+ assert.equal(player.textTracks().length, 0, 'textTracks is empty');
+
+ player.dispose();
+});
+
+QUnit.test('descriptions should not be displayed when last text track is removed', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [descriptionstrack]
+ });
+
+ player.removeRemoteTextTrack(player.textTracks()[0]);
+
+ assert.ok(
+ player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
+ 'control is not displayed'
+ );
+ assert.equal(player.textTracks().length, 0, 'textTracks is empty');
+
+ player.dispose();
+});
+
+QUnit.test('descriptions menu should contain "Off" and one track', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [descriptionstrack]
+ });
+
+ this.clock.tick(1000);
+
+ const menuItems = player.controlBar.descriptionsButton.items;
+
+ assert.equal(menuItems.length, 2, 'descriptions menu contains two items');
+ assert.equal(
+ menuItems[0].track.label,
+ 'descriptions off',
+ 'menu contains "descriptions off"'
+ );
+ assert.equal(menuItems[1].track.label, 'desc', 'menu contains "desc" track');
+
+ player.dispose();
+});
+
+QUnit.test('enabling a captions track should disable the descriptions menu button', function(assert) {
+ assert.expect(14);
+
+ const player = TestHelpers.makePlayer({
+ tracks: [track, descriptionstrack]
+ });
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-hidden'),
+ 'captions control is displayed'
+ );
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
+ 'descriptions control is displayed'
+ );
+ assert.equal(player.textTracks().length, 2, 'textTracks contains two items');
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
+ 'captions control is NOT disabled'
+ );
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
+ 'descriptions control is NOT disabled'
+ );
+
+ for (let i = 0; i < player.textTracks().length; i++) {
+ if (player.textTracks()[i].kind === 'descriptions') {
+ player.textTracks()[i].mode = 'showing';
+ assert.ok(
+ player.textTracks()[i].kind === 'descriptions' &&
+ player.textTracks()[i].mode === 'showing',
+ 'descriptions mode set to showing'
+ );
+ }
+ }
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
+ 'captions control is NOT disabled'
+ );
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
+ 'descriptions control is NOT disabled'
+ );
+
+ for (let i = 0; i < player.textTracks().length; i++) {
+ if (player.textTracks()[i].kind === 'captions') {
+ player.textTracks()[i].mode = 'showing';
+ assert.ok(
+ player.textTracks()[i].kind === 'captions' &&
+ player.textTracks()[i].mode === 'showing',
+ 'captions mode set to showing'
+ );
+ }
+ }
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
+ 'captions control is NOT disabled'
+ );
+ assert.ok(
+ player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
+ 'descriptions control IS disabled'
+ );
+
+ for (let i = 0; i < player.textTracks().length; i++) {
+ if (player.textTracks()[i].kind === 'captions') {
+ player.textTracks()[i].mode = 'disabled';
+ assert.ok(
+ player.textTracks()[i].kind === 'captions' &&
+ player.textTracks()[i].mode === 'disabled',
+ 'captions mode set to disabled'
+ );
+ }
+ }
+
+ this.clock.tick(1000);
+
+ assert.ok(
+ !player.controlBar.subsCapsButton.hasClass('vjs-disabled'),
+ 'captions control is NOT disabled'
+ );
+ assert.ok(
+ !player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
+ 'descriptions control is NOT disabled'
+ );
+
+ player.dispose();
+});
+
+// This test tests a specific with iOS7 where
+// the TextTrackList doesn't report track mode changes.
+QUnit.test('menu items should polyfill mode change events', function(assert) {
+ const player = TestHelpers.makePlayer({});
+ let changes;
+
+ // emulate a TextTrackList that doesn't report track mode changes,
+ // like iOS7
+ player.textTracks().onchange = undefined;
+ const trackMenuItem = new TextTrackMenuItem(player, {
+ track
+ });
+
+ player.textTracks().on('change', function() {
+ changes++;
+ });
+ changes = 0;
+ trackMenuItem.trigger('tap');
+ assert.equal(changes, 1, 'taps trigger change events');
+
+ trackMenuItem.trigger('click');
+ assert.equal(changes, 2, 'clicks trigger change events');
+
+ player.dispose();
+ trackMenuItem.dispose();
+ player.textTracks().off('change');
+});
+
+const chaptersTrack = {
+ kind: 'chapters',
+ label: 'Test Chapters'
+};
+
+QUnit.test('chapters should not be displayed when text tracks list is empty', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ assert.ok(player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'control is not displayed');
+ assert.equal(player.textTracks().length, 0, 'textTracks is empty');
+
+ player.dispose();
+});
+
+QUnit.test('chapters should not be displayed when there is chapters track but no cues', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [chaptersTrack]
+ });
+
+ this.clock.tick(1000);
+
+ assert.ok(player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is not displayed');
+ assert.equal(player.textTracks().length, 1, 'textTracks contains one item');
+
+ player.dispose();
+});
+
+QUnit.test('chapters should be displayed when cues added to initial track and button updated', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [chaptersTrack]
+ });
+
+ this.clock.tick(1000);
+
+ const chapters = player.textTracks()[0];
+
+ chapters.addCue({
+ startTime: 0,
+ endTime: 2,
+ text: 'Chapter 1'
+ });
+ chapters.addCue({
+ startTime: 2,
+ endTime: 4,
+ text: 'Chapter 2'
+ });
+ assert.equal(chapters.cues.length, 2);
+
+ player.controlBar.chaptersButton.update();
+
+ assert.ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');
+
+ const menuItems = player.controlBar.chaptersButton.items;
+
+ assert.equal(menuItems.length, 2, 'menu contains two item');
+
+ player.dispose();
+});
+
+QUnit.test('chapters should be displayed when a track and its cures added and button updated', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ this.clock.tick(1000);
+
+ const chapters = player.addTextTrack('chapters', 'Test Chapters', 'en');
+
+ chapters.addCue({
+ startTime: 0,
+ endTime: 2,
+ text: 'Chapter 1'
+ });
+ chapters.addCue({
+ startTime: 2,
+ endTime: 4,
+ text: 'Chapter 2'
+ });
+ assert.equal(chapters.cues.length, 2);
+
+ player.controlBar.chaptersButton.update();
+
+ assert.ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');
+
+ const menuItems = player.controlBar.chaptersButton.items;
+
+ assert.equal(menuItems.length, 2, 'menu contains two item');
+
+ player.dispose();
+});
+
+QUnit.test('chapters menu should use track label as menu title', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks: [chaptersTrack]
+ });
+
+ this.clock.tick(1000);
+
+ const chapters = player.textTracks()[0];
+
+ chapters.addCue({
+ startTime: 0,
+ endTime: 2,
+ text: 'Chapter 1'
+ });
+ chapters.addCue({
+ startTime: 2,
+ endTime: 4,
+ text: 'Chapter 2'
+ });
+ assert.equal(chapters.cues.length, 2);
+
+ player.controlBar.chaptersButton.update();
+
+ const menu = player.controlBar.chaptersButton.menu;
+ const titleEl = menu.contentEl().firstChild;
+ const menuTitle = titleEl.textContent || titleEl.innerText;
+
+ assert.equal(menuTitle, 'Test Chapters', 'menu gets track label as title');
+
+ player.dispose();
+});
+
+QUnit.test('chapters should be displayed when remote track added and load event fired', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ this.clock.tick(1000);
+
+ const chaptersEl = player.addRemoteTextTrack(chaptersTrack, true);
+
+ chaptersEl.track.addCue({
+ startTime: 0,
+ endTime: 2,
+ text: 'Chapter 1'
+ });
+ chaptersEl.track.addCue({
+ startTime: 2,
+ endTime: 4,
+ text: 'Chapter 2'
+ });
+
+ assert.equal(chaptersEl.track.cues.length, 2);
+
+ // Anywhere where we support using native text tracks, we can trigger a custom DOM event.
+ // On IE8 and other places where we have emulated tracks, either we cannot trigger custom
+ // DOM events (like IE8 with the custom DOM element) or we aren't using a DOM element at all.
+ // In those cases just trigger `load` directly on the chaptersEl object.
+ if (player.tech_.featuresNativeTextTracks) {
+ TestHelpers.triggerDomEvent(chaptersEl, 'load');
+ } else {
+ chaptersEl.trigger('load');
+ }
+
+ assert.ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');
+
+ const menuItems = player.controlBar.chaptersButton.items;
+
+ assert.equal(menuItems.length, 2, 'menu contains two item');
+
+ player.dispose();
+});
+
+QUnit.test('chapters button should update selected menu item', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ this.clock.tick(1000);
+
+ const chaptersEl = player.addRemoteTextTrack(chaptersTrack, true);
+
+ chaptersEl.track.addCue({
+ startTime: 0,
+ endTime: 2,
+ text: 'Chapter 1'
+ });
+ chaptersEl.track.addCue({
+ startTime: 2,
+ endTime: 4,
+ text: 'Chapter 2'
+ });
+
+ assert.equal(chaptersEl.track.cues.length, 2);
+
+ if (player.tech_.featuresNativeTextTracks) {
+ TestHelpers.triggerDomEvent(chaptersEl, 'load');
+ } else {
+ chaptersEl.trigger('load');
+ }
+
+ const menuItems = player.controlBar.chaptersButton.items;
+
+ assert.ok(menuItems.find(i => i.isSelected_) === menuItems[0], 'item with startTime 0 selected on init');
+
+ player.currentTime(4);
+ chaptersEl.track.timeupdateHandler();
+
+ assert.ok(menuItems.find(i => i.isSelected_) === menuItems[1], 'second item selected on cuechange');
+
+ player.currentTime(1);
+ chaptersEl.track.timeupdateHandler();
+
+ assert.ok(menuItems.find(i => i.isSelected_) === menuItems[0], 'first item selected on cuechange');
+
+ player.dispose();
+});
diff --git a/js/videojs/test/unit/tracks/text-track-cue-list.test.js b/js/videojs/test/unit/tracks/text-track-cue-list.test.js
new file mode 100644
index 0000000..e62142d
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-cue-list.test.js
@@ -0,0 +1,99 @@
+/* eslint-env qunit */
+import TextTrackCueList from '../../../src/js/tracks/text-track-cue-list.js';
+
+const genericTracks = [
+ {
+ id: '1'
+ }, {
+ id: '2'
+ }, {
+ id: '3'
+ }
+];
+
+QUnit.module('Text Track Cue List');
+
+QUnit.test('TextTrackCueList\'s length is set correctly', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ assert.equal(ttcl.length, genericTracks.length, 'the length is ' + genericTracks.length);
+});
+
+QUnit.test('can get cues by id', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ assert.equal(ttcl.getCueById('1').id, 1, 'id "1" has id of "1"');
+ assert.equal(ttcl.getCueById('2').id, 2, 'id "2" has id of "2"');
+ assert.equal(ttcl.getCueById('3').id, 3, 'id "3" has id of "3"');
+ assert.ok(!ttcl.getCueById(1), 'there isn\'t an item with "numeric" id of `1`');
+});
+
+QUnit.test('length is updated when new tracks are added or removed', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ ttcl.setCues_(genericTracks.concat([{id: '100'}]));
+ assert.equal(
+ ttcl.length, genericTracks.length + 1,
+ 'the length is ' + (genericTracks.length + 1)
+ );
+ ttcl.setCues_(genericTracks.concat([{id: '100'}, {id: '101'}]));
+ assert.equal(
+ ttcl.length, genericTracks.length + 2,
+ 'the length is ' + (genericTracks.length + 2)
+ );
+
+ ttcl.setCues_(genericTracks.concat([{id: '100'}]));
+ assert.equal(
+ ttcl.length, genericTracks.length + 1,
+ 'the length is ' + (genericTracks.length + 1)
+ );
+ ttcl.setCues_(genericTracks);
+ assert.equal(ttcl.length, genericTracks.length, 'the length is ' + genericTracks.length);
+});
+
+QUnit.test('can access items by index', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+ const length = ttcl.length;
+
+ assert.expect(length);
+
+ for (let i = 0; i < length; i++) {
+ assert.equal(ttcl[i].id, String(i + 1), 'the id of a track matches the index + 1');
+ }
+});
+
+QUnit.test('can access new items by index', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ ttcl.setCues_(genericTracks.concat([{id: '100'}]));
+
+ assert.equal(ttcl[3].id, '100', 'id of item at index 3 is 100');
+ ttcl.setCues_(genericTracks.concat([{id: '100'}, {id: '101'}]));
+ assert.equal(ttcl[4].id, '101', 'id of item at index 4 is 101');
+});
+
+QUnit.test('cannot access removed items by index', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ ttcl.setCues_(genericTracks.concat([{id: '100'}, {id: '101'}]));
+ assert.equal(ttcl[3].id, '100', 'id of item at index 3 is 100');
+ assert.equal(ttcl[4].id, '101', 'id of item at index 4 is 101');
+
+ ttcl.setCues_(genericTracks);
+
+ assert.ok(!ttcl[3], 'nothing at index 3');
+ assert.ok(!ttcl[4], 'nothing at index 4');
+});
+
+QUnit.test('new item available at old index', function(assert) {
+ const ttcl = new TextTrackCueList(genericTracks);
+
+ ttcl.setCues_(genericTracks.concat([{id: '100'}]));
+ assert.equal(ttcl[3].id, '100', 'id of item at index 3 is 100');
+
+ ttcl.setCues_(genericTracks);
+ assert.ok(!ttcl[3], 'nothing at index 3');
+
+ ttcl.setCues_(genericTracks.concat([{id: '101'}]));
+ assert.equal(ttcl[3].id, '101', 'id of new item at index 3 is now 101');
+});
diff --git a/js/videojs/test/unit/tracks/text-track-display.test.js b/js/videojs/test/unit/tracks/text-track-display.test.js
new file mode 100644
index 0000000..6dcfb01
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-display.test.js
@@ -0,0 +1,622 @@
+/* eslint-env qunit */
+import window from 'global/window';
+import Html5 from '../../../src/js/tech/html5.js';
+import { constructColor } from '../../../src/js/tracks/text-track-display.js';
+import Component from '../../../src/js/component.js';
+
+import * as browser from '../../../src/js/utils/browser.js';
+import TestHelpers from '../test-helpers.js';
+import document from 'global/document';
+import sinon from 'sinon';
+
+QUnit.module('Text Track Display', {
+ beforeEach(assert) {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ }
+});
+
+const getMenuItemByLanguage = function(items, language) {
+ for (let i = items.length - 1; i > 0; i--) {
+ const captionMenuItem = items[i];
+ const trackLanguage = captionMenuItem.track.language;
+
+ if (trackLanguage && trackLanguage === language) {
+ return captionMenuItem;
+ }
+ }
+};
+
+QUnit.test('if native text tracks are not supported, create a texttrackdisplay', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+ const oldIsFirefox = browser.IS_FIREFOX;
+ const oldTextTrackDisplay = Component.getComponent('TextTrackDisplay');
+ const tag = document.createElement('video');
+ const track1 = document.createElement('track');
+ const track2 = document.createElement('track');
+
+ track1.kind = 'captions';
+ track1.label = 'en';
+ track1.language = 'English';
+ track1.src = 'en.vtt';
+ tag.appendChild(track1);
+
+ track2.kind = 'captions';
+ track2.label = 'es';
+ track2.language = 'Spanish';
+ track2.src = 'es.vtt';
+ tag.appendChild(track2);
+
+ Html5.TEST_VID = {
+ textTracks: []
+ };
+
+ browser.stub_IS_FIREFOX(true);
+
+ const fakeTTDSpy = sinon.spy();
+
+ class FakeTTD extends Component {
+ constructor(player, options) {
+ super(player, options);
+ fakeTTDSpy();
+ }
+ }
+
+ Component.registerComponent('TextTrackDisplay', FakeTTD);
+
+ const player = TestHelpers.makePlayer({}, tag);
+
+ assert.strictEqual(fakeTTDSpy.callCount, 1, 'text track display was created');
+
+ Html5.TEST_VID = oldTestVid;
+ browser.stub_IS_FIREFOX(oldIsFirefox);
+ Component.registerComponent('TextTrackDisplay', oldTextTrackDisplay);
+
+ player.dispose();
+});
+
+QUnit.test('shows the default caption track first', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt',
+ default: true
+ };
+ const track2 = {
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ src: 'es.vtt'
+ };
+
+ // Add the text tracks
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+ const spanishTrack = player.addRemoteTextTrack(track2, true).track;
+
+ // Make sure the ready handler runs
+ this.clock.tick(1);
+
+ assert.ok(englishTrack.mode === 'showing', 'English track should be showing');
+ assert.ok(spanishTrack.mode === 'disabled', 'Spanish track should not be showing');
+ player.dispose();
+});
+
+if (!Html5.supportsNativeTextTracks()) {
+ QUnit.test('text track display should attach screen orientation change event handler', function(assert) {
+ const oldScreen = window.screen;
+ const removeHandlerSpy = sinon.spy();
+ let changeHandlerSpy;
+ let changeHandlerAttached;
+
+ window.screen = {
+ orientation: {
+ addEventListener: (type, func) => {
+ changeHandlerAttached = true;
+ changeHandlerSpy = sinon.spy();
+ },
+ dispatchEvent: (type) => changeHandlerSpy(),
+ removeEventListener: removeHandlerSpy
+ }
+ };
+
+ const player = TestHelpers.makePlayer();
+
+ this.clock.tick(1);
+
+ assert.true(changeHandlerAttached, 'screen orientation change event handler was not attached');
+ assert.strictEqual(changeHandlerSpy.callCount, 0, 'screen orientation change event handler should not be called');
+
+ window.screen.orientation.dispatchEvent('change');
+
+ assert.strictEqual(changeHandlerSpy.callCount, 1, 'screen orientation change event handler was not called');
+
+ player.dispose();
+
+ assert.strictEqual(
+ removeHandlerSpy.callCount,
+ 1,
+ 'screen orientation change event handler was not removed during player dispose'
+ );
+ window.screen = oldScreen;
+ });
+
+ QUnit.test('selectedlanguagechange is triggered by a track mode change', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const spy = sinon.spy();
+ const selectedLanguageHandler = function(event) {
+ spy();
+ };
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+
+ player.textTracks().addEventListener('selectedlanguagechange', selectedLanguageHandler);
+ englishTrack.mode = 'showing';
+
+ assert.strictEqual(spy.callCount, 1, 'selectedlanguagechange event was fired');
+ player.dispose();
+ player.textTracks().removeEventListener('selectedlanguagechange', selectedLanguageHandler);
+ });
+
+ QUnit.test("if user-selected language is unavailable, don't pick a track to show", function(assert) {
+ // The video has no default language but has ‘English’ captions only
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const captionsButton = player.controlBar.getChild('SubsCapsButton');
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+
+ // Force 'es' as user-selected track
+ player.cache_.selectedLanguage = { language: 'es', kind: 'captions' };
+
+ this.clock.tick(1);
+ player.play();
+
+ assert.ok(!captionsButton.hasClass('vjs-hidden'), 'The captions button is shown');
+ assert.ok(englishTrack.mode === 'disabled', 'English track should be disabled');
+ player.dispose();
+ });
+
+ QUnit.test('the user-selected language takes priority over default language', function(assert) {
+ // The video has ‘English’ captions as default, but has ‘Spanish’ captions also
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt',
+ default: true
+ };
+ const track2 = {
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ src: 'es.vtt'
+ };
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+ const spanishTrack = player.addRemoteTextTrack(track2, true).track;
+
+ // Force 'es' as user-selected track
+ player.cache_.selectedLanguage = { enabled: true, language: 'es', kind: 'captions' };
+ this.clock.tick(1);
+
+ assert.ok(spanishTrack.mode === 'showing', 'Spanish captions should be shown');
+ assert.ok(englishTrack.mode === 'disabled', 'English captions should be hidden');
+ player.dispose();
+ });
+
+ QUnit.test("don't select user language if it is an empty string", function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const track2 = {
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ src: 'es.vtt'
+ };
+ const track3 = {
+ kind: 'metadata',
+ label: 'segment-metadata'
+ };
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+ const spanishTrack = player.addRemoteTextTrack(track2, true).track;
+ const metadataTrack = player.addRemoteTextTrack(track3, true).track;
+
+ // Force empty string ('') as "user-selected" track
+ player.cache_.selectedLanguage = { enabled: true, language: '', kind: 'captions' };
+ this.clock.tick(1);
+
+ assert.equal(spanishTrack.mode, 'disabled', 'Spanish captions should be disabled');
+ assert.equal(englishTrack.mode, 'disabled', 'English captions should be disabled');
+ assert.notEqual(metadataTrack.mode, 'showing', 'Metadata track should not be showing');
+
+ // Force es as "user-selected" track
+ player.cache_.selectedLanguage = { enabled: true, language: 'es', kind: 'captions' };
+ player.trigger('loadedmetadata');
+
+ assert.equal(spanishTrack.mode, 'showing', 'Spanish captions should be showing');
+ assert.equal(englishTrack.mode, 'disabled', 'English captions should be disabled');
+ assert.notEqual(metadataTrack.mode, 'showing', 'Metadata track should not be showing');
+
+ player.dispose();
+ });
+
+ QUnit.test("matching both the selectedLanguage's language and kind takes priority over just matching the language", function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const track2 = {
+ kind: 'subtitles',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ const captionTrack = player.addRemoteTextTrack(track1, true).track;
+ const subsTrack = player.addRemoteTextTrack(track2, true).track;
+
+ // Force English captions as user-selected track
+ player.cache_.selectedLanguage = { enabled: true, language: 'en', kind: 'captions' };
+ this.clock.tick(1);
+
+ assert.ok(captionTrack.mode === 'showing', 'Captions track should be preselected');
+ assert.ok(subsTrack.mode === 'disabled', 'Subtitles track should remain disabled');
+ player.dispose();
+ });
+
+ QUnit.test('the user-selected language is used for subsequent source changes', function(assert) {
+ // Start with two captions tracks: English and Spanish
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const track2 = {
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ src: 'es.vtt'
+ };
+ const tracks = player.tech_.remoteTextTracks();
+ const captionsButton = player.controlBar.getChild('SubsCapsButton');
+ let esCaptionMenuItem;
+ let enCaptionMenuItem;
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ player.addRemoteTextTrack(track1, true);
+ player.addRemoteTextTrack(track2, true);
+
+ // Keep track of menu items
+ esCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'es');
+ enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');
+
+ // The user chooses Spanish
+ player.play();
+ esCaptionMenuItem.trigger('click');
+
+ // Track mode changes on user-selection
+ assert.ok(
+ esCaptionMenuItem.track.mode === 'showing',
+ 'Spanish should be showing after selection'
+ );
+ assert.ok(
+ enCaptionMenuItem.track.mode === 'disabled',
+ 'English should be disabled after selecting Spanish'
+ );
+ assert.deepEqual(
+ player.cache_.selectedLanguage,
+ { enabled: true, language: 'es', kind: 'captions' }
+ );
+
+ // Switch source and remove old tracks
+ player.tech_.src({type: 'video/mp4', src: 'http://example.com'});
+ while (tracks.length > 0) {
+ player.removeRemoteTextTrack(tracks[0]);
+ }
+ // Add tracks for the new source
+ // change the kind of track to subtitles
+ track1.kind = 'subtitles';
+ track2.kind = 'subtitles';
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+ const spanishTrack = player.addRemoteTextTrack(track2, true).track;
+
+ // Make sure player ready handler runs
+ this.clock.tick(1);
+
+ // Keep track of menu items
+ esCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'es');
+ enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');
+
+ // The user-selection should have persisted
+ assert.ok(
+ esCaptionMenuItem.track.mode === 'showing',
+ 'Spanish should remain showing'
+ );
+ assert.ok(
+ enCaptionMenuItem.track.mode === 'disabled',
+ 'English should remain disabled'
+ );
+ assert.deepEqual(
+ player.cache_.selectedLanguage,
+ { enabled: true, language: 'es', kind: 'captions' }
+ );
+
+ assert.ok(spanishTrack.mode === 'showing', 'Spanish track remains showing');
+ assert.ok(englishTrack.mode === 'disabled', 'English track remains disabled');
+ player.dispose();
+ });
+
+ QUnit.test('the user-selected language is cleared on turning off captions', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt'
+ };
+ const captionsButton = player.controlBar.getChild('SubsCapsButton');
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ // manualCleanUp = true by default
+ const englishTrack = player.addRemoteTextTrack(track1, true).track;
+ // Keep track of menu items
+ const enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');
+ // we know the position of the OffTextTrackMenuItem
+ const offMenuItem = captionsButton.items[1];
+
+ // Select English initially
+ player.play();
+ enCaptionMenuItem.trigger('click');
+
+ assert.deepEqual(
+ player.cache_.selectedLanguage,
+ { enabled: true, language: 'en', kind: 'captions' }, 'English track is selected'
+ );
+ assert.ok(englishTrack.mode === 'showing', 'English track should be showing');
+
+ // Select the off button
+ offMenuItem.trigger('click');
+
+ assert.deepEqual(
+ player.cache_.selectedLanguage,
+ { enabled: false }, 'selectedLanguage is cleared'
+ );
+ assert.ok(englishTrack.mode === 'disabled', 'English track is disabled');
+ player.dispose();
+ });
+
+ QUnit.test('a color can be constructed from a three digit hex code', function(assert) {
+ const hex = '#f0e';
+
+ // f gets mapped to ff -> 255 in decimal,
+ // 0 gets mapped to 00 -> 0 in decimal,
+ // e gets mapped to ee -> 238 in decimal.
+ assert.equal(constructColor(hex, 1), 'rgba(255,0,238,1)');
+ });
+
+ QUnit.test('a color can be constructed from a six digit hex code', function(assert) {
+ const hex = '#f604e2';
+
+ // f6 -> 246 in decimal,
+ // 04 -> 4 in decimal,
+ // e2 -> 226 in decimal.
+ assert.equal(constructColor(hex, 1), 'rgba(246,4,226,1)');
+ });
+
+ QUnit.test('an invalid hex code will throw an error', function(assert) {
+ const hex = '#f';
+
+ assert.throws(
+ function() {
+ constructColor(hex, 1);
+ },
+ new Error('Invalid color code provided, #f; must be formatted as e.g. #f0e or #f604e2.'),
+ 'colors must be valid hex codes.'
+ );
+ });
+
+ const skipOnOldChrome = window.CSS.supports('inset-inline: 10px') ? 'test' : 'skip';
+
+ QUnit[skipOnOldChrome]('text track display should overlay a video', function(assert) {
+ const tag = document.createElement('video');
+
+ tag.width = 320;
+ tag.height = 180;
+ const player = TestHelpers.makePlayer({}, tag);
+ const textTrackDisplay = player.getChild('TextTrackDisplay');
+ const textTrackDisplayStyle = textTrackDisplay.el().style;
+
+ assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
+ assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');
+
+ // video aspect ratio equal to NaN
+ player.tech_.videoWidth = () => 0;
+ player.tech_.videoHeight = () => 0;
+
+ assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
+ assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');
+
+ // video aspect ratio 2:1
+ player.tech_.videoWidth = () => 100;
+ player.tech_.videoHeight = () => 50;
+
+ textTrackDisplay.updateDisplayOverlay();
+
+ assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
+ assert.ok(textTrackDisplayStyle.insetBlock === '10px', 'text track display style insetBlock equal to 10px');
+
+ // video aspect ratio 4:3
+ player.tech_.videoWidth = () => 100;
+ player.tech_.videoHeight = () => 75;
+
+ textTrackDisplay.updateDisplayOverlay();
+
+ assert.ok(textTrackDisplayStyle.insetInline === '40px', 'text track display style insetInline equal to 40px');
+ assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');
+
+ // video aspect ratio 16:9
+ player.tech_.videoWidth = () => 320;
+ player.tech_.videoHeight = () => 180;
+
+ textTrackDisplay.updateDisplayOverlay();
+
+ assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
+ assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');
+
+ player.dispose();
+ });
+
+ QUnit.test('should use relative position for vjs-text-track-display element if environment does not support window.CSS', function(assert) {
+ const cssDescriptor = Object.getOwnPropertyDescriptor(window, 'CSS');
+
+ try {
+ // Set conditions for the use of the style modifications. Temporarily
+ // remove window.CSS to match the environment created by jsdom.
+ // https://github.com/jsdom/jsdom/issues/3991
+ delete window.CSS;
+
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt',
+ default: true
+ };
+
+ // Add the text track
+ player.addRemoteTextTrack(track1, true);
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ player.play();
+
+ // as if metadata was loaded
+ player.textTrackDisplay.updateDisplayOverlay();
+
+ // Make sure the ready handler runs
+ this.clock.tick(1);
+
+ const textTrack = window.document.querySelector('.vjs-text-track-display');
+
+ assert.ok(textTrack.style.position === 'relative', 'Style of position for vjs-text-track-display element should be relative');
+ assert.ok(textTrack.style.top === 'unset', 'Style of position for vjs-text-track-display element should be unset');
+ assert.ok(textTrack.style.bottom === '0px', 'Style of bottom for vjs-text-track-display element should be 0px');
+ player.dispose();
+ } finally {
+ // Restore window.CSS
+ Object.defineProperty(window, 'CSS', cssDescriptor);
+ }
+ });
+
+ QUnit.test('should use relative position for vjs-text-track-display element if browser does not support inset property', function(assert) {
+ // Set conditions for the use of the style modifications
+ window.CSS.supports = () => false;
+ browser.IS_SMART_TV = () => true;
+
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt',
+ default: true
+ };
+
+ // Add the text track
+ player.addRemoteTextTrack(track1, true);
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ player.play();
+
+ // as if metadata was loaded
+ player.textTrackDisplay.updateDisplayOverlay();
+
+ // Make sure the ready handler runs
+ this.clock.tick(1);
+
+ const textTrack = window.document.querySelector('.vjs-text-track-display');
+
+ assert.ok(textTrack.style.position === 'relative', 'Style of position for vjs-text-track-display element should be relative');
+ assert.ok(textTrack.style.top === 'unset', 'Style of position for vjs-text-track-display element should be unset');
+ assert.ok(textTrack.style.bottom === '0px', 'Style of bottom for vjs-text-track-display element should be 0px');
+ player.dispose();
+ });
+
+ QUnit.test('track cue should use values of top, right, botton, left if browser does not support inset property', function(assert) {
+ // Set conditions for the use of the style modifications
+ window.CSS.supports = () => false;
+ browser.IS_SMART_TV = () => true;
+
+ const player = TestHelpers.makePlayer();
+ const track1 = {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'en.vtt',
+ default: true
+ };
+
+ // Add the text track
+ player.addRemoteTextTrack(track1, true);
+
+ player.src({type: 'video/mp4', src: 'http://google.com'});
+ player.play();
+
+ // mock caption
+ const textTrackDisplay = window.document.querySelector('.vjs-text-track-display').firstChild;
+ const node = document.createElement('div');
+
+ node.classList.add('vjs-text-track-cue');
+ node.style.inset = '1px 2px 3px';
+ const textnode = document.createTextNode('Sample text');
+
+ node.appendChild(textnode);
+ textTrackDisplay.appendChild(node);
+
+ // avoid captions clear
+ player.textTrackDisplay.clearDisplay = () => '';
+
+ // as if metadata was loaded
+ player.textTrackDisplay.updateDisplay();
+
+ assert.ok(player.textTrackDisplay.el_.querySelector('.vjs-text-track-cue').style.left === 'unset', 'Style of left for vjs-text-track-cue element should be unset');
+ assert.ok(player.textTrackDisplay.el_.querySelector('.vjs-text-track-cue').style.top === '1px', 'Style of top for vjs-text-track-cue element should be 1px');
+ assert.ok(player.textTrackDisplay.el_.querySelector('.vjs-text-track-cue').style.right === '2px', 'Style of right for vjs-text-track-cue element should be 2px');
+ player.dispose();
+ });
+}
diff --git a/js/videojs/test/unit/tracks/text-track-list-converter.test.js b/js/videojs/test/unit/tracks/text-track-list-converter.test.js
new file mode 100644
index 0000000..33501e0
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-list-converter.test.js
@@ -0,0 +1,336 @@
+/* eslint-env qunit */
+import c from '../../../src/js/tracks/text-track-list-converter.js';
+import TextTrack from '../../../src/js/tracks/text-track.js';
+import TextTrackList from '../../../src/js/tracks/text-track-list.js';
+import Html5 from '../../../src/js/tech/html5.js';
+import document from 'global/document';
+
+QUnit.module('Text Track List Converter', {});
+
+const clean = (item) => {
+ delete item.id;
+ delete item.inBandMetadataTrackDispatchType;
+ delete item.cues;
+};
+
+const cleanup = (item) => {
+ if (Array.isArray(item)) {
+ item.forEach(clean);
+ } else {
+ clean(item);
+ }
+
+ return item;
+};
+
+if (Html5.supportsNativeTextTracks()) {
+ QUnit.test('trackToJson_ produces correct representation for native track object', function(assert) {
+ const track = document.createElement('track');
+
+ track.src = 'example.com/english.vtt';
+ track.kind = 'captions';
+ track.srclang = 'en';
+ track.label = 'English';
+
+ assert.deepEqual(cleanup(c.trackToJson(track.track)), {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ mode: 'disabled'
+ }, 'the json output is same');
+ });
+
+ QUnit.test('textTracksToJson produces good json output', function(assert) {
+ const emulatedTrack = new TextTrack({
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const nativeTrack = document.createElement('track');
+
+ nativeTrack.kind = 'captions';
+ nativeTrack.srclang = 'es';
+ nativeTrack.label = 'Spanish';
+
+ const tt = new TextTrackList();
+
+ tt.addTrack(nativeTrack.track);
+ tt.addTrack(emulatedTrack);
+
+ const tech = {
+ $$() {
+ return [nativeTrack];
+ },
+
+ el() {
+ return {
+ querySelectorAll() {
+ return [nativeTrack];
+ }
+ };
+ },
+ on() {},
+ crossOrigin() {
+ return null;
+ },
+ textTracks() {
+ return tt;
+ }
+ };
+
+ assert.deepEqual(cleanup(c.textTracksToJson(tech)), [{
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ mode: 'disabled'
+ }, {
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ mode: 'disabled'
+ }], 'the output is correct');
+
+ tt.removeTrack(nativeTrack.track);
+ tt.removeTrack(emulatedTrack);
+ });
+
+ QUnit.test('jsonToTextTracks calls addRemoteTextTrack on the tech with mixed tracks', function(assert) {
+ const emulatedTrack = new TextTrack({
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'example.com/english.vtt',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const nativeTrack = document.createElement('track');
+
+ nativeTrack.src = 'example.com/spanish.vtt';
+ nativeTrack.kind = 'captions';
+ nativeTrack.srclang = 'es';
+ nativeTrack.label = 'Spanish';
+
+ const tt = new TextTrackList();
+
+ tt.addTrack(nativeTrack.track);
+ tt.addTrack(emulatedTrack);
+
+ let addRemotes = 0;
+ const tech = {
+ $$() {
+ return [nativeTrack];
+ },
+
+ el() {
+ return {
+ querySelectorAll() {
+ return [nativeTrack];
+ }
+ };
+ },
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ textTracks() {
+ return tt;
+ },
+ addRemoteTextTrack() {
+ addRemotes++;
+ return {
+ track: {}
+ };
+ }
+ };
+
+ c.jsonToTextTracks(cleanup(c.textTracksToJson(tech)), tech);
+
+ assert.equal(addRemotes, 2, 'we added two text tracks');
+
+ tt.removeTrack(nativeTrack.track);
+ tt.removeTrack(emulatedTrack);
+ });
+}
+
+QUnit.test('trackToJson_ produces correct representation for emulated track object', function(assert) {
+ const track = new TextTrack({
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'example.com/english.vtt',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ assert.deepEqual(cleanup(c.trackToJson(track)), {
+ src: 'example.com/english.vtt',
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ mode: 'disabled'
+ }, 'the json output is same');
+});
+
+QUnit.test('textTracksToJson produces good json output for emulated only', function(assert) {
+ const emulatedTrack = new TextTrack({
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'example.com/english.vtt',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const anotherTrack = new TextTrack({
+ src: 'example.com/spanish.vtt',
+ kind: 'captions',
+ srclang: 'es',
+ label: 'Spanish',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const tt = new TextTrackList();
+
+ tt.addTrack(anotherTrack);
+ tt.addTrack(emulatedTrack);
+
+ const tech = {
+ $$() {
+ return [];
+ },
+
+ el() {
+ return {
+ querySelectorAll() {
+ return [];
+ }
+ };
+ },
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {},
+ textTracks() {
+ return tt;
+ }
+ };
+
+ assert.deepEqual(cleanup(c.textTracksToJson(tech)), [{
+ src: 'example.com/spanish.vtt',
+ kind: 'captions',
+ label: 'Spanish',
+ language: 'es',
+ mode: 'disabled'
+ }, {
+ src: 'example.com/english.vtt',
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ mode: 'disabled'
+ }], 'the output is correct');
+
+ tt.removeTrack(anotherTrack);
+ tt.removeTrack(emulatedTrack);
+});
+
+QUnit.test('jsonToTextTracks calls addRemoteTextTrack on the tech with emulated tracks only', function(assert) {
+ const emulatedTrack = new TextTrack({
+ kind: 'captions',
+ label: 'English',
+ language: 'en',
+ src: 'example.com/english.vtt',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const anotherTrack = new TextTrack({
+ src: 'example.com/spanish.vtt',
+ kind: 'captions',
+ srclang: 'es',
+ label: 'Spanish',
+ tech: {
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {}
+ }
+ });
+
+ const tt = new TextTrackList();
+
+ tt.addTrack(anotherTrack);
+ tt.addTrack(emulatedTrack);
+
+ let addRemotes = 0;
+ const tech = {
+ $$() {
+ return [];
+ },
+
+ el() {
+ return {
+ querySelectorAll() {
+ return [];
+ }
+ };
+ },
+ crossOrigin() {
+ return null;
+ },
+ on() {},
+ one() {},
+ textTracks() {
+ return tt;
+ },
+ addRemoteTextTrack() {
+ addRemotes++;
+ return {
+ track: {}
+ };
+ }
+ };
+
+ c.jsonToTextTracks(cleanup(c.textTracksToJson(tech)), tech);
+
+ assert.equal(addRemotes, 2, 'we added two text tracks');
+
+ tt.removeTrack(anotherTrack);
+ tt.removeTrack(emulatedTrack);
+});
diff --git a/js/videojs/test/unit/tracks/text-track-list.test.js b/js/videojs/test/unit/tracks/text-track-list.test.js
new file mode 100644
index 0000000..47c5f68
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-list.test.js
@@ -0,0 +1,105 @@
+/* eslint-env qunit */
+import TextTrackList from '../../../src/js/tracks/text-track-list.js';
+import TextTrack from '../../../src/js/tracks/text-track.js';
+import EventTarget from '../../../src/js/event-target.js';
+import TechFaker from '../tech/tech-faker';
+import sinon from 'sinon';
+
+QUnit.module('Text Track List', {
+ beforeEach() {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach() {
+ this.clock.restore();
+ }
+});
+
+QUnit.test('trigger "change" event when "modechange" is fired on a track', function(assert) {
+ const tt = new EventTarget();
+ const ttl = new TextTrackList([tt]);
+ let changes = 0;
+ const changeHandler = function() {
+ changes++;
+ };
+
+ ttl.on('change', changeHandler);
+ tt.trigger('modechange');
+ this.clock.tick(1);
+
+ ttl.off('change', changeHandler);
+ ttl.onchange = changeHandler;
+
+ tt.trigger('modechange');
+ this.clock.tick(1);
+ assert.equal(changes, 2, 'two change events should have fired');
+
+ ttl.removeTrack(tt);
+});
+
+QUnit.test('trigger "change" event when mode changes on a TextTrack', function(assert) {
+ const tech = new TechFaker();
+ const tt = new TextTrack({tech});
+ const ttl = new TextTrackList([tt]);
+ let changes = 0;
+ const changeHandler = function() {
+ changes++;
+ };
+
+ ttl.on('change', changeHandler);
+ tt.mode = 'showing';
+ this.clock.tick(1);
+
+ ttl.off('change', changeHandler);
+ ttl.onchange = changeHandler;
+
+ tt.mode = 'hidden';
+ tt.mode = 'disabled';
+ this.clock.tick(1);
+
+ assert.equal(changes, 2, 'two change events should have fired');
+ ttl.removeTrack(tt);
+ tech.dispose();
+});
+
+QUnit.test('toJSON', function(assert) {
+ const tech = new TechFaker();
+ const ttl = new TextTrackList([]);
+
+ let textTrackListJSON = ttl.toJSON();
+
+ assert.ok(textTrackListJSON.length === 0, 'an empty array is returned when the list is empty');
+
+ const tt1 = new TextTrack({tech});
+
+ ttl.addTrack(tt1);
+ textTrackListJSON = ttl.toJSON();
+
+ assert.equal(textTrackListJSON[0].id, tt1.id, 'text track in array of JSON should match the original track');
+ assert.notOk(textTrackListJSON[0].tech_, 'tech_ should not exist on the text track value in the JSON list');
+
+ const tt2 = new TextTrack({tech});
+ const tt3 = new TextTrack({tech});
+
+ ttl.addTrack(tt2);
+ ttl.addTrack(tt3);
+ textTrackListJSON = ttl.toJSON();
+
+ assert.equal(textTrackListJSON[1].id, tt2.id, 'text track in second spot of array should match the original track');
+ assert.equal(textTrackListJSON[2].id, tt3.id, 'text track in third spot of array should match the original track');
+});
+
+QUnit.test('serialize', function(assert) {
+ const tech = new TechFaker();
+ const tt1 = new TextTrack({tech});
+ const tt2 = new TextTrack({tech});
+ const tt3 = new TextTrack({tech});
+ const ttl = new TextTrackList([tt1, tt2, tt3]);
+
+ const serializedTrackList = JSON.stringify(ttl);
+
+ assert.notOk(serializedTrackList.includes('"tech_":'), 'tech_ does not exist in the serialized data');
+
+ assert.ok(serializedTrackList.includes(`"id":"${tt1.id}"`), 'serialzed track is found for text track 1');
+ assert.ok(serializedTrackList.includes(`"id":"${tt2.id}"`), 'serialzed track is found for text track 2');
+ assert.ok(serializedTrackList.includes(`"id":"${tt3.id}"`), 'serialzed track is found for text track 3');
+});
diff --git a/js/videojs/test/unit/tracks/text-track-select.test.js b/js/videojs/test/unit/tracks/text-track-select.test.js
new file mode 100644
index 0000000..059dab6
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-select.test.js
@@ -0,0 +1,43 @@
+/* eslint-env qunit */
+import TestHelpers from '../test-helpers.js';
+
+const tracks = [{
+ kind: 'captions',
+ label: 'test'
+}];
+
+QUnit.module('Text Track Select');
+
+QUnit.test('should associate with <select>s with <options>s', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ const select = player.textTrackSettings.el_.querySelector('select');
+ const option = select.querySelector('option');
+ const selectAriaLabelledby = select.getAttribute('aria-labelledby');
+ const optionAriaLabelledby = option.getAttribute('aria-labelledby');
+
+ assert.ok(
+ optionAriaLabelledby.includes(selectAriaLabelledby),
+ "select property 'aria-labelledby' is included in its option's property 'aria-labelledby'"
+ );
+});
+
+QUnit.test('aria-labelledby values must be valid and unique', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+ const albs = player.$$('.vjs-text-track-settings select[aria-labelledby]');
+
+ albs.forEach(el => {
+ const ids = el.getAttribute('aria-labelledby').split(' ');
+ const invalidIds = ids.find(id => {
+ return !(player.$(`#${id}`));
+ });
+
+ assert.notOk(invalidIds, `${el.id} has valid aria-labelledby ids`);
+
+ assert.ok((new Set(ids)).size === ids.length, `${el.id} does not contain duplicate ids`);
+ });
+});
diff --git a/js/videojs/test/unit/tracks/text-track-settings.test.js b/js/videojs/test/unit/tracks/text-track-settings.test.js
new file mode 100644
index 0000000..1eec3e6
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track-settings.test.js
@@ -0,0 +1,416 @@
+/* eslint-env qunit */
+import TextTrackSettings from '../../../src/js/tracks/text-track-settings.js';
+import TestHelpers from '../test-helpers.js';
+import * as Events from '../../../src/js/utils/events.js';
+import sinon from 'sinon';
+import window from 'global/window';
+import Component from '../../../src/js/component.js';
+import videojs from '../../../src/js/video.js';
+
+const tracks = [{
+ kind: 'captions',
+ label: 'test'
+}];
+
+const defaultSettings = {
+ backgroundColor: '#000',
+ backgroundOpacity: '1',
+ color: '#FFF',
+ fontFamily: 'proportionalSansSerif',
+ textOpacity: '1',
+ windowColor: '#000',
+ windowOpacity: '0'
+};
+
+QUnit.module('Text Track Settings', {
+ beforeEach() {
+ window.localStorage.clear();
+ this.oldComponentFocus = Component.prototype.focus;
+ this.oldComponentBlur = Component.prototype.blur;
+ Component.prototype.focus = function() {};
+ Component.prototype.blur = function() {};
+ },
+ afterEach() {
+ Component.prototype.focus = this.oldComponentFocus;
+ Component.prototype.blur = this.oldComponentBlur;
+ }
+});
+
+QUnit.test('should update settings', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: true
+ });
+
+ const newSettings = {
+ backgroundOpacity: '0.5',
+ textOpacity: '0.5',
+ windowOpacity: '0.5',
+ edgeStyle: 'raised',
+ fontFamily: 'monospaceSerif',
+ color: '#F00',
+ backgroundColor: '#FFF',
+ windowColor: '#FFF',
+ fontPercent: 1.25
+ };
+
+ player.textTrackSettings.setValues(newSettings);
+
+ assert.deepEqual(
+ player.textTrackSettings.getValues(),
+ newSettings,
+ 'values are updated'
+ );
+
+ assert.equal(
+ player.$('.vjs-text-color > select').selectedIndex,
+ 2,
+ 'text-color is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-bg-color > select').selectedIndex,
+ 1,
+ 'bg-color is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-window-color > select').selectedIndex,
+ 1,
+ 'window-color is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-text-opacity > select').selectedIndex,
+ 1,
+ 'text-opacity is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-bg-opacity > select').selectedIndex,
+ 1,
+ 'bg-opacity is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-window-opacity > select').selectedIndex,
+ 1,
+ 'window-opacity is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-edge-style select').selectedIndex,
+ 1,
+ 'edge-style is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-font-family select').selectedIndex,
+ 3,
+ 'font-family is set to new value'
+ );
+
+ assert.equal(
+ player.$('.vjs-font-percent select').selectedIndex,
+ 3,
+ 'font-percent is set to new value'
+ );
+
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+
+ assert.deepEqual(
+ JSON.parse(window.localStorage.getItem('vjs-text-track-settings')),
+ newSettings,
+ 'values are saved'
+ );
+
+ player.dispose();
+});
+
+QUnit.test('should restore default settings', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: true
+ });
+
+ player.$('.vjs-text-color > select').selectedIndex = 1;
+ player.$('.vjs-bg-color > select').selectedIndex = 1;
+ player.$('.vjs-window-color > select').selectedIndex = 1;
+ player.$('.vjs-text-opacity > select').selectedIndex = 1;
+ player.$('.vjs-bg-opacity > select').selectedIndex = 1;
+ player.$('.vjs-window-opacity > select').selectedIndex = 1;
+ player.$('.vjs-edge-style select').selectedIndex = 1;
+ player.$('.vjs-font-family select').selectedIndex = 1;
+ player.$('.vjs-font-percent select').selectedIndex = 3;
+
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-default-button'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+
+ assert.deepEqual(
+ player.textTrackSettings.getValues(),
+ defaultSettings,
+ 'values are defaulted'
+ );
+ // TODO:
+ // MikeA: need to figure out how to modify saveSettings
+ // to factor in defaults are no longer null
+ // assert.deepEqual(window.localStorage.getItem('vjs-text-track-settings'),
+ // defaultSettings,
+ // 'values are saved');
+
+ assert.equal(
+ player.$('.vjs-text-color > select').selectedIndex,
+ 0,
+ 'text-color is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-bg-color > select').selectedIndex,
+ 0,
+ 'bg-color is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-window-color > select').selectedIndex,
+ 0,
+ 'window-color is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-text-opacity > select').selectedIndex,
+ 0,
+ 'text-opacity is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-bg-opacity > select').selectedIndex,
+ 0,
+ 'bg-opacity is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-window-opacity > select').selectedIndex,
+ 0,
+ 'window-opacity is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-edge-style select').selectedIndex,
+ 0,
+ 'edge-style is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-font-family select').selectedIndex,
+ 0,
+ 'font-family is set to default value'
+ );
+
+ assert.equal(
+ player.$('.vjs-font-percent select').selectedIndex,
+ 2,
+ 'font-percent is set to default value'
+ );
+
+ player.dispose();
+});
+
+QUnit.test('should open on click', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ clock.tick(1);
+
+ Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
+ assert.ok(!player.textTrackSettings.hasClass('vjs-hidden'), 'settings open');
+
+ player.dispose();
+ clock.restore();
+});
+
+QUnit.test('should close on done click', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ clock.tick(1);
+
+ Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+ assert.ok(player.textTrackSettings.hasClass('vjs-hidden'), 'settings closed');
+
+ player.dispose();
+ clock.restore();
+});
+
+QUnit.test('if persist option is set, restore settings on init', function(assert) {
+ const oldRestoreSettings = TextTrackSettings.prototype.restoreSettings;
+ let restore = 0;
+
+ TextTrackSettings.prototype.restoreSettings = function() {
+ restore++;
+ };
+
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: true
+ });
+
+ assert.equal(restore, 1, 'restore was called');
+
+ TextTrackSettings.prototype.restoreSettings = oldRestoreSettings;
+
+ player.dispose();
+});
+
+QUnit.test('if persist option is set, save settings when "done"', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: true
+ });
+
+ const oldSaveSettings = TextTrackSettings.prototype.saveSettings;
+ let save = 0;
+
+ TextTrackSettings.prototype.saveSettings = function() {
+ save++;
+ };
+
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+
+ assert.equal(save, 1, 'save was called');
+
+ TextTrackSettings.prototype.saveSettings = oldSaveSettings;
+
+ player.dispose();
+});
+
+QUnit.test('do not try to restore or save settings if persist option is not set', function(assert) {
+ const oldRestoreSettings = TextTrackSettings.prototype.restoreSettings;
+ const oldSaveSettings = TextTrackSettings.prototype.saveSettings;
+ let save = 0;
+ let restore = 0;
+
+ TextTrackSettings.prototype.restoreSettings = function() {
+ restore++;
+ };
+ TextTrackSettings.prototype.saveSettings = function() {
+ save++;
+ };
+
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: false
+ });
+
+ assert.equal(restore, 0, 'restore was not called');
+
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+
+ // saveSettings is called but does nothing
+ assert.equal(save, 1, 'save was not called');
+
+ TextTrackSettings.prototype.saveSettings = oldSaveSettings;
+ TextTrackSettings.prototype.restoreSettings = oldRestoreSettings;
+
+ player.dispose();
+});
+
+QUnit.test('should restore saved settings', function(assert) {
+ const newSettings = {
+ backgroundOpacity: '0.5',
+ textOpacity: '0.5',
+ windowOpacity: '0.5',
+ edgeStyle: 'raised',
+ fontFamily: 'monospaceSerif',
+ color: '#F00',
+ backgroundColor: '#FFF',
+ windowColor: '#FFF',
+ fontPercent: 1.25
+ };
+
+ window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(newSettings));
+
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: true
+ });
+
+ assert.deepEqual(player.textTrackSettings.getValues(), newSettings);
+
+ player.dispose();
+});
+
+QUnit.test('should not restore saved settings', function(assert) {
+ const newSettings = {
+ backgroundOpacity: '0.5',
+ textOpacity: '0.5',
+ windowOpacity: '0.5',
+ edgeStyle: 'raised',
+ fontFamily: 'monospaceSerif',
+ color: '#F00',
+ backgroundColor: '#FFF',
+ windowColor: '#FFF',
+ fontPercent: 1.25
+ };
+
+ window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(newSettings));
+
+ const player = TestHelpers.makePlayer({
+ tracks,
+ persistTextTrackSettings: false
+ });
+
+ assert.deepEqual(player.textTrackSettings.getValues(), defaultSettings);
+
+ player.dispose();
+});
+
+QUnit.test('should update on languagechange', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ videojs.addLanguage('test', {
+ 'Font Size': 'FONTSIZE',
+ 'Color': 'COLOR',
+ 'White': 'WHITE'
+ });
+ player.language('test');
+
+ assert.equal(player.$('.vjs-font-percent legend').textContent, 'FONTSIZE', 'settings dialog updates on languagechange');
+ assert.equal(player.$('.vjs-text-color label').textContent, 'COLOR', 'settings dialog label updates on languagechange');
+ assert.equal(player.$('.vjs-text-color select option').textContent, 'WHITE', 'settings dialog select updates on languagechange');
+
+ player.dispose();
+});
+
+QUnit.test('should associate <label>s with <select>s', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ const firstLabelFor = player.textTrackSettings.el_.querySelector('label').getAttribute('for');
+
+ assert.ok(
+ videojs.dom.isEl(player.textTrackSettings.el_.querySelector(`#${firstLabelFor}`)),
+ 'label has a `for` attribute matching an `id`'
+ );
+
+});
+
+QUnit.test('should not duplicate ids', function(assert) {
+ const player = TestHelpers.makePlayer({
+ tracks
+ });
+
+ const elements = [...player.el().querySelectorAll('[id]')];
+ const ids = elements.map(el => el.id);
+ const duplicates = elements.filter(el => ids.filter(id => id === el.id).length > 1);
+
+ assert.strictEqual(duplicates.length, 0, 'there should be no duplicate ids');
+});
diff --git a/js/videojs/test/unit/tracks/text-track.test.js b/js/videojs/test/unit/tracks/text-track.test.js
new file mode 100644
index 0000000..b96122a
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-track.test.js
@@ -0,0 +1,806 @@
+/* eslint-env qunit */
+import window from 'global/window';
+import EventTarget from '../../../src/js/event-target.js';
+import TrackBaseline from './track-baseline';
+import TechFaker from '../tech/tech-faker';
+import TextTrack from '../../../src/js/tracks/text-track.js';
+import TestHelpers from '../test-helpers.js';
+import sinon from 'sinon';
+import log from '../../../src/js/utils/log.js';
+import XHR from '@videojs/xhr';
+
+QUnit.module('Text Track', {
+ beforeEach() {
+ this.tech = new TechFaker();
+ this.oldXMLHttpRequest = XHR.XMLHttpRequest;
+ this.oldXDomainRequest = XHR.XDomainRequest;
+ this.xhr = sinon.useFakeXMLHttpRequest();
+ XHR.XMLHttpRequest = this.xhr;
+ XHR.XDomainRequest = this.xhr;
+ },
+ afterEach() {
+ this.tech.dispose();
+ this.tech = null;
+ XHR.XMLHttpRequest = this.oldXMLHttpRequest;
+ XHR.XDomainRequest = this.oldXDomainRequest;
+ this.xhr.restore();
+ }
+});
+
+// do baseline track testing
+TrackBaseline(TextTrack, {
+ id: '1',
+ kind: 'subtitles',
+ mode: 'disabled',
+ label: 'English',
+ language: 'en'
+ // tech is added in baseline
+ // tech: new TechFaker()
+});
+
+QUnit.test('requires a tech', function(assert) {
+ const error = new Error('A tech was not provided.');
+
+ assert.throws(() => new TextTrack({}), error, 'a tech is required');
+ assert.throws(() => new TextTrack({tech: null}), error, 'a tech is required');
+});
+
+QUnit.test('can create a TextTrack with a mode property', function(assert) {
+ const mode = 'disabled';
+ const tt = new TextTrack({
+ mode,
+ tech: this.tech
+ });
+
+ assert.equal(tt.mode, mode, 'we have a mode');
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'kind defaulted to subtitles');
+ assert.equal(tt.mode, 'disabled', 'mode defaulted to disabled');
+ assert.equal(tt.label, '', 'label defaults to empty string');
+ assert.equal(tt.language, '', 'language defaults to empty string');
+});
+
+QUnit.test('kind can only be one of several options, defaults to subtitles', function(assert) {
+ let tt = new TextTrack({
+ tech: this.tech,
+ kind: 'foo'
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles, not foo');
+ assert.notEqual(tt.kind, 'foo', 'the kind is set to subtitles, not foo');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'subtitles'
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'captions'
+ });
+
+ assert.equal(tt.kind, 'captions', 'the kind is set to captions');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'descriptions'
+ });
+
+ assert.equal(tt.kind, 'descriptions', 'the kind is set to descriptions');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'chapters'
+ });
+
+ assert.equal(tt.kind, 'chapters', 'the kind is set to chapters');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'metadata'
+ });
+
+ assert.equal(tt.kind, 'metadata', 'the kind is set to metadata');
+});
+
+QUnit.test('mode can only be one of several options, defaults to disabled', function(assert) {
+ let tt = new TextTrack({
+ tech: this.tech,
+ mode: 'foo'
+ });
+
+ assert.equal(tt.mode, 'disabled', 'the mode is set to disabled, not foo');
+ assert.notEqual(tt.mode, 'foo', 'the mode is set to disabld, not foo');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'disabled'
+ });
+
+ assert.equal(tt.mode, 'disabled', 'the mode is set to disabled');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'hidden'
+ });
+
+ assert.equal(tt.mode, 'hidden', 'the mode is set to hidden');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'showing'
+ });
+
+ assert.equal(tt.mode, 'showing', 'the mode is set to showing');
+});
+
+QUnit.test('cue and activeCues are read only', function(assert) {
+ const mode = 'disabled';
+ const tt = new TextTrack({
+ mode,
+ tech: this.tech
+ });
+
+ tt.cues = 'foo';
+ tt.activeCues = 'bar';
+
+ assert.notEqual(tt.cues, 'foo', 'cues is still original value');
+ assert.notEqual(tt.activeCues, 'bar', 'activeCues is still original value');
+});
+
+QUnit.test('mode can only be set to a few options', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.mode = 'foo';
+
+ assert.notEqual(tt.mode, 'foo', 'the mode is still the old value, disabled');
+ assert.equal(tt.mode, 'disabled', 'still on the default mode, disabled');
+
+ tt.mode = 'hidden';
+ assert.equal(tt.mode, 'hidden', 'mode set to hidden');
+
+ tt.mode = 'bar';
+ assert.notEqual(tt.mode, 'bar', 'the mode is still the old value, hidden');
+ assert.equal(tt.mode, 'hidden', 'still on the previous mode, hidden');
+
+ tt.mode = 'showing';
+ assert.equal(tt.mode, 'showing', 'mode set to showing');
+
+ tt.mode = 'baz';
+ assert.notEqual(tt.mode, 'baz', 'the mode is still the old value, showing');
+ assert.equal(tt.mode, 'showing', 'still on the previous mode, showing');
+});
+
+QUnit.test('cues and activeCues return a TextTrackCueList', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ assert.ok(tt.cues.getCueById, 'cues are a TextTrackCueList');
+ assert.ok(tt.activeCues.getCueById, 'activeCues are a TextTrackCueList');
+});
+
+QUnit.test('cues can be added and removed from a TextTrack', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const cues = tt.cues;
+
+ assert.equal(cues.length, 0, 'start with zero cues');
+
+ tt.addCue({id: '1'});
+
+ assert.equal(cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cues.getCueById('1'));
+
+ assert.equal(cues.length, 0, 'we have removed our one cue');
+
+ tt.addCue({id: '1'});
+ tt.addCue({id: '2'});
+ tt.addCue({id: '3'});
+
+ assert.equal(cues.length, 3, 'we now have 3 cues');
+});
+
+QUnit.test('original cue can be used to remove cue from cues list', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const Cue = window.VTTCue ||
+ window.vttjs && window.vttjs.VTTCue ||
+ window.TextTrackCue;
+
+ const cue1 = new Cue(0, 1, 'some-cue');
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed cue1');
+});
+
+QUnit.test('non-VTTCue can be used to remove cue from cues list', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ const cue1 = { id: 1, text: 'test' };
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed cue1');
+});
+
+QUnit.test('can only remove one cue at a time', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const Cue = window.VTTCue ||
+ window.vttjs && window.vttjs.VTTCue ||
+ window.TextTrackCue;
+
+ const cue1 = new Cue(0, 1, 'some-cue');
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 2, 'we have two cues');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have removed one instance of cue1');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed the other instance of cue1');
+});
+
+QUnit.test('does not include past cues in activeCues', function(assert) {
+ // Testing for the absence of a previous behaviour, which considered cues with equal
+ // start and end times as active 0.5s after ending
+ const player = TestHelpers.makePlayer();
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const expectedCue = {
+ id: '2',
+ startTime: 2.555,
+ endTime: 3
+ };
+
+ player.tech_.currentTime = function() {
+ return 2.556;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+ // start 2.55
+ tt.addCue(expectedCue);
+
+ player.tech_.trigger('playing');
+
+ assert.equal(tt.activeCues_.length, 1, 'only one cue is present');
+ assert.equal(tt.activeCues_[0].originalCue_, expectedCue, 'correct active cue is present');
+});
+
+QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
+ const done = assert.async();
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer({techfaker: {autoReady: false}});
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 0,
+ endTime: 5
+ });
+
+ tt.oncuechange = cuechangeHandler;
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 0;
+ };
+
+ // `playing` would trigger rvfc or raf, `timeupdate` for fallback
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+ assert.equal(changes, 0, 'a cuechange event is not triggered');
+
+ player.tech_.on('ready', function() {
+ player.tech_.currentTime = function() {
+ return 0.2;
+ };
+
+ player.tech_.trigger('playing');
+ clock.tick(1);
+
+ assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');
+
+ player.tech_.trigger('timeupdate');
+ clock.tick(1);
+
+ assert.equal(changes, 2, 'a cuechange event trigger not duplicated by timeupdate');
+
+ tt.off();
+ player.dispose();
+ clock.restore();
+ done();
+ });
+ player.tech_.triggerReady();
+ clock.tick(1);
+});
+
+QUnit.test('fires cuechange when cues become active and inactive', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+ let fakeCurrentTime = 0;
+
+ player.tech_.currentTime = function() {
+ return fakeCurrentTime;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 11,
+ endTime: 14
+ });
+
+ tt.oncuechange = cuechangeHandler;
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ fakeCurrentTime = 2;
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange (rvfc/raf)');
+
+ fakeCurrentTime = 7;
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 4, 'a cuechange event trigger addEventListener and oncuechange (rvfc/raf)');
+
+ fakeCurrentTime = 12;
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 6, 'a cuechange event trigger addEventListener and oncuechange (timeupdate)');
+
+ fakeCurrentTime = 17;
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 8, 'a cuechange event trigger addEventListener and oncuechange (timeupdate)');
+
+ tt.off();
+ player.dispose();
+});
+
+QUnit.test('enabled and disabled cuechange handler when changing mode to hidden', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.mode = 'hidden';
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 2;
+ };
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 1, 'a cuechange event trigger');
+
+ changes = 0;
+ // debugger;
+ tt.mode = 'disabled';
+
+ player.tech_.currentTime = function() {
+ return 7;
+ };
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 0, 'NO cuechange event trigger');
+
+ tt.off();
+ player.dispose();
+});
+
+QUnit.test('enabled and disabled cuechange handler when changing mode to showing', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.mode = 'showing';
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 2;
+ };
+ player.tech_.trigger('playing');
+ clock.tick(10);
+
+ assert.equal(changes, 1, 'a cuechange event trigger');
+
+ changes = 0;
+ tt.mode = 'disabled';
+
+ player.tech_.currentTime = function() {
+ return 7;
+ };
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 0, 'NO cuechange event trigger');
+
+ tt.off();
+ player.dispose();
+ clock.restore();
+});
+
+QUnit.test('if preloadTextTracks is false, default tracks are not parsed until mode is showing', function(assert) {
+ this.tech.preloadTextTracks = false;
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com',
+ default: true
+ });
+
+ assert.notOk(reqs.length, 'Default track is not requested');
+ assert.notOk(parserCreated, 'Parser is not created');
+
+ tt.mode = 'showing';
+
+ const req = reqs.pop();
+
+ req.respond(200, null, 'WEBVTT\n');
+
+ assert.ok(parserCreated, 'Parser is created after track is showing');
+
+ clock.restore();
+ tt.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are parsed if vttjs is loaded', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req = reqs.pop();
+
+ req.respond(200, null, 'WEBVTT\n');
+
+ assert.ok(parserCreated, 'WebVTT is loaded, so we can just parse');
+ assert.notOk(req.withCredentials, 'the request defaults not to send credentials');
+
+ clock.restore();
+ tt.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are loaded withCredentials is crossorigin is set to use-credentials', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ this.tech.crossOrigin = () => 'use-credentials';
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req = reqs.pop();
+
+ assert.ok(req.withCredentials, 'the request was made withCredentials');
+
+ this.tech.crossOrigin = () => 'anonymous';
+
+ const tt2 = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req2 = reqs.pop();
+
+ assert.notOk(req2.withCredentials, 'the request was not made withCredentials');
+
+ req.abort();
+ req2.abort();
+ clock.restore();
+ tt.off();
+ tt2.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are parsed once vttjs is loaded', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = true;
+
+ const testTech = new EventTarget();
+
+ testTech.textTracks = () => {};
+ testTech.currentTime = () => {};
+ testTech.crossOrigin = () => null;
+
+ const tt = new TextTrack({
+ tech: testTech,
+ src: 'http://example.com'
+ });
+
+ reqs.pop().respond(200, null, 'WEBVTT\n');
+
+ assert.ok(!parserCreated, 'WebVTT is not loaded, do not try to parse yet');
+
+ clock.tick(100);
+ assert.ok(!parserCreated, 'WebVTT still not loaded, do not try to parse yet');
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ testTech.trigger('vttjsloaded');
+ assert.ok(parserCreated, 'WebVTT is loaded, so we can parse now');
+
+ clock.restore();
+ tt.off();
+ testTech.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('stops processing if vttjs loading errored out', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const errorSpy = sinon.spy();
+ const oldVTT = window.WebVTT;
+ const oldLogError = log.error;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ log.error = errorSpy;
+
+ window.WebVTT = true;
+
+ const testTech = new EventTarget();
+
+ testTech.textTracks = () => {};
+ testTech.currentTime = () => {};
+ testTech.crossOrigin = () => null;
+
+ sinon.stub(testTech, 'off');
+ testTech.off.withArgs('vttjsloaded');
+
+ const tt = new TextTrack({
+ tech: testTech,
+ src: 'http://example.com'
+ });
+
+ reqs.pop().respond(200, null, 'WEBVTT\n');
+
+ testTech.trigger('vttjserror');
+
+ assert.equal(errorSpy.callCount, 1, 'vttjs failed to load, so log.error was called');
+
+ testTech.trigger('vttjserror');
+
+ // vttjserror not called again
+ assert.equal(errorSpy.callCount, 1, 'vttjserror handler not called again');
+
+ clock.restore();
+ window.WebVTT = oldVTT;
+ tt.off();
+ testTech.off.restore();
+ testTech.off();
+ log.error = oldLogError;
+});
+
+QUnit.test('toJSON', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+
+ const jsonTrack = tt.toJSON();
+
+ // Properties we want copied are copied correctly
+ assert.equal(tt.id, jsonTrack.id, 'the id for the copied track stayed the same');
+ assert.equal(tt.mode, jsonTrack.mode, 'the mode for the copied track stayed the same');
+ assert.equal(tt.kind, jsonTrack.kind, 'the kind for the copied track stayed the same');
+
+ // The tech_ property stays on the original track, but is removed from the copy
+ assert.ok(tt.tech_, 'the tech exists on the original track');
+ assert.notOk(jsonTrack.tech_, 'the tech does not exist on the copied track');
+});
+
+QUnit.test('serialize', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+
+ const serializedTrack = JSON.stringify(tt);
+
+ // Ensure tech was not removed from the actual track
+ assert.ok(tt.tech_, 'the tech exists on the original track');
+
+ // Values from the track should be found in the serialized string
+ assert.ok(serializedTrack.includes(`"id":"${tt.id}"`), 'serialized data should include id');
+ assert.ok(serializedTrack.includes(`"mode":"${tt.mode}"`), 'serialized data should include mode');
+ assert.ok(serializedTrack.includes(`"kind":"${tt.kind}"`), 'serialized data should include cues');
+
+ // tech_ should not be found in the serialized string
+ assert.notOk(serializedTrack.includes('"tech_":'), 'serialized data should not include tech_');
+});
diff --git a/js/videojs/test/unit/tracks/text-tracks.test.js b/js/videojs/test/unit/tracks/text-tracks.test.js
new file mode 100644
index 0000000..e49e8f9
--- /dev/null
+++ b/js/videojs/test/unit/tracks/text-tracks.test.js
@@ -0,0 +1,690 @@
+/* eslint-env qunit */
+import ChaptersButton from '../../../src/js/control-bar/text-track-controls/chapters-button.js';
+import DescriptionsButton from '../../../src/js/control-bar/text-track-controls/descriptions-button.js';
+import SubtitlesButton from '../../../src/js/control-bar/text-track-controls/subtitles-button.js';
+import CaptionsButton from '../../../src/js/control-bar/text-track-controls/captions-button.js';
+import SubsCapsButton from '../../../src/js/control-bar/text-track-controls/subs-caps-button.js';
+
+import TextTrack from '../../../src/js/tracks/text-track.js';
+import TextTrackDisplay from '../../../src/js/tracks/text-track-display.js';
+import Html5 from '../../../src/js/tech/html5.js';
+import Tech from '../../../src/js/tech/tech.js';
+
+import * as browser from '../../../src/js/utils/browser.js';
+import TestHelpers from '../test-helpers.js';
+import document from 'global/document';
+import sinon from 'sinon';
+
+QUnit.module('Text Tracks', {
+ beforeEach(assert) {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ }
+});
+
+QUnit.test('should place title list item into ul', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const chaptersButton = new ChaptersButton(player);
+
+ const menuContentElement = chaptersButton.el().getElementsByTagName('UL')[0];
+ const titleElement = menuContentElement.children[0];
+
+ assert.ok(titleElement.innerHTML === 'Chapters', 'title element placed in ul');
+
+ player.dispose();
+ chaptersButton.dispose();
+});
+
+QUnit.test('Player track methods call the tech', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let calls = 0;
+
+ player.tech_.textTracks = function() {
+ calls++;
+ };
+ player.tech_.addTextTrack = function() {
+ calls++;
+ };
+
+ player.addTextTrack();
+ player.textTracks();
+
+ assert.equal(calls, 2, 'both textTrack and addTextTrack defer to the tech');
+
+ player.dispose();
+});
+
+QUnit.test('TextTrackDisplay initializes tracks on player ready', function(assert) {
+ let calls = 0;
+ const player = TestHelpers.makePlayer();
+
+ player.addTextTrack = () => calls--;
+ player.getChild = () => calls--;
+ player.ready = () => calls++;
+
+ const ttd = new TextTrackDisplay(player, {});
+
+ assert.equal(calls, 1, 'only a player.ready call was made');
+
+ ttd.dispose();
+ player.dispose();
+});
+
+QUnit.test('listen to remove and add track events in native text tracks', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+ const oldTextTracks = Html5.prototype.textTracks;
+ const events = {};
+
+ Html5.prototype.textTracks = function() {
+ return {
+ removeEventListener() {},
+ addEventListener(type, handler) {
+ events[type] = true;
+ }
+ };
+ };
+
+ Html5.TEST_VID = {
+ textTracks: []
+ };
+
+ const player = {
+ // Function.prototype is a built-in no-op function.
+ controls() {},
+ ready() {},
+ options() {
+ return {};
+ },
+ addChild() {},
+ id() {},
+ el() {
+ return {
+ insertBefore() {},
+ appendChild() {}
+ };
+ }
+ };
+
+ player.player_ = player;
+ player.options_ = {};
+
+ const html = new Html5({});
+
+ assert.ok(events.removetrack, 'removetrack listener was added');
+ assert.ok(events.addtrack, 'addtrack listener was added');
+
+ Html5.TEST_VID = oldTestVid;
+ Html5.prototype.textTracks = oldTextTracks;
+
+ html.dispose();
+});
+
+QUnit.test('update texttrack buttons on removetrack or addtrack', function(assert) {
+ let update = 0;
+ const events = {};
+ const oldCaptionsUpdate = CaptionsButton.prototype.update;
+ const oldSubsUpdate = SubtitlesButton.prototype.update;
+ const oldDescriptionsUpdate = DescriptionsButton.prototype.update;
+ const oldChaptersUpdate = ChaptersButton.prototype.update;
+ const oldSubsCapsUpdate = SubsCapsButton.prototype.update;
+
+ CaptionsButton.prototype.update = function() {
+ update++;
+ oldCaptionsUpdate.call(this);
+ };
+ SubtitlesButton.prototype.update = function() {
+ update++;
+ oldSubsUpdate.call(this);
+ };
+ DescriptionsButton.prototype.update = function() {
+ update++;
+ oldDescriptionsUpdate.call(this);
+ };
+ ChaptersButton.prototype.update = function() {
+ update++;
+ oldChaptersUpdate.call(this);
+ };
+ SubsCapsButton.prototype.update = function() {
+ update++;
+ oldSubsCapsUpdate.call(this);
+ };
+
+ const oldFeaturesNativeTextTracks = Tech.prototype.featuresNativeTextTracks;
+ const oldTextTracks = Tech.prototype.textTracks;
+
+ Tech.prototype.featuresNativeTextTracks = true;
+ Tech.prototype.textTracks = function() {
+ return {
+ length: 0,
+ addEventListener(type, handler) {
+ if (!events[type]) {
+ events[type] = [];
+ }
+ events[type].push(handler);
+ },
+ // Required in player.dispose()
+ removeEventListener() {}
+ };
+ };
+
+ const tag = document.createElement('video');
+ const track1 = document.createElement('track');
+ const track2 = document.createElement('track');
+
+ track1.kind = 'captions';
+ track1.label = 'en';
+ track1.language = 'English';
+ track1.src = '#en.vtt';
+ tag.appendChild(track1);
+
+ track2.kind = 'captions';
+ track2.label = 'es';
+ track2.language = 'Spanish';
+ track2.src = '#es.vtt';
+ tag.appendChild(track2);
+
+ const player = TestHelpers.makePlayer({
+ controlBar: {
+ captionsButton: true,
+ subtitlesButton: true
+ }
+ }, tag);
+
+ player.player_ = player;
+
+ assert.equal(update, 5, 'update was called on the five buttons during init');
+
+ for (let i = 0; i < events.removetrack.length; i++) {
+ events.removetrack[i]();
+ }
+
+ assert.equal(update, 10, 'update was called on the five buttons for remove track');
+
+ for (let i = 0; i < events.addtrack.length; i++) {
+ events.addtrack[i]();
+ }
+
+ assert.equal(update, 15, 'update was called on the five buttons for remove track');
+
+ Tech.prototype.textTracks = oldTextTracks;
+ Tech.prototype.featuresNativeTextTracks = oldFeaturesNativeTextTracks;
+ CaptionsButton.prototype.update = oldCaptionsUpdate;
+ SubtitlesButton.prototype.update = oldSubsUpdate;
+ ChaptersButton.prototype.update = oldChaptersUpdate;
+ SubsCapsButton.prototype.update = oldSubsCapsUpdate;
+ DescriptionsButton.prototype.update = oldDescriptionsUpdate;
+
+ player.dispose();
+});
+
+QUnit.test('emulated tracks are always used, except in safari', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+ const oldIsAnySafari = browser.IS_ANY_SAFARI;
+
+ Html5.TEST_VID = {
+ textTracks: []
+ };
+
+ browser.stub_IS_ANY_SAFARI(false);
+
+ assert.ok(!Html5.supportsNativeTextTracks(), 'Html5 does not support native text tracks, in non-safari');
+
+ browser.stub_IS_ANY_SAFARI(true);
+
+ assert.ok(Html5.supportsNativeTextTracks(), 'Html5 does support native text tracks in safari');
+
+ Html5.TEST_VID = oldTestVid;
+ browser.stub_IS_ANY_SAFARI(oldIsAnySafari);
+});
+
+QUnit.test('when switching techs, we should not get a new text track', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ player.loadTech_('TechFaker');
+ const firstTracks = player.textTracks();
+
+ player.loadTech_('TechFaker');
+ const secondTracks = player.textTracks();
+
+ assert.ok(firstTracks === secondTracks, 'the tracks are equal');
+
+ player.dispose();
+});
+
+if (Html5.supportsNativeTextTracks()) {
+ QUnit.test('listen to native remove and add track events in native text tracks', function(assert) {
+ const done = assert.async();
+
+ const el = document.createElement('video');
+ const html = new Html5({el});
+ const tt = el.textTracks;
+ const emulatedTt = html.textTracks();
+ const track = document.createElement('track');
+
+ el.appendChild(track);
+
+ const addtrack = function() {
+ assert.equal(emulatedTt.length, tt.length, 'we have matching tracks length');
+ assert.equal(emulatedTt.length, 1, 'we have one text track');
+
+ el.removeChild(track);
+ };
+
+ emulatedTt.one('addtrack', addtrack);
+ emulatedTt.one('removetrack', function() {
+ assert.equal(emulatedTt.length, tt.length, 'we have matching tracks length');
+ assert.equal(emulatedTt.length, 0, 'we have no more text tracks');
+
+ html.dispose();
+ done();
+ });
+ });
+
+ QUnit.test('should have removed tracks on dispose', function(assert) {
+ const done = assert.async();
+
+ const el = document.createElement('video');
+ const html = new Html5({el});
+ const tt = el.textTracks;
+ const emulatedTt = html.textTracks();
+ const track = document.createElement('track');
+
+ el.appendChild(track);
+
+ const addtrack = function() {
+ assert.equal(emulatedTt.length, tt.length, 'we have matching tracks length');
+ assert.equal(emulatedTt.length, 1, 'we have one text track');
+
+ emulatedTt.off('addtrack', addtrack);
+ html.dispose();
+
+ assert.equal(emulatedTt.length, tt.length, 'we have matching tracks length');
+ assert.equal(emulatedTt.length, 0, 'we have no more text tracks');
+
+ done();
+ };
+
+ emulatedTt.on('addtrack', addtrack);
+ });
+}
+
+QUnit.test('should check for text track changes when emulating text tracks', function(assert) {
+ const tech = new Tech();
+ let numTextTrackChanges = 0;
+
+ tech.on('texttrackchange', function() {
+ numTextTrackChanges++;
+ });
+ tech.emulateTextTracks();
+ assert.equal(numTextTrackChanges, 1, 'we got a texttrackchange event');
+
+ tech.dispose();
+});
+
+QUnit.test('no lang attribute on cue elements if one is provided', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 2,
+ endTime: 5
+ });
+ player.tech_.textTracks().addTrack(tt);
+
+ player.currentTime(2);
+ player.tech_.trigger('playing');
+
+ assert.notOk(tt.activeCues[0].displayState.hasAttribute('lang'), 'no lang attribute should be set');
+
+ player.dispose();
+});
+
+QUnit.test('set lang attribute on cue elements if one is provided', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const tt = new TextTrack({
+ srclang: 'en',
+ tech: player.tech_,
+ mode: 'showing'
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 2,
+ endTime: 5
+ });
+ player.tech_.textTracks().addTrack(tt);
+
+ player.currentTime(2);
+ player.tech_.trigger('playing');
+
+ assert.equal(tt.activeCues[0].displayState.getAttribute('lang'), 'en', 'the lang should be set to en');
+
+ player.dispose();
+});
+
+QUnit.test('removes cuechange event when text track is hidden for emulated tracks', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 2,
+ endTime: 5
+ });
+ player.tech_.textTracks().addTrack(tt);
+
+ let numTextTrackChanges = 0;
+
+ player.tech_.on('texttrackchange', function() {
+ numTextTrackChanges++;
+ });
+
+ tt.mode = 'disabled';
+ this.clock.tick(1);
+ assert.equal(
+ numTextTrackChanges, 1,
+ 'texttrackchange should be called once for mode change'
+ );
+ tt.mode = 'showing';
+ this.clock.tick(1);
+ assert.equal(
+ numTextTrackChanges, 2,
+ 'texttrackchange should be called once for mode change'
+ );
+
+ player.tech_.currentTime = function() {
+ return 3;
+ };
+ player.tech_.trigger('playing');
+ assert.equal(
+ numTextTrackChanges, 3,
+ 'texttrackchange should be triggered once for the cuechange'
+ );
+
+ tt.mode = 'hidden';
+ this.clock.tick(1);
+ assert.equal(
+ numTextTrackChanges, 4,
+ 'texttrackchange should be called once for the mode change'
+ );
+
+ player.tech_.currentTime = function() {
+ return 7;
+ };
+ player.tech_.trigger('timeupdate');
+ assert.equal(
+ numTextTrackChanges, 4,
+ 'texttrackchange should be not be called since mode is hidden'
+ );
+ player.dispose();
+});
+
+QUnit.test('should return correct remote text track values', function(assert) {
+ const fixture = document.getElementById('qunit-fixture');
+ const html = `
+ <video id="example_1" class="video-js" autoplay preload="none">
+ <source src="http://google.com" type="video/mp4">
+ <source src="http://google.com" type="video/webm">
+ <track kind="captions" label="label">
+ </video>
+ `;
+
+ fixture.innerHTML += html;
+ const tag = document.getElementById('example_1');
+ const player = TestHelpers.makePlayer({}, tag);
+
+ this.clock.tick(10);
+
+ assert.equal(player.remoteTextTracks().length, 1, 'add text track via html');
+ assert.equal(player.remoteTextTrackEls().length, 1, 'add html track element via html');
+
+ const htmlTrackElement = player.addRemoteTextTrack({
+ kind: 'captions',
+ label: 'label'
+ }, true);
+
+ assert.equal(player.remoteTextTracks().length, 2, 'add text track via method');
+ assert.equal(player.remoteTextTrackEls().length, 2, 'add html track element via method');
+
+ player.removeRemoteTextTrack(htmlTrackElement.track);
+
+ assert.equal(player.remoteTextTracks().length, 1, 'remove text track via method');
+ assert.equal(
+ player.remoteTextTrackEls().length,
+ 1,
+ 'remove html track element via method'
+ );
+
+ player.dispose();
+});
+
+QUnit.test('should uniformly create html track element when adding text track', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track = {
+ kind: 'kind',
+ src: 'src',
+ language: 'language',
+ label: 'label',
+ default: 'default'
+ };
+
+ assert.equal(player.remoteTextTrackEls().length, 0, 'no html text tracks');
+
+ const htmlTrackElement = player.addRemoteTextTrack(track, true);
+
+ assert.equal(
+ htmlTrackElement.kind,
+ htmlTrackElement.track.kind,
+ 'verify html track element kind'
+ );
+ assert.equal(
+ htmlTrackElement.src,
+ htmlTrackElement.track.src,
+ 'verify html track element src'
+ );
+ assert.equal(
+ htmlTrackElement.srclang,
+ htmlTrackElement.track.language,
+ 'verify html track element language'
+ );
+ assert.equal(
+ htmlTrackElement.label,
+ htmlTrackElement.track.label,
+ 'verify html track element label'
+ );
+ assert.equal(
+ htmlTrackElement.default,
+ htmlTrackElement.track.default,
+ 'verify html track element default'
+ );
+
+ assert.equal(player.remoteTextTrackEls().length, 1, 'html track element exist');
+ assert.equal(
+ player.remoteTextTrackEls().getTrackElementByTrack_(htmlTrackElement.track),
+ htmlTrackElement,
+ 'verify same html track element'
+ );
+
+ player.dispose();
+});
+
+// disable in Firefox because while the code works in practice, during the
+// tests, somehow the text track object isn't ready and thus it won't allow
+// us to change the mode of the track rendering the test non-functional.
+if (!browser.IS_FIREFOX) {
+ QUnit.test('remote text tracks change event should fire when using native text tracks', function(assert) {
+ const done = assert.async();
+
+ const player = TestHelpers.makePlayer({
+ techOrder: ['html5'],
+ html5: { nativeTextTracks: true }
+ });
+
+ player.remoteTextTracks().on('change', function(e) {
+ assert.ok(true, 'change event triggered');
+ player.dispose();
+ done();
+ });
+
+ const track = {
+ kind: 'kind',
+ src: 'src',
+ language: 'language',
+ label: 'label',
+ default: 'default'
+ };
+
+ player.addRemoteTextTrack(track, true);
+ });
+}
+
+QUnit.test('default text tracks should show by default', function(assert) {
+ const tag = TestHelpers.makeTag();
+ const capt = document.createElement('track');
+
+ capt.setAttribute('kind', 'captions');
+ capt.setAttribute('default', 'default');
+
+ tag.appendChild(capt);
+
+ const player = TestHelpers.makePlayer({
+ html5: {
+ nativeTextTracks: false
+ }
+ }, tag);
+
+ // native tracks are initialized after the player is ready
+ this.clock.tick(1);
+
+ const tracks = player.textTracks();
+
+ assert.equal(tracks[0].kind, 'captions', 'the captions track is present');
+ assert.equal(tracks[0].mode, 'showing', 'the captions track is showing');
+
+ player.dispose();
+});
+
+QUnit.test('default captions take precedence over default descriptions', function(assert) {
+ const tag = TestHelpers.makeTag();
+ const desc = document.createElement('track');
+ const capt = document.createElement('track');
+
+ desc.setAttribute('kind', 'descriptions');
+ desc.setAttribute('default', 'default');
+ capt.setAttribute('kind', 'captions');
+ capt.setAttribute('default', 'default');
+
+ tag.appendChild(desc);
+ tag.appendChild(capt);
+
+ const player = TestHelpers.makePlayer({
+ html5: {
+ nativeTextTracks: false
+ }
+ }, tag);
+
+ // native tracks are initialized after the player is ready
+ this.clock.tick(1);
+
+ const tracks = player.textTracks();
+
+ assert.equal(tracks[0].kind, 'descriptions', 'the descriptions track is first');
+ assert.equal(tracks[0].mode, 'disabled', 'the descriptions track is disabled');
+ assert.equal(tracks[1].kind, 'captions', 'the captions track is second');
+ assert.equal(tracks[1].mode, 'showing', 'the captions track is showing');
+ player.dispose();
+});
+
+QUnit.test('removeRemoteTextTrack should be able to take both a track and the response from addRemoteTextTrack', function(assert) {
+ const player = TestHelpers.makePlayer();
+ const track = {
+ kind: 'kind',
+ src: 'src',
+ language: 'language',
+ label: 'label',
+ default: 'default'
+ };
+ let htmlTrackElement = player.addRemoteTextTrack(track, true);
+
+ assert.equal(player.remoteTextTrackEls().length, 1, 'html track element exist');
+
+ player.removeRemoteTextTrack(htmlTrackElement);
+
+ assert.equal(
+ player.remoteTextTrackEls().length,
+ 0,
+ 'the track element was removed correctly'
+ );
+
+ htmlTrackElement = player.addRemoteTextTrack(track, true);
+ assert.equal(player.remoteTextTrackEls().length, 1, 'html track element exist');
+
+ player.removeRemoteTextTrack(htmlTrackElement.track);
+ assert.equal(
+ player.remoteTextTrackEls().length,
+ 0,
+ 'the track element was removed correctly'
+ );
+ player.dispose();
+});
+
+if (Html5.isSupported()) {
+ QUnit.test('auto remove tracks should not clean up tracks added while source is being added', function(assert) {
+ const player = TestHelpers.makePlayer({
+ techOrder: ['html5'],
+ html5: {
+ nativeTextTracks: false
+ }
+ });
+
+ const track = {
+ kind: 'kind',
+ src: 'src',
+ language: 'language',
+ label: 'label',
+ default: 'default'
+ };
+
+ player.src({src: 'example.mp4', type: 'video/mp4'});
+ player.addRemoteTextTrack(track, false);
+
+ this.clock.tick(1);
+ assert.equal(player.textTracks().length, 1, 'we have one text track');
+
+ player.dispose();
+ });
+
+ QUnit.test('auto remove tracks added right before a source change will be cleaned up', function(assert) {
+ const player = TestHelpers.makePlayer({
+ techOrder: ['html5'],
+ html5: {
+ nativeTextTracks: false
+ }
+ });
+
+ const track = {
+ kind: 'kind',
+ src: 'src',
+ language: 'language',
+ label: 'label',
+ default: 'default'
+ };
+
+ player.addRemoteTextTrack(track, false);
+ player.src({src: 'example.mp4', type: 'video/mp4'});
+
+ this.clock.tick(1);
+ assert.equal(player.textTracks().length, 0, 'we do not have any tracks left');
+
+ player.dispose();
+ });
+}
diff --git a/js/videojs/test/unit/tracks/track-baseline.js b/js/videojs/test/unit/tracks/track-baseline.js
new file mode 100644
index 0000000..4d24873
--- /dev/null
+++ b/js/videojs/test/unit/tracks/track-baseline.js
@@ -0,0 +1,49 @@
+/* eslint-env qunit */
+import TechFaker from '../tech/tech-faker';
+
+/**
+ * Tests baseline functionality for all tracks
+ *
+ # @param {Track} TrackClass the track class object to use for testing
+ # @param {Object} options the options to setup a track with
+ */
+const TrackBaseline = function(TrackClass, options) {
+
+ QUnit.test('is setup with id, kind, label, and language', function(assert) {
+ const tech = new TechFaker();
+ const track = new TrackClass(Object.assign({tech}, options));
+
+ assert.equal(track.kind, options.kind, 'we have a kind');
+ assert.equal(track.label, options.label, 'we have a label');
+ assert.equal(track.language, options.language, 'we have a language');
+ assert.equal(track.id, options.id, 'we have a id');
+
+ tech.dispose();
+ });
+
+ QUnit.test('kind, language, id, are read only', function(assert) {
+ const tech = new TechFaker();
+ const track = new TrackClass(Object.assign({tech}, options));
+
+ track.kind = 'subtitles';
+ track.language = 'es';
+ track.id = '2';
+
+ assert.equal(track.kind, options.kind, 'we have a kind');
+ assert.equal(track.language, options.language, 'we have a language');
+ assert.equal(track.id, options.id, 'we have an id');
+
+ tech.dispose();
+ });
+
+ QUnit.test('returns an instance of itself on non ie8 browsers', function(assert) {
+ const tech = new TechFaker();
+ const track = new TrackClass(Object.assign({tech}, options));
+
+ assert.ok(track instanceof TrackClass, 'returns an instance');
+
+ tech.dispose();
+ });
+};
+
+export default TrackBaseline;
diff --git a/js/videojs/test/unit/tracks/track-list.test.js b/js/videojs/test/unit/tracks/track-list.test.js
new file mode 100644
index 0000000..88e1caf
--- /dev/null
+++ b/js/videojs/test/unit/tracks/track-list.test.js
@@ -0,0 +1,177 @@
+/* eslint-env qunit */
+import TrackList from '../../../src/js/tracks/track-list.js';
+import EventTarget from '../../../src/js/event-target.js';
+
+const newTrack = function(id) {
+ return {
+ id,
+ addEventListener() {},
+ off() {}
+ };
+};
+
+QUnit.module('Track List', {
+ beforeEach() {
+ this.tracks = [newTrack('1'), newTrack('2'), newTrack('3')];
+ }
+});
+
+QUnit.test('TrackList\'s length is set correctly', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ assert.equal(trackList.length, this.tracks.length, 'length is ' + this.tracks.length);
+});
+
+QUnit.test('can get tracks by int and string id', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ assert.equal(trackList.getTrackById('1').id, '1', 'id "1" has id of "1"');
+ assert.equal(trackList.getTrackById('2').id, '2', 'id "2" has id of "2"');
+ assert.equal(trackList.getTrackById('3').id, '3', 'id "3" has id of "3"');
+
+});
+
+QUnit.test('length is updated when new tracks are added or removed', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ trackList.addTrack(newTrack('100'));
+ assert.equal(
+ trackList.length,
+ this.tracks.length + 1,
+ 'the length is ' + (this.tracks.length + 1)
+ );
+ trackList.addTrack(newTrack('101'));
+ assert.equal(
+ trackList.length,
+ this.tracks.length + 2,
+ 'the length is ' + (this.tracks.length + 2)
+ );
+
+ trackList.removeTrack(trackList.getTrackById('101'));
+ assert.equal(
+ trackList.length,
+ this.tracks.length + 1,
+ 'the length is ' + (this.tracks.length + 1)
+ );
+ trackList.removeTrack(trackList.getTrackById('100'));
+ assert.equal(
+ trackList.length,
+ this.tracks.length,
+ 'the length is ' + this.tracks.length
+ );
+});
+
+QUnit.test('can access items by index', function(assert) {
+ const trackList = new TrackList(this.tracks);
+ const length = trackList.length;
+
+ assert.expect(length);
+
+ for (let i = 0; i < length; i++) {
+ assert.equal(
+ trackList[i].id,
+ String(i + 1),
+ 'the id of a track matches the index + 1'
+ );
+ }
+});
+
+QUnit.test('can access new items by index', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ trackList.addTrack(newTrack('100'));
+ assert.equal(trackList[3].id, '100', 'id of item at index 3 is 100');
+
+ trackList.addTrack(newTrack('101'));
+ assert.equal(trackList[4].id, '101', 'id of item at index 4 is 101');
+});
+
+QUnit.test('cannot access removed items by index', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ trackList.addTrack(newTrack('100'));
+ trackList.addTrack(newTrack('101'));
+ assert.equal(trackList[3].id, '100', 'id of item at index 3 is 100');
+ assert.equal(trackList[4].id, '101', 'id of item at index 4 is 101');
+
+ trackList.removeTrack(trackList.getTrackById('101'));
+ trackList.removeTrack(trackList.getTrackById('100'));
+
+ assert.ok(!trackList[3], 'nothing at index 3');
+ assert.ok(!trackList[4], 'nothing at index 4');
+});
+
+QUnit.test('new item available at old index', function(assert) {
+ const trackList = new TrackList(this.tracks);
+
+ trackList.addTrack(newTrack('100'));
+ assert.equal(trackList[3].id, '100', 'id of item at index 3 is 100');
+
+ trackList.removeTrack(trackList.getTrackById('100'));
+ assert.ok(!trackList[3], 'nothing at index 3');
+
+ trackList.addTrack(newTrack('101'));
+ assert.equal(trackList[3].id, '101', 'id of new item at index 3 is now 101');
+});
+
+QUnit.test('a "addtrack" event is triggered when new tracks are added', function(assert) {
+ const trackList = new TrackList(this.tracks);
+ let tracks = 0;
+ let adds = 0;
+ const addHandler = (e) => {
+ if (e.track) {
+ tracks++;
+ }
+ adds++;
+ };
+
+ trackList.on('addtrack', addHandler);
+
+ trackList.addTrack(newTrack('100'));
+ trackList.addTrack(newTrack('101'));
+
+ trackList.off('addtrack', addHandler);
+ trackList.onaddtrack = addHandler;
+
+ trackList.addTrack(newTrack('102'));
+ trackList.addTrack(newTrack('103'));
+
+ assert.equal(adds, 4, 'we got ' + adds + ' "addtrack" events');
+ assert.equal(tracks, 4, 'we got a track with every event');
+});
+
+QUnit.test('a "removetrack" event is triggered when tracks are removed', function(assert) {
+ const trackList = new TrackList(this.tracks);
+ let tracks = 0;
+ let rms = 0;
+ const rmHandler = (e) => {
+ if (e.track) {
+ tracks++;
+ }
+ rms++;
+ };
+
+ trackList.on('removetrack', rmHandler);
+ trackList.removeTrack(trackList.getTrackById('1'));
+ trackList.removeTrack(trackList.getTrackById('2'));
+
+ trackList.off('removetrack', rmHandler);
+ trackList.onremovetrack = rmHandler;
+ trackList.removeTrack(trackList.getTrackById('3'));
+
+ assert.equal(rms, 3, 'we got ' + rms + ' "removetrack" events');
+ assert.equal(tracks, 3, 'we got a track with every event');
+});
+
+QUnit.test('labelchange event is fired for the list when a child track fires labelchange', function(assert) {
+ const trackList = new TrackList([new EventTarget()]);
+ let labelchanges = 0;
+ const labelchangeHandler = (e) => {
+ labelchanges++;
+ };
+
+ trackList.on('labelchange', labelchangeHandler);
+ trackList[0].trigger('labelchange');
+ assert.equal(labelchanges, '1', 'labelchange event is fired on tracklist');
+});
+
diff --git a/js/videojs/test/unit/tracks/track.test.js b/js/videojs/test/unit/tracks/track.test.js
new file mode 100644
index 0000000..e5d9b3f
--- /dev/null
+++ b/js/videojs/test/unit/tracks/track.test.js
@@ -0,0 +1,58 @@
+/* eslint-env qunit */
+import TrackBaseline from './track-baseline';
+import Track from '../../../src/js/tracks/track.js';
+import TextTrackList from '../../../src/js/tracks/text-track-list.js';
+
+const defaultTech = {
+ textTracks() {
+ return new TextTrackList();
+ },
+ on() {},
+ off() {},
+ currentTime() {}
+};
+
+// do baseline track testing
+QUnit.module('Track');
+
+TrackBaseline(Track, {
+ id: '1',
+ kind: 'subtitles',
+ mode: 'disabled',
+ label: 'English',
+ language: 'en'
+ // tech is added in baseline
+ // tech: new TechFaker()
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const track = new Track({
+ tech: defaultTech
+ });
+
+ assert.equal(track.kind, '', 'kind defaulted to empty string');
+ assert.equal(track.label, '', 'label defaults to empty string');
+ assert.equal(track.language, '', 'language defaults to empty string');
+ assert.ok(track.id.match(/vjs_track_\d+/), 'id defaults to vjs_track_GUID');
+});
+
+QUnit.test('label is updated and labelchange event is fired when label is changed', function(assert) {
+ const track = new Track({
+ tech: defaultTech
+ });
+ let eventsTriggered = 0;
+
+ track.addEventListener('labelchange', () => {
+ eventsTriggered++;
+ });
+
+ track.label = 'English (auto)';
+ assert.equal(eventsTriggered, 1, 'one label change');
+ assert.equal(track.label, 'English (auto)');
+
+ track.label = 'English (auto)';
+ assert.equal(eventsTriggered, 1, 'additional label change not fired when new label is the same as old');
+ assert.equal(track.label, 'English (auto)');
+
+ track.off();
+});
diff --git a/js/videojs/test/unit/tracks/video-track-list.test.js b/js/videojs/test/unit/tracks/video-track-list.test.js
new file mode 100644
index 0000000..611ec1e
--- /dev/null
+++ b/js/videojs/test/unit/tracks/video-track-list.test.js
@@ -0,0 +1,117 @@
+/* eslint-env qunit */
+import VideoTrackList from '../../../src/js/tracks/video-track-list.js';
+import VideoTrack from '../../../src/js/tracks/video-track.js';
+import EventTarget from '../../../src/js/event-target.js';
+
+QUnit.module('Video Track List');
+
+QUnit.test('trigger "change" when "selectedchange" is fired on a track', function(assert) {
+ const track = new EventTarget();
+
+ track.loaded_ = true;
+ const videoTrackList = new VideoTrackList([track]);
+ let changes = 0;
+ const changeHandler = function() {
+ changes++;
+ };
+
+ videoTrackList.on('change', changeHandler);
+ track.trigger('selectedchange');
+ assert.equal(changes, 1, 'one change events for trigger');
+
+ videoTrackList.off('change', changeHandler);
+ videoTrackList.onchange = changeHandler;
+
+ track.trigger('selectedchange');
+ assert.equal(changes, 2, 'one change events for another trigger');
+
+ videoTrackList.removeTrack(track);
+ videoTrackList.off('change');
+});
+
+QUnit.test('only one track is ever selected', function(assert) {
+ const track = new VideoTrack({selected: true});
+ const track2 = new VideoTrack({selected: true});
+ const track3 = new VideoTrack({selected: true});
+ const track4 = new VideoTrack();
+ const list = new VideoTrackList([track, track2]);
+
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, true, 'track2 is selected');
+
+ track.selected = true;
+ assert.equal(track.selected, true, 'track is selected');
+ assert.equal(track2.selected, false, 'track2 is unselected');
+
+ list.addTrack(track3);
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, false, 'track2 is unselected');
+ assert.equal(track3.selected, true, 'track3 is selected');
+
+ track2.selected = true;
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, true, 'track2 is selected');
+ assert.equal(track3.selected, false, 'track3 is unselected');
+
+ list.addTrack(track4);
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, true, 'track2 is selected');
+ assert.equal(track3.selected, false, 'track3 is unselected');
+ assert.equal(track4.selected, false, 'track4 is unselected');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+ list.removeTrack(track3);
+ list.removeTrack(track4);
+});
+
+QUnit.test('all tracks can be unselected', function(assert) {
+ const track = new VideoTrack();
+ const track2 = new VideoTrack();
+ const list = new VideoTrackList([track, track2]);
+
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, false, 'track2 is unselected');
+
+ track.selected = true;
+ assert.equal(track.selected, true, 'track is selected');
+ assert.equal(track2.selected, false, 'track2 is unselected');
+
+ track.selected = false;
+ assert.equal(track.selected, false, 'track is unselected');
+ assert.equal(track2.selected, false, 'track2 is unselected');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+});
+
+QUnit.test('trigger a change event per selected change', function(assert) {
+ const track = new VideoTrack({selected: true});
+ const track2 = new VideoTrack({selected: true});
+ const track3 = new VideoTrack({selected: true});
+ const track4 = new VideoTrack();
+ const list = new VideoTrackList([track, track2]);
+ let change = 0;
+
+ list.on('change', () => change++);
+ track.selected = true;
+ assert.equal(change, 1, 'one change triggered');
+
+ list.addTrack(track3);
+ assert.equal(change, 2, 'another change triggered by adding an selected track');
+
+ track.selected = true;
+ assert.equal(change, 3, 'another change trigger by changing selected');
+
+ track.selected = false;
+ assert.equal(change, 4, 'another change trigger by changing selected');
+
+ list.addTrack(track4);
+ assert.equal(change, 4, 'no change triggered by adding a unselected track');
+
+ list.removeTrack(track);
+ list.removeTrack(track2);
+ list.removeTrack(track3);
+ list.removeTrack(track4);
+ list.off('change');
+});
diff --git a/js/videojs/test/unit/tracks/video-track.test.js b/js/videojs/test/unit/tracks/video-track.test.js
new file mode 100644
index 0000000..a48cbf7
--- /dev/null
+++ b/js/videojs/test/unit/tracks/video-track.test.js
@@ -0,0 +1,120 @@
+/* eslint-env qunit */
+import VideoTrack from '../../../src/js/tracks/video-track';
+import {VideoTrackKind} from '../../../src/js/tracks/track-enums';
+import TrackBaseline from './track-baseline';
+
+QUnit.module('Video Track');
+
+// do baseline track testing
+TrackBaseline(VideoTrack, {
+ id: '1',
+ language: 'en',
+ label: 'English',
+ kind: 'main'
+});
+
+QUnit.test('can create an VideoTrack a selected property', function(assert) {
+ const selected = true;
+ const track = new VideoTrack({
+ selected
+ });
+
+ assert.equal(track.selected, selected, 'selected value matches what we passed in');
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const track = new VideoTrack();
+
+ assert.equal(track.kind, '', 'kind defaulted to empty string');
+ assert.equal(
+ track.selected,
+ false,
+ 'selected defaulted to true since there is one track'
+ );
+ assert.equal(track.label, '', 'label defaults to empty string');
+ assert.equal(track.language, '', 'language defaults to empty string');
+ assert.ok(track.id.match(/vjs_track_\d+/), 'id defaults to vjs_track_GUID');
+});
+
+QUnit.test('kind can only be one of several options, defaults to empty string', function(assert) {
+ const track1 = new VideoTrack({
+ kind: 'foo'
+ });
+
+ assert.equal(track1.kind, '', 'the kind is set to empty string, not foo');
+ assert.notEqual(track1.kind, 'foo', 'the kind is set to empty string, not foo');
+
+ // loop through all possible kinds to verify
+ for (const key in VideoTrackKind) {
+ const currentKind = VideoTrackKind[key];
+ const track = new VideoTrack({kind: currentKind});
+
+ assert.equal(track.kind, currentKind, 'the kind is set to ' + currentKind);
+ }
+});
+
+QUnit.test('selected can only be instantiated to true or false, defaults to false', function(assert) {
+ let track = new VideoTrack({
+ selected: 'foo'
+ });
+
+ assert.equal(track.selected, false, 'the selected value is set to false, not foo');
+ assert.notEqual(track.selected, 'foo', 'the selected value is not set to foo');
+
+ track = new VideoTrack({
+ selected: true
+ });
+
+ assert.equal(track.selected, true, 'the selected value is set to true');
+
+ track = new VideoTrack({
+ selected: false
+ });
+
+ assert.equal(track.selected, false, 'the selected value is set to false');
+});
+
+QUnit.test('selected can only be changed to true or false', function(assert) {
+ const track = new VideoTrack();
+
+ track.selected = 'foo';
+ assert.notEqual(track.selected, 'foo', 'selected not set to invalid value, foo');
+ assert.equal(track.selected, false, 'selected remains on the old value, false');
+
+ track.selected = true;
+ assert.equal(track.selected, true, 'selected was set to true');
+
+ track.selected = 'baz';
+ assert.notEqual(track.selected, 'baz', 'selected not set to invalid value, baz');
+ assert.equal(track.selected, true, 'selected remains on the old value, true');
+
+ track.selected = false;
+ assert.equal(track.selected, false, 'selected was set to false');
+});
+
+QUnit.test('when selected is changed selectedchange event is fired', function(assert) {
+ const track = new VideoTrack({
+ selected: false
+ });
+ let eventsTriggered = 0;
+
+ track.addEventListener('selectedchange', () => {
+ eventsTriggered++;
+ });
+
+ // two events
+ track.selected = true;
+ track.selected = false;
+ assert.equal(eventsTriggered, 2, 'two selected changes');
+
+ // no event here
+ track.selected = false;
+ track.selected = false;
+ assert.equal(eventsTriggered, 2, 'still two selected changes');
+
+ // one event
+ track.selected = true;
+ assert.equal(eventsTriggered, 3, 'three selected changes');
+
+ track.off();
+});
diff --git a/js/videojs/test/unit/tracks/video-tracks.test.js b/js/videojs/test/unit/tracks/video-tracks.test.js
new file mode 100644
index 0000000..40b236c
--- /dev/null
+++ b/js/videojs/test/unit/tracks/video-tracks.test.js
@@ -0,0 +1,112 @@
+/* eslint-env qunit */
+import Html5 from '../../../src/js/tech/html5.js';
+import TestHelpers from '../test-helpers.js';
+import sinon from 'sinon';
+
+QUnit.module('Video Tracks', {
+ beforeEach(assert) {
+ this.clock = sinon.useFakeTimers();
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ }
+});
+
+QUnit.test('Player track methods call the tech', function(assert) {
+ let calls = 0;
+ const player = TestHelpers.makePlayer();
+
+ player.tech_.videoTracks = function() {
+ calls++;
+ };
+
+ player.videoTracks();
+
+ assert.equal(calls, 1, 'videoTrack defers to the tech');
+ player.dispose();
+});
+
+QUnit.test('listen to remove and add track events in native video tracks', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+ const oldVideoTracks = Html5.prototype.videoTracks;
+ const events = {};
+
+ Html5.prototype.videoTracks = function() {
+ return {
+ removeEventListener() {},
+ addEventListener(type, handler) {
+ events[type] = true;
+ }
+ };
+ };
+
+ Html5.TEST_VID = {
+ videoTracks: []
+ };
+
+ const player = {
+ // Function.prototype is a built-in no-op function.
+ controls() {},
+ ready() {},
+ options() {
+ return {};
+ },
+ addChild() {},
+ id() {},
+ el() {
+ return {
+ insertBefore() {},
+ appendChild() {}
+ };
+ }
+ };
+
+ player.player_ = player;
+ player.options_ = {};
+
+ const html = new Html5({});
+
+ assert.ok(events.removetrack, 'removetrack listener was added');
+ assert.ok(events.addtrack, 'addtrack listener was added');
+
+ Html5.TEST_VID = oldTestVid;
+ Html5.prototype.videoTracks = oldVideoTracks;
+
+ html.dispose();
+});
+
+QUnit.test('html5 tech supports native video tracks if the video supports it', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+
+ Html5.TEST_VID = {
+ videoTracks: []
+ };
+
+ assert.ok(Html5.supportsNativeVideoTracks(), 'native video tracks are supported');
+
+ Html5.TEST_VID = oldTestVid;
+});
+
+QUnit.test('html5 tech does not support native video tracks if the video does not supports it', function(assert) {
+ const oldTestVid = Html5.TEST_VID;
+
+ Html5.TEST_VID = {};
+
+ assert.ok(!Html5.supportsNativeVideoTracks(), 'native video tracks are not supported');
+
+ Html5.TEST_VID = oldTestVid;
+});
+
+QUnit.test('when switching techs, we should not get a new video track', function(assert) {
+ const player = TestHelpers.makePlayer();
+
+ player.loadTech_('TechFaker');
+ const firstTracks = player.videoTracks();
+
+ player.loadTech_('TechFaker');
+ const secondTracks = player.videoTracks();
+
+ assert.ok(firstTracks === secondTracks, 'the tracks are equal');
+
+ player.dispose();
+});