1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview This file contains the |MockFeedback| class which is a
7 * combined mock class for speech and braille feedback. A test that uses
8 * this class may add expectations for speech utterances and braille display
9 * content to be output. The |install| method sets appropriate mock classes
10 * as the |cvox.ChromeVox.tts| and |cvox.ChromeVox.braille| objects,
11 * respectively. Output sent to those objects will then be collected in
14 * Expectations can be added using the |expectSpeech| and |expectBraille|
15 * methods. These methods take either strings or regular expressions to match
16 * against. Strings must match a full utterance (or display content) exactly,
17 * while a regular expression must match a substring (use anchor operators if
20 * Function calls may be inserted in the stream of expectations using the
21 * |call| method. Such callbacks are called after all preceding expectations
22 * have been met, and before any further expectations are matched. Callbacks
23 * are called in the order they were added to the mock.
25 * The |replay| method starts processing any pending utterances and braille
26 * display content and will try to match expectations as new feedback enters
27 * the queue asynchronously. When all expectations have been met and callbacks
28 * called, the finish callback, if any was provided to the constructor, is
31 * This mock class is lean, meaning that feedback that doesn't match
32 * any expectations is silently ignored.
34 * NOTE: for asynchronous tests, the processing will never finish if there
35 * are unmet expectations. To help debugging in such situations, the mock
36 * will output its pending state if there are pending expectations and no
37 * output is received within a few seconds.
39 * See mock_feedback_test.js for example usage of this class.
43 * Combined mock class for braille and speech output.
44 * @param {function=} opt_finishedCallback Called when all expectations have
48 var MockFeedback = function(opt_finishedCallback) {
53 this.finishedCallback_ = opt_finishedCallback || null;
55 * True when |replay| has been called and actions are being replayed.
59 this.replaying_ = false;
61 * True when inside the |process| function to prevent nested calls.
65 this.inProcess_ = false;
67 * Pending expectations and callbacks.
68 * @type {Array<{perform: function(): boolean, toString: function(): string}>}
71 this.pendingActions_ = [];
73 * Pending speech utterances.
74 * @type {Array<{text: string, callback: (function|undefined)}>}
77 this.pendingUtterances_ = [];
79 * Pending braille output.
80 * @type {Array<{text: string, callback: (function|undefined)}>}
83 this.pendingBraille_ = [];
85 * Handle for the timeout set for debug logging.
89 this.logTimeoutId_ = 0;
91 * @type {cvox.NavBraille}
94 this.lastMatchedBraille_ = null;
97 MockFeedback.prototype = {
100 * Install mock objects as |cvox.ChromeVox.tts| and |cvox.ChromeVox.braille|
101 * to collect feedback.
103 install: function() {
104 assertFalse(this.replaying_);
106 var MockTts = function() {};
107 MockTts.prototype = {
108 __proto__: cvox.TtsInterface.prototype,
109 speak: this.addUtterance_.bind(this)
112 cvox.ChromeVox.tts = new MockTts();
114 var MockBraille = function() {};
115 MockBraille.prototype = {
116 __proto__: cvox.BrailleInterface.prototype,
117 write: this.addBraille_.bind(this)
120 cvox.ChromeVox.braille = new MockBraille();
124 * Adds an expectation for one or more spoken utterances.
125 * @param {...(string|RegExp)} var_args One or more utterance to add as
127 * @return {MockFeedback} |this| for chaining
129 expectSpeech: function() {
130 assertFalse(this.replaying_);
131 Array.prototype.forEach.call(arguments, function(text) {
132 this.pendingActions_.push({
133 perform: function() {
134 return !!MockFeedback.matchAndConsume_(
135 text, {}, this.pendingUtterances_);
137 toString: function() { return 'Speak \'' + text + '\''; }
144 * Adds an expectation for braille output.
145 * @param {string|RegExp} text
146 * @param {Object=} opt_props Additional properties to match in the
148 * @return {MockFeedback} |this| for chaining
150 expectBraille: function(text, opt_props) {
151 assertFalse(this.replaying_);
152 var props = opt_props || {};
153 this.pendingActions_.push({
154 perform: function() {
155 var match = MockFeedback.matchAndConsume_(
156 text, props, this.pendingBraille_);
158 this.lastMatchedBraille_ = match;
161 toString: function() {
162 return 'Braille \'' + text + '\' ' + JSON.stringify(props);
169 * Arranges for a callback to be invoked when all expectations that were
170 * added before this call have been met. Callbacks are called in the
171 * order they are added.
172 * @param {Function} callback
173 * @return {MockFeedback} |this| for chaining
175 call: function(callback) {
176 assertFalse(this.replaying_);
177 this.pendingActions_.push({
178 perform: function() {
182 toString: function() {
190 * Processes any feedback that has been received so far and treis to
191 * satisfy the registered expectations. Any feedback that is received
192 * after this call (via the installed mock objects) is processed immediately.
193 * When all expectations are satisfied and registered callbacks called,
194 * the finish callbcak, if any, is called.
195 * This function may only be called once.
198 assertFalse(this.replaying_);
199 this.replaying_ = true;
204 * Returns the |NavBraille| that matched an expectation. This is
205 * intended to be used by a callback to invoke braille commands that
206 * depend on display contents.
207 * @type {cvox.NavBraille}
209 get lastMatchedBraille() {
210 assertTrue(this.replaying_);
211 return this.lastMatchedBraille_;
215 * @param {string} textString
216 * @param {cvox.QueueMode} queueMode
217 * @param {Object=} properties
220 addUtterance_: function(textString, queueMode, properties) {
222 if (properties && (properties.startCallback || properties.endCallback)) {
223 var startCallback = properties.startCallback;
224 var endCallback = properties.endCallback;
225 callback = function() {
226 startCallback && startCallback();
227 endCallback && endCallback();
230 this.pendingUtterances_.push(
232 callback: callback});
237 addBraille_: function(navBraille) {
238 this.pendingBraille_.push(navBraille);
243 process_: function() {
244 if (!this.replaying_ || this.inProcess_)
247 this.inProcess_ = true;
248 while (this.pendingActions_.length > 0) {
249 var action = this.pendingActions_[0];
250 if (action.perform()) {
251 this.pendingActions_.shift();
252 if (this.logTimeoutId_) {
253 window.clearTimeout(this.logTimeoutId_);
254 this.logTimeoutId_ = 0;
260 if (this.pendingActions_.length == 0) {
261 if (this.finishedCallback_) {
262 this.finishedCallback_();
263 this.finishedCallback_ = null;
266 // If there are pending actions and no matching feedback for a few
267 // seconds, log the pending state to ease debugging.
268 if (!this.logTimeoutId_) {
269 this.logTimeoutId_ = window.setTimeout(
270 this.logPendingState_.bind(this), 2000);
274 this.inProcess_ = false;
279 logPendingState_: function() {
280 if (this.pendingActions_.length > 0)
281 console.log('Still waiting for ' + this.pendingActions_[0].toString());
282 function logPending(desc, list) {
284 console.log('Pending ' + desc + ':\n ' +
285 list.map(function(i) {
286 var ret = '\'' + i.text + '\'';
287 if ('startIndex' in i)
288 ret += ' startIndex=' + i.startIndex;
290 ret += ' endIndex=' + i.endIndex;
292 }).join('\n ') + '\n ');
294 logPending('speech utterances', this.pendingUtterances_);
295 logPending('braille', this.pendingBraille_);
296 this.logTimeoutId_ = 0;
301 * @param {string} text
302 * @param {Object} props
303 * @param {Array<{text: (string|RegExp), callback: (function|undefined)}>}
308 MockFeedback.matchAndConsume_ = function(text, props, pending) {
309 for (var i = 0, candidate; candidate = pending[i]; ++i) {
310 var candidateText = candidate.text.toString();
311 if (text === candidateText ||
312 (text instanceof RegExp && text.test(candidateText))) {
314 for (prop in props) {
315 if (candidate[prop] !== props[prop]) {
325 var consumed = pending.splice(0, i + 1);
326 consumed.forEach(function(item) {