OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 /** |
| 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 |
| 12 * an internal queue. |
| 13 * |
| 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 |
| 18 * needed). |
| 19 * |
| 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. |
| 24 * |
| 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 |
| 29 * called. |
| 30 * |
| 31 * This mock class is lean, meaning that feedback that doesn't match |
| 32 * any expectations is silently ignored. |
| 33 * |
| 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. |
| 38 * |
| 39 * See mock_feedback_test.js for example usage of this class. |
| 40 */ |
| 41 |
| 42 /** |
| 43 * Combined mock class for braille and speech output. |
| 44 * @param {function=} opt_finishedCallback Called when all expectations have |
| 45 * been met. |
| 46 * @constructor |
| 47 */ |
| 48 var MockFeedback = function(opt_finishedCallback) { |
| 49 /** |
| 50 * @type {function} |
| 51 * @private |
| 52 */ |
| 53 this.finishedCallback_ = opt_finishedCallback || null; |
| 54 /** |
| 55 * True when |replay| has been called and actions are being replayed. |
| 56 * @type {boolean} |
| 57 * @private |
| 58 */ |
| 59 this.replaying_ = false; |
| 60 /** |
| 61 * True when inside the |process| function to prevent nested calls. |
| 62 * @type {boolean} |
| 63 * @private |
| 64 */ |
| 65 this.inProcess_ = false; |
| 66 /** |
| 67 * Pending expectations and callbacks. |
| 68 * @type {Array<{perform: function(): boolean, toString: function(): string}>} |
| 69 * @private |
| 70 */ |
| 71 this.pendingActions_ = []; |
| 72 /** |
| 73 * Pending speech utterances. |
| 74 * @type {Array<{text: string, callback: (function|undefined)}>} |
| 75 * @private |
| 76 */ |
| 77 this.pendingUtterances_ = []; |
| 78 /** |
| 79 * Pending braille output. |
| 80 * @type {Array<{text: string, callback: (function|undefined)}>} |
| 81 * @private |
| 82 */ |
| 83 this.pendingBraille_ = []; |
| 84 /** |
| 85 * Handle for the timeout set for debug logging. |
| 86 * @type {number} |
| 87 * @private |
| 88 */ |
| 89 this.logTimeoutId_ = 0; |
| 90 /** |
| 91 * @type {cvox.NavBraille} |
| 92 * @private |
| 93 */ |
| 94 this.lastMatchedBraille_ = null; |
| 95 }; |
| 96 |
| 97 MockFeedback.prototype = { |
| 98 |
| 99 /** |
| 100 * Install mock objects as |cvox.ChromeVox.tts| and |cvox.ChromeVox.braille| |
| 101 * to collect feedback. |
| 102 */ |
| 103 install: function() { |
| 104 assertFalse(this.replaying_); |
| 105 |
| 106 var MockTts = function() {}; |
| 107 MockTts.prototype = { |
| 108 __proto__: cvox.TtsInterface.prototype, |
| 109 speak: this.addUtterance_.bind(this) |
| 110 }; |
| 111 |
| 112 cvox.ChromeVox.tts = new MockTts(); |
| 113 |
| 114 var MockBraille = function() {}; |
| 115 MockBraille.prototype = { |
| 116 __proto__: cvox.BrailleInterface.prototype, |
| 117 write: this.addBraille_.bind(this) |
| 118 }; |
| 119 |
| 120 cvox.ChromeVox.braille = new MockBraille(); |
| 121 }, |
| 122 |
| 123 /** |
| 124 * Adds an expectation for one or more spoken utterances. |
| 125 * @param {...(string|RegExp)} var_args One or more utterance to add as |
| 126 * expectations. |
| 127 * @return {MockFeedback} |this| for chaining |
| 128 */ |
| 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_); |
| 136 }.bind(this), |
| 137 toString: function() { return 'Speak \'' + text + '\''; } |
| 138 }); |
| 139 }.bind(this)); |
| 140 return this; |
| 141 }, |
| 142 |
| 143 /** |
| 144 * Adds an expectation for braille output. |
| 145 * @param {string|RegExp} text |
| 146 * @param {Object=} opt_props Additional properties to match in the |
| 147 * |NavBraille| |
| 148 * @return {MockFeedback} |this| for chaining |
| 149 */ |
| 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_); |
| 157 if (match) |
| 158 this.lastMatchedBraille_ = match; |
| 159 return !!match; |
| 160 }.bind(this), |
| 161 toString: function() { |
| 162 return 'Braille \'' + text + '\' ' + JSON.stringify(props); |
| 163 } |
| 164 }); |
| 165 return this; |
| 166 }, |
| 167 |
| 168 /** |
| 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 |
| 174 */ |
| 175 call: function(callback) { |
| 176 assertFalse(this.replaying_); |
| 177 this.pendingActions_.push({ |
| 178 perform: function() { |
| 179 callback(); |
| 180 return true; |
| 181 }, |
| 182 toString: function() { |
| 183 return 'Callback'; |
| 184 } |
| 185 }); |
| 186 return this; |
| 187 }, |
| 188 |
| 189 /** |
| 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. |
| 196 */ |
| 197 replay: function() { |
| 198 assertFalse(this.replaying_); |
| 199 this.replaying_ = true; |
| 200 this.process_(); |
| 201 }, |
| 202 |
| 203 /** |
| 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} |
| 208 */ |
| 209 get lastMatchedBraille() { |
| 210 assertTrue(this.replaying_); |
| 211 return this.lastMatchedBraille_; |
| 212 }, |
| 213 |
| 214 /** |
| 215 * @param {string} textString |
| 216 * @param {cvox.QueueMode} queueMode |
| 217 * @param {Object=} properties |
| 218 * @private |
| 219 */ |
| 220 addUtterance_: function(textString, queueMode, properties) { |
| 221 var callback; |
| 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(); |
| 228 }; |
| 229 } |
| 230 this.pendingUtterances_.push( |
| 231 {text: textString, |
| 232 callback: callback}); |
| 233 this.process_(); |
| 234 }, |
| 235 |
| 236 /** @private */ |
| 237 addBraille_: function(navBraille) { |
| 238 this.pendingBraille_.push(navBraille); |
| 239 this.process_(); |
| 240 }, |
| 241 |
| 242 /*** @private */ |
| 243 process_: function() { |
| 244 if (!this.replaying_ || this.inProcess_) |
| 245 return; |
| 246 try { |
| 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; |
| 255 } |
| 256 } else { |
| 257 break; |
| 258 } |
| 259 } |
| 260 if (this.pendingActions_.length == 0) { |
| 261 if (this.finishedCallback_) { |
| 262 this.finishedCallback_(); |
| 263 this.finishedCallback_ = null; |
| 264 } |
| 265 } else { |
| 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); |
| 271 } |
| 272 } |
| 273 } finally { |
| 274 this.inProcess_ = false; |
| 275 } |
| 276 }, |
| 277 |
| 278 /** @private */ |
| 279 logPendingState_: function() { |
| 280 if (this.pendingActions_.length > 0) |
| 281 console.log('Still waiting for ' + this.pendingActions_[0].toString()); |
| 282 function logPending(desc, list) { |
| 283 if (list.length > 0) |
| 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; |
| 289 if ('endIndex' in i) |
| 290 ret += ' endIndex=' + i.endIndex; |
| 291 return ret; |
| 292 }).join('\n ') + '\n '); |
| 293 } |
| 294 logPending('speech utterances', this.pendingUtterances_); |
| 295 logPending('braille', this.pendingBraille_); |
| 296 this.logTimeoutId_ = 0; |
| 297 }, |
| 298 }; |
| 299 |
| 300 /** |
| 301 * @param {string} text |
| 302 * @param {Object} props |
| 303 * @param {Array<{text: (string|RegExp), callback: (function|undefined)}>} |
| 304 * pending |
| 305 * @return {Object} |
| 306 * @private |
| 307 */ |
| 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))) { |
| 313 var matched = true; |
| 314 for (prop in props) { |
| 315 if (candidate[prop] !== props[prop]) { |
| 316 matched = false; |
| 317 break; |
| 318 } |
| 319 } |
| 320 if (matched) |
| 321 break; |
| 322 } |
| 323 } |
| 324 if (candidate) { |
| 325 var consumed = pending.splice(0, i + 1); |
| 326 consumed.forEach(function(item) { |
| 327 if (item.callback) |
| 328 item.callback(); |
| 329 }); |
| 330 } |
| 331 return candidate; |
| 332 }; |
OLD | NEW |