blob: 7ab871561230f4a4615cca246f1789e94cb22048 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview hterm.AccessibilityReader unit tests.
*/
describe('hterm_accessibility_reader_tests.js', () => {
/**
* Set up state for all the tests in this suite.
*/
before(() => {
// Stub out the delay loops. We don't have to worry about waiting for input
// from the user to accumulate as we don't do that.
hterm.AccessibilityReader.DELAY = 0;
});
/*
* Create a new hterm.AccessibilityReader object for testing.
*
* Called before each test case in this suite.
*/
beforeEach(function() {
const document = window.document;
const div = this.div = document.createElement('div');
div.style.position = 'absolute';
div.style.height = '100%';
div.style.width = '100%';
this.accessibilityReader = new hterm.AccessibilityReader(div);
this.accessibilityReader.setAccessibilityEnabled(true);
this.liveElement = div.firstChild.firstChild;
this.assertiveLiveElement = this.liveElement.nextSibling;
document.body.appendChild(div);
});
/**
* Clean up the hterm.AccessibilityReader object.
*/
afterEach(function() {
window.document.body.removeChild(this.div);
});
/**
* Test that printing text to the terminal will cause nodes to be added to the
* live region for accessibility purposes. This shouldn't happen until after a
* small delay has passed.
*/
it('a11y-live-region-single-delay', function(done) {
this.accessibilityReader.announce('Some test output');
this.accessibilityReader.announce('Some other test output');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('More output');
assert.equal('', this.liveElement.getAttribute('aria-label'));
const observer = new MutationObserver(() => {
assert.equal('Some test output Some other test output\nMore output',
this.liveElement.getAttribute('aria-label'));
observer.disconnect();
done();
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that after text has been added to the live region, there is again a
* delay before adding more text.
*/
it('a11y-live-region-double-delay', function(done) {
this.accessibilityReader.announce('Some test output');
this.accessibilityReader.announce('Some other test output');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('More output');
assert.equal('', this.liveElement.getAttribute('aria-label'));
const checkFirstAnnounce = () => {
assert.equal('Some test output Some other test output\nMore output',
this.liveElement.getAttribute('aria-label'));
this.accessibilityReader.announce('more text');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('...and more');
return true;
};
const checkSecondAnnounce = () => {
assert.equal('more text\n...and more',
this.liveElement.getAttribute('aria-label'));
return true;
};
const checksToComplete = [checkFirstAnnounce, checkSecondAnnounce];
const observer = new MutationObserver(() => {
if (checksToComplete[0]()) {
checksToComplete.shift();
}
if (checksToComplete.length == 0) {
observer.disconnect();
done();
}
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that adding the same text twice to the live region gets slightly
* modified to trigger an attribute change.
*/
it('a11y-live-region-duplicate-text', function(done) {
this.accessibilityReader.announce('Some test output');
assert.equal('', this.liveElement.getAttribute('aria-label'));
const checkFirstAnnounce = () => {
assert.equal('Some test output',
this.liveElement.getAttribute('aria-label'));
this.accessibilityReader.announce('Some test output');
return true;
};
const checkSecondAnnounce = () => {
assert.equal('\nSome test output',
this.liveElement.getAttribute('aria-label'));
return true;
};
const checksToComplete = [checkFirstAnnounce, checkSecondAnnounce];
const observer = new MutationObserver(() => {
if (checksToComplete[0]()) {
checksToComplete.shift();
}
if (checksToComplete.length == 0) {
observer.disconnect();
done();
}
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that adding text to the assertive live region works correctly.
*/
it('a11y-assertive-live-region', function() {
this.accessibilityReader.assertiveAnnounce('Some test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'Some test output');
this.accessibilityReader.clear();
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
});
/**
* Test that adding the same text twice to the assertive live region gets
* slightly modified to trigger an attribute change.
*/
it('a11y-assertive-live-region-duplicate-text', function() {
this.accessibilityReader.assertiveAnnounce('Some test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'Some test output');
this.accessibilityReader.assertiveAnnounce('Some test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'\nSome test output');
});
/**
* Test that adding text to the assertive live region interrupts polite
* announcements.
*/
it('a11y-assertive-live-region-interrupts-polite', function(done) {
this.accessibilityReader.announce('Some test output');
this.accessibilityReader.announce('Some other test output');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('More output');
assert.equal(this.liveElement.getAttribute('aria-label'), '');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// The live element should not change because we interrupt it. It should only
// announce the 'PASS' string which comes after all the output above.
const observer = new MutationObserver(() => {
if (this.liveElement.getAttribute('aria-label') == 'PASS') {
done();
} else {
assert.equal(this.liveElement.getAttribute('aria-label'), '');
}
});
observer.observe(this.liveElement, {attributes: true});
this.accessibilityReader.assertiveAnnounce('Some test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'Some test output');
this.accessibilityReader.announce('PASS');
});
/**
* Test that nothing is announced when accessibility is disabled.
*/
it('a11y-disabled-enabled', function(done) {
this.accessibilityReader.setAccessibilityEnabled(false);
this.accessibilityReader.announce('Some test output');
this.accessibilityReader.announce('Some other test output');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('More output');
assert.equal(this.liveElement.getAttribute('aria-label'), '');
// Only 'Other output' should be announced now.
this.accessibilityReader.setAccessibilityEnabled(true);
this.accessibilityReader.announce('Other output');
const observer = new MutationObserver(() => {
if (this.liveElement.getAttribute('aria-label') == 'Other output') {
done();
} else {
assert.equal(this.liveElement.getAttribute('aria-label'), '');
}
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that when accessibility is disabled, nothing else will be announced.
*/
it('a11y-enabled-disabled', function(done) {
this.accessibilityReader.announce('Some test output');
this.accessibilityReader.announce('Some other test output');
this.accessibilityReader.newLine();
this.accessibilityReader.announce('More output');
assert.equal(this.liveElement.getAttribute('aria-label'), '');
// The live element should not change because accessibility is disabled. It
// should only announce the 'PASS' string which comes after all the output
// above.
const observer = new MutationObserver(() => {
if (this.liveElement.getAttribute('aria-label') == 'PASS') {
done();
} else {
assert.equal(this.liveElement.getAttribute('aria-label'), '');
}
});
observer.observe(this.liveElement, {attributes: true});
this.accessibilityReader.setAccessibilityEnabled(false);
this.accessibilityReader.setAccessibilityEnabled(true);
this.accessibilityReader.announce('PASS');
});
/**
* Test that when accessibility is disabled, assertive announcements still work.
* These are not performance sensitive so they don't need to be gated on the
* flag.
*/
it('a11y-assertive-disabled-enabled', function() {
this.accessibilityReader.setAccessibilityEnabled(false);
this.accessibilityReader.assertiveAnnounce('Some test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'Some test output');
this.accessibilityReader.setAccessibilityEnabled(true);
this.accessibilityReader.assertiveAnnounce('More test output');
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'),
'More test output');
});
/**
* Regression test for a bug that is caused by adding 2 newlines and then
* calling announce. In this case an exception was thrown.
*/
it('a11y-newlines-then-announce', function() {
this.accessibilityReader.newLine();
this.accessibilityReader.newLine();
this.accessibilityReader.announce('Some test output');
});
/**
* Test that moving the cursor left/right through output will cause the output
* to get assertively announced.
*/
it('a11y-selection-change-left-right', function() {
// Move the cursor right 1 character.
// Simulatue a user gesture.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'a');
// Move the cursor left 1 character.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 2);
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'b');
// Move the cursor right 1 character with wide chars in the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('匂へどabc', 0, 0);
this.accessibilityReader.afterCursorChange('匂へどabc', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '匂');
// Move the cursor left 1 character with wide chars in the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('匂へどabc', 0, 9);
this.accessibilityReader.afterCursorChange('匂へどabc', 0, 8);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'c');
// Move the cursor to the end of the output.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abc', 0, 3);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'abc');
// Move the cursor to the start of the output.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.afterCursorChange('abc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'abc');
// Don't move the cursor at all.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Move the cursor 1 character but without a user gesture.
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Move the cursor 1 character but have the output change at the same time.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abcd', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Move the cursor 1 character but have the output change elsewhere on the
// screen at the same time.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.announce('foo bar');
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Move the cursor 1 character but have the row change as well.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('abc', 1, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
});
/**
* Test that other announcements are properly ignored or spoken when navigating
* left and right through output.
*/
it('a11y-selection-change-left-right-with-announce', function(done) {
// Move the cursor 1 character. In the process of doing this, a space
// character is printed somewhere in the terminal. It should get consumed and
// not announced as it may just be a side effect of the cursor change.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.announce(' ');
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'a');
// Do this again but 'foo bar' is announced during the cursor change.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.announce('foo bar');
this.accessibilityReader.afterCursorChange('abc', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// We check that the space gets consumed and isn't announced but 'foo bar'
// still gets announced.
const observer = new MutationObserver(() => {
if (this.liveElement.getAttribute('aria-label') == 'foo bar') {
done();
}
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that changes to the cursor due to backspace and deletion are properly
* announced.
*/
it('a11y-selection-change-backspace-delete', function() {
// Backspace a character at the start of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 1);
this.accessibilityReader.afterCursorChange('bc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'a');
// Backspace a character at the end of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.afterCursorChange('ab', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'c');
// Backspace a wide character.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('匂へど', 0, 6);
this.accessibilityReader.afterCursorChange('匂へ', 0, 4);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'ど');
// Do this again but add an empty space in place of the deleted character. The
// terminal may do this as spaces are no different from empty space in the
// terminal.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.afterCursorChange('ab ', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'c');
// Do the same thing but add other text as well. The backspace shouldn't be
// announced.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.afterCursorChange('ab e ', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Backspace a character in the middle of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 2);
this.accessibilityReader.afterCursorChange('ac', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'b');
// Delete a character at the start of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 0);
this.accessibilityReader.afterCursorChange('bc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'a');
// Delete a character at the end of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 2);
this.accessibilityReader.afterCursorChange('ab', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'c');
// Delete the entire end of the line of text.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc: xyzabc', 0, 11);
this.accessibilityReader.afterCursorChange('abc: ', 0, 5);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'xyzabc');
// Do this again but add an empty space in place of the deleted character. The
// terminal may do this as spaces are no different from empty space in the
// terminal.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 2);
this.accessibilityReader.afterCursorChange('ab ', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'c');
// Do the same thing but add other text as well. The delete shouldn't be
// announced.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 2);
this.accessibilityReader.afterCursorChange('ab e ', 0, 2);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// Delete a character in the middle of the string.
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 1);
this.accessibilityReader.afterCursorChange('ac', 0, 1);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'b');
// Backspace a character without a user gesture.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.beforeCursorChange('abc', 0, 1);
this.accessibilityReader.afterCursorChange('bc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
});
/**
* Test that other output isn't announced during a backspace/deletion selection
* change.
*/
it('a11y-selection-change-backspace-with-announce', function(done) {
// Backspace a character. If other text is announced to the terminal in the
// process, we ignore it. This is because lots of updates can happen during a
// backspace (e.g. all the characers after the deleted character need to be
// moved and reprinted).
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 1);
this.accessibilityReader.announce('bc');
this.accessibilityReader.afterCursorChange('bc', 0, 0);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'a');
// Announce something afterward to ensure the mutation observer fires and
// avoid timing out the test..
this.accessibilityReader.announce('foo');
const observer = new MutationObserver(() => {
if (this.liveElement.getAttribute('aria-label') == 'foo') {
done();
}
});
observer.observe(this.liveElement, {attributes: true});
});
/**
* Test that entering a space character triggers 'Space' to be spoken.
*/
it('a11y-selection-space', function() {
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.announce(' ');
this.accessibilityReader.afterCursorChange('abc ', 0, 4);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), 'Space');
// No space announced if the cursor doesn't move.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.announce(' ');
this.accessibilityReader.afterCursorChange('abc ', 0, 3);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// No space announced if a space is not printed to the screen.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.hasUserGesture = true;
this.accessibilityReader.beforeCursorChange('abc ', 0, 4);
this.accessibilityReader.announce('d');
this.accessibilityReader.afterCursorChange('dabc ', 0, 5);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
// No space announced if there's not a user gesture.
this.assertiveLiveElement.setAttribute('aria-label', '');
this.accessibilityReader.beforeCursorChange('abc', 0, 3);
this.accessibilityReader.announce(' ');
this.accessibilityReader.afterCursorChange('abc ', 0, 4);
assert.equal(this.assertiveLiveElement.getAttribute('aria-label'), '');
});
});