diff options
Diffstat (limited to 'chrome/browser')
16 files changed, 853 insertions, 271 deletions
diff --git a/chrome/browser/resources/chromeos/chromevox/chromevox.gni b/chrome/browser/resources/chromeos/chromevox/chromevox.gni index 2a4e8c8..caa35d0 100644 --- a/chrome/browser/resources/chromeos/chromevox/chromevox.gni +++ b/chrome/browser/resources/chromeos/chromevox/chromevox.gni @@ -126,9 +126,11 @@ chromevox_modules = [ "cvox2/background/automation_util.js", "cvox2/background/background.js", "cvox2/background/base_automation_handler.js", + "cvox2/background/chromevox_state.js", "cvox2/background/cursors.js", "cvox2/background/desktop_automation_handler.js", "cvox2/background/earcon_engine.js", + "cvox2/background/live_regions.js", "cvox2/background/next_earcons.js", "cvox2/background/output.js", "cvox2/background/panel.js", diff --git a/chrome/browser/resources/chromeos/chromevox/chromevox_tests.gypi b/chrome/browser/resources/chromeos/chromevox/chromevox_tests.gypi index 73a5ea7..28ed0bb 100644 --- a/chrome/browser/resources/chromeos/chromevox/chromevox_tests.gypi +++ b/chrome/browser/resources/chromeos/chromevox/chromevox_tests.gypi @@ -83,6 +83,7 @@ 'cvox2/background/automation_util_test.extjs', 'cvox2/background/background_test.extjs', 'cvox2/background/cursors_test.extjs', + 'cvox2/background/live_regions_test.extjs', 'cvox2/background/output_test.extjs', 'host/chrome/tts_background_test.extjs', ], diff --git a/chrome/browser/resources/chromeos/chromevox/common/chrome_extension_externs.js b/chrome/browser/resources/chromeos/chromevox/common/chrome_extension_externs.js index 630c365..a087fbe 100644 --- a/chrome/browser/resources/chromeos/chromevox/common/chrome_extension_externs.js +++ b/chrome/browser/resources/chromeos/chromevox/common/chrome_extension_externs.js @@ -275,37 +275,23 @@ chrome.automation.AutomationNode = function() {}; /** - * Get the automation tree for the tab with the given tabId, or the current tab - * if no tabID is given, enabling automation if necessary. Returns a tree with a - * placeholder root node; listen for the "loadComplete" event to get a - * notification that the tree has fully loaded (the previous root node reference - * will stop working at or before this point). * @param {number} tabId * @param {function(chrome.automation.AutomationNode):void} callback - * Called when the <code>AutomationNode</code> for the page is available. */ chrome.automation.getTree = function(tabId, callback) {}; -/** - * Get the automation tree for the whole desktop which consists of all on screen - * views. Note this API is currently only supported on Chrome OS. - * @param {function(!chrome.automation.AutomationNode):void} callback - * Called when the <code>AutomationNode</code> for the page is available. - */ +/** @param {function(!chrome.automation.AutomationNode):void} callback */ chrome.automation.getDesktop = function(callback) {}; /** - * Add a tree change observer. Tree change observers are static/global, - * they listen to tree changes across all trees. - * @param {function(chrome.automation.TreeChange):void} observer - * A listener for tree changes on the <code>AutomationNode</code> tree. + * @param {string} filter + * @param {function(chrome.automation.TreeChange) : void} + * observer */ -chrome.automation.addTreeChangeObserver = function(observer) {}; +chrome.automation.addTreeChangeObserver = function(filter, observer) {}; /** - * Remove a tree change observer. - * @param {function(chrome.automation.TreeChange):void} observer - * A listener for tree changes on the <code>AutomationNode</code> tree. + * @param {function(chrome.automation.TreeChange) : void} observer */ chrome.automation.removeTreeChangeObserver = function(observer) {}; @@ -477,22 +463,6 @@ chrome.automation.TreeChange.prototype.target; chrome.automation.TreeChange.prototype.type; -/** - * @param {function(chrome.automation.TreeChange) : void} - * callback - */ -chrome.automation.AutomationNode.prototype.addTreeChangeObserver = - function(callback) {}; - - -/** - * @param {function(chrome.automation.TreeChange) : void} - * callback - */ -chrome.automation.AutomationNode.prototype.removeTreeChangeObserver = - function(callback) {}; - - chrome.automation.AutomationNode.prototype.doDefault = function() {}; @@ -522,6 +492,18 @@ chrome.automation.AutomationNode.prototype.containerLiveAtomic; /** @type {boolean} */ chrome.automation.AutomationNode.prototype.containerLiveBusy; +/** @type {string} */ +chrome.automation.AutomationNode.prototype.liveStatus; + +/** @type {string} */ +chrome.automation.AutomationNode.prototype.liveRelevant; + +/** @type {boolean} */ +chrome.automation.AutomationNode.prototype.liveAtomic; + +/** @type {boolean} */ +chrome.automation.AutomationNode.prototype.liveBusy; + /** * @param {Object} findParams diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js index c7c743a..c45aac8 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js @@ -276,4 +276,25 @@ AutomationUtil.isTraversalRoot = function(node) { } }; +/** + * Determines whether the two given nodes come from the same webpage. + * @param {AutomationNode} a + * @param {AutomationNode} b + * @return {boolean} + */ +AutomationUtil.isInSameWebpage = function(a, b) { + if (!a || !b) + return false; + + a = a.root; + while (a && a.parent && AutomationUtil.isInSameTree(a.parent, a)) + a = a.parent.root; + + b = b.root; + while (b && b.parent && AutomationUtil.isInSameTree(b.parent, b)) + b = b.parent.root; + + return a == b; +}; + }); // goog.scope diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util_test.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util_test.extjs index 2c75b9f..86ff2bf 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util_test.extjs +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util_test.extjs @@ -25,6 +25,22 @@ AutomationUtilE2ETest.prototype = { basicDoc: function() {/*! <p><a href='#'></a>hello</p> <h1><ul><li>a</ul><button></h1> + */}, + + secondDoc: function() {/*! + <html> + <head><title>Second doc</title></head> + <body><div>Second</div></body> + </html> + */}, + + iframeDoc: function() {/*! + <html> + <head><title>Second doc</title></head> + <body> + <iframe src="data:text/html,<p>Inside</p>"></iframe> + </body> + </html> */} }; @@ -97,6 +113,61 @@ TEST_F('AutomationUtilE2ETest', 'GetDirection', function() { right = right.lastChild; assertEquals(Dir.BACKWARD, AutomationUtil.getDirection(right, left)); assertEquals(Dir.FORWARD, AutomationUtil.getDirection(left, right)); - }); }); + +TEST_F('AutomationUtilE2ETest', 'IsInSameWebpage', function() { + this.runWithLoadedTree(this.basicDoc, function(root) { + this.runWithLoadedTree(this.secondDoc, function(root2) { + chrome.automation.getDesktop(this.newCallback(function(desktop) { + assertTrue(AutomationUtil.isInSameWebpage(root, root)); + assertTrue(AutomationUtil.isInSameWebpage(root.firstChild, root)); + assertTrue(AutomationUtil.isInSameWebpage(root, root.firstChild)); + + assertTrue(AutomationUtil.isInSameWebpage(root2, root2)); + assertTrue(AutomationUtil.isInSameWebpage(root2.firstChild, root2)); + assertTrue(AutomationUtil.isInSameWebpage(root2, root2.firstChild)); + + assertFalse(AutomationUtil.isInSameWebpage(root, root2)); + assertFalse(AutomationUtil.isInSameWebpage(root.firstChild, root2)); + assertFalse(AutomationUtil.isInSameWebpage(root2.firstChild)); + assertFalse(AutomationUtil.isInSameWebpage( + root.firstChild, root2.firstChild)); + + assertFalse(AutomationUtil.isInSameWebpage(root, desktop)); + assertFalse(AutomationUtil.isInSameWebpage(root2, desktop)); + }.bind(this))); + }.bind(this)); + }.bind(this)); +}); + +TEST_F('AutomationUtilE2ETest', 'IsInSameWebpageIframe', function() { + // Wait for load complete on both outer frame and iframe. + var outerFrame; + var innerFrame; + var desktop; + var onSuccess = this.newCallback(function() { + assertTrue(AutomationUtil.isInSameWebpage(outerFrame, innerFrame)); + assertFalse(AutomationUtil.isInSameWebpage(outerFrame, desktop)); + assertFalse(AutomationUtil.isInSameWebpage(innerFrame, desktop)); + assertFalse(AutomationUtil.isInSameWebpage(outerFrame.parent, innerFrame)); + }.bind(this)); + + chrome.automation.getDesktop(function(r) { + desktop = r; + this.runWithTab(this.iframeDoc, function(newTabUrl) { + var listener = function(evt) { + if (evt.target.docUrl == newTabUrl) + outerFrame = evt.target; + if (evt.target.docUrl.indexOf('data:text/html') == 0) + innerFrame = evt.target; + + if (outerFrame && innerFrame) { + desktop.removeEventListener('loadComplete', listener, true); + onSuccess(); + } + }.bind(this); + desktop.addEventListener('loadComplete', listener, true); + }.bind(this)); + }.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 523e200..b7ba66a 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js @@ -8,11 +8,12 @@ */ goog.provide('Background'); -goog.provide('ChromeVoxMode'); goog.provide('global'); goog.require('AutomationPredicate'); goog.require('AutomationUtil'); +goog.require('ChromeVoxState'); +goog.require('LiveRegions'); goog.require('NextEarcons'); goog.require('Output'); goog.require('Output.EventType'); @@ -31,21 +32,13 @@ var EventType = chrome.automation.EventType; var RoleType = chrome.automation.RoleType; /** - * All possible modes ChromeVox can run. - * @enum {string} - */ -ChromeVoxMode = { - CLASSIC: 'classic', - COMPAT: 'compat', - NEXT: 'next', - FORCE_NEXT: 'force_next' -}; - -/** * ChromeVox2 background page. * @constructor + * @extends {ChromeVoxState} */ Background = function() { + ChromeVoxState.call(this); + /** * A list of site substring patterns to use with ChromeVox next. Keep these * strings relatively specific. @@ -127,35 +120,115 @@ Background = function() { // Classic keymap. cvox.ChromeVoxKbHandler.handlerKeyMap = cvox.KeyMap.fromDefaults(); - chrome.automation.addTreeChangeObserver(this.onTreeChange); + // Live region handler. + this.liveRegions_ = new LiveRegions(this); }; Background.prototype = { - /** Forces ChromeVox Next to be active for all tabs. */ - forceChromeVoxNextActive: function() { - this.setChromeVoxMode(ChromeVoxMode.FORCE_NEXT); - }, + __proto__: ChromeVoxState.prototype, - /** @type {ChromeVoxMode} */ - get mode() { + /** + * @override + */ + getMode: function() { return this.mode_; }, - /** @type {cursors.Range} */ - get currentRange() { + /** + * @override + */ + setMode: function(mode, opt_injectClassic) { + // Switching key maps potentially affects the key codes that involve + // sequencing. Without resetting this list, potentially stale key codes + // remain. The key codes themselves get pushed in + // cvox.KeySequence.deserialize which gets called by cvox.KeyMap. + cvox.ChromeVox.sequenceSwitchKeyCodes = []; + if (mode === ChromeVoxMode.CLASSIC || mode === ChromeVoxMode.COMPAT) + cvox.ChromeVoxKbHandler.handlerKeyMap = cvox.KeyMap.fromDefaults(); + else + cvox.ChromeVoxKbHandler.handlerKeyMap = cvox.KeyMap.fromNext(); + + if (mode == ChromeVoxMode.CLASSIC) { + if (chrome.commands && + chrome.commands.onCommand.hasListener(this.onGotCommand)) + chrome.commands.onCommand.removeListener(this.onGotCommand); + } else { + if (chrome.commands && + !chrome.commands.onCommand.hasListener(this.onGotCommand)) + chrome.commands.onCommand.addListener(this.onGotCommand); + } + + chrome.tabs.query({active: true}, function(tabs) { + if (mode === ChromeVoxMode.CLASSIC) { + // Generally, we don't want to inject classic content scripts as it is + // done by the extension system at document load. The exception is when + // we toggle classic on manually as part of a user command. + if (opt_injectClassic) + cvox.ChromeVox.injectChromeVoxIntoTabs(tabs); + } else { + // When in compat mode, if the focus is within the desktop tree proper, + // then do not disable content scripts. + if (this.currentRange_ && + this.currentRange_.start.node.root.role == RoleType.desktop) + return; + + this.disableClassicChromeVox_(); + } + }.bind(this)); + + // If switching out of a ChromeVox Next mode, make sure we cancel + // the progress loading sound just in case. + if ((this.mode_ === ChromeVoxMode.NEXT || + this.mode_ === ChromeVoxMode.FORCE_NEXT) && + this.mode_ != mode) { + cvox.ChromeVox.earcons.cancelEarcon(cvox.Earcon.PAGE_START_LOADING); + } + + this.mode_ = mode; + }, + + /** + * @override + */ + refreshMode: function(url) { + var mode = this.mode_; + if (mode != ChromeVoxMode.FORCE_NEXT) { + if (this.isWhitelistedForNext_(url)) + mode = ChromeVoxMode.NEXT; + else if (this.isBlacklistedForClassic_(url)) + mode = ChromeVoxMode.COMPAT; + else + mode = ChromeVoxMode.CLASSIC; + } + + this.setMode(mode); + }, + + /** + * @override + */ + getCurrentRange: function() { return this.currentRange_; }, - set currentRange(value) { - if (!value) + /** + * @override + */ + setCurrentRange: function(newRange) { + if (!newRange) return; - this.currentRange_ = value; + this.currentRange_ = newRange; if (this.currentRange_) this.currentRange_.start.node.makeVisible(); }, + /** Forces ChromeVox Next to be active for all tabs. */ + forceChromeVoxNextActive: function() { + this.setMode(ChromeVoxMode.FORCE_NEXT); + }, + /** * Handles ChromeVox Next commands. * @param {string} command @@ -331,8 +404,8 @@ Background.prototype = { .onSpeechEnd(function() { continueReading(prevRange); }) .go(); prevRange = this.currentRange_; - this.currentRange = - this.currentRange.move(cursors.Unit.NODE, Dir.FORWARD); + this.setCurrentRange( + this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD)); if (!this.currentRange_ || this.currentRange_.equals(prevRange)) global.isReadingContinuously = false; @@ -372,7 +445,7 @@ Background.prototype = { } else { newMode = ChromeVoxMode.FORCE_NEXT; } - this.setChromeVoxMode(newMode, true); + this.setMode(newMode, true); var isClassic = newMode == ChromeVoxMode.CLASSIC || newMode == ChromeVoxMode.COMPAT; @@ -408,7 +481,7 @@ Background.prototype = { actionNode.focus(); var prevRange = this.currentRange_; - this.currentRange = current; + this.setCurrentRange(current); new Output().withSpeechAndBraille( this.currentRange_, prevRange, Output.EventType.NAVIGATE) @@ -429,6 +502,8 @@ Background.prototype = { evt.preventDefault(); evt.stopPropagation(); } + + Output.flushNextSpeechUtterance(); }, /** @@ -481,67 +556,6 @@ Background.prototype = { }, /** - * Refreshes the current mode based on a url. - * @param {string} url - */ - refreshMode: function(url) { - var mode = this.mode_; - if (mode != ChromeVoxMode.FORCE_NEXT) { - if (this.isWhitelistedForNext_(url)) - mode = ChromeVoxMode.NEXT; - else if (this.isBlacklistedForClassic_(url)) - mode = ChromeVoxMode.COMPAT; - else - mode = ChromeVoxMode.CLASSIC; - } - - this.setChromeVoxMode(mode); - }, - - /** - * Called when the automation tree is changed. - * @param {chrome.automation.TreeChange} treeChange - */ - onTreeChange: function(treeChange) { - if (this.mode_ === ChromeVoxMode.CLASSIC || !cvox.ChromeVox.isActive) - return; - - var node = treeChange.target; - if (!node.containerLiveStatus) - return; - - if (node.containerLiveRelevant.indexOf('additions') >= 0 && - treeChange.type == 'nodeCreated') - this.outputLiveRegionChange_(node, null); - if (node.containerLiveRelevant.indexOf('text') >= 0 && - treeChange.type == 'nodeChanged') - this.outputLiveRegionChange_(node, null); - if (node.containerLiveRelevant.indexOf('removals') >= 0 && - treeChange.type == 'nodeRemoved') - this.outputLiveRegionChange_(node, '@live_regions_removed'); - }, - - /** - * Given a node that needs to be spoken as part of a live region - * change and an additional optional format string, output the - * live region description. - * @param {!chrome.automation.AutomationNode} node The changed node. - * @param {?string} opt_prependFormatStr If set, a format string for - * cvox2.Output to prepend to the output. - * @private - */ - outputLiveRegionChange_: function(node, opt_prependFormatStr) { - var range = cursors.Range.fromNode(node); - var output = new Output(); - if (opt_prependFormatStr) { - output.format(opt_prependFormatStr); - } - output.withSpeech(range, null, Output.EventType.NAVIGATE); - output.withSpeechCategory(cvox.TtsCategory.LIVE); - output.go(); - }, - - /** * Returns true if the url should have Classic running. * @return {boolean} * @private @@ -583,62 +597,6 @@ Background.prototype = { }, /** - * Sets the current ChromeVox mode. - * @param {ChromeVoxMode} mode - * @param {boolean=} opt_injectClassic Injects ChromeVox classic into tabs; - * defaults to false. - */ - setChromeVoxMode: function(mode, opt_injectClassic) { - // Switching key maps potentially affects the key codes that involve - // sequencing. Without resetting this list, potentially stale key codes - // remain. The key codes themselves get pushed in - // cvox.KeySequence.deserialize which gets called by cvox.KeyMap. - cvox.ChromeVox.sequenceSwitchKeyCodes = []; - if (mode === ChromeVoxMode.CLASSIC || mode === ChromeVoxMode.COMPAT) - cvox.ChromeVoxKbHandler.handlerKeyMap = cvox.KeyMap.fromDefaults(); - else - cvox.ChromeVoxKbHandler.handlerKeyMap = cvox.KeyMap.fromNext(); - - if (mode == ChromeVoxMode.CLASSIC) { - if (chrome.commands && - chrome.commands.onCommand.hasListener(this.onGotCommand)) - chrome.commands.onCommand.removeListener(this.onGotCommand); - } else { - if (chrome.commands && - !chrome.commands.onCommand.hasListener(this.onGotCommand)) - chrome.commands.onCommand.addListener(this.onGotCommand); - } - - chrome.tabs.query({active: true}, function(tabs) { - if (mode === ChromeVoxMode.CLASSIC) { - // Generally, we don't want to inject classic content scripts as it is - // done by the extension system at document load. The exception is when - // we toggle classic on manually as part of a user command. - if (opt_injectClassic) - cvox.ChromeVox.injectChromeVoxIntoTabs(tabs); - } else { - // When in compat mode, if the focus is within the desktop tree proper, - // then do not disable content scripts. - if (this.currentRange_ && - this.currentRange_.start.node.root.role == RoleType.desktop) - return; - - this.disableClassicChromeVox_(); - } - }.bind(this)); - - // If switching out of a ChromeVox Next mode, make sure we cancel - // the progress loading sound just in case. - if ((this.mode_ === ChromeVoxMode.NEXT || - this.mode_ === ChromeVoxMode.FORCE_NEXT) && - this.mode_ != mode) { - cvox.ChromeVox.earcons.cancelEarcon(cvox.Earcon.PAGE_START_LOADING); - } - - this.mode_ = mode; - }, - - /** * @param {!Spannable} text * @param {number} position * @private diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background_test.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background_test.extjs index fbeb836..36191c4 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/background_test.extjs +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/background_test.extjs @@ -216,49 +216,6 @@ TEST_F('BackgroundTest', 'ContinuousRead', function() { }); }); -TEST_F('BackgroundTest', 'LiveRegionAddElement', function() { - var mockFeedback = this.createMockFeedback(); - this.runWithLoadedTree( - function() {/*! - <h1>Document with live region</h1> - <p id="live" aria-live="polite"></p> - <button id="go">Go</button> - <script> - document.getElementById('go').addEventListener('click', function() { - document.getElementById('live').innerHTML = 'Hello, world'; - }, false); - </script> - */}, - function(rootNode) { - var go = rootNode.find({ role: RoleType.button }); - mockFeedback.call(go.doDefault.bind(go)) - .expectSpeech('Hello, world'); - mockFeedback.replay(); - }); -}); - -TEST_F('BackgroundTest', 'LiveRegionRemoveElement', function() { - var mockFeedback = this.createMockFeedback(); - this.runWithLoadedTree( - function() {/*! - <h1>Document with live region</h1> - <p id="live" aria-live="polite" aria-relevant="removals">Hello, world</p> - <button id="go">Go</button> - <script> - document.getElementById('go').addEventListener('click', function() { - document.getElementById('live').innerHTML = ''; - }, false); - </script> - */}, - function(rootNode) { - var go = rootNode.find({ role: RoleType.button }); - go.doDefault(); - mockFeedback.expectSpeech('removed:') - .expectSpeech('Hello, world'); - mockFeedback.replay(); - }); -}); - TEST_F('BackgroundTest', 'InitialFocus', function() { var mockFeedback = this.createMockFeedback(); this.runWithLoadedTree('<a href="a">a</a>', diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/chromevox_state.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/chromevox_state.js new file mode 100644 index 0000000..25ba09e --- /dev/null +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/chromevox_state.js @@ -0,0 +1,93 @@ +// Copyright 2015 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 An interface for querying and modifying the global + * ChromeVox state, to avoid direct dependencies on the Background + * object and to facilitate mocking for tests. + */ + +goog.provide('ChromeVoxMode'); +goog.provide('ChromeVoxState'); + +goog.require('cursors.Cursor'); + +/** + * All possible modes ChromeVox can run. + * @enum {string} + */ +ChromeVoxMode = { + CLASSIC: 'classic', + COMPAT: 'compat', + NEXT: 'next', + FORCE_NEXT: 'force_next' +}; + +/** + * ChromeVox2 state object. + * @constructor + */ +ChromeVoxState = function() { + if (ChromeVoxState.instance) + throw 'Trying to create two instances of singleton ChromeVoxState.'; + ChromeVoxState.instance = this; +}; + +/** + * @type {ChromeVoxState} + */ +ChromeVoxState.instance; + +ChromeVoxState.prototype = { + /** @type {ChromeVoxMode} */ + get mode() { + return this.getMode(); + }, + + /** + * @return {ChromeVoxMode} The current mode. + * @protected + */ + getMode: function() { + return ChromeVoxMode.NEXT; + }, + + /** + * Sets the current ChromeVox mode. + * @param {ChromeVoxMode} mode + * @param {boolean=} opt_injectClassic Injects ChromeVox classic into tabs; + * defaults to false. + */ + setMode: function(mode, opt_injectClassic) { + throw new Error('setMode must be implemented by subclass.'); + }, + + /** + * Refreshes the current mode based on a url. + * @param {string} url + */ + refreshMode: function(url) { + throw new Error('refresthMode must be implemented by subclass.'); + }, + + /** @type {cursors.Range} */ + get currentRange() { + return this.getCurrentRange(); + }, + + /** + * @return {cursors.Range} The current range. + * @protected + */ + getCurrentRange: function() { + return null; + }, + + /** + * @param {cursors.Range} newRange The new range. + */ + setCurrentRange: function(newRange) { + throw new Error('setCurrentRange must be implemented by subclass.'); + }, +}; diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/desktop_automation_handler.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/desktop_automation_handler.js index c164545..4b6df72 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/desktop_automation_handler.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/desktop_automation_handler.js @@ -8,8 +8,8 @@ goog.provide('DesktopAutomationHandler'); -goog.require('Background'); goog.require('BaseAutomationHandler'); +goog.require('ChromeVoxState'); goog.scope(function() { var AutomationEvent = chrome.automation.AutomationEvent; @@ -61,20 +61,20 @@ DesktopAutomationHandler.prototype = { if (!node) return; - var prevRange = global.backgroundObj.currentRange; + var prevRange = ChromeVoxState.instance.currentRange; - global.backgroundObj.currentRange = cursors.Range.fromNode(node); + ChromeVoxState.instance.setCurrentRange(cursors.Range.fromNode(node)); // Check to see if we've crossed roots. Continue if we've crossed roots or // are not within web content. if (node.root.role == RoleType.desktop || !prevRange || prevRange.start.node.root != node.root) - global.backgroundObj.refreshMode(node.root.docUrl || ''); + ChromeVoxState.instance.refreshMode(node.root.docUrl || ''); // Don't process nodes inside of web content if ChromeVox Next is inactive. if (node.root.role != RoleType.desktop && - global.backgroundObj.mode === ChromeVoxMode.CLASSIC) { + ChromeVoxState.instance.mode === ChromeVoxMode.CLASSIC) { if (cvox.ChromeVox.isChromeOS) chrome.accessibilityPrivate.setFocusRing([]); return; @@ -83,11 +83,11 @@ DesktopAutomationHandler.prototype = { // Don't output if focused node hasn't changed. if (prevRange && evt.type == 'focus' && - global.backgroundObj.currentRange.equals(prevRange)) + ChromeVoxState.instance.currentRange.equals(prevRange)) return; new Output().withSpeechAndBraille( - global.backgroundObj.currentRange, prevRange, evt.type) + ChromeVoxState.instance.currentRange, prevRange, evt.type) .go(); }, @@ -102,7 +102,7 @@ DesktopAutomationHandler.prototype = { // Don't process nodes inside of web content if ChromeVox Next is inactive. if (node.root.role != RoleType.desktop && - global.backgroundObj.mode === ChromeVoxMode.CLASSIC) { + ChromeVoxState.instance.mode === ChromeVoxMode.CLASSIC) { return; } @@ -129,7 +129,7 @@ DesktopAutomationHandler.prototype = { if (node.role == RoleType.rootWebArea) { // Discard focus events for root web areas when focus was previously // placed on a descendant. - var currentRange = global.backgroundObj.currentRange; + var currentRange = ChromeVoxState.instance.currentRange; if (currentRange && currentRange.start.node.root == node) return; @@ -152,20 +152,20 @@ DesktopAutomationHandler.prototype = { * @param {Object} evt */ onLoadComplete: function(evt) { - global.backgroundObj.refreshMode(evt.target.docUrl); + ChromeVoxState.instance.refreshMode(evt.target.docUrl); // Don't process nodes inside of web content if ChromeVox Next is inactive. if (evt.target.root.role != RoleType.desktop && - global.backgroundObj.mode === ChromeVoxMode.CLASSIC) + ChromeVoxState.instance.mode === ChromeVoxMode.CLASSIC) return; // If initial focus was already placed on this page (e.g. if a user starts // tabbing before load complete), then don't move ChromeVox's position on // the page. - if (global.backgroundObj.currentRange && - global.backgroundObj.currentRange.start.node.role != + if (ChromeVoxState.instance.currentRange && + ChromeVoxState.instance.currentRange.start.node.role != RoleType.rootWebArea && - global.backgroundObj.currentRange.start.node.root.docUrl == + ChromeVoxState.instance.currentRange.start.node.root.docUrl == evt.target.docUrl) return; @@ -182,11 +182,11 @@ DesktopAutomationHandler.prototype = { AutomationPredicate.leaf); if (node) - global.backgroundObj.currentRange = cursors.Range.fromNode(node); + ChromeVoxState.instance.setCurrentRange(cursors.Range.fromNode(node)); - if (global.backgroundObj.currentRange) + if (ChromeVoxState.instance.currentRange) new Output().withSpeechAndBraille( - global.backgroundObj.currentRange, null, evt.type) + ChromeVoxState.instance.currentRange, null, evt.type) .go(); }, @@ -200,15 +200,16 @@ DesktopAutomationHandler.prototype = { // Don't process nodes inside of web content if ChromeVox Next is inactive. if (evt.target.root.role != RoleType.desktop && - global.backgroundObj.mode === ChromeVoxMode.CLASSIC) + ChromeVoxState.instance.mode === ChromeVoxMode.CLASSIC) return; if (!evt.target.state.focused) return; - if (!global.backgroundObj.currentRange) { + if (!ChromeVoxState.instance.currentRange) { this.onEventDefault(evt); - global.backgroundObj.currentRange = cursors.Range.fromNode(evt.target); + ChromeVoxState.instance.setCurrentRange( + cursors.Range.fromNode(evt.target)); } this.createEditableTextHandlerIfNeeded_(evt.target); @@ -222,7 +223,7 @@ DesktopAutomationHandler.prototype = { this.editableTextHandler_.changed(textChangeEvent); new Output().withBraille( - global.backgroundObj.currentRange, null, evt.type) + ChromeVoxState.instance.currentRange, null, evt.type) .go(); }, @@ -233,16 +234,18 @@ DesktopAutomationHandler.prototype = { onValueChanged: function(evt) { // Don't process nodes inside of web content if ChromeVox Next is inactive. if (evt.target.root.role != RoleType.desktop && - global.backgroundObj.mode === ChromeVoxMode.CLASSIC) + ChromeVoxState.instance.mode === ChromeVoxMode.CLASSIC) return; if (!evt.target.state.focused) return; // Value change events fire on web editables when typing. Suppress them. - if (!global.backgroundObj.currentRange || !this.isEditable_(evt.target)) { + if (!ChromeVoxState.instance.currentRange || + !this.isEditable_(evt.target)) { this.onEventDefault(evt); - global.backgroundObj.currentRange = cursors.Range.fromNode(evt.target); + ChromeVoxState.instance.setCurrentRange( + cursors.Range.fromNode(evt.target)); } }, @@ -251,7 +254,7 @@ DesktopAutomationHandler.prototype = { * @override */ onScrollPositionChanged: function(evt) { - var currentRange = global.backgroundObj.currentRange; + var currentRange = ChromeVoxState.instance.currentRange; if (currentRange) new Output().withLocation(currentRange, null, evt.type).go(); }, @@ -262,7 +265,7 @@ DesktopAutomationHandler.prototype = { */ createEditableTextHandlerIfNeeded_: function(node) { if (!this.editableTextHandler_ || - node != global.backgroundObj.currentRange.start.node) { + node != ChromeVoxState.instance.currentRange.start.node) { var start = node.textSelStart; var end = node.textSelEnd; if (start > end) { diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions.js new file mode 100644 index 0000000..42ed81c --- /dev/null +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions.js @@ -0,0 +1,159 @@ +// Copyright 2015 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 Implements support for live regions in ChromeVox Next. + */ + +goog.provide('LiveRegions'); + +goog.require('ChromeVoxState'); + +goog.scope(function() { +var AutomationNode = chrome.automation.AutomationNode; +var TreeChange = chrome.automation.TreeChange; + +/** + * ChromeVox2 live region handler. + * @param {!ChromeVoxState} chromeVoxState The ChromeVox state object, + * keeping track of the current mode and current range. + * @constructor + */ +LiveRegions = function(chromeVoxState) { + /** + * @type {!ChromeVoxState} + * @private + */ + this.chromeVoxState_ = chromeVoxState; + + /** + * The time the last live region event was output. + * @type {!Date} + * @private + */ + this.lastLiveRegionTime_ = new Date(0); + + /** + * Set of nodes that have been announced as part of a live region since + * |this.lastLiveRegionTime_|, to prevent duplicate announcements. + * @type {!WeakSet<AutomationNode>} + * @private + */ + this.liveRegionNodeSet_ = new WeakSet(); + + chrome.automation.addTreeChangeObserver( + 'liveRegionTreeChanges', this.onTreeChange.bind(this)); +}; + +/** + * Live region events received in fewer than this many milliseconds will + * queue, otherwise they'll be output with a category flush. + * @type {number} + * @const + */ +LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 500; + +/** + * Whether live regions from background tabs should be announced or not. + * @type {boolean} + * @private + */ +LiveRegions.announceLiveRegionsFromBackgroundTabs_ = true; + +LiveRegions.prototype = { + /** + * Called when the automation tree is changed. + * @param {TreeChange} treeChange + */ + onTreeChange: function(treeChange) { + var node = treeChange.target; + if (!node.containerLiveStatus) + return; + + var mode = this.chromeVoxState_.mode; + var currentRange = this.chromeVoxState_.currentRange; + + if (mode === ChromeVoxMode.CLASSIC || !cvox.ChromeVox.isActive) + return; + + if (!currentRange) + return; + + if (!LiveRegions.announceLiveRegionsFromBackgroundTabs_ && + !AutomationUtil.isInSameWebpage(node, currentRange.start.node)) { + return; + } + + var type = treeChange.type; + var relevant = node.containerLiveRelevant; + if (relevant.indexOf('additions') >= 0 && + (type == 'nodeCreated' || type == 'subtreeCreated')) { + this.outputLiveRegionChange_(node, null); + } + + if (relevant.indexOf('text') >= 0 && type == 'nodeChanged') + this.outputLiveRegionChange_(node, null); + + if (relevant.indexOf('removals') >= 0 && type == 'nodeRemoved') + this.outputLiveRegionChange_(node, '@live_regions_removed'); + }, + + /** + * Given a node that needs to be spoken as part of a live region + * change and an additional optional format string, output the + * live region description. + * @param {!AutomationNode} node The changed node. + * @param {?string=} opt_prependFormatStr If set, a format string for + * cvox2.Output to prepend to the output. + * @private + */ + outputLiveRegionChange_: function(node, opt_prependFormatStr) { + if (node.containerLiveBusy) + return; + + if (node.containerLiveAtomic && !node.liveAtomic) { + if (node.parent) + this.outputLiveRegionChange_(node.parent, opt_prependFormatStr); + return; + } + + var range = cursors.Range.fromNode(node); + var output = new Output(); + if (opt_prependFormatStr) + output.format(opt_prependFormatStr); + output.withSpeech(range, range, Output.EventType.NAVIGATE); + + if (!output.hasSpeech && node.liveAtomic) + output.format('$descendants', node); + + output.withSpeechCategory(cvox.TtsCategory.LIVE); + + if (!output.hasSpeech) + return; + + // Enqueue live region updates that were received at approximately + // the same time, otherwise flush previous live region updates. + var currentTime = new Date(); + var queueTime = LiveRegions.LIVE_REGION_QUEUE_TIME_MS; + if (currentTime - this.lastLiveRegionTime_ > queueTime) { + this.liveRegionNodeSet_ = new WeakSet(); + output.withQueueMode(cvox.QueueMode.CATEGORY_FLUSH); + this.lastLiveRegionTime_ = currentTime; + } else { + output.withQueueMode(cvox.QueueMode.QUEUE); + } + + var parent = node; + while (parent) { + if (this.liveRegionNodeSet_.has(parent)) + return; + parent = parent.parent; + } + + this.liveRegionNodeSet_.add(node); + output.go(); + }, +}; + +}); // goog.scope diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions_test.extjs b/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions_test.extjs new file mode 100644 index 0000000..3a06754 --- /dev/null +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions_test.extjs @@ -0,0 +1,216 @@ +// Copyright 2015 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 Live Regions. + * @constructor + * @extends {ChromeVoxNextE2ETest} + */ +function LiveRegionsTest() { + ChromeVoxNextE2ETest.call(this); +} + +LiveRegionsTest.prototype = { + __proto__: ChromeVoxNextE2ETest.prototype, + + /** @override */ + setUp: function() { + global.backgroundObj.forceChromeVoxNextActive(); + window.RoleType = chrome.automation.RoleType; + }, + + /** + * @return {!MockFeedback} + */ + createMockFeedback: function() { + var mockFeedback = new MockFeedback(this.newCallback(), + this.newCallback.bind(this)); + mockFeedback.install(); + return mockFeedback; + }, + + /** + * Create a function which performs the command |cmd|. + * @param {string} cmd + * @return {function() : void} + */ + doCmd: function(cmd) { + return function() { + global.backgroundObj.onGotCommand(cmd); + }; + }, +}; + +TEST_F('LiveRegionsTest', 'LiveRegionAddElement', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <h1>Document with live region</h1> + <p id="live" aria-live="polite"></p> + <button id="go">Go</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('live').innerHTML = 'Hello, world'; + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectCategoryFlushSpeech('Hello, world'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'LiveRegionRemoveElement', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <h1>Document with live region</h1> + <p id="live" aria-live="polite" aria-relevant="removals">Hello, world</p> + <button id="go">Go</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('live').innerHTML = ''; + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + go.doDefault(); + mockFeedback.expectCategoryFlushSpeech('removed:') + .expectQueuedSpeech('Hello, world'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'LiveRegionChangeAtomic', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <div id="live" aria-live="polite" aria-atomic="true"> + <div id="a"></div><div id="b">Bravo</div><div id="c"></div> + </div> + <button id="go">Go</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('c').textContent = 'Charlie'; + document.getElementById('a').textContent = 'Alpha'; + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectQueuedSpeech('Alpha') + .expectQueuedSpeech('Bravo') + .expectQueuedSpeech('Charlie'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'LiveRegionChangeImageAlt', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <div id="live" aria-live="polite"> + <img id="img" src="#" alt="Before"> + </div> + <button id="go">Go</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('img').setAttribute('alt', 'After'); + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectCategoryFlushSpeech('After') + .expectQueuedSpeech('Image'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'LiveRegionThenFocus', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <div id="live" aria-live="polite"></div> + <button id="go">Go</button> + <button id="focus">Focus</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('live').textContent = 'Live'; + window.setTimeout(function() { + document.getElementById('focus').focus(); + }, 50); + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectCategoryFlushSpeech('Live') + .expectQueuedSpeech('Focus'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'FocusThenLiveRegion', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <div id="live" aria-live="polite"></div> + <button id="go">Go</button> + <button id="focus">Focus</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('focus').focus(); + window.setTimeout(function() { + document.getElementById('live').textContent = 'Live'; + }, 50); + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectQueuedSpeech('Focus') + .expectCategoryFlushSpeech('Live'); + mockFeedback.replay(); + }); +}); + +TEST_F('LiveRegionsTest', 'LiveRegionCategoryFlush', function() { + var mockFeedback = this.createMockFeedback(); + this.runWithLoadedTree( + function() {/*! + <div id="live1" aria-live="polite"></div> + <div id="live2" aria-live="polite"></div> + <button id="go">Go</button> + <button id="focus">Focus</button> + <script> + document.getElementById('go').addEventListener('click', function() { + document.getElementById('live1').textContent = 'Live1'; + window.setTimeout(function() { + document.getElementById('live2').textContent = 'Live2'; + }, 1000); + }, false); + </script> + */}, + function(rootNode) { + var go = rootNode.find({ role: RoleType.button }); + mockFeedback.call(go.doDefault.bind(go)) + .expectCategoryFlushSpeech('Live1') + .expectCategoryFlushSpeech('Live2'); + mockFeedback.replay(); + }); +}); diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js index 1bea384..0b9313e 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js @@ -64,14 +64,30 @@ Output = function() { /** * Current global options. * @type {{speech: boolean, braille: boolean}} + * @private */ this.formatOptions_ = {speech: true, braille: false}; /** * Speech properties to apply to the entire output. * @type {!Object<*>} + * @private */ this.speechProperties_ = {}; + + /** + * The speech category for the generated speech utterance. + * @type {cvox.TtsCategory} + * @private + */ + this.speechCategory_ = cvox.TtsCategory.NAV; + + /** + * The speech queue mode for the generated speech utterance. + * @type {cvox.QueueMode} + * @private + */ + this.queueMode_ = cvox.QueueMode.QUEUE; }; /** @@ -587,6 +603,22 @@ Output.EventType = { NAVIGATE: 'navigate' }; +/** + * If true, the next speech utterance will flush instead of the normal + * queueing mode. + * @type {boolean} + * @private + */ +Output.flushNextSpeechUtterance_ = false; + +/** + * Calling this will make the next speech utterance flush even if it would + * normally queue or do a category flush. + */ +Output.flushNextSpeechUtterance = function() { + Output.flushNextSpeechUtterance_ = true; +}; + Output.prototype = { /** * Gets the spoken output with separator '|'. @@ -611,6 +643,17 @@ Output.prototype = { }, /** + * @return {boolean} True if there's any speech that will be output. + */ + get hasSpeech() { + for (var i = 0; i < this.speechBuffer_.length; i++) { + if (this.speechBuffer_[i].trim().length) + return true; + } + return false; + }, + + /** * Specify ranges for speech. * @param {!cursors.Range} range * @param {cursors.Range} prevRange @@ -668,7 +711,17 @@ Output.prototype = { * @return {!Output} */ withSpeechCategory: function(category) { - this.speechProperties_['category'] = category; + this.speechCategory_ = category; + return this; + }, + + /** + * Applies the given speech queue mode to the output. + * @param {cvox.QueueMode} queueMode The queueMode for the speech. + * @return {!Output} + */ + withQueueMode: function(queueMode) { + this.queueMode_ = queueMode; return this; }, @@ -676,14 +729,18 @@ Output.prototype = { * Apply a format string directly to the output buffer. This lets you * output a message directly to the buffer using the format syntax. * @param {string} formatStr + * @param {!chrome.automation.AutomationNode=} opt_node An optional + * node to apply the formatting to. * @return {!Output} */ - format: function(formatStr) { + format: function(formatStr, opt_node) { + var node = opt_node || null; + this.formatOptions_ = {speech: true, braille: false}; - this.format_(null, formatStr, this.speechBuffer_); + this.format_(node, formatStr, this.speechBuffer_); this.formatOptions_ = {speech: false, braille: true}; - this.format_(null, formatStr, this.brailleBuffer_); + this.format_(node, formatStr, this.brailleBuffer_); return this; }, @@ -705,8 +762,14 @@ Output.prototype = { */ go: function() { // Speech. - var queueMode = this.speechProperties_['category'] ? - cvox.QueueMode.CATEGORY_FLUSH : cvox.QueueMode.FLUSH; + var queueMode = this.queueMode_; + if (Output.flushNextSpeechUtterance_) { + queueMode = cvox.QueueMode.FLUSH; + Output.flushNextSpeechUtterance_ = false; + } + + this.speechProperties_.category = this.speechCategory_; + this.speechBuffer_.forEach(function(buff, i, a) { (function() { var scopedBuff = buff; @@ -754,8 +817,10 @@ Output.prototype = { } // Display. - if (cvox.ChromeVox.isChromeOS) + if (cvox.ChromeVox.isChromeOS && + this.speechCategory_ != cvox.TtsCategory.LIVE) { chrome.accessibilityPrivate.setFocusRing(this.locations_); + } }, /** diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/tabs_automation_handler.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/tabs_automation_handler.js index f507215..943e0cb 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/tabs_automation_handler.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/tabs_automation_handler.js @@ -40,7 +40,7 @@ TabsAutomationHandler.prototype = { /** @override */ onLoadComplete: function(evt) { - global.backgroundObj.refreshMode(evt.target.docUrl); + ChromeVoxState.instance.refreshMode(evt.target.docUrl); var focused = evt.target.find({state: {focused: true}}) || evt.target; this.onFocus({target: focused, type: EventType.focus}); } 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 bc7744c..d9d5089 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 @@ -73,7 +73,8 @@ ChromeVoxE2ETest.prototype = { /** * Launches the given document in a new tab. * @param {function() : void} doc Snippet wrapped inside of a function. - * @param {function()} opt_callback Called once the document is created. + * @param {function(url: string)} opt_callback Called once the + * document is created. */ runWithTab: function(doc, opt_callback) { var docString = TestUtils.extractHtmlFromCommentEncodedString(doc); @@ -84,7 +85,10 @@ ChromeVoxE2ETest.prototype = { active: true, url: url }; - chrome.tabs.create(createParams, opt_callback); + chrome.tabs.create(createParams, function(tab) { + if (opt_callback) + opt_callback(tab.url); + }); }, /** diff --git a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_next_e2e_test_base.js b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_next_e2e_test_base.js index bb9f1fb0..a346bf8 100644 --- a/chrome/browser/resources/chromeos/chromevox/testing/chromevox_next_e2e_test_base.js +++ b/chrome/browser/resources/chromeos/chromevox/testing/chromevox_next_e2e_test_base.js @@ -34,17 +34,17 @@ ChromeVoxNextE2ETest.prototype = { runWithLoadedTree: function(doc, callback) { callback = this.newCallback(callback); chrome.automation.getDesktop(function(r) { - var listener = function(evt) { - if (!evt.target.docUrl || - evt.target.docUrl.indexOf('test') == -1) - return; + this.runWithTab(doc, function(newTabUrl) { + var listener = function(evt) { + if (!evt.target.docUrl || evt.target.docUrl != newTabUrl) + return; - r.removeEventListener('loadComplete', listener, true); - callback && callback(evt.target); - callback = null; - }; - r.addEventListener('loadComplete', listener, true); - this.runWithTab(doc); + r.removeEventListener('loadComplete', listener, true); + callback && callback(evt.target); + callback = null; + }; + r.addEventListener('loadComplete', listener, true); + }.bind(this)); }.bind(this)); }, diff --git a/chrome/browser/resources/chromeos/chromevox/testing/mock_feedback.js b/chrome/browser/resources/chromeos/chromevox/testing/mock_feedback.js index 4c846e8..ec90c10 100644 --- a/chrome/browser/resources/chromeos/chromevox/testing/mock_feedback.js +++ b/chrome/browser/resources/chromeos/chromevox/testing/mock_feedback.js @@ -160,6 +160,55 @@ MockFeedback.prototype = { }, /** + * Adds an expectation for one spoken utterance that will be enqueued + * with a given queue mode. + * @param {string|RegExp} text One utterance expectation. + * @param {cvox.QueueMode} queueMode The expected queue mode. + * @return {MockFeedback} |this| for chaining + */ + expectSpeechWithQueueMode: function(text, queueMode) { + assertFalse(this.replaying_); + this.pendingActions_.push({ + perform: function() { + return !!MockFeedback.matchAndConsume_( + text, {queueMode: queueMode}, this.pendingUtterances_); + }.bind(this), + toString: function() { + return 'Speak \'' + text + '\' with mode ' + queueMode; + } + }); + return this; + }, + + /** + * Adds an expectation for one spoken utterance that will be queued. + * @param {string|RegExp} text One utterance expectation. + * @return {MockFeedback} |this| for chaining + */ + expectQueuedSpeech: function(text) { + return this.expectSpeechWithQueueMode(text, cvox.QueueMode.QUEUE); + }, + + /** + * Adds an expectation for one spoken utterance that will be flushed. + * @param {string|RegExp} text One utterance expectation. + * @return {MockFeedback} |this| for chaining + */ + expectFlushingSpeech: function(text) { + return this.expectSpeechWithQueueMode(text, cvox.QueueMode.FLUSH); + }, + + /** + * Adds an expectation for one spoken utterance that will be queued + * with the category flush mode. + * @param {string|RegExp} text One utterance expectation. + * @return {MockFeedback} |this| for chaining + */ + expectCategoryFlushSpeech: function(text) { + return this.expectSpeechWithQueueMode(text, cvox.QueueMode.CATEGORY_FLUSH); + }, + + /** * Adds an expectation that the next spoken utterances do *not* match * the given arguments. * @@ -299,6 +348,7 @@ MockFeedback.prototype = { } this.pendingUtterances_.push( {text: textString, + queueMode: queueMode, callback: callback}); this.process_(); }, |