blob: c370884f09c5bd6c4316c40319ec8870cff0ee9d [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']);
/**
* Test fixture for Background.
* @constructor
* @extends {ChromeVoxNextE2ETest}
*/
function BackgroundTest() {
ChromeVoxNextE2ETest.call(this);
}
BackgroundTest.prototype = {
__proto__: ChromeVoxNextE2ETest.prototype,
/** @override */
setUp: function() {
window.RoleType = chrome.automation.RoleType;
window.doCmd = this.doCmd;
// Reset notifications so only explicit mode changes can cause them to trigger.
Notifications.reset();
this.forceContextualLastOutput();
},
/**
* @return {!MockFeedback}
*/
createMockFeedback: function() {
var 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: function(cmd) {
return function() {
CommandHandler.onCommand(cmd);
};
},
linksAndHeadingsDoc: function() {/*!
<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>
*/},
formsDoc: function() {/*!
<select id="fruitSelect">
<option>apple</option>
<option>grape</option>
<option> banana</option>
</select>
*/},
iframesDoc: function() {/*!
<p>start</p>
<button>Before</button>
<iframe srcdoc="<button>Inside</button><h1>Inside</h1>"></iframe>
<button>After</button>
*/},
disappearingObjectDoc: function() {/*!
<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="polite"></div>
<div id="delete" role="button">Delete</div>
<script>
document.getElementById('delete').addEventListener('click', function() {
var d = document.getElementById('disappearing');
d.parentElement.removeChild(d);
document.getElementById('live').innerText = 'Deleted';
});
</script>
*/},
};
/** Tests that ChromeVox classic is in this context. */
SYNC_TEST_F('BackgroundTest', 'ClassicNamespaces', function() {
assertEquals('object', typeof(cvox));
assertEquals('function', typeof(cvox.ChromeVoxBackground));
});
/** Tests that ChromeVox next is in this context. */
SYNC_TEST_F('BackgroundTest', 'NextNamespaces', function() {
assertEquals('function', typeof(Background));
});
/** Tests consistency of navigating forward and backward. */
TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
var 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('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('BackgroundTest', 'CaretNavigation', function() {
// TODO(plundblad): Add braille expectaions when crbug.com/523285 is fixed.
var 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('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();
});
});
TEST_F('BackgroundTest', 'SelectSingleBasic', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.formsDoc, function() {
var incrementSelectedIndex =
this.incrementSelectedIndex.bind(this, undefined, '#fruitSelect');
mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
.expectBraille('apple btn +popup +')
.call(incrementSelectedIndex)
.expectSpeech('grape', /2 of 3/)
.expectBraille('grape mnuitm 2/3 (x)')
.call(incrementSelectedIndex)
.expectSpeech('banana', /3 of 3/)
.expectBraille('banana mnuitm 3/3 (x)');
mockFeedback.replay();
});
});
TEST_F('BackgroundTest', 'ContinuousRead', function() {
var 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('BackgroundTest', 'InitialFocus', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<a href="a">a</a>',
function(rootNode) {
mockFeedback.expectSpeech('a')
.expectSpeech('Link');
mockFeedback.replay();
});
});
TEST_F('BackgroundTest', 'AriaLabel', function() {
var 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')
.expectBraille('foo lnk');
mockFeedback.replay();
}
);
});
TEST_F('BackgroundTest', 'ShowContextMenu', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<p>before</p><a href="a">a</a>',
function(rootNode) {
var 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('BackgroundTest', 'BrailleRouting', function() {
var mockFeedback = this.createMockFeedback();
var route = function(position) {
assertTrue(ChromeVoxState.instance.onBrailleKeyEvent(
{command: cvox.BrailleKeyCommand.ROUTING,
displayPosition: position},
mockFeedback.lastMatchedBraille));
};
this.runWithLoadedTree(
function() {/*!
<p>start</p>
<button id="btn1">Click me</button>
<p>Some text</p>
<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) {
var button1 = rootNode.find({role: RoleType.BUTTON,
attributes: { name: 'Click me' }});
var 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('BackgroundTest', 'FocusInputElement', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
function() {/*!
<input id="name" value="Lancelot">
<input id="quest" value="Grail">
<input id="color" value="Blue">
*/},
function(rootNode) {
var name = rootNode.find({ attributes: { value: 'Lancelot' } });
var quest = rootNode.find({ attributes: { value: 'Grail' } });
var 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 http://crbug.com/643902.
TEST_F('BackgroundTest', 'DISABLED_UseEditableState', function() {
this.runWithLoadedTree(
function() {/*!
<input type="text"></input>
<p tabindex=0>hi</p>
*/},
function(rootNode) {
var assertExists = this.newCallback(function (evt) {
assertNotNullNorUndefined(
ChromeVoxState.desktopAutomationHandler.textEditHandler_);
evt.stopPropagation();
});
var assertDoesntExist = this.newCallback(function (evt) {
assertTrue(
!ChromeVoxState.desktopAutomationHandler.editableTextHandler_);
evt.stopPropagation();
// Focus the other text field here to make this test not racey.
editable.focus();
});
var editable = rootNode.find({ role: RoleType.TEXT_FIELD });
var nonEditable = rootNode.find({ role: RoleType.PARAGRAPH });
this.listenOnce(nonEditable, 'focus', assertDoesntExist);
this.listenOnce(editable, 'focus', assertExists);
nonEditable.focus();
}.bind(this));
});
TEST_F('BackgroundTest', 'EarconsForControls', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(
function() {/*!
<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(cvox.Earcon.LINK)
.call(doCmd('nextObject'))
.expectSpeech('MyButton')
.expectEarcon(cvox.Earcon.BUTTON)
.call(doCmd('nextObject'))
.expectSpeech('Check box')
.expectEarcon(cvox.Earcon.CHECK_OFF)
.call(doCmd('nextObject'))
.expectSpeech('Check box')
.expectEarcon(cvox.Earcon.CHECK_ON)
.call(doCmd('nextObject'))
.expectSpeech('Edit text')
.expectEarcon(cvox.Earcon.EDITABLE_TEXT)
.call(doCmd('nextObject'))
.expectSpeech('List box')
.expectEarcon(cvox.Earcon.LISTBOX)
.call(doCmd('nextObject'))
.expectSpeech('Button', 'has pop up')
.expectEarcon(cvox.Earcon.POP_UP_BUTTON)
.call(doCmd('nextObject'))
.expectSpeech(/Slider/)
.expectEarcon(cvox.Earcon.SLIDER);
mockFeedback.replay();
}.bind(this));
});
TEST_F('BackgroundTest', 'ToggleChromeVoxVersion', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
var gotCmd = CommandHandler.onCommand;
// The command came from the background keyboard handler.
var togglerFromBackground = gotCmd.bind(gotCmd, 'toggleChromeVoxVersion');
// The command came from a content script.
var togglerFromContent = gotCmd.bind(gotCmd, 'toggleChromeVoxVersion',
true);
mockFeedback.call(togglerFromBackground)
.expectSpeech('Switched to Classic ChromeVox')
.call(togglerFromContent)
.expectSpeech('Switched to ChromeVox Next')
.call(togglerFromBackground)
.expectSpeech('Switched to Classic ChromeVox');
mockFeedback.replay();
});
});
SYNC_TEST_F('BackgroundTest', '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());
});
// Flaky, see http://crbug.com/635032
TEST_F('BackgroundTest', 'DISABLED_ActiveOrInactive', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<a href="a">a</a>
<button>b</button>
<input type="text"></input>
*/},
function(rootNode) {
var focusButton = function() {
rootNode.find({role: RoleType.BUTTON}).focus();
};
var on = function() { cvox.ChromeVox.isActive = true; };
var off = function() { cvox.ChromeVox.isActive = false; };
function focusThen(toFocus, then) {
toFocus.addEventListener('focus', function innerFocus(e) {
if (e.target != toFocus)
return;
rootNode.removeEventListener('focus', innerFocus, true);
then && then();
}, true);
toFocus.focus();
}
mockFeedback.call(focusButton)
.expectSpeech('b').expectSpeech('Button')
.call(off)
.call(focusThen.bind(this, rootNode.find(
{ role: RoleType.LINK }), on))
.call(focusThen.bind(this, rootNode.find(
{ role: RoleType.TEXT_FIELD })))
.expectNextSpeechUtteranceIsNot('a')
.expectSpeech('Edit text');
mockFeedback.replay();
});
});
TEST_F('BackgroundTest', 'ModeSwitching', function() {
this.runWithLoadedTree('<button></button>', function(root) {
var fakeDesktop = {};
fakeDesktop.role = 'desktop';
fakeDesktop.root = fakeDesktop;
var fakeWebRoot = {};
fakeWebRoot.root = fakeWebRoot;
fakeWebRoot.parent = fakeDesktop;
fakeWebRoot.role = RoleType.ROOT_WEB_AREA;
fakeWebRoot.makeVisible = function() {};
fakeWebRoot.location = {left: 1, top: 1, width: 1, height: 1};
var fakeSubRoot = {};
fakeSubRoot.root = fakeSubRoot;
fakeSubRoot.parent = fakeWebRoot;
fakeSubRoot.role = RoleType.ROOT_WEB_AREA;
fakeSubRoot.makeVisible = function() {};
fakeSubRoot.location = {left: 1, top: 1, width: 1, height: 1};
var bk = ChromeVoxState.instance;
// Tests default to force next mode.
assertEquals('force_next', bk.mode);
// Force next mode stays set regardless of where the range lands.
fakeWebRoot.docUrl = 'http://google.com';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('force_next', bk.mode);
// Empty urls occur before document load or when root is desktop.
fakeWebRoot.docUrl = '';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('force_next', bk.mode);
// Verify force next -> classic compat switching.
localStorage['useClassic'] = true;
fakeWebRoot.docUrl = 'chrome://foobar';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('classic_compat', bk.mode);
// Classic compat -> classic.
fakeWebRoot.docUrl = 'http://google.com';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('classic', bk.mode);
// Ensure we switch to classic compat if our current range has focused
// state set and is not in web content.
assertTrue(root.parent.state.focused);
bk.setCurrentRange(cursors.Range.fromNode(root.parent));
assertEquals('classic_compat', bk.mode);
// And back to classic.
bk.setCurrentRange(cursors.Range.fromNode(root));
assertEquals('classic', bk.mode);
// Now, verify mode switching uses the top level root.
fakeWebRoot.docUrl = 'http://google.com/#chromevox_next_test';
fakeSubRoot.docUrl = 'http://chromevox.com';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('next', bk.mode);
// Next compat switching.
localStorage['useClassic'] = false;
fakeWebRoot.docUrl = 'http://docs.google.com/document/#123123';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('force_next', bk.mode);
// And, back to force next.
fakeWebRoot.docUrl = 'http://docs.google.com/form/123';
bk.setCurrentRange(cursors.Range.fromNode(fakeWebRoot));
assertEquals('force_next', bk.mode);
}.bind(this));
});
TEST_F('BackgroundTest', 'ShouldNotFocusIframe', function() {
this.runWithLoadedTree( function() {/*!
<iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe>
<button>outside</button>
*/}, function(root) {
var iframe = root.find({role: RoleType.IFRAME});
var button = root.find({role: RoleType.BUTTON});
assertEquals('iframe', iframe.role);
assertEquals('button', button.role);
var didFocus = false;
iframe.addEventListener('focus', function() {
didFocus = true;
});
var b = ChromeVoxState.instance;
b.currentRange_ = cursors.Range.fromNode(button);
doCmd('previousElement');
assertFalse(didFocus);
}.bind(this));
});
TEST_F('BackgroundTest', 'ShouldFocusLink', function() {
this.runWithLoadedTree( function() {/*!
<div><a href="#">mylink</a></div>
<button>after</button>
*/}, function(root) {
var link = root.find({role: RoleType.LINK});
var button = root.find({role: RoleType.BUTTON});
assertEquals('link', link.role);
assertEquals('button', button.role);
var didFocus = false;
link.addEventListener('focus', this.newCallback(function() {
// Success
}));
var b = ChromeVoxState.instance;
b.currentRange_ = cursors.Range.fromNode(button);
doCmd('previousElement');
});
});
TEST_F('BackgroundTest', 'NoisySlider', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree( function() {/*!
<button id="go">go</button>
<div id="slider" tabindex=0 role="slider"></div>
<script>
function update() {
var s = document.getElementById('slider');
s.setAttribute('aria-valuetext', 'noisy');
setTimeout(update, 500);
}
update();
</script>
*/}, function(root) {
var go = root.find({role: RoleType.BUTTON});
var slider = root.find({role: RoleType.SLIDER});
var focusButton = go.focus.bind(go);
var focusSlider = slider.focus.bind(slider);
mockFeedback.call(focusButton)
.expectNextSpeechUtteranceIsNot('noisy')
.call(focusSlider)
.expectSpeech('noisy')
.expectSpeech('noisy')
.replay();
}.bind(this));
});
TEST_F('BackgroundTest', 'Checkbox', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<div id="go" role="checkbox">go</div>
<script>
var go = document.getElementById('go');
var isChecked = true;
go.addEventListener('click', function(e) {
if (isChecked)
go.setAttribute('aria-checked', true);
else
go.removeAttribute('aria-checked');
isChecked = !isChecked;
});
</script>
*/}, function(root) {
var cbx = root.find({role: RoleType.CHECK_BOX});
var click = cbx.doDefault.bind(cbx);
var 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('BackgroundTest', 'MixedCheckbox', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<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('BackgroundTest', 'ForwardNavigationThroughIframeButtons', function() {
var mockFeedback = this.createMockFeedback();
var running = false;
var runTestIfIframeIsLoaded = function(rootNode) {
if (running)
return;
// Return if the iframe hasn't loaded yet.
var iframe = rootNode.find({role: RoleType.IFRAME});
var childDoc = iframe.firstChild;
if (!childDoc || childDoc.children.length == 0)
return;
running = true;
var 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('BackgroundTest', 'ForwardObjectNavigationThroughIframes', function() {
var mockFeedback = this.createMockFeedback();
var running = false;
var runTestIfIframeIsLoaded = function(rootNode) {
if (running)
return;
// Return if the iframe hasn't loaded yet.
var iframe = rootNode.find({role: 'iframe'});
var childDoc = iframe.firstChild;
if (!childDoc || childDoc.children.length == 0)
return;
running = true;
var beforeButton = rootNode.find({role: RoleType.BUTTON,
name: 'Before'});
beforeButton.focus();
mockFeedback.expectSpeech('Before', 'Button');
mockFeedback.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('BackgroundTest', 'SelectOptionSelected', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<select>
<option>apple
<option>banana
<option>grapefruit
</select>
*/}, function(root) {
var select = root.find({role: RoleType.POP_UP_BUTTON});
var clickSelect = select.doDefault.bind(select);
var lastOption = select.lastChild.lastChild;
var selectLastOption = lastOption.doDefault.bind(lastOption);
mockFeedback.call(clickSelect)
.expectSpeech('apple')
.expectSpeech('Button')
.call(selectLastOption)
.expectNextSpeechUtteranceIsNot('apple')
.expectSpeech('grapefruit')
.replay();
});
});
TEST_F('BackgroundTest', 'ToggleButton', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<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) {
var b = ChromeVoxState.instance;
var move = doCmd('nextObject');
mockFeedback.call(move)
.expectSpeech('boldface')
.expectSpeech('Button')
.expectSpeech('Partially pressed')
.call(move)
.expectSpeech('ok')
.expectSpeech('Button')
.expectSpeech('Pressed')
.call(move)
.expectSpeech('cancel')
.expectSpeech('Button')
.expectSpeech('Not pressed')
.call(move)
.expectSpeech('close')
.expectSpeech('Button')
.replay();
});
});
TEST_F('BackgroundTest', 'EditText', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<input type="text"></input>
<input role="combobox" type="text"></input>
*/}, function(root) {
var nextEditText = doCmd('nextEditText');
var previousEditText = doCmd('previousEditText');
mockFeedback.call(nextEditText)
.expectSpeech('Combo box')
.call(previousEditText)
.expectSpeech('Edit text')
.replay();
});
});
TEST_F('BackgroundTest', 'BackwardForwardSync', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*!
<div aria-label="Group" role="group" tabindex=0>
<input type="text"></input>
</div>
<ul>
<li tabindex=0>
<button>ok</button>
</li>
</ul>
*/}, function(root) {
var 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('BackgroundTest', 'DisappearingObject', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.disappearingObjectDoc, function(rootNode) {
var deleteButton = rootNode.find({role: RoleType.BUTTON,
attributes: { name: 'Delete' }});
var 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('previousObject'))
.expectSpeech('Before3');
mockFeedback.replay();
});
});
TEST_F('BackgroundTest', 'ButtonNameValueDescription', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<input type="submit" aria-label="foo" value="foo"></input>
*/}, function(root) {
var btn = root.find({role: RoleType.BUTTON});
mockFeedback.call(btn.focus.bind(btn))
.expectSpeech('foo')
.expectSpeech('Button')
.replay();
});
});
TEST_F('BackgroundTest', 'NameFromHeadingLink', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<p>before</p>
<h1><a href="google.com">go</a><p>here</p></h1>
*/}, function(root) {
var link = root.find({role: RoleType.LINK});
mockFeedback.call(link.focus.bind(link))
.expectSpeech('go')
.expectSpeech('Link')
.expectSpeech('Heading 1')
.replay();
});
});
TEST_F('BackgroundTest', 'OptionChildIndexCount', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'ListMarkerIsIgnored', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<ul><li>apple</ul>
*/}, function(root) {
mockFeedback.call(doCmd('nextObject'))
.expectNextSpeechUtteranceIsNot('listMarker')
.expectSpeech('apple')
.replay();
});
});
TEST_F('BackgroundTest', 'SymetricComplexHeading', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'ContentEditableJumpSyncsRange', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<p>start</p>
<div contenteditable>
<h1>Top News</h1>
<h1>Most Popular</h1>
<h1>Sports</h1>
</div>
*/}, function(root) {
var 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('BackgroundTest', 'Selection', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'BasicTableCommands', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'MissingTableCells', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'DisabledState', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<button aria-disabled="true">ok</button>
*/}, function(root) {
mockFeedback.expectSpeech('ok', 'Disabled', 'Button').replay();
});
});
TEST_F('BackgroundTest', 'HeadingLevels', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<h1>1</h1><h2>2</h2><h3>3</h3><h4>4</h4><h5>5</h5><h6>6</h6>
*/}, function(root) {
var makeLevelAssertions = function(level) {
mockFeedback.call(doCmd('nextHeading' + level))
.expectSpeech('Heading ' + level)
.call(doCmd('nextHeading' + level))
.expectEarcon('wrap')
.call(doCmd('previousHeading' + level))
.expectEarcon('wrap');
};
for (var i = 1; i <= 6; i++)
makeLevelAssertions(i);
mockFeedback.replay();
});
});
// Flaky, see crbug.com/693928.
TEST_F('BackgroundTest', 'DISABLED_EditableNavigation', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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('BackgroundTest', 'NavigationMovesFocus', function() {
this.runWithLoadedTree(function(root) {/*!
<p>start</p>
<input type="text"></input>
*/}, function(root) {
this.listenOnce(root, 'focus', function(e) {
var focus = ChromeVoxState.instance.currentRange.start.node;
assertEquals(RoleType.TEXT_FIELD, focus.role);
assertTrue(focus.state.focused);
});
doCmd('nextEditText')();
});
});
TEST_F('BackgroundTest', 'BrailleCaretNavigation', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<p>This is a<em>test</em> of inline braille<br>with a second line</p>
*/}, function(root) {
var 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('BackgroundTest', 'InPageLinks', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<a href="#there">hi</a>
<button id="there">there</button>
*/}, function(root) {
mockFeedback.expectSpeech('hi', 'Internal link')
.call(doCmd('forceClickOnCurrentItem'))
.expectSpeech('there', 'Button')
.replay();
});
});
TEST_F('BackgroundTest', 'ListItem', function() {
this.resetContextualOutput();
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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 list +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 list +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('BackgroundTest', 'BusyHeading', function() {
this.resetContextualOutput();
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function(root) {/*!
<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();
});
});