diff options
author | dtseng <dtseng@chromium.org> | 2014-09-29 12:36:32 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-09-29 19:36:46 +0000 |
commit | 930fcbcccbb7a4c0c8892acf175503555099e29c (patch) | |
tree | 62353b1d9bb51fa46c19fad7d85bb716f22cad35 | |
parent | af111ac4b1deff9861bef0dd6a76004b0dae265e (diff) | |
download | chromium_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}
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(/\*\/[^\/]+$/, ''); +}; |