Commit 930fcbcc authored by dtseng's avatar dtseng Committed by Commit bot

ImplementChromeVox next/previous line, link, and heading.

Review URL: https://codereview.chromium.org/586103004

Cr-Commit-Position: refs/heads/master@{#297234}
parent af111ac4
......@@ -191,7 +191,7 @@
'type': 'none',
'variables': {
'output_manifest_path': '<(chromevox_dest_dir)/manifest_next.json',
'use_chromevox_next': 1,
'use_chromevox_next': 1,
},
'includes': [ 'generate_manifest.gypi', ],
},
......
// 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.
/**
* @fileoverview ChromeVox utilities for the automation extension API.
*/
goog.provide('cvox2.AutomationPredicates');
goog.provide('cvox2.AutomationUtil');
goog.provide('cvox2.Dir');
/**
* @constructor
*/
cvox2.AutomationPredicates = function() {};
/**
* Constructs a predicate given a role.
* @param {string} role
* @return {function(AutomationNode) : boolean}
*/
cvox2.AutomationPredicates.makeRolePredicate = function(role) {
return function(node) {
return node.role == role;
};
};
/** @type {function(AutomationNode) : boolean} */
cvox2.AutomationPredicates.heading =
cvox2.AutomationPredicates.makeRolePredicate(
chrome.automation.RoleType.heading);
/** @type {function(AutomationNode) : boolean} */
cvox2.AutomationPredicates.inlineTextBox =
cvox2.AutomationPredicates.makeRolePredicate(
chrome.automation.RoleType.inlineTextBox);
/** @type {function(AutomationNode) : boolean} */
cvox2.AutomationPredicates.link =
cvox2.AutomationPredicates.makeRolePredicate(
chrome.automation.RoleType.link);
/**
* Possible directions to perform tree traversals.
* @enum {string}
*/
cvox2.Dir = {
// Search from left to right.
FORWARD: 'forward',
// Search from right to left.
BACKWARD: 'backward'
};
/**
* @constructor
*/
cvox2.AutomationUtil = function() {};
/**
* Find a node in subtree of |cur| satisfying |pred| using pre-order traversal.
* @param {AutomationNode} cur Node to begin the search from.
* @param {cvox2.Dir} dir
* @param {function(AutomationNode) : boolean} pred A predicate to apply to a
* candidate node.
* @return {AutomationNode}
*/
cvox2.AutomationUtil.findNodePre = function(cur, dir, pred) {
if (pred(cur))
return cur;
var child = dir == cvox2.Dir.BACKWARD ? cur.lastChild() : cur.firstChild();
while (child) {
var ret = cvox2.AutomationUtil.findNodePre(child, dir, pred);
if (ret)
return ret;
child = dir == cvox2.Dir.BACKWARD ?
child.previousSibling() : child.nextSibling();
}
};
/**
* Find a node in subtree of |cur| satisfying |pred| using post-order traversal.
* @param {AutomationNode} cur Node to begin the search from.
* @param {cvox2.Dir} dir
* @param {function(AutomationNode) : boolean} pred A predicate to apply to a
* candidate node.
* @return {AutomationNode}
*/
cvox2.AutomationUtil.findNodePost = function(cur, dir, pred) {
var child = dir == cvox2.Dir.BACKWARD ? cur.lastChild() : cur.firstChild();
while (child) {
var ret = cvox2.AutomationUtil.findNodePost(child, dir, pred);
if (ret)
return ret;
child = dir == cvox2.Dir.BACKWARD ?
child.previousSibling() : child.nextSibling();
}
if (pred(cur))
return cur;
};
/**
* Find the next node in the given direction that is either an immediate
* sibling or a sibling of an ancestor.
* @param {AutomationNode} cur Node to start search from.
* @param {cvox2.Dir} dir
* @return {AutomationNode}
*/
cvox2.AutomationUtil.findNextSubtree = function(cur, dir) {
while (cur) {
var next = dir == cvox2.Dir.BACKWARD ?
cur.previousSibling() : cur.nextSibling();
if (next)
return next;
cur = cur.parent();
}
};
/**
* Find the next node in the given direction in depth first order.
* @param {AutomationNode} cur Node to begin the search from.
* @param {cvox2.Dir} dir
* @param {function(AutomationNode) : boolean} pred A predicate to apply to a
* candidate node.
* @return {AutomationNode}
*/
cvox2.AutomationUtil.findNextNode = function(cur, dir, pred) {
var next = cur;
do {
if (!(next = cvox2.AutomationUtil.findNextSubtree(cur, dir)))
return null;
cur = next;
next = cvox2.AutomationUtil.findNodePre(next, dir, pred);
} while (!next);
return next;
};
......@@ -49,25 +49,62 @@ var MockTts = function() {
};
MockTts.prototype = {
/** Tracks all spoken text. @type {!Array.<string>} */
utterances: [],
/**
* A list of predicate callbacks.
* @type {!Array.<function(string) : boolean>}
* @private
*/
callbacks_: [],
/**
* A list of strings stored whenever there are no expectations.
* @type {!Array.<string}
* @private
*/
idleUtterances_: [],
/** @override */
speak: function(textString, queueMode, properties) {
this.utterances.push(textString);
this.process_(textString);
},
/**
* Checks to see if a string was spoken.
* @param {string} textString The string to check.
* @return {boolean} True if the string was spoken (possibly as part of a
* larger utterance).
* Adds an expectation for the given string to be spoken. If satisfied,
* |opt_callback| is called.
* @param {string} expected
* @param {function() : void=} opt_callback
*/
checkIfSubstringWasSpoken: function(textString) {
return this.utterances.some(function(t) {
return t.indexOf(textString) != -1;
expectSpeech: function(expected, opt_callback) {
this.callbacks_.push(function(actual) {
var match = actual.indexOf(expected) != -1;
if (opt_callback && match)
opt_callback();
return match;
});
}
// Process any idleUtterances.
this.idleUtterances_.forEach(this.process_, true);
},
/**
* @param {string} textString Utterance to match against callbacks.
* @param {boolean=} opt_manual True if called outside of tts.speak.
* @private
*/
process_: function(textString, opt_manual) {
if (this.callbacks_.length == 0) {
if (!opt_manual)
this.idleUtterances_.push(textString);
return;
}
var allUtterances = this.idleUtterances_.concat([textString]);
var targetCallback = this.callbacks_.shift();
if (allUtterances.some(targetCallback))
this.idleUtterances_.length = 0;
else
this.callbacks_.unshift(targetCallback);
},
};
/** Tests that ChromeVox classic is in this context. */
......@@ -100,13 +137,21 @@ TEST_F('BackgroundTest', 'DesktopFocus', function() {
chrome.automation.getDesktop(function(root) {
var testButton = findStatusTray(root);
testButton.addEventListener(chrome.automation.EventType.focus,
function(e) {
var result =
cvox.ChromeVox.tts.checkIfSubstringWasSpoken('Status tray');
testDone([result, '']);
},
true);
cvox.ChromeVox.tts.expectSpeech('Status tray', testDone);
testButton.focus();
});
});
/** Tests feedback once a page loads. */
TEST_F('BackgroundTest', 'InitialFeedback', function() {
this.runWithDocument(function() {/*!
<p>start
<p>end
*/},
function() {
cvox.ChromeVox.tts.expectSpeech('start', function() {
cvox2.global.backgroundObj.onGotCommand('nextLine');
});
cvox.ChromeVox.tts.expectSpeech('end', testDone);
}.bind(this));
});
......@@ -11,6 +11,9 @@ goog.provide('cvox2.Background');
goog.provide('cvox2.global');
goog.require('cvox.TabsApiHandler');
goog.require('cvox2.AutomationPredicates');
goog.require('cvox2.AutomationUtil');
goog.require('cvox2.Dir');
/** Classic Chrome accessibility API. */
cvox2.global.accessibility =
......@@ -27,6 +30,14 @@ cvox2.Background = function() {
*/
this.whitelist_ = ['http://www.chromevox.com/', 'chromevox_next_test'];
/** @type {cvox.TabsApiHandler} @private */
this.tabsHandler_ = new cvox.TabsApiHandler(cvox.ChromeVox.tts,
cvox.ChromeVox.braille,
cvox.ChromeVox.earcons);
/** @type {AutomationNode} @private */
this.currentNode_ = null;
/** @type {cvox.TabsApiHandler} @private */
this.tabsHandler_ = new cvox.TabsApiHandler(cvox.ChromeVox.tts,
cvox.ChromeVox.braille,
......@@ -66,9 +77,8 @@ cvox2.Background.prototype = {
return;
}
if (!chrome.commands.onCommand.hasListeners()) {
if (!chrome.commands.onCommand.hasListeners())
chrome.commands.onCommand.addListener(this.onGotCommand);
}
this.disableClassicChromeVox_(tab.id);
......@@ -83,26 +93,114 @@ cvox2.Background.prototype = {
onGotTree: function(root) {
// Register all automation event listeners.
root.addEventListener(chrome.automation.EventType.focus,
this.onAutomationEvent.bind(this),
this.onFocus,
true);
root.addEventListener(chrome.automation.EventType.loadComplete,
this.onLoadComplete,
true);
if (root.attributes.docLoaded)
this.onLoadComplete({target: root});
},
/**
* Handles chrome.commands.onCommand.
* @param {string} command
*/
onGotCommand: function(command) {
if (!this.current_)
return;
var previous = this.current_;
var current = this.current_;
var dir = cvox2.Dir.FORWARD;
var pred = null;
switch (command) {
case 'nextHeading':
dir = cvox2.Dir.FORWARD;
pred = cvox2.AutomationPredicates.heading;
break;
case 'previousHeading':
dir = cvox2.Dir.BACKWARD;
pred = cvox2.AutomationPredicates.heading;
break;
case 'nextLine':
dir = cvox2.Dir.FORWARD;
pred = cvox2.AutomationPredicates.inlineTextBox;
break;
case 'previousLine':
dir = cvox2.Dir.BACKWARD;
pred = cvox2.AutomationPredicates.inlineTextBox;
break;
case 'nextLink':
dir = cvox2.Dir.FORWARD;
pred = cvox2.AutomationPredicates.link;
break;
case 'previousLink':
dir = cvox2.Dir.BACKWARD;
pred = cvox2.AutomationPredicates.link;
break;
case 'nextElement':
current = current.role == chrome.automation.RoleType.inlineTextBox ?
current.parent() : current;
current = cvox2.AutomationUtil.findNextNode(current,
cvox2.Dir.FORWARD,
cvox2.AutomationPredicates.inlineTextBox);
current = current ? current.parent() : current;
break;
case 'previousElement':
current = current.role == chrome.automation.RoleType.inlineTextBox ?
current.parent() : current;
current = cvox2.AutomationUtil.findNextNode(current,
cvox2.Dir.BACKWARD,
cvox2.AutomationPredicates.inlineTextBox);
current = current ? current.parent() : current;
break;
}
if (pred)
current = cvox2.AutomationUtil.findNextNode(current, dir, pred);
if (current)
current.focus();
this.onFocus({target: current || previous});
},
/**
* A generic handler for all desktop automation events.
* @param {AutomationEvent} evt The event.
* Provides all feedback once ChromeVox's focus changes.
* @param {Object} evt
*/
onAutomationEvent: function(evt) {
var output = evt.target.attributes.name + ' ' + evt.target.role;
onFocus: function(evt) {
var node = evt.target;
if (!node)
return;
var container = node;
while (container && (container.role == 'inlineTextBox' ||
container.role == 'staticText'))
container = container.parent();
var role = container ? container.role : node.role;
var output =
[node.attributes.name, node.attributes.value, role].join(', ');
cvox.ChromeVox.tts.speak(output, cvox.AbstractTts.QUEUE_MODE_FLUSH);
cvox.ChromeVox.braille.write(cvox.NavBraille.fromText(output));
chrome.accessibilityPrivate.setFocusRing([evt.target.location]);
this.current_ = node;
},
/**
* Handles chrome.commands.onCommand.
* @param {string} command
* Provides all feedback once a load complete event fires.
* @param {Object} evt
*/
onGotCommand: function(command) {
onLoadComplete: function(evt) {
this.current_ = cvox2.AutomationUtil.findNodePost(evt.target,
cvox2.Dir.FORWARD,
cvox2.AutomationPredicates.inlineTextBox);
this.onFocus({target: this.current_});
},
/**
......@@ -118,7 +216,7 @@ cvox2.Background.prototype = {
/**
* Disables classic ChromeVox.
* @param {number} tabId The tab where ChromeVox classic is running.
* @param {number} tabId The tab where ChromeVox classic is running in.
*/
disableClassicChromeVox_: function(tabId) {
chrome.tabs.executeScript(
......
......@@ -59,25 +59,49 @@
"nextElement": {
"description": "Moves to the next element",
"suggested_key": {
"chromeos": "Search+Shift+Right"
"chromeos": "Search+Right"
}
},
"previousElement": {
"description": "Moves to the previous element",
"suggested_key": {
"chromeos": "Search+Shift+Left"
"chromeos": "Search+Left"
}
},
"nextLine": {
"description": "Moves to the next line",
"description": "__MSG_CHROMEVOX_NEXT_LINE__",
"suggested_key": {
"chromeos": "Search+Shift+Down"
"chromeos": "Search+Down"
}
},
"previousLine": {
"description": "Moves to the previous line",
"description": "__MSG_CHROMEVOX_PREVIOUS_LINE__",
"suggested_key": {
"chromeos": "Search+Shift+Up"
"chromeos": "Search+Up"
}
},
"nextLink": {
"description": "__MSG_CHROMEVOX_NEXT_LINK__",
"suggested_key": {
"chromeos": "Search+L"
}
},
"previousLink": {
"description": "__MSG_CHROMEVOX_PREVIOUS_LINK__",
"suggested_key": {
"chromeos": "Search+Shift+L"
}
},
"nextHeading": {
"description": "Moves to the next heading",
"suggested_key": {
"chromeos": "Search+H"
}
},
"previousHeading": {
"description": "Moves to the previous heading",
"suggested_key": {
"chromeos": "Search+Shift+H"
}
}
},
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
GEN_INCLUDE([
'chrome/browser/resources/chromeos/chromevox/testing/common.js']);
/**
* Base test fixture for ChromeVox end to end tests.
*
......@@ -52,6 +55,29 @@ ChromeVoxE2ETest.prototype = {
ash::A11Y_NOTIFICATION_NONE);
WaitForExtension(extension_misc::kChromeVoxExtensionId, load_cb);
*/});
},
/**
* Run a test with the specified HTML snippet loaded.
* @param {function() : void} doc Snippet wrapped inside of a function.
* @param {function()} callback Called once the document is ready.
*/
runWithDocument: function(doc, callback) {
var docString = TestUtils.extractHtmlFromCommentEncodedString(doc);
var url = 'data:text/html,<!doctype html>' +
docString +
'<!-- chromevox_next_test -->';
var createParams = {
active: true,
url: url
};
chrome.tabs.create(createParams, function(tab) {
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo) {
if (tabId == tab.id && changeInfo.status == 'complete') {
callback();
}
});
});
}
};
......
......@@ -4,15 +4,8 @@
GEN_INCLUDE([
'chrome/browser/resources/chromeos/chromevox/testing/assert_additions.js']);
/**
* Shortcut for document.getElementById.
* @param {string} id of the element.
* @return {HTMLElement} with the id.
*/
function $(id) {
return document.getElementById(id);
}
GEN_INCLUDE([
'chrome/browser/resources/chromeos/chromevox/testing/common.js']);
/**
* Base test fixture for ChromeVox unit tests.
......@@ -74,7 +67,8 @@ ChromeVoxUnitTestBase.prototype = {
* comment inside an anonymous function - see example, above.
*/
loadDoc: function(commentEncodedHtml) {
var html = this.extractHtmlFromCommentEncodedString_(commentEncodedHtml);
var html =
TestUtils.extractHtmlFromCommentEncodedString(commentEncodedHtml);
this.loadHtml(html);
},
......@@ -91,7 +85,8 @@ ChromeVoxUnitTestBase.prototype = {
* comment inside an anonymous function - see example, above.
*/
appendDoc: function(commentEncodedHtml) {
var html = this.extractHtmlFromCommentEncodedString_(commentEncodedHtml);
var html =
TestUtils.extractHtmlFromCommentEncodedString(commentEncodedHtml);
this.appendHtml(html);
},
......@@ -110,24 +105,6 @@ ChromeVoxUnitTestBase.prototype = {
document.body.appendChild(fragment);
},
/**
* Extracts some inlined html encoded as a comment inside a function,
* so you can use it like this:
*
* this.appendDoc(function() {/*!
* <p>Html goes here</p>
* * /});
*
* @param {Function} commentEncodedHtml The html , embedded as a
* comment inside an anonymous function - see example, above.
@ @return {String} The html text.
*/
extractHtmlFromCommentEncodedString_: function(commentEncodedHtml) {
return commentEncodedHtml.toString().
replace(/^[^\/]+\/\*!?/, '').
replace(/\*\/[^\/]+$/, '');
},
/**
* Waits for the queued events in ChromeVoxEventWatcher to be
* handled, then calls a callback function with provided arguments
......
// 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.
// Common testing utilities.
/**
* Shortcut for document.getElementById.
* @param {string} id of the element.
* @return {HTMLElement} with the id.
*/
function $(id) {
return document.getElementById(id);
}
/**
* @constructor
*/
var TestUtils = function() {};
/**
* Extracts some inlined html encoded as a comment inside a function,
* so you can use it like this:
*
* this.appendDoc(function() {/*!
* <p>Html goes here</p>
* * /});
*
* @param {Function} commentEncodedHtml The html , embedded as a
* comment inside an anonymous function - see example, above.
* @return {string} The html text.
*/
TestUtils.extractHtmlFromCommentEncodedString = function(commentEncodedHtml) {
return commentEncodedHtml.toString().
replace(/^[^\/]+\/\*!?/, '').
replace(/\*\/[^\/]+$/, '');
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment