1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-23 02:04:34 +02:00
video.js/test/unit/modal-dialog.test.js

524 lines
15 KiB
JavaScript
Raw Permalink Normal View History

/* eslint-env qunit */
import CloseButton from '../../src/js/close-button';
import sinon from 'sinon';
import ModalDialog from '../../src/js/modal-dialog';
import * as Dom from '../../src/js/utils/dom';
import TestHelpers from './test-helpers';
const getMockEscapeEvent = () => ({
key: 'Escape',
which: 27,
preventDefault() {},
stopPropagation() {}
});
QUnit.module('ModalDialog', {
beforeEach() {
this.player = TestHelpers.makePlayer();
this.modal = new ModalDialog(this.player, {temporary: false});
this.el = this.modal.el();
},
afterEach() {
this.player.dispose();
this.modal.dispose();
this.el = null;
}
});
const tabTestHelper = function(assert, player) {
return function(fromIndex, toIndex, shift = false) {
const oldFocusableEls = ModalDialog.prototype.focusableEls_;
const focus = function() {
assert.equal(this.i, toIndex, `we should focus back on the ${toIndex} element, we got ${this.i}.`);
};
const els = [
{i: 0, focus},
{i: 1, focus},
{i: 2, focus},
{i: 3, focus}
];
ModalDialog.prototype.focusableEls_ = () => els;
const modal = new ModalDialog(player, {});
const oldEl = modal.el_;
modal.el_ = {
querySelector() {
const focusableEls = modal.focusableEls_();
return focusableEls[fromIndex];
}
};
let prevented = false;
modal.handleKeyDown({
which: 9,
shiftKey: shift,
preventDefault() {
prevented = true;
},
stopPropagation() {
}
});
if (!prevented) {
const newIndex = shift ? fromIndex - 1 : fromIndex + 1;
const newEl = modal.focusableEls_()[newIndex];
if (newEl) {
newEl.focus(newEl.i);
}
}
modal.el_ = oldEl;
modal.dispose();
ModalDialog.prototype.focusableEls_ = oldFocusableEls;
};
};
QUnit.test('should create the expected element', function(assert) {
const elAssertions = TestHelpers.assertEl(assert, this.el, {
tagName: 'div',
classes: [
'vjs-modal-dialog',
'vjs-hidden'
],
attrs: {
'aria-describedby': this.modal.descEl_.id,
'aria-hidden': 'true',
'aria-label': this.modal.label(),
'role': 'dialog'
},
props: {
tabIndex: -1
}
});
assert.expect(elAssertions.count);
elAssertions();
});
QUnit.test('should create the expected description element', function(assert) {
const elAssertions = TestHelpers.assertEl(assert, this.modal.descEl_, {
tagName: 'p',
innerHTML: this.modal.description(),
classes: [
'vjs-modal-dialog-description',
feat: fix accessibility of the captions setting dialog (#4050) This fixes a lot of the issues from #2746 by making the dialog inherit from our actual ModalDialog which now has tab focus trapping. The Captions Settings dialog has some accessibility issues: - Field labels and fields are not explicitly associated - Keyboard focus does not move into the dialog when it is opened - Keyboard focus is not trapped inside the dialog while it is open - Keyboard focus does not return to the control which opened the dialog when it is closed - The extent (top and bottom) of the dialog is not indicated to screen readers - The dialog cannot be closed with the Esc key - The meaning of '---' in the select fields is not clear - The control to close the dialog is labeled "Done" rather than "Close" - The purpose of the "Defaults" button may not be obvious, and its effect may not be apparent to screen reader users - Focus highlighting (outline) on the Default and Done buttons is *very* hard to see - Pressing the Done button doesn’t seem to have the same effect as pressing the Close (x) button; does it trigger the same focus movement - This requirement to move it back to the triggering element is tricky, since clicking on that item in the CC menu dismisses the CC menu. I need to think about this a little more - either the menu should open again, or the focus should go to the main CC Menu Button - The focus outline on the whole dialog goes too far to the left (all the way to the edge of the video window, not just to the edge of the dialog) Fixes #2746.
2017-02-21 22:58:04 +02:00
'vjs-control-text'
],
attrs: {
id: this.el.getAttribute('aria-describedby')
}
});
assert.expect(elAssertions.count);
elAssertions();
});
QUnit.test('should create the expected contentEl', function(assert) {
const elAssertions = TestHelpers.assertEl(assert, this.modal.contentEl(), {
tagName: 'div',
classes: [
'vjs-modal-dialog-content'
],
props: {
parentNode: this.el
}
});
assert.expect(elAssertions.count);
elAssertions();
});
QUnit.test('should create a close button by default', function(assert) {
const btn = this.modal.getChild('closeButton');
// We only check the aspects of the button that relate to the modal. Other
// aspects of the button (classes, etc) are tested in their appropriate test
// module.
assert.expect(2);
assert.ok(btn instanceof CloseButton, 'close button is a CloseButton');
assert.strictEqual(btn.el().parentNode, this.el, 'close button is a child of el');
});
QUnit.test('open() triggers events', function(assert) {
const modal = this.modal;
const beforeModalOpenSpy = sinon.spy(function() {
assert.notOk(modal.opened(), 'modal is not opened before opening event');
});
const modalOpenSpy = sinon.spy(function() {
assert.ok(modal.opened(), 'modal is opened on opening event');
});
assert.expect(4);
modal.on('beforemodalopen', beforeModalOpenSpy);
modal.on('modalopen', modalOpenSpy);
modal.open();
assert.strictEqual(beforeModalOpenSpy.callCount, 1, 'beforemodalopen spy was called');
assert.strictEqual(modalOpenSpy.callCount, 1, 'modalopen spy was called');
});
QUnit.test('open() removes "vjs-hidden" class', function(assert) {
assert.expect(2);
assert.ok(this.modal.hasClass('vjs-hidden'), 'modal starts hidden');
this.modal.open();
assert.notOk(this.modal.hasClass('vjs-hidden'), 'modal is not hidden after opening');
});
QUnit.test('open() cannot be called on an opened modal', function(assert) {
const spy = sinon.spy();
this.modal.on('modalopen', spy);
this.modal.open();
this.modal.open();
assert.expect(1);
assert.strictEqual(spy.callCount, 1, 'modal was only opened once');
});
QUnit.test('close() triggers events', function(assert) {
const modal = this.modal;
const beforeModalCloseSpy = sinon.spy(function() {
assert.ok(modal.opened(), 'modal is not closed before closing event');
});
const modalCloseSpy = sinon.spy(function() {
assert.notOk(modal.opened(), 'modal is closed on closing event');
});
assert.expect(4);
modal.on('beforemodalclose', beforeModalCloseSpy);
modal.on('modalclose', modalCloseSpy);
modal.open();
modal.close();
assert.strictEqual(beforeModalCloseSpy.callCount, 1, 'beforemodalclose spy was called');
assert.strictEqual(modalCloseSpy.callCount, 1, 'modalclose spy was called');
});
QUnit.test('close() adds the "vjs-hidden" class', function(assert) {
assert.expect(1);
this.modal.open();
this.modal.close();
assert.ok(this.modal.hasClass('vjs-hidden'), 'modal is hidden upon close');
});
QUnit.test('pressing ESC triggers close(), but only when the modal is opened', function(assert) {
const spy = sinon.spy();
this.modal.on('modalclose', spy);
this.modal.handleKeyDown(getMockEscapeEvent());
assert.expect(2);
assert.strictEqual(spy.callCount, 0, 'ESC did not close the closed modal');
this.modal.open();
this.modal.handleKeyDown(getMockEscapeEvent());
assert.strictEqual(spy.callCount, 1, 'ESC closed the now-opened modal');
});
QUnit.test('close() cannot be called on a closed modal', function(assert) {
const spy = sinon.spy();
this.modal.on('modalclose', spy);
this.modal.open();
this.modal.close();
this.modal.close();
assert.expect(1);
assert.strictEqual(spy.callCount, 1, 'modal was only closed once');
});
QUnit.test('open() pauses playback, close() resumes', function(assert) {
const playSpy = sinon.spy();
const pauseSpy = sinon.spy();
// Quick and dirty; make it looks like the player is playing.
this.player.paused = function() {
return false;
};
this.player.play = function() {
playSpy();
};
this.player.pause = function() {
pauseSpy();
};
this.modal.open();
assert.expect(2);
assert.strictEqual(pauseSpy.callCount, 1, 'player is paused when the modal opens');
this.modal.close();
assert.strictEqual(playSpy.callCount, 1, 'player is resumed when the modal closes');
});
QUnit.test('open() does not pause, close() does not play() with pauseOnOpen set to false', function(assert) {
const playSpy = sinon.spy();
const pauseSpy = sinon.spy();
// don't pause the video on modal open
this.modal.options_.pauseOnOpen = false;
// Quick and dirty; make it looks like the player is playing.
this.player.paused = function() {
return false;
};
this.player.play = function() {
playSpy();
};
this.player.pause = function() {
pauseSpy();
};
this.modal.open();
assert.expect(2);
assert.strictEqual(pauseSpy.callCount, 0, 'player remains playing when the modal opens');
this.modal.close();
assert.strictEqual(playSpy.callCount, 0, 'player is resumed when the modal closes');
});
QUnit.test('open() hides controls, close() shows controls', function(assert) {
this.player.controls(true);
this.modal.open();
assert.expect(2);
assert.notOk(this.player.controls_, 'controls are hidden');
this.modal.close();
assert.ok(this.player.controls_, 'controls are no longer hidden');
});
QUnit.test('open() hides controls, close() does not show controls if previously hidden', function(assert) {
this.player.controls(false);
this.modal.open();
assert.expect(2);
assert.notOk(this.player.controls_, 'controls are hidden');
this.modal.close();
assert.notOk(this.player.controls_, 'controls are still hidden');
});
QUnit.test('opened()', function(assert) {
const openSpy = sinon.spy();
const closeSpy = sinon.spy();
assert.expect(4);
assert.strictEqual(this.modal.opened(), false, 'the modal is closed');
this.modal.open();
assert.strictEqual(this.modal.opened(), true, 'the modal is open');
this.modal.close();
this.modal.on('modalopen', openSpy);
this.modal.on('modalclose', closeSpy);
this.modal.opened(true);
this.modal.opened(true);
this.modal.opened(false);
assert.strictEqual(openSpy.callCount, 1, 'modal was opened only once');
assert.strictEqual(closeSpy.callCount, 1, 'modal was closed only once');
});
QUnit.test('content()', function(assert) {
assert.expect(3);
assert.strictEqual(typeof this.modal.content(), 'undefined', 'no content by default');
const content = this.modal.content(Dom.createEl());
assert.ok(Dom.isEl(content), 'content was set from a single DOM element');
assert.strictEqual(this.modal.content(null), null, 'content was nullified');
});
QUnit.test('fillWith()', function(assert) {
const contentEl = this.modal.contentEl();
const children = [Dom.createEl(), Dom.createEl(), Dom.createEl()];
const beforeFillSpy = sinon.spy();
const fillSpy = sinon.spy();
children.forEach(function(el) {
contentEl.appendChild(el);
});
this.modal.on('beforemodalfill', beforeFillSpy);
this.modal.on('modalfill', fillSpy);
this.modal.fillWith(children);
assert.expect(5 + children.length);
assert.strictEqual(contentEl.children.length, children.length, 'has the right number of children');
children.forEach(function(el) {
assert.strictEqual(el.parentNode, contentEl, 'new child appended');
});
assert.strictEqual(beforeFillSpy.callCount, 1, 'the "beforemodalfill" callback was called');
assert.strictEqual(beforeFillSpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
assert.strictEqual(fillSpy.callCount, 1, 'the "modalfill" callback was called');
assert.strictEqual(fillSpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
});
QUnit.test('empty()', function(assert) {
const beforeEmptySpy = sinon.spy();
const emptySpy = sinon.spy();
this.modal.fillWith([Dom.createEl(), Dom.createEl()]);
this.modal.on('beforemodalempty', beforeEmptySpy);
this.modal.on('modalempty', emptySpy);
this.modal.empty();
assert.expect(5);
assert.strictEqual(this.modal.contentEl().children.length, 0, 'removed all `contentEl()` children');
assert.strictEqual(beforeEmptySpy.callCount, 1, 'the "beforemodalempty" callback was called');
assert.strictEqual(beforeEmptySpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
assert.strictEqual(emptySpy.callCount, 1, 'the "modalempty" callback was called');
assert.strictEqual(emptySpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
});
QUnit.test('closeable()', function(assert) {
const initialCloseButton = this.modal.getChild('closeButton');
assert.expect(8);
assert.strictEqual(this.modal.closeable(), true, 'the modal is closed');
this.modal.open();
this.modal.closeable(false);
assert.notOk(this.modal.getChild('closeButton'), 'the close button is no longer a child of the modal');
assert.notOk(initialCloseButton.el(), 'the initial close button was disposed');
this.modal.handleKeyDown(getMockEscapeEvent());
assert.ok(this.modal.opened(), 'the modal was not closed by the ESC key');
this.modal.close();
assert.notOk(this.modal.opened(), 'the modal was closed programmatically');
this.modal.open();
this.modal.closeable(true);
assert.ok(this.modal.getChild('closeButton'), 'a new close button was created');
this.modal.getChild('closeButton').trigger('click');
assert.notOk(this.modal.opened(), 'the modal was closed by the new close button');
this.modal.open();
this.modal.handleKeyDown(getMockEscapeEvent());
assert.notOk(this.modal.opened(), 'the modal was closed by the ESC key');
});
QUnit.test('"content" option (fills on first open() invocation)', function(assert) {
const modal = new ModalDialog(this.player, {
content: Dom.createEl(),
temporary: false
});
const spy = sinon.spy();
modal.on('modalfill', spy);
modal.open();
modal.close();
modal.open();
assert.expect(3);
assert.strictEqual(modal.content(), modal.options_.content, 'has the expected content');
assert.strictEqual(spy.callCount, 1, 'auto-fills only once');
assert.strictEqual(modal.contentEl().firstChild, modal.options_.content, 'has the expected content in the DOM');
modal.dispose();
});
QUnit.test('"temporary" option', function(assert) {
const temp = new ModalDialog(this.player, {temporary: true});
const tempSpy = sinon.spy();
const perm = new ModalDialog(this.player, {temporary: false});
const permSpy = sinon.spy();
temp.on('dispose', tempSpy);
perm.on('dispose', permSpy);
temp.open();
temp.close();
perm.open();
perm.close();
assert.expect(2);
assert.strictEqual(tempSpy.callCount, 1, 'temporary modals are disposed');
assert.strictEqual(permSpy.callCount, 0, 'permanent modals are not disposed');
temp.dispose();
perm.dispose();
});
QUnit.test('"fillAlways" option', function(assert) {
const modal = new ModalDialog(this.player, {
content: 'foo',
fillAlways: true,
temporary: false
});
const spy = sinon.spy();
modal.on('modalfill', spy);
modal.open();
modal.close();
modal.open();
assert.expect(1);
assert.strictEqual(spy.callCount, 2, 'the modal was filled on each open call');
modal.dispose();
});
QUnit.test('"label" option', function(assert) {
const label = 'foo';
const modal = new ModalDialog(this.player, {label});
assert.expect(1);
assert.strictEqual(modal.el().getAttribute('aria-label'), label, 'uses the label as the aria-label');
modal.dispose();
});
QUnit.test('"uncloseable" option', function(assert) {
const modal = new ModalDialog(this.player, {
temporary: false,
uncloseable: true
});
const spy = sinon.spy();
modal.on('modalclose', spy);
assert.expect(3);
assert.strictEqual(modal.closeable(), false, 'the modal is uncloseable');
assert.notOk(modal.getChild('closeButton'), 'the close button is not present');
modal.open();
modal.handleKeyDown(getMockEscapeEvent());
assert.strictEqual(spy.callCount, 0, 'ESC did not close the modal');
modal.dispose();
});
QUnit.test('handleKeyDown traps tab focus', function(assert) {
const tabTester = tabTestHelper(assert, this.player);
// tabbing forward from first element to last and cycling back to first
tabTester(0, 1, false);
tabTester(1, 2, false);
tabTester(2, 3, false);
tabTester(3, 0, false);
// tabbing backwards from last element to first and cycling back to last
tabTester(3, 2, true);
tabTester(2, 1, true);
tabTester(1, 0, true);
tabTester(0, 3, true);
});