blob: 485516e404eda2aed091d88affd58fea92c4cc54 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Include test fixture.
GEN_INCLUDE([
'../testing/chromevox_next_e2e_test_base.js', '../testing/assert_additions.js'
]);
GEN_INCLUDE(['../testing/mock_feedback.js', '../testing/fake_objects.js']);
/**
* Test fixture for Background.
*/
ChromeVoxBackgroundTest = class extends ChromeVoxNextE2ETest {
/** @override */
setUp() {
window.EventType = chrome.automation.EventType;
window.RoleType = chrome.automation.RoleType;
window.doCmd = this.doCmd;
window.press = this.press;
window.Mod = constants.ModifierFlag;
this.forceContextualLastOutput();
}
/**
* @return {!MockFeedback}
*/
createMockFeedback() {
const mockFeedback =
new MockFeedback(this.newCallback(), this.newCallback.bind(this));
mockFeedback.install();
return mockFeedback;
}
/**
* Create a function which perform the command |cmd|.
* @param {string} cmd
* @return {function() : void}
*/
doCmd(cmd) {
return function() {
CommandHandler.onCommand(cmd);
};
}
press(keyCode, modifiers) {
return function() {
BackgroundKeyboardHandler.sendKeyPress(keyCode, modifiers);
};
}
get linksAndHeadingsDoc() {
return `
<p>start</p>
<a href='#a'>alpha</a>
<a href='#b'>beta</a>
<p>
<h1>charlie</h1>
<a href='foo'>delta</a>
</p>
<a href='#bar'>echo</a>
<h2>foxtraut</h2>
<p>end<span>of test</span></p>
`;
}
get buttonDoc() {
return `
<p>start</p>
<button>hello button one</button>
cats
<button>hello button two</button>
<p>end</p>
`;
}
get formsDoc() {
return `
<select id="fruitSelect">
<option>apple</option>
<option>grape</option>
<option> banana</option>
</select>
`;
}
get iframesDoc() {
return `
<p>start</p>
<button>Before</button>
<iframe srcdoc="<button>Inside</button><h1>Inside</h1>"></iframe>
<button>After</button>
`;
}
get disappearingObjectDoc() {
return `
<p>start</p>
<div role="group">
<p>Before1</p>
<p>Before2</p>
<p>Before3</p>
</div>
<div role="group">
<p id="disappearing">Disappearing</p>
</div>
<div role="group">
<p>After1</p>
<p>After2</p>
<p>After3</p>
</div>
<div id="live" aria-live="assertive"></div>
<div id="delete" role="button">Delete</div>
<script>
document.getElementById('delete').addEventListener('click', function() {
let d = document.getElementById('disappearing');
d.parentElement.removeChild(d);
document.getElementById('live').innerText = 'Deleted';
document.body.offsetTop
});
</script>
`;
}
get detailsDoc() {
return `
<p aria-details="details">start</p>
<div role="group">
<p>Before</p>
<p id="details">Details</p>
<p>After</p>
</div>
`;
}
/**
* Fires an onCustomSpokenFeedbackToggled event with enabled state of
* |enabled|.
* @param {boolean} enabled TalkBack-enabled state.
*/
dispatchOnCustomSpokenFeedbackToggledEvent(enabled) {
chrome.accessibilityPrivate.onCustomSpokenFeedbackToggled.dispatch(enabled);
}
};
/** Tests that ChromeVox classic is in this context. */
SYNC_TEST_F('ChromeVoxBackgroundTest', 'ClassicNamespaces', function() {
assertEquals('function', typeof (ChromeVoxBackground));
});
/** Tests that ChromeVox next is in this context. */
SYNC_TEST_F('ChromeVoxBackgroundTest', 'NextNamespaces', function() {
assertEquals('function', typeof (Background));
});
/** Tests consistency of navigating forward and backward. */
TEST_F('ChromeVoxBackgroundTest', 'ForwardBackwardNavigation', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
mockFeedback.expectSpeech('start').expectBraille('start');
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('alpha', 'Link')
.expectBraille('alpha lnk');
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('beta', 'Link')
.expectBraille('beta lnk');
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('delta', 'Link')
.expectBraille('delta lnk');
mockFeedback.call(doCmd('previousLink'))
.expectSpeech('beta', 'Link')
.expectBraille('beta lnk');
mockFeedback.call(doCmd('nextHeading'))
.expectSpeech('charlie', 'Heading 1')
.expectBraille('charlie h1');
mockFeedback.call(doCmd('nextHeading'))
.expectSpeech('foxtraut', 'Heading 2')
.expectBraille('foxtraut h2');
mockFeedback.call(doCmd('previousHeading'))
.expectSpeech('charlie', 'Heading 1')
.expectBraille('charlie h1');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('delta', 'Link')
.expectBraille('delta lnk');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('echo', 'Link')
.expectBraille('echo lnk');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('foxtraut', 'Heading 2')
.expectBraille('foxtraut h2');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('end')
.expectBraille('end');
mockFeedback.call(doCmd('previousObject'))
.expectSpeech('foxtraut', 'Heading 2')
.expectBraille('foxtraut h2');
mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut');
mockFeedback.call(doCmd('nextLine'))
.expectSpeech('end', 'of test')
.expectBraille('endof test');
mockFeedback.call(doCmd('jumpToTop'))
.expectSpeech('start')
.expectBraille('start');
mockFeedback.call(doCmd('jumpToBottom'))
.expectSpeech('of test')
.expectBraille('of test');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'CaretNavigation', function() {
// TODO(plundblad): Add braille expectaions when crbug.com/523285 is fixed.
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
mockFeedback.expectSpeech('start');
mockFeedback.call(doCmd('nextCharacter')).expectSpeech('t');
mockFeedback.call(doCmd('nextCharacter')).expectSpeech('a');
mockFeedback.call(doCmd('nextWord')).expectSpeech('alpha', 'Link');
mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
mockFeedback.call(doCmd('previousWord')).expectSpeech('alpha', 'Link');
mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
mockFeedback.call(doCmd('nextWord')).expectSpeech('charlie', 'Heading 1');
mockFeedback.call(doCmd('nextLine')).expectSpeech('delta', 'Link');
mockFeedback.call(doCmd('nextLine')).expectSpeech('echo', 'Link');
mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut', 'Heading 2');
mockFeedback.call(doCmd('nextLine')).expectSpeech('end', 'of test');
mockFeedback.call(doCmd('nextCharacter')).expectSpeech('n');
mockFeedback.call(doCmd('previousCharacter')).expectSpeech('e');
mockFeedback.call(doCmd('previousCharacter'))
.expectSpeech('t', 'Heading 2');
mockFeedback.call(doCmd('previousWord')).expectSpeech('foxtraut');
mockFeedback.call(doCmd('previousWord')).expectSpeech('echo', 'Link');
mockFeedback.call(doCmd('previousCharacter')).expectSpeech('a', 'Link');
mockFeedback.call(doCmd('previousCharacter')).expectSpeech('t');
mockFeedback.call(doCmd('nextWord')).expectSpeech('echo', 'Link');
mockFeedback.replay();
});
});
/** Tests that individual buttons are stops for move-by-word functionality. */
TEST_F(
'ChromeVoxBackgroundTest', 'CaretNavigationMoveThroughButtonByWord',
function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.buttonDoc, function() {
mockFeedback.expectSpeech('start');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('hello button one', 'Button');
mockFeedback.call(doCmd('previousWord')).expectSpeech('start');
mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
mockFeedback.call(doCmd('nextWord')).expectSpeech('one');
mockFeedback.call(doCmd('nextWord')).expectSpeech('cats');
mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
mockFeedback.call(doCmd('nextWord')).expectSpeech('two');
mockFeedback.call(doCmd('nextWord')).expectSpeech('end');
mockFeedback.call(doCmd('previousWord')).expectSpeech('two');
mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
mockFeedback.call(doCmd('previousWord')).expectSpeech('cats');
mockFeedback.call(doCmd('previousWord')).expectSpeech('one');
mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'SelectSingleBasic', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.formsDoc, function() {
mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
.expectBraille('apple btn +popup +')
.call(press(40 /* ArrowDown */))
.expectSpeech('grape', /2 of 3/)
.expectBraille('grape mnuitm 2/3 (x)')
.call(press(40 /* ArrowDown */))
.expectSpeech('banana', /3 of 3/)
.expectBraille('banana mnuitm 3/3 (x)');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ContinuousRead', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
mockFeedback.expectSpeech('start')
.call(doCmd('readFromHere'))
.expectSpeech(
'start', 'alpha', 'Link', 'beta', 'Link', 'charlie', 'Heading 1');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'InitialFocus', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<a href="a">a</a>', function(rootNode) {
mockFeedback.expectSpeech('a').expectSpeech('Link');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'AriaLabel', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
'<a aria-label="foo" href="a">a</a>', function(rootNode) {
rootNode.find({role: RoleType.LINK}).focus();
mockFeedback.expectSpeech('foo')
.expectSpeech('Link')
.expectSpeech('Press Search+Space to activate')
.expectBraille('foo lnk');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenu', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<p>before</p><a href="a">a</a>', function(rootNode) {
const go = rootNode.find({role: RoleType.LINK});
// Menus no longer nest a message loop, so we can launch menu and confirm
// expected speech. The menu will not block test shutdown.
mockFeedback.call(go.focus.bind(go))
.expectSpeech('a', 'Link')
.call(doCmd('contextMenu'))
.expectSpeech(/menu opened/);
mockFeedback.replay();
}.bind(this));
});
TEST_F('ChromeVoxBackgroundTest', 'BrailleRouting', function() {
const mockFeedback = this.createMockFeedback();
const route = function(position) {
assertTrue(ChromeVoxState.instance.onBrailleKeyEvent(
{command: BrailleKeyCommand.ROUTING, displayPosition: position},
mockFeedback.lastMatchedBraille));
};
this.runWithLoadedTree(
`
<p>start</p>
<button type="button" id="btn1">Click me</button>
<p>Some text</p>
<button type="button" id="btn2">Focus me</button>
<p>Some more text</p>
<input type="text" id ="text" value="Edit me">
<script>
document.getElementById('btn1').addEventListener('click', function() {
document.getElementById('btn2').focus();
}, false);
</script>
`,
function(rootNode) {
const button1 = rootNode.find(
{role: RoleType.BUTTON, attributes: {name: 'Click me'}});
const textField = rootNode.find({role: RoleType.TEXT_FIELD});
mockFeedback.expectBraille('start')
.call(button1.focus.bind(button1))
.expectBraille(/^Click me btn/)
.call(route.bind(null, 5))
.expectBraille(/Focus me btn/)
.call(textField.focus.bind(textField))
.expectBraille('Edit me ed', {startIndex: 0})
.call(route.bind(null, 3))
.expectBraille('Edit me ed', {startIndex: 3})
.call(function() {
assertEquals(3, textField.textSelStart);
});
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'FocusInputElement', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<input id="name" value="Lancelot">
<input id="quest" value="Grail">
<input id="color" value="Blue">
`,
function(rootNode) {
const name = rootNode.find({attributes: {value: 'Lancelot'}});
const quest = rootNode.find({attributes: {value: 'Grail'}});
const color = rootNode.find({attributes: {value: 'Blue'}});
mockFeedback.call(quest.focus.bind(quest))
.expectSpeech('Grail', 'Edit text')
.call(color.focus.bind(color))
.expectSpeech('Blue', 'Edit text')
.call(name.focus.bind(name))
.expectNextSpeechUtteranceIsNot('Blue')
.expectSpeech('Lancelot', 'Edit text');
mockFeedback.replay();
}.bind(this));
});
// Flaky, see https://crbug.com/622387.
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_UseEditableState', function() {
this.runWithLoadedTree(
`
<input type="text"></input>
<p tabindex=0>hi</p>
`,
function(rootNode) {
const assertExists = this.newCallback(function(evt) {
assertNotNullNorUndefined(
DesktopAutomationHandler.instance.textEditHandler);
evt.stopPropagation();
});
const assertDoesntExist = this.newCallback(function(evt) {
assertTrue(!DesktopAutomationHandler.instance.textEditHandler
.editableTextHandler_);
evt.stopPropagation();
// Focus the other text field here to make this test not racey.
editable.focus();
});
const editable = rootNode.find({role: RoleType.TEXT_FIELD});
const nonEditable = rootNode.find({role: RoleType.PARAGRAPH});
this.listenOnce(nonEditable, 'focus', assertDoesntExist);
this.listenOnce(editable, 'focus', assertExists);
nonEditable.focus();
}.bind(this));
});
TEST_F('ChromeVoxBackgroundTest', 'EarconsForControls', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>Initial focus will be on something that's not a control.</p>
<a href="#">MyLink</a>
<button>MyButton</button>
<input type=checkbox>
<input type=checkbox checked>
<input>
<select multiple><option>1</option></select>
<select><option>2</option></select>
<input type=range value=5>
`,
function(rootNode) {
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('MyLink')
.expectEarcon(Earcon.LINK)
.call(doCmd('nextObject'))
.expectSpeech('MyButton')
.expectEarcon(Earcon.BUTTON)
.call(doCmd('nextObject'))
.expectSpeech('Check box')
.expectEarcon(Earcon.CHECK_OFF)
.call(doCmd('nextObject'))
.expectSpeech('Check box')
.expectEarcon(Earcon.CHECK_ON)
.call(doCmd('nextObject'))
.expectSpeech('Edit text')
.expectEarcon(Earcon.EDITABLE_TEXT)
// Editable text Search re-mappings are in effect.
.call(doCmd('toggleStickyMode'))
.expectSpeech('Sticky mode enabled')
.call(doCmd('nextObject'))
.expectSpeech('List box')
.expectEarcon(Earcon.LISTBOX)
.call(doCmd('nextObject'))
.expectSpeech('Button', 'has pop up')
.expectEarcon(Earcon.POP_UP_BUTTON)
.call(doCmd('nextObject'))
.expectSpeech(/Slider/)
.expectEarcon(Earcon.SLIDER);
mockFeedback.replay();
}.bind(this));
});
SYNC_TEST_F('ChromeVoxBackgroundTest', 'GlobsToRegExp', function() {
assertEquals('/^()$/', Background.globsToRegExp_([]).toString());
assertEquals(
'/^(http:\\/\\/host\\/path\\+here)$/',
Background.globsToRegExp_(['http://host/path+here']).toString());
assertEquals(
'/^(url1.*|u.l2|.*url3)$/',
Background.globsToRegExp_(['url1*', 'u?l2', '*url3']).toString());
});
TEST_F('ChromeVoxBackgroundTest', 'ShouldNotFocusIframe', function() {
this.runWithLoadedTree(
`
<iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe>
<button>outside</button>
`,
function(root) {
const iframe = root.find({role: RoleType.IFRAME});
const button = root.find({role: RoleType.BUTTON});
assertEquals('iframe', iframe.role);
assertEquals('button', button.role);
let didFocus = false;
iframe.addEventListener('focus', function() {
didFocus = true;
});
const b = ChromeVoxState.instance;
b.currentRange_ = cursors.Range.fromNode(button);
doCmd('previousElement');
assertFalse(didFocus);
}.bind(this));
});
TEST_F('ChromeVoxBackgroundTest', 'ShouldFocusLink', function() {
this.runWithLoadedTree(
`
<div><a href="#">mylink</a></div>
<button>after</button>
`,
function(root) {
const link = root.find({role: RoleType.LINK});
const button = root.find({role: RoleType.BUTTON});
assertEquals('link', link.role);
assertEquals('button', button.role);
const didFocus = false;
link.addEventListener('focus', this.newCallback(function() {
// Success
}));
const b = ChromeVoxState.instance;
b.currentRange_ = cursors.Range.fromNode(button);
doCmd('previousElement');
});
});
TEST_F('ChromeVoxBackgroundTest', 'NoisySlider', function() {
const mockFeedback = this.createMockFeedback();
// Slider aria-valuetext must change otherwise blink suppresses event.
this.runWithLoadedTree(
`
<button id="go">go</button>
<div id="slider" tabindex=0 role="slider"></div>
<script>
function update() {
let s = document.getElementById('slider');
s.setAttribute('aria-valuetext', '');
s.setAttribute('aria-valuetext', 'noisy');
setTimeout(update, 500);
}
update();
</script>
`,
function(root) {
const go = root.find({role: RoleType.BUTTON});
const slider = root.find({role: RoleType.SLIDER});
const focusButton = go.focus.bind(go);
const focusSlider = slider.focus.bind(slider);
mockFeedback.call(focusButton)
.expectNextSpeechUtteranceIsNot('noisy')
.call(focusSlider)
.expectSpeech('noisy')
.expectSpeech('noisy')
.replay();
}.bind(this));
});
TEST_F('ChromeVoxBackgroundTest', 'Checkbox', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div id="go" role="checkbox">go</div>
<script>
let go = document.getElementById('go');
let isChecked = true;
go.addEventListener('click', function(e) {
if (isChecked) {
go.setAttribute('aria-checked', true);
} else {
go.removeAttribute('aria-checked');
}
isChecked = !isChecked;
});
</script>
`,
function(root) {
const cbx = root.find({role: RoleType.CHECK_BOX});
const click = cbx.doDefault.bind(cbx);
const focus = cbx.focus.bind(cbx);
mockFeedback.call(focus)
.expectSpeech('go')
.expectSpeech('Check box')
.expectSpeech('Not checked')
.call(click)
.expectSpeech('go')
.expectSpeech('Check box')
.expectSpeech('Checked')
.call(click)
.expectSpeech('go')
.expectSpeech('Check box')
.expectSpeech('Not checked')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'MixedCheckbox', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div id="go" role="checkbox" aria-checked="mixed">go</div>
`,
function(root) {
mockFeedback.expectSpeech('go', 'Check box', 'Partially checked')
.replay();
});
});
/** Tests navigating into and out of iframes using nextButton */
TEST_F(
'ChromeVoxBackgroundTest', 'ForwardNavigationThroughIframeButtons',
function() {
const mockFeedback = this.createMockFeedback();
let running = false;
const runTestIfIframeIsLoaded = function(rootNode) {
if (running) {
return;
}
// Return if the iframe hasn't loaded yet.
const iframe = rootNode.find({role: RoleType.IFRAME});
const childDoc = iframe.firstChild;
if (!childDoc || childDoc.children.length == 0) {
return;
}
running = true;
const beforeButton =
rootNode.find({role: RoleType.BUTTON, name: 'Before'});
beforeButton.focus();
mockFeedback.expectSpeech('Before', 'Button');
mockFeedback.call(doCmd('nextButton')).expectSpeech('Inside', 'Button');
mockFeedback.call(doCmd('nextButton')).expectSpeech('After', 'Button');
mockFeedback.call(doCmd('previousButton'))
.expectSpeech('Inside', 'Button');
mockFeedback.call(doCmd('previousButton'))
.expectSpeech('Before', 'Button');
mockFeedback.replay();
}.bind(this);
this.runWithLoadedTree(this.iframesDoc, function(rootNode) {
chrome.automation.getDesktop(function(desktopNode) {
runTestIfIframeIsLoaded(rootNode);
desktopNode.addEventListener('loadComplete', function(evt) {
runTestIfIframeIsLoaded(rootNode);
}, true);
});
});
});
/** Tests navigating into and out of iframes using nextObject */
TEST_F(
'ChromeVoxBackgroundTest', 'ForwardObjectNavigationThroughIframes',
function() {
const mockFeedback = this.createMockFeedback();
let running = false;
const runTestIfIframeIsLoaded = function(rootNode) {
if (running) {
return;
}
// Return if the iframe hasn't loaded yet.
const iframe = rootNode.find({role: 'iframe'});
const childDoc = iframe.firstChild;
if (!childDoc || childDoc.children.length == 0) {
return;
}
running = true;
const suppressFocusActionOutput = function() {
DesktopAutomationHandler.announceActions = false;
};
const beforeButton =
rootNode.find({role: RoleType.BUTTON, name: 'Before'});
mockFeedback.call(beforeButton.focus.bind(beforeButton))
.expectSpeech('Before', 'Button')
.call(suppressFocusActionOutput)
.call(doCmd('nextObject'))
.expectSpeech('Inside', 'Button');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('Inside', 'Heading 1');
mockFeedback.call(doCmd('nextObject')).expectSpeech('After', 'Button');
mockFeedback.call(doCmd('previousObject'))
.expectSpeech('Inside', 'Heading 1');
mockFeedback.call(doCmd('previousObject'))
.expectSpeech('Inside', 'Button');
mockFeedback.call(doCmd('previousObject'))
.expectSpeech('Before', 'Button');
mockFeedback.replay();
}.bind(this);
this.runWithLoadedTree(this.iframesDoc, function(rootNode) {
chrome.automation.getDesktop(function(desktopNode) {
runTestIfIframeIsLoaded(rootNode);
desktopNode.addEventListener('loadComplete', function(evt) {
runTestIfIframeIsLoaded(rootNode);
}, true);
});
});
});
TEST_F('ChromeVoxBackgroundTest', 'SelectOptionSelected', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<select>
<option>apple
<option>banana
<option>grapefruit
</select>
`,
function(root) {
const select = root.find({role: RoleType.POP_UP_BUTTON});
const clickSelect = select.doDefault.bind(select);
const lastOption = select.lastChild.lastChild;
const selectLastOption = lastOption.doDefault.bind(lastOption);
mockFeedback.call(clickSelect)
.expectSpeech('apple')
.expectSpeech('Button')
.call(selectLastOption)
.expectNextSpeechUtteranceIsNot('apple')
.expectSpeech('grapefruit')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ToggleButton', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div aria-pressed="mixed" role="button">boldface</div>
<div aria-pressed="true" role="button">ok</div>
<div aria-pressed="false" role="button">cancel</div>
<div aria-pressed role="button">close</div>
`,
function(root) {
const b = ChromeVoxState.instance;
const move = doCmd('nextObject');
mockFeedback.call(move)
.expectSpeech('boldface')
.expectSpeech('Toggle Button')
.expectSpeech('Partially pressed')
.call(move)
.expectSpeech('ok')
.expectSpeech('Toggle Button')
.expectSpeech('Pressed')
.call(move)
.expectSpeech('cancel')
.expectSpeech('Toggle Button')
.expectSpeech('Not pressed')
.call(move)
.expectSpeech('close')
.expectSpeech('Button')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'EditText', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<input type="text"></input>
<input role="combobox" type="text"></input>
`,
function(root) {
const nextEditText = doCmd('nextEditText');
const previousEditText = doCmd('previousEditText');
mockFeedback.call(nextEditText)
.expectSpeech('Combo box')
.call(previousEditText)
.expectSpeech('Edit text')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'BackwardForwardSync', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div aria-label="Group" role="group" tabindex=0>
<input type="text"></input>
</div>
<ul>
<li tabindex=0>
<button>ok</button>
</li>
</ul>
`,
function(root) {
const listItem = root.find({role: RoleType.LIST_ITEM});
mockFeedback.call(listItem.focus.bind(listItem))
.expectSpeech('List item')
.call(this.doCmd('nextObject'))
.expectSpeech('\u2022 ') // bullet
.call(this.doCmd('nextObject'))
.expectSpeech('Button')
.call(this.doCmd('previousObject'))
.expectSpeech('\u2022 ') // bullet
.call(this.doCmd('previousObject'))
.expectSpeech('List item')
.call(this.doCmd('previousObject'))
.expectSpeech('Edit text')
.call(this.doCmd('previousObject'))
.expectSpeech('Group')
.replay();
});
});
/** Tests that navigation works when the current object disappears. */
TEST_F('ChromeVoxBackgroundTest', 'DisappearingObject', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.disappearingObjectDoc, function(rootNode) {
const deleteButton =
rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Delete'}});
const pressDelete = deleteButton.doDefault.bind(deleteButton);
mockFeedback.expectSpeech('start').expectBraille('start');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('Before1')
.call(doCmd('nextObject'))
.expectSpeech('Before2')
.call(doCmd('nextObject'))
.expectSpeech('Before3')
.call(doCmd('nextObject'))
.expectSpeech('Disappearing')
.call(pressDelete)
.expectSpeech('Deleted')
.call(doCmd('nextObject'))
.expectSpeech('After1')
.call(doCmd('nextObject'))
.expectSpeech('After2')
.call(doCmd('previousObject'))
.expectSpeech('After1')
.call(doCmd('dumpTree'));
/*
// This is broken by cl/1260523 making tree updating (more)
asynchronous.
// TODO(aboxhall/dtseng): Add a function to wait for next tree update?
mockFeedback
.call(doCmd('previousObject'))
.expectSpeech('Before3');
*/
mockFeedback.replay();
});
});
/** Tests that focus jumps to details properly when indicated. */
TEST_F('ChromeVoxBackgroundTest', 'JumpToDetails', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.detailsDoc, function(rootNode) {
mockFeedback.call(doCmd('jumpToDetails')).expectSpeech('Details');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ButtonNameValueDescription', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<input type="submit" aria-label="foo" value="foo"></input>
`,
function(root) {
const btn = root.find({role: RoleType.BUTTON});
mockFeedback.call(btn.focus.bind(btn))
.expectSpeech('foo')
.expectSpeech('Button')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NameFromHeadingLink', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>before</p>
<h1><a href="google.com">go</a><p>here</p></h1>
`,
function(root) {
const link = root.find({role: RoleType.LINK});
mockFeedback.call(link.focus.bind(link))
.expectSpeech('go')
.expectSpeech('Link')
.expectSpeech('Heading 1')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'OptionChildIndexCount', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role="listbox">
<p>Fruits</p>
<div role="option">apple</div>
<div role="option">banana</div>
</div>
`,
function(root) {
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('Fruits')
.expectSpeech('with 2 items')
.expectSpeech('apple')
.expectSpeech(' 1 of 2 ')
.call(doCmd('nextObject'))
.expectSpeech('banana')
.expectSpeech(' 2 of 2 ')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ListMarkerIsIgnored', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<ul><li>apple</ul>
`,
function(root) {
mockFeedback.call(doCmd('nextObject'))
.expectNextSpeechUtteranceIsNot('listMarker')
.expectSpeech('apple')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'SymetricComplexHeading', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<h4><p>NW</p><p>NE</p></h4>
<h4><p>SW</p><p>SE</p></h4>
`,
function(root) {
mockFeedback.call(doCmd('nextHeading'))
.expectNextSpeechUtteranceIsNot('NE')
.expectSpeech('NW')
.call(doCmd('previousHeading'))
.expectNextSpeechUtteranceIsNot('NE')
.expectSpeech('NW')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ContentEditableJumpSyncsRange', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<div contenteditable>
<h1>Top News</h1>
<h1>Most Popular</h1>
<h1>Sports</h1>
</div>
`,
function(root) {
const assertRangeHasText = function(text) {
return function() {
assertEquals(
text,
ChromeVoxState.instance.getCurrentRange().start.node.name);
};
};
mockFeedback.call(doCmd('nextEditText'))
.expectSpeech('Top News Most Popular Sports')
.call(doCmd('nextHeading'))
.expectSpeech('Top News')
.call(assertRangeHasText('Top News'))
.call(doCmd('nextHeading'))
.expectSpeech('Most Popular')
.call(assertRangeHasText('Most Popular'))
.call(doCmd('nextHeading'))
.expectSpeech('Sports')
.call(assertRangeHasText('Sports'))
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'Selection', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>simple</p>
<p>doc</p>
`,
function(root) {
// Fakes a toggleSelection command.
root.addEventListener('textSelectionChanged', function() {
if (root.focusOffset == 3) {
CommandHandler.onCommand('toggleSelection');
}
}, true);
mockFeedback.call(doCmd('toggleSelection'))
.expectSpeech('simple', 'selected')
.call(doCmd('nextCharacter'))
.expectSpeech('i', 'selected')
.call(doCmd('previousCharacter'))
.expectSpeech('i', 'unselected')
.call(doCmd('nextCharacter'))
.call(doCmd('nextCharacter'))
.expectSpeech('End selection', 'sim')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'BasicTableCommands', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<table border=1>
<tr><td>name</td><td>title</td><td>address</td><td>phone</td></tr>
<tr><td>Dan</td><td>Mr</td><td>666 Elm Street</td><td>212 222 5555</td></tr>
</table>
`,
function(root) {
mockFeedback.call(doCmd('nextRow'))
.expectSpeech('Dan', 'row 2 column 1')
.call(doCmd('previousRow'))
.expectSpeech('name', 'row 1 column 1')
.call(doCmd('previousRow'))
.expectSpeech('No cell above')
.call(doCmd('nextCol'))
.expectSpeech('title', 'row 1 column 2')
.call(doCmd('nextRow'))
.expectSpeech('Mr', 'row 2 column 2')
.call(doCmd('previousRow'))
.expectSpeech('title', 'row 1 column 2')
.call(doCmd('nextCol'))
.expectSpeech('address', 'row 1 column 3')
.call(doCmd('nextCol'))
.expectSpeech('phone', 'row 1 column 4')
.call(doCmd('nextCol'))
.expectSpeech('No cell right')
.call(doCmd('previousRow'))
.expectSpeech('No cell above')
.call(doCmd('nextRow'))
.expectSpeech('212 222 5555', 'row 2 column 4')
.call(doCmd('nextRow'))
.expectSpeech('No cell below')
.call(doCmd('nextCol'))
.expectSpeech('No cell right')
.call(doCmd('previousCol'))
.expectSpeech('666 Elm Street', 'row 2 column 3')
.call(doCmd('previousCol'))
.expectSpeech('Mr', 'row 2 column 2')
.call(doCmd('goToRowLastCell'))
.expectSpeech('212 222 5555', 'row 2 column 4')
.call(doCmd('goToRowLastCell'))
.expectSpeech('212 222 5555')
.call(doCmd('goToRowFirstCell'))
.expectSpeech('Dan', 'row 2 column 1')
.call(doCmd('goToRowFirstCell'))
.expectSpeech('Dan')
.call(doCmd('goToColFirstCell'))
.expectSpeech('name', 'row 1 column 1')
.call(doCmd('goToColFirstCell'))
.expectSpeech('name')
.call(doCmd('goToColLastCell'))
.expectSpeech('Dan', 'row 2 column 1')
.call(doCmd('goToColLastCell'))
.expectSpeech('Dan')
.call(doCmd('goToLastCell'))
.expectSpeech('212 222 5555', 'row 2 column 4')
.call(doCmd('goToLastCell'))
.expectSpeech('212 222 5555')
.call(doCmd('goToFirstCell'))
.expectSpeech('name', 'row 1 column 1')
.call(doCmd('goToFirstCell'))
.expectSpeech('name')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'MissingTableCells', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<table border=1>
<tr><td>a</td><td>b</td><td>c</td></tr>
<tr><td>d</td><td>e</td></tr>
<tr><td>f</td></tr>
</table>
`,
function(root) {
mockFeedback.call(doCmd('goToRowLastCell'))
.expectSpeech('c', 'row 1 column 3')
.call(doCmd('goToRowLastCell'))
.expectSpeech('c')
.call(doCmd('goToRowFirstCell'))
.expectSpeech('a', 'row 1 column 1')
.call(doCmd('goToRowFirstCell'))
.expectSpeech('a')
.call(doCmd('nextCol'))
.expectSpeech('b', 'row 1 column 2')
.call(doCmd('goToColLastCell'))
.expectSpeech('e', 'row 2 column 2')
.call(doCmd('goToColLastCell'))
.expectSpeech('e')
.call(doCmd('goToColFirstCell'))
.expectSpeech('b', 'row 1 column 2')
.call(doCmd('goToColFirstCell'))
.expectSpeech('b')
.call(doCmd('goToFirstCell'))
.expectSpeech('a', 'row 1 column 1')
.call(doCmd('goToFirstCell'))
.expectSpeech('a')
.call(doCmd('goToLastCell'))
.expectSpeech('f', 'row 3 column 1')
.call(doCmd('goToLastCell'))
.expectSpeech('f')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'DisabledState', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<button aria-disabled="true">ok</button>
`,
function(root) {
mockFeedback.expectSpeech('ok', 'Disabled', 'Button').replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'HeadingLevels', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<h1>1</h1><h2>2</h2><h3>3</h3><h4>4</h4><h5>5</h5><h6>6</h6>
`,
function(root) {
const makeLevelAssertions = function(level) {
mockFeedback.call(doCmd('nextHeading' + level))
.expectSpeech('Heading ' + level)
.call(doCmd('nextHeading' + level))
.expectEarcon('wrap')
.call(doCmd('previousHeading' + level))
.expectEarcon('wrap');
};
for (let i = 1; i <= 6; i++) {
makeLevelAssertions(i);
}
mockFeedback.replay();
});
});
// Flaky, see https://crbug.com/622387.
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_EditableNavigation', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div contenteditable>this is a test</div>
`,
function(root) {
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('this is a test')
.call(doCmd('nextObject'))
.expectSpeech(/data*/)
.call(doCmd('nextObject'))
.expectSpeech('this is a test')
.call(doCmd('nextWord'))
.expectSpeech('is', 'selected')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigationMovesFocus', function() {
this.runWithLoadedTree(
`
<p>start</p>
<input type="text"></input>
`,
function(root) {
this.listenOnce(
root.find({role: RoleType.TEXT_FIELD}), 'focus', function(e) {
const focus = ChromeVoxState.instance.currentRange.start.node;
assertEquals(RoleType.TEXT_FIELD, focus.role);
assertTrue(focus.state.focused);
});
doCmd('nextEditText')();
});
});
TEST_F('ChromeVoxBackgroundTest', 'BrailleCaretNavigation', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>This is a<em>test</em> of inline braille<br>with a second line</p>
`,
function(root) {
const text = 'This is a';
mockFeedback.call(doCmd('nextCharacter'))
.expectBraille(text, {startIndex: 1, endIndex: 2}) // h
.call(doCmd('nextCharacter'))
.expectBraille(text, {startIndex: 2, endIndex: 3}) // i
.call(doCmd('nextWord'))
.expectBraille(text, {startIndex: 5, endIndex: 7}) // is
.call(doCmd('previousWord'))
.expectBraille(text, {startIndex: 0, endIndex: 4}) // This
.call(doCmd('nextLine'))
// Ensure nothing is selected when the range covers the entire line.
.expectBraille('with a second line', {startIndex: -1, endIndex: -1})
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'InPageLinks', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<a href="#there">hi</a>
<button id="there">there</button>
`,
function(root) {
mockFeedback.expectSpeech('hi', 'Internal link')
.call(doCmd('forceClickOnCurrentItem'))
.expectSpeech('there', 'Button')
.replay();
});
});
// Timeout on ChromeOS with LayoutNG & Viz.
// https://crbug.com/959261
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_ListItem', function() {
this.resetContextualOutput();
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<ul><li>apple<li>grape<li>banana</ul>
<ol><li>pork<li>beef<li>chicken</ol>
`,
function(root) {
mockFeedback.call(doCmd('nextLine'))
.expectSpeech('\u2022 ', 'apple', 'List item')
.expectBraille('\u2022 apple lstitm lst +3')
.call(doCmd('nextLine'))
.expectSpeech('\u2022 ', 'grape', 'List item')
.expectBraille('\u2022 grape lstitm')
.call(doCmd('nextLine'))
.expectSpeech('\u2022 ', 'banana', 'List item')
.expectBraille('\u2022 banana lstitm')
.call(doCmd('nextLine'))
.expectSpeech('1. ', 'pork', 'List item')
.expectBraille('1. pork lstitm lst +3')
.call(doCmd('nextLine'))
.expectSpeech('2. ', 'beef', 'List item')
.expectBraille('2. beef lstitm')
.call(doCmd('nextLine'))
.expectSpeech('3. ', 'chicken', 'List item')
.expectBraille('3. chicken lstitm')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'BusyHeading', function() {
this.resetContextualOutput();
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<h2><a href="#">Lots</a><a href="#">going</a><a href="#">here</a></h2>
`,
function(root) {
// In the past, this would have inserted the 'heading 2' after the first
// link's output. Make sure it goes to the end.
mockFeedback.call(doCmd('nextLine'))
.expectSpeech(
'Lots', 'Link', 'going', 'Link', 'here', 'Link', 'Heading 2')
.expectBraille('Lots lnk going lnk here lnk h2')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NodeVsSubnode', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<a href="#">test</a>
`,
function(root) {
const link = root.find({role: RoleType.LINK});
function outputLinkRange(start, end) {
return function() {
new Output()
.withSpeech(new cursors.Range(
new cursors.Cursor(link, start),
new cursors.Cursor(link, end)))
.go();
};
}
mockFeedback.call(outputLinkRange(0, 0))
.expectSpeech('test', 'Link')
.call(outputLinkRange(0, 1))
.expectSpeech('t')
.call(outputLinkRange(1, 1))
.expectSpeech('test', 'Link')
.call(outputLinkRange(1, 2))
.expectSpeech('e')
.call(outputLinkRange(1, 3))
.expectNextSpeechUtteranceIsNot('Link')
.expectSpeech('es')
.call(outputLinkRange(0, 4))
.expectSpeech('test', 'Link')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NativeFind', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<a href="#">grape</a>
<a href="#">pineapple</a>
`,
function(root) {
mockFeedback.call(press(70, {ctrl: true}))
.expectSpeech('Find', 'Edit text')
.call(press(71))
.expectSpeech('grape', 'Link')
.call(press(8))
.call(press(76))
.expectSpeech('pineapple', 'Link')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'EditableKeyCommand', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<input type="text"></input>
<textarea>test</textarea>
<div role="textbox" contenteditable>test</div>
`,
function(root) {
const assertCurNode = function(node) {
return function() {
assertEquals(node, ChromeVoxState.instance.currentRange.start.node);
};
};
const textField = root.firstChild;
const textArea = textField.nextSibling;
const contentEditable = textArea.nextSibling;
mockFeedback.call(assertCurNode(textField))
.call(doCmd('nextObject'))
.call(assertCurNode(textArea))
.call(doCmd('nextObject'))
.call(assertCurNode(contentEditable))
.call(doCmd('previousObject'))
.expectSpeech('Text area')
.call(assertCurNode(textArea))
.call(doCmd('previousObject'))
.call(assertCurNode(textField))
.replay();
});
});
// TODO(crbug.com/935678): Test times out flakily in MSAN builds.
TEST_F_WITH_PREAMBLE(
`
#if defined(MEMORY_SANITIZER)
#define MAYBE_TextSelectionAndLiveRegion DISABLED_TextSelectionAndLiveRegion
#else
#define MAYBE_TextSelectionAndLiveRegion TextSelectionAndLiveRegion
#endif
`,
'ChromeVoxBackgroundTest', 'MAYBE_TextSelectionAndLiveRegion', function() {
DesktopAutomationHandler.announceActions = true;
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<div><input value="test" type="text"></input></div>
<div id="live" aria-live="assertive"></div>
<script>
const input = document.querySelector('input');
const [div, live] = document.querySelectorAll('div');
let clicks = 0;
div.addEventListener('click', function() {
clicks++;
if (clicks == 1) {
live.textContent = 'go';
} else if (clicks == 2) {
input.selectionStart = 1;
live.textContent = 'queued';
} else {
input.selectionStart = 2;
live.textContent = 'interrupted';
}
});
</script>
`,
function(root) {
const textField = root.find({role: RoleType.TEXT_FIELD});
const div = textField.parent;
mockFeedback.call(textField.focus.bind(textField))
.expectSpeech('Edit text')
.call(div.doDefault.bind(div))
.expectSpeechWithQueueMode('go', QueueMode.CATEGORY_FLUSH)
.call(div.doDefault.bind(div))
.expectSpeechWithQueueMode('e', QueueMode.FLUSH)
.expectSpeechWithQueueMode('queued', QueueMode.QUEUE)
.call(div.doDefault.bind(div))
.expectSpeechWithQueueMode('s', QueueMode.FLUSH)
.expectSpeechWithQueueMode('interrupted', QueueMode.QUEUE)
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'TableColumnHeaders', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role="grid">
<div role="rowgroup">
<div role="row">
<div role="columnheader">city</div>
<div role="columnheader">state</div>
<div role="columnheader">zip</div>
</div>
</div>
<div role="rowgroup">
<div role="row">
<div role="gridcell">Mountain View</div>
<div role="gridcell">CA</div>
<div role="gridcell">94043</div>
</div>
<div role="row">
<div role="gridcell">San Jose</div>
<div role="gridcell">CA</div>
<div role="gridcell">95128</div>
</div>
</div>
</div>
`,
function(root) {
mockFeedback.call(doCmd('nextRow'))
.expectSpeech('Mountain View', 'row 2 column 1')
.call(doCmd('nextRow'))
.expectNextSpeechUtteranceIsNot('city')
.expectSpeech('San Jose', 'row 3 column 1')
.call(doCmd('nextCol'))
.expectSpeech('CA', 'row 3 column 2', 'state')
.call(doCmd('previousRow'))
.expectSpeech('CA', 'row 2 column 2')
.call(doCmd('previousRow'))
.expectSpeech('state', 'row 1 column 2')
.replay();
});
});
// Flaky, see https://crbug.com/622387.
TEST_F(
'ChromeVoxBackgroundTest', 'DISABLED_ActiveDescendantUpdates', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div aria-label="container" tabindex=0 role="group" id="active"
aria-activedescendant="1">
<div id="1" role="treeitem"></div>
<div id="2" role="treeitem"></div>
<script>
let alt = false;
let active = document.getElementById('active');
let one = document.getElementById('1');
let two = document.getElementById('2');
active.addEventListener('click', function() {
let sel = alt ? one : two;
let unsel = alt ? two : one;
active.setAttribute('aria-activedescendant', sel.id);
sel.setAttribute('aria-selected', true);
unsel.setAttribute('aria-selected', false);
alt = !alt;
});
</script>
`,
function(root) {
const group = root.firstChild;
mockFeedback.call(group.focus.bind(group))
.call(group.doDefault.bind(group))
.expectSpeech('Tree item', 'Selected', ' 2 of 2 ')
.call(group.doDefault.bind(group))
.expectSpeech('Tree item', 'Selected', ' 1 of 2 ')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>before content editable</p>
<div role="textbox" contenteditable>this<br>is<br>a<br>test</div>
<p>after content editable, before text area</p>
<textarea style="word-spacing: 1000px">this is a test</textarea>
<p>after text area</p>
`,
function(root) {
const assertBeginning = function(expected) {
const textEditHandler =
DesktopAutomationHandler.instance.textEditHandler;
assertNotNullNorUndefined(textEditHandler);
assertEquals(expected, textEditHandler.isSelectionOnFirstLine());
};
const assertEnd = function(expected) {
const textEditHandler =
DesktopAutomationHandler.instance.textEditHandler;
assertNotNullNorUndefined(textEditHandler);
assertEquals(expected, textEditHandler.isSelectionOnLastLine());
};
const [contentEditable, textArea] =
root.findAll({role: RoleType.TEXT_FIELD});
this.listenOnce(contentEditable, EventType.FOCUS, function() {
mockFeedback.call(assertBeginning.bind(this, true))
.call(assertEnd.bind(this, false))
.call(press(40 /* ArrowDown */))
.expectSpeech('is')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, false))
.call(press(40 /* ArrowDown */))
.expectSpeech('a')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, false))
.call(press(40 /* ArrowDown */))
.expectSpeech('test')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, true))
.call(textArea.focus.bind(textArea))
.expectSpeech('Text area')
.call(assertBeginning.bind(this, true))
.call(assertEnd.bind(this, true))
.replay();
// TODO: soft line breaks currently won't work in <textarea>.
}.bind(this));
contentEditable.focus();
});
});
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_NavigationSyncsSelect', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<select>
<option>apple</option>
<option>grape</option>
</select>
`,
function(root) {
const select = root.find({role: RoleType.POP_UP_BUTTON});
mockFeedback.call(select.doDefault.bind(select))
.expectSpeech('apple', 'Menu item', ' 1 of 2 ')
.call(doCmd('nextObject'))
.expectNextSpeechUtteranceIsNot('Selected')
.expectNextSpeechUtteranceIsNot('Unselected')
.expectSpeech('grape', 'Menu item')
.expectNextSpeechUtteranceIsNot('Selected')
.expectNextSpeechUtteranceIsNot('Unselected')
.expectSpeech(' 2 of 2 ')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresLabels', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>before</p>
<p id="label">label</p>
<a href="#next" id="lebal">lebal</a>
<p>after</p>
<button aria-labelledby="label"></button>
`,
function(root) {
mockFeedback.expectSpeech('before')
.call(doCmd('nextObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('nextObject'))
.expectSpeech('after')
.call(doCmd('previousObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('previousObject'))
.expectSpeech('before')
.call(doCmd('nextObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('nextObject'))
.expectSpeech('after')
.call(doCmd('nextObject'))
.expectSpeech('label', 'lebal', 'Button')
.call(doCmd('nextObject'))
.expectEarcon(Earcon.WRAP)
.call(doCmd('nextObject'))
.expectSpeech('before')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresDescriptions', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>before</p>
<p id="desc">label</p>
<a href="#next" id="csed">lebal</a>
<p>after</p>
<button aria-describedby="desc"></button>
`,
function(root) {
mockFeedback.expectSpeech('before')
.call(doCmd('nextObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('nextObject'))
.expectSpeech('after')
.call(doCmd('previousObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('previousObject'))
.expectSpeech('before')
.call(doCmd('nextObject'))
.expectSpeech('lebal', 'Link')
.call(doCmd('nextObject'))
.expectSpeech('after')
.call(doCmd('nextObject'))
.expectSpeech('label', 'lebal', 'Button')
.call(doCmd('nextObject'))
.expectEarcon(Earcon.WRAP)
.call(doCmd('nextObject'))
.expectSpeech('before')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'MathContentViaInnerHtml', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role="math">
<semantics>
<mrow class="MJX-TeXAtom-ORD">
<mstyle displaystyle="true" scriptlevel="0">
<mi>a</mi>
<mo stretchy="false">(</mo>
<mi>y</mi>
<mo>+</mo>
<mi>m</mi>
<msup>
<mo stretchy="false">)</mo>
<mrow class="MJX-TeXAtom-ORD">
<mn>2</mn>
</mrow>
</msup>
<mo>+</mo>
<mi>b</mi>
<mo stretchy="false">(</mo>
<mi>y</mi>
<mo>+</mo>
<mi>m</mi>
<mo stretchy="false">)</mo>
<mo>+</mo>
<mi>c</mi>
<mo>=</mo>
<mn>0.</mn>
</mstyle>
</mrow>
<annotation encoding="application/x-tex">{\displaystyle a(y+m)^{2}+b(y+m)+c=0.}</annotation>
</semantics>
</div>
`,
function(root) {
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('a ( y + m ) squared + b ( y + m ) + c = 0 .')
.expectSpeech('Press up, down, left, or right to explore math')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'GestureGranularity', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>This is a test</p>
<h2>hello</h2>
<a href="#">greetings</a>
<h2>here</h2>
<button>and</button>
<a href="#">there</a>
<button>world</button>
`,
function(root) {
const doGesture = (gesture) => {
return () => {
GestureCommandHandler.onAccessibilityGesture_(gesture);
};
};
mockFeedback.call(doGesture('swipeLeft3'))
.expectSpeech('Word')
.call(doGesture('swipeDown1'))
.expectSpeech('is')
.call(doGesture('swipeDown1'))
.expectSpeech('a')
.call(doGesture('swipeUp1'))
.expectSpeech('is')
.call(doGesture('swipeLeft3'))
.expectSpeech('Character')
.call(doGesture('swipeDown1'))
.expectSpeech('s')
.call(doGesture('swipeUp1'))
.expectSpeech('i')
.call(doGesture('swipeLeft3'))
.expectSpeech('Form field control')
.call(doGesture('swipeDown1'))
.expectSpeech('and', 'Button')
.call(doGesture('swipeUp1'))
.expectSpeech('world', 'Button')
.call(doGesture('swipeLeft3'))
.expectSpeech('Link')
.call(doGesture('swipeDown1'))
.expectSpeech('greetings', 'Link')
.call(doGesture('swipeUp1'))
.expectSpeech('there', 'Link')
.call(doGesture('swipeLeft3'))
.expectSpeech('Heading')
.call(doGesture('swipeDown1'))
.expectSpeech('hello', 'Heading 2')
.call(doGesture('swipeUp1'))
.expectSpeech('here', 'Heading 2')
.call(doGesture('swipeUp1'))
.expectSpeech('hello', 'Heading 2')
.call(doGesture('swipeLeft3'))
.expectSpeech('Line')
.call(doGesture('swipeUp1'))
.expectSpeech('This is a test')
.call(doGesture('swipeRight3'))
.expectSpeech('Heading')
.call(doGesture('swipeRight3'))
.expectSpeech('Link')
.call(doGesture('swipeRight3'))
.expectSpeech('Form field control')
.call(doGesture('swipeRight3'))
.expectSpeech('Character')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'LinesFilterWhitespace', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<div role="list">
<div role="listitem">
<span>Munich</span>
<span>London</span>
</div>
</div>
`,
function(root) {
mockFeedback.expectSpeech('start')
.clearPendingOutput()
.call(doCmd('nextLine'))
.expectSpeech('Munich')
.expectNextSpeechUtteranceIsNot(' ')
.expectSpeech('London')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'TabSwitchAndRefreshRecovery', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>tab1</p>
`,
function(root1) {
this.runWithLoadedTree(
`
<p>tab2</p>
`,
function(root2) {
mockFeedback.expectSpeech('tab2')
.clearPendingOutput()
.call(press(9 /* tab */, {shift: true, ctrl: true}))
.expectSpeech('tab1')
.clearPendingOutput()
.call(press(9 /* tab */, {ctrl: true}))
.expectSpeech('tab2')
.clearPendingOutput()
.call(press(82 /* R */, {ctrl: true}))
// ChromeVox stays on the same node due to tree path recovery.
.call(() => {
assertEquals(
'tab2',
ChromeVoxState.instance.currentRange.start.node.name);
})
.replay();
});
});
});
TEST_F('ChromeVoxBackgroundTest', 'ListName', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div id="_md-chips-wrapper-76" tabindex="-1" class="md-chips md-readonly"
aria-setsize="4" aria-label="Favorite Sports" role="list"
aria-describedby="chipsNote">
<div role="listitem">Baseball</div>
<div role="listitem">Hockey</div>
<div role="listitem">Lacrosse</div>
<div role="listitem">Football</div>
</div>
`,
function(root) {
mockFeedback.expectSpeech('Favorite Sports').replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'LayoutTable', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<table><tr><td>start</td></tr></table><p>end</p>
`,
function(root) {
mockFeedback.expectSpeech('start')
.call(doCmd('nextObject'))
.expectNextSpeechUtteranceIsNot('row 1 column 1')
.expectNextSpeechUtteranceIsNot('Table , 1 by 1')
.expectSpeech('end')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ReinsertedNodeRecovery', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div>
<button id="start">start</button>
<button id="hot">hot</button>
</div>
<button id="end">end</button>
<script>
let div = document.body.firstElementChild;
let start = document.getElementById('start');
document.getElementById('hot').addEventListener('focus', (evt) => {
let hot = evt.target;
hot.remove();
div.insertAfter(hot, start);
});
</script>
`,
function(root) {
mockFeedback.expectSpeech('start')
.clearPendingOutput()
.call(doCmd('nextObject'))
.call(doCmd('nextObject'))
.call(doCmd('nextObject'))
.expectSpeech('end', 'Button')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'HoverTargetsLeafNode', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role=button><p>Washington</p></div>
<div role=button><p>Adams</p></div>
<div role=button><p>Jefferson</p></div>
`,
function(root) {
const button =
root.find({role: RoleType.BUTTON, attributes: {name: 'Jefferson'}});
const buttonP = button.firstChild;
assertNotNullNorUndefined(buttonP);
const buttonText = buttonP.firstChild;
assertNotNullNorUndefined(buttonText);
const doHover = () => {
const event =
new CustomAutomationEvent(EventType.HOVER, buttonText, 'action');
DesktopAutomationHandler.instance.onHover(event);
};
mockFeedback.call(doHover)
.expectSpeech('Jefferson')
.expectSpeech('Button')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueNow', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div id="slider" role="slider" tabindex="0" aria-valuemin="0"
aria-valuenow="50" aria-valuemax="100"></div>
<script>
let slider = document.getElementById('slider');
slider.addEventListener('click', () => {
slider.setAttribute('aria-valuenow',
parseInt(slider.getAttribute('aria-valuenow'), 10) + 1);
});
</script>
`,
function(root) {
const slider = root.find({role: RoleType.SLIDER});
assertNotNullNorUndefined(slider);
mockFeedback.call(slider.doDefault.bind(slider))
.expectSpeech('51')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueText', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div id="slider" role="slider" tabindex="0" aria-valuemin="0"
aria-valuenow="50" aria-valuemax="100" aria-valuetext="tiny"></div>
<script>
let slider = document.getElementById('slider');
slider.addEventListener('click', () => {
slider.setAttribute('aria-valuenow',
parseInt(slider.getAttribute('aria-valuenow'), 10) + 1);
slider.setAttribute('aria-valuetext', 'large');
});
</script>
`,
function(root) {
const slider = root.find({role: RoleType.SLIDER});
assertNotNullNorUndefined(slider);
mockFeedback.clearPendingOutput()
.call(slider.doDefault.bind(slider))
.expectNextSpeechUtteranceIsNot('51')
.expectSpeech('large')
.replay();
});
});
// See https://crbug.com/924976
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_ValidationTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<label for="in1">Name:</label>
<input id="in1" required autofocus>
<script>
const in1 = document.querySelector('input');
in1.addEventListener('focus', () => {
setTimeout(() => {
in1.setCustomValidity('Please enter name');
in1.reportValidity();
}, 500);
});
</script>
`,
function(root) {
mockFeedback.expectSpeech('Name:')
.expectSpeech('Edit text')
.expectSpeech('Required')
.expectSpeech('Alert')
.expectSpeech('Please enter name')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'EventFromAction', function() {
this.runWithLoadedTree(
`
<button>ok</button><button>cancel</button>
`,
function(root) {
const button = root.findAll({role: RoleType.BUTTON})[1];
button.addEventListener(
EventType.FOCUS, this.newCallback(function(evt) {
assertEquals(RoleType.BUTTON, evt.target.role);
assertEquals('action', evt.eventFrom);
assertEquals('cancel', evt.target.name);
}));
button.focus();
});
});
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_EventFromUser', function() {
this.runWithLoadedTree(
`
<button>ok</button><button>cancel</button>
`,
function(root) {
const button = root.findAll({role: RoleType.BUTTON})[1];
button.addEventListener(
EventType.FOCUS, this.newCallback(function(evt) {
assertEquals(RoleType.BUTTON, evt.target.role);
assertEquals('user', evt.eventFrom);
assertEquals('cancel', evt.target.name);
}));
press(9 /* tab */)();
});
});
// See https://crbug.com/997688
TEST_F('ChromeVoxBackgroundTest', 'DISABLED_PopUpButtonSetSize', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div>
<select id="button">
<option value="Apple">Apple</option>
<option value="Banana">Banana</option>
</select>
</div>
<script>
let button = document.getElementById('button');
let expanded = false;
button.addEventListener('click', function(e) {
if (expanded) {
button.setAttribute('aria-expanded', false);
} else {
button.setAttribute('aria-expanded', true);
}
expanded = !expanded;
});
</script>
`,
function(root) {
const button = root.find({role: RoleType.POP_UP_BUTTON});
const click = button.doDefault.bind(button);
const focus = button.focus.bind(button);
mockFeedback.call(focus)
.expectSpeech('Apple')
.expectSpeech('Button')
.expectSpeech('has pop up')
.expectSpeech('Press Search+Space to activate')
.call(click)
.expectSpeech('Apple')
.expectSpeech('Button')
.expectSpeech('has pop up')
// SetSize is only reported if popup button is expanded.
.expectSpeech('with 2 items')
.expectSpeech('Expanded')
.expectSpeech('Press Search+Space to activate')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ReadPhoneticPronunciationTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<button>This is a button</button>
<input type="text"></input>
`,
function(root) {
root.find({role: RoleType.BUTTON}).focus();
mockFeedback.call(doCmd('readPhoneticPronunciation'))
.expectSpeech('T')
.expectSpeech('tango')
.expectSpeech('h')
.expectSpeech('hotel')
.expectSpeech('i')
.expectSpeech('india')
.expectSpeech('s')
.expectSpeech('sierra')
.call(doCmd('nextWord'))
.call(doCmd('readPhoneticPronunciation'))
.expectSpeech('i')
.expectSpeech('india')
.expectSpeech('s')
.expectSpeech('sierra')
.call(doCmd('nextWord'))
.call(doCmd('nextWord'))
.call(doCmd('readPhoneticPronunciation'))
.expectSpeech('b')
.expectSpeech('bravo')
.expectSpeech('u')
.expectSpeech('uniform')
.expectSpeech('t')
.expectSpeech('tango')
.expectSpeech('t')
.expectSpeech('tango')
.expectSpeech('o')
.expectSpeech('oscar')
.expectSpeech('n')
.expectSpeech('november')
.call(doCmd('nextEditText'))
.call(doCmd('readPhoneticPronunciation'))
.expectSpeech('No available text for this item');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'SimilarItemNavigation', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<h3><a href="#a">inner</a></h3>
<p>some text</p>
<button>some other text</button>
<a href="#b">outer1</a>
<h3>outer2</h3>
`,
function(root) {
assertEquals(
RoleType.LINK,
ChromeVoxState.instance.currentRange.start.node.role);
assertEquals(
'inner', ChromeVoxState.instance.currentRange.start.node.name);
mockFeedback.call(doCmd('nextSimilarItem'))
.expectSpeech('outer1', 'Link')
.call(doCmd('nextSimilarItem'))
.expectSpeech('inner', 'Link')
.call(doCmd('nextSimilarItem'))
.call(doCmd('previousSimilarItem'))
.expectSpeech('inner', 'Link')
.call(doCmd('nextHeading'))
.expectSpeech('outer2', 'Heading 3')
.call(doCmd('previousSimilarItem'))
.expectSpeech('inner', 'Heading 3');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'TableWithAriaRowCol', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role="table">
<div role="row" aria-rowindex=3>
<div role="cell">test</div>
</div>
</div>
`,
function(root) {
mockFeedback.call(doCmd('fullyDescribe'))
.expectSpeech('test', 'row 3 column 1', 'Table , 1 by 1')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NonModalDialogHeadingJump', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<h2>Heading outside dialog</h2>
<div role="dialog">
<h2>Heading inside dialog</h2>
</div>
`,
function(root) {
mockFeedback.call(doCmd('nextHeading'))
.expectSpeech('Heading inside dialog')
.call(doCmd('previousHeading'))
.expectSpeech('Heading outside dialog')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigationByListTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>Start here</p>
<ul>
Drinks
<li>Coffee</li>
<li>Tea</li>
</ul>
<p>A random paragraph</p>
<ul></ul>
<ol>
Lunch
<li>Burgers</li>
<li>Fries</li>
<li>Soda</li>
<ul>
Nested list
<li>Element</li>
</ul>
</ol>
<p>Another random paragraph</p>
<dl>
Colors
<dt>Red</dt>
<dd>Description for red</dd>
<dt>Blue</dt>
<dd>Description for blue</dd>
<dt>Green</dt>
<dd>Description for green</dd>
</dl>
`,
function(root) {
mockFeedback.call(doCmd('jumpToTop'))
.call(doCmd('nextList'))
.expectSpeech('Drinks', 'List', 'with 2 items')
.call(doCmd('nextList'))
.expectSpeech('List', 'with 0 items')
.call(doCmd('nextList'))
.expectSpeech('Lunch', 'List', 'with 3 items')
.call(doCmd('nextList'))
.expectSpeech('Nested list', 'List', 'with 1 item')
.call(doCmd('nextList'))
.expectSpeech('Colors', 'Description list', 'with 3 items')
.call(doCmd('nextList'))
// Ensure we wrap correctly.
.expectSpeech('Drinks', 'List', 'with 2 items')
.call(doCmd('nextObject'))
.call(doCmd('nextObject'))
.expectSpeech('Coffee')
// Ensure we wrap correctly and go to previous list, not top of
// current list.
.call(doCmd('previousList'))
.expectSpeech('Colors')
.call(doCmd('previousObject'))
.expectSpeech('Another random paragraph')
// Ensure we dive into the nested list.
.call(doCmd('previousList'))
.expectSpeech('Nested list', 'List', 'with 1 item')
.call(doCmd('previousList'))
.expectSpeech('Lunch')
.call(doCmd('nextObject'))
.call(doCmd('nextObject'))
.expectSpeech('Burgers')
// Ensure we go to the previous list, not the top of the current
// list.
.call(doCmd('previousList'))
.expectSpeech('List', 'with 0 items')
.call(doCmd('previousObject'))
.expectSpeech('A random paragraph')
.call(doCmd('previousList'))
.expectSpeech('Drinks', 'List', 'with 2 items')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NoListTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<button>Click me</button>
`,
function(root) {
mockFeedback.call(doCmd('nextList'))
.expectSpeech('No next list')
.call(doCmd('previousList'))
.expectSpeech('No previous list');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigateToLastHeading', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<h1>First</h1>
<h1>Second</h1>
<h1>Third</h1>
`,
function(root) {
mockFeedback.call(doCmd('jumpToTop'))
.expectSpeech('First', 'Heading 1')
.call(doCmd('previousHeading'))
.expectSpeech('Third', 'Heading 1')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ReadLinkURLTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<a href="https://www.google.com/">A popular link</a>
<button>Not a link</button>
`,
function(root) {
mockFeedback.call(doCmd('nextLink'))
.expectSpeech(
'A popular link', 'Link', 'Press Search+Space to activate')
.call(doCmd('readLinkURL'))
.expectSpeech('Link URL: https://www.google.com/')
.call(doCmd('nextObject'))
.expectSpeech(
'Not a link', 'Button', 'Press Search+Space to activate')
.call(doCmd('readLinkURL'))
.expectSpeech('No URL found')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NoRepeatTitle', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<div role="button" aria-label="title" title="title"></div>
`,
function(root) {
mockFeedback.expectSpeech('title')
.expectSpeech('Button')
.expectNextSpeechUtteranceIsNot('title')
.expectSpeech('Press Search+Space to activate')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'PhoneticsAndCommands', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>some sample text</p>
<button>ok</button>
<p>A</p>
`,
function(root) {
const noPhonetics = {phoneticCharacters: undefined};
const phonetics = {phoneticCharacters: true};
mockFeedback.call(doCmd('nextObject'))
.expectSpeechWithProperties(noPhonetics, 'ok')
.call(doCmd('previousObject'))
.expectSpeechWithProperties(noPhonetics, 'some sample text')
.call(doCmd('nextWord'))
.expectSpeechWithProperties(noPhonetics, 'sample')
.call(doCmd('previousWord'))
.expectSpeechWithProperties(noPhonetics, 'some')
.call(doCmd('nextCharacter'))
.expectSpeechWithProperties(phonetics, 'o')
.call(doCmd('nextCharacter'))
.expectSpeechWithProperties(phonetics, 'm')
.call(doCmd('previousCharacter'))
.expectSpeechWithProperties(phonetics, 'o')
.call(doCmd('jumpToBottom'))
.expectSpeechWithProperties(noPhonetics, 'A');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ToggleDarkScreen', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<div>Unimportant web content</div>', function() {
mockFeedback.call(doCmd('toggleDarkScreen'))
.expectSpeech('Darken screen')
.call(doCmd('toggleDarkScreen'))
.expectSpeech('Undarken screen')
.call(doCmd('toggleDarkScreen'))
.expectSpeech('Darken screen')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackDisabled', function() {
// Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is
// off for the current app.
this.dispatchOnCustomSpokenFeedbackToggledEvent(false);
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<p>Test document</p>', function() {
ChromeVoxState.instance.setCurrentRange(null);
mockFeedback.call(doCmd('nextObject'))
.expectSpeech(
'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
'launcher.')
.call(doCmd('previousObject'))
.expectSpeech(
'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
'launcher.');
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackEnabled', function() {
// Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is
// on for the current app. We don't want to announce the no-focus hint message
// when TalkBack is on because we expect ChromeVox to have no focus in that
// case. If we announce the hint message, TalkBack and ChromeVox will
// try to speak at the same time.
this.dispatchOnCustomSpokenFeedbackToggledEvent(true);
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<p>Start here</p>', function() {
ChromeVoxState.instance.setCurrentRange(null);
mockFeedback.call(doCmd('nextObject'));
assertFalse(mockFeedback.utteranceInQueue(
'No current ChromeVox focus. ' +
'Press Alt+Shift+L to go to the launcher.'));
mockFeedback.call(doCmd('previousObject'));
assertFalse(mockFeedback.utteranceInQueue(
'No current ChromeVox focus. ' +
'Press Alt+Shift+L to go to the launcher.'));
mockFeedback.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'NavigateOutOfMultiline', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<p>before</p>
<div role="textbox" contenteditable>
Testing testing<br>
one two three
</div>
<p>after</p>
`,
function(root) {
const textField = root.find({role: RoleType.TEXT_FIELD});
mockFeedback.call(textField.focus.bind(textField))
.expectSpeech('Testing testing\none two three')
.expectSpeech('Edit text')
.call(doCmd('nextLine'))
.expectSpeech('one two three')
.call(doCmd('nextLine'))
.expectSpeech('after')
// In reverse (explicitly focus, instead of moving to previous line,
// because all subsequent commands require the text field be focused
// first):
.clearPendingOutput()
.call(textField.focus.bind(textField))
.expectSpeech('Edit text')
.call(doCmd('nextLine'))
.expectSpeech('one two three')
.call(doCmd('previousLine'))
.expectSpeech('Testing testing')
.call(doCmd('previousLine'))
.expectSpeech('before')
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ReadWindowTitle', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
`
<p>start</p>
<button id="click"></button>
<script>
const button = document.getElementById('click');
button.addEventListener('click', _ => document.title = 'bar');
</script>
`,
function(root) {
const clickButtonThenReadCurrentTitle = () => {
const desktop = root.parent.root;
desktop.addEventListener(EventType.TREE_CHANGED, (evt) => {
if (evt.target.role == RoleType.WINDOW &&
/bar/.test(evt.target.name)) {
doCmd('readCurrentTitle')();
}
});
const button = root.find({role: RoleType.BUTTON});
button.doDefault();
};
mockFeedback.clearPendingOutput()
.call(clickButtonThenReadCurrentTitle)
// This test may run against official builds, so match against
// utterances starting with 'bar'. This should exclude any other
// utterances that contain 'bar' e.g. data:...bar.. or the data url.
.expectSpeech(/^bar*/)
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'OutputEmptyQueueMode', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<p>unused</p>', function(root) {
const output = new Output();
Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
output.append_(
output.speechBuffer_, new Spannable(''),
{annotation: [new Output.Action()]});
output.withString('test');
mockFeedback.clearPendingOutput()
.call(output.go.bind(output))
.expectSpeechWithQueueMode('', QueueMode.CATEGORY_FLUSH)
.expectSpeechWithQueueMode('test', QueueMode.CATEGORY_FLUSH)
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'SetAccessibilityFocus', function() {
this.runWithLoadedTree('<p>Text.</p><button>Button</button>', function(root) {
const node = root.find({role: RoleType.BUTTON});
node.addEventListener(EventType.FOCUS, this.newCallback(function() {
chrome.automation.getAccessibilityFocus((focusedNode) => {
assertEquals(node, focusedNode);
});
}));
node.focus();
});
});