summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordtseng <dtseng@chromium.org>2014-09-29 12:36:32 -0700
committerCommit bot <commit-bot@chromium.org>2014-09-29 19:36:46 +0000
commit930fcbcccbb7a4c0c8892acf175503555099e29c (patch)
tree62353b1d9bb51fa46c19fad7d85bb716f22cad35
parentaf111ac4b1deff9861bef0dd6a76004b0dae265e (diff)
downloadchromium_src-930fcbcccbb7a4c0c8892acf175503555099e29c.zip
chromium_src-930fcbcccbb7a4c0c8892acf175503555099e29c.tar.gz
chromium_src-930fcbcccbb7a4c0c8892acf175503555099e29c.tar.bz2
ImplementChromeVox next/previous line, link, and heading.
Review URL: https://codereview.chromium.org/586103004 Cr-Commit-Position: refs/heads/master@{#297234}
-rw-r--r--chrome/browser/resources/chromeos/chromevox/chromevox.gyp2
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js138
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/background.extjs81
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js120
-rw-r--r--chrome/browser/resources/chromeos/chromevox/manifest.json.jinja236
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js26
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/chromevox_unittest_base.js35
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/common.js37
8 files changed, 410 insertions, 65 deletions
diff --git a/chrome/browser/resources/chromeos/chromevox/chromevox.gyp b/chrome/browser/resources/chromeos/chromevox/chromevox.gyp
index 2903d7a..deb6d8c 100644
--- a/chrome/browser/resources/chromeos/chromevox/chromevox.gyp
+++ b/chrome/browser/resources/chromeos/chromevox/chromevox.gyp
@@ -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', ],
},
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js
new file mode 100644
index 0000000..374052e
--- /dev/null
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js
@@ -0,0 +1,138 @@
+// 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;
+};
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.extjs
index 207d707..7db6e21 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.extjs
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.extjs
@@ -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));
+});
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js
index 701cff2..e39f303 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js
@@ -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 =
@@ -32,6 +35,14 @@ cvox2.Background = function() {
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,
+ cvox.ChromeVox.earcons);
+
// Only needed with unmerged ChromeVox classic loaded before.
cvox2.global.accessibility.setAccessibilityEnabled(false);
@@ -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(
diff --git a/chrome/browser/resources/chromeos/chromevox/manifest.json.jinja2 b/chrome/browser/resources/chromeos/chromevox/manifest.json.jinja2
index f754b04..547dcd2 100644
--- a/chrome/browser/resources/chromeos/chromevox/manifest.json.jinja2
+++ b/chrome/browser/resources/chromeos/chromevox/manifest.json.jinja2
@@ -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"
}
}
},
diff --git a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js
index bef9ee7..d51d63f 100644
--- a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js
+++ b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js
@@ -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();
+ }
+ });
+ });
}
};
diff --git a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_unittest_base.js b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_unittest_base.js
index 1f5c96d..869a0c3 100644
--- a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_unittest_base.js
+++ b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_unittest_base.js
@@ -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);
},
@@ -111,24 +106,6 @@ ChromeVoxUnitTestBase.prototype = {
},
/**
- * 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
* in the test case scope. Very useful for asserting the results of events.
diff --git a/chrome/browser/resources/chromeos/chromevox/testing/common.js b/chrome/browser/resources/chromeos/chromevox/testing/common.js
new file mode 100644
index 0000000..38f2d59
--- /dev/null
+++ b/chrome/browser/resources/chromeos/chromevox/testing/common.js
@@ -0,0 +1,37 @@
+// 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(/\*\/[^\/]+$/, '');
+};