Testing Text Track Control Components in Video.js
This test suite validates the text track controls functionality in Video.js, focusing on captions, subtitles, descriptions, and chapter menu implementations. It ensures proper display, interaction, and state management of track controls in the video player interface.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
videojs/videoJs
test/unit/tracks/text-track-controls.test.js
/* 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();
});