summaryrefslogtreecommitdiffstats
path: root/chrome/browser
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser')
-rw-r--r--chrome/browser/resources/chromeos/chromevox/chromevox.gni2
-rw-r--r--chrome/browser/resources/chromeos/chromevox/chromevox_tests.gypi1
-rw-r--r--chrome/browser/resources/chromeos/chromevox/common/chrome_extension_externs.js54
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util.js21
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/automation_util_test.extjs73
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/background.js248
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/background_test.extjs43
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/chromevox_state.js93
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/desktop_automation_handler.js55
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions.js159
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/live_regions_test.extjs216
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js79
-rw-r--r--chrome/browser/resources/chromeos/chromevox/cvox2/background/tabs_automation_handler.js2
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/chromevox_e2e_test_base.js8
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/chromevox_next_e2e_test_base.js20
-rw-r--r--chrome/browser/resources/chromeos/chromevox/testing/mock_feedback.js50
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_();
},