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
Gary Katsevman eddc1d71a3 feat: modal dialog accessibility updates (#4025)
If the modal dialog was opened and the focus was preset inside the
player, move the focus to the modal dialog.
When the modal dialog is closed, move the focus back to the previously
active element.
When focus is inside the dialog, trap tab focus. This was inspired by https://github.com/gdkraus/accessible-modal-dialog and ally.js.
2017-02-08 17:29:05 -05:00

494 lines
14 KiB
JavaScript

/* 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 ESC = 27;
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 mockFocusableEls = function(Modal, focuscallback) {
Modal.prototype.oldFocusableEls = Modal.prototype.focusableEls_;
const focus = function() {
return focuscallback(this.i);
};
const els = [ {
i: 0,
focus
}, {
i: 1,
focus
}, {
i: 2,
focus
}, {
i: 3,
focus
}];
Modal.prototype.focusableEls_ = () => els;
};
const restoreFocusableEls = function(Modal) {
Modal.prototype.focusableEls_ = Modal.prototype.oldFocusableEls;
};
const mockActiveEl = function(modal, index) {
modal.oldEl = modal.el_;
modal.el_ = {
querySelector() {
const focusableEls = modal.focusableEls_();
return focusableEls[index];
}
};
};
const restoreActiveEl = function(modal) {
modal.el_ = modal.oldEl;
};
const tabTestHelper = function(assert, player) {
return function(from, to, shift = false) {
mockFocusableEls(ModalDialog, (focusIndex) => {
assert.equal(focusIndex, to, `we should focus back on the ${to} element, we got ${focusIndex}.`);
});
const modal = new ModalDialog(player, {});
mockActiveEl(modal, from);
let prevented = false;
modal.handleKeyDown({
which: 9,
shiftKey: shift,
preventDefault() {
prevented = true;
}
});
if (!prevented) {
const newIndex = shift ? from - 1 : from + 1;
const newEl = modal.focusableEls_()[newIndex];
if (newEl) {
newEl.focus(newEl.i);
}
}
restoreActiveEl(modal);
modal.dispose();
restoreFocusableEls(ModalDialog);
};
};
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',
'vjs-offscreen'
],
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.handleKeyPress({which: ESC});
assert.expect(2);
assert.strictEqual(spy.callCount, 0, 'ESC did not close the closed modal');
this.modal.open();
this.modal.handleKeyPress({which: ESC});
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() hides controls, close() shows controls', function(assert) {
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('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.handleKeyPress({which: ESC});
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.handleKeyPress({which: ESC});
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');
});
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');
});
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');
});
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');
});
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.handleKeyPress({which: ESC});
assert.strictEqual(spy.callCount, 0, 'ESC did not close the modal');
});
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);
});