summaryrefslogtreecommitdiffstats
path: root/chrome/browser
diff options
context:
space:
mode:
authordmazzoni <dmazzoni@chromium.org>2015-12-01 13:16:57 -0800
committerCommit bot <commit-bot@chromium.org>2015-12-01 21:17:57 +0000
commiteaa0c42071267f268cf77b5fc66fbfc9b12d2e41 (patch)
tree093e1c97a683a0d069aa0ff99668119d395b0bca /chrome/browser
parent788eaa142aaf2187981ebaf954f8efe9fce77eb5 (diff)
downloadchromium_src-eaa0c42071267f268cf77b5fc66fbfc9b12d2e41.zip
chromium_src-eaa0c42071267f268cf77b5fc66fbfc9b12d2e41.tar.gz
chromium_src-eaa0c42071267f268cf77b5fc66fbfc9b12d2e41.tar.bz2
Complete live region support in ChromeVox Next.
Optimizes tree change notifications so that ChromeVox can only listen to those relevant to live regions, and skip the rest. Implements aria-atomic, and adds some code to avoid duplicate speaking of changes to a live region due to multiple tree changes. Improves speech queue mode support by always flushing following any key event, queueing otherwise, and doing a category flush when a new live region event happens 500 ms after the last one. The last one is a heuristic, but the idea is that multiple live region events at the same time should queue up, but a new live region some discrete time later should interrupt previous live region events. Adds some tests for live region output including flushing and queueing behavior. Manual tests include the live region test suite created for Google Docs testing. There are probably some bugs and corner cases left, but this gets us a lot closer. BUG=478217 Review URL: https://codereview.chromium.org/1457683009 Cr-Commit-Position: refs/heads/master@{#362500}
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_();
},