Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / testing / mock_feedback.js
blobe3578aa0bf7563addbe94540191e5bad33dbeb7c
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.
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  */
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;
97 MockFeedback.prototype = {
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_);
106     var MockTts = function() {};
107     MockTts.prototype = {
108       __proto__: cvox.TtsInterface.prototype,
109       speak: this.addUtterance_.bind(this)
110     };
112     cvox.ChromeVox.tts = new MockTts();
114     var MockBraille = function() {};
115     MockBraille.prototype = {
116       __proto__: cvox.BrailleInterface.prototype,
117       write: this.addBraille_.bind(this)
118     };
120     cvox.ChromeVox.braille = new MockBraille();
121   },
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   },
143   /**
144    * Adds an expectation that the next spoken utterances do *not* match
145    * the given arguments.
146    *
147    * This is only guaranteed to work for the immediately following utterance.
148    * If you use it to check an utterance other than the immediately following
149    * one the results may be flaky.
150    *
151    * @param {...(string|RegExp)} var_args One or more utterance to add as
152    *     negative expectations.
153    * @return {MockFeedback} |this| for chaining
154    */
155   expectNextSpeechUtteranceIsNot: function() {
156     assertFalse(this.replaying_);
157     Array.prototype.forEach.call(arguments, function(text) {
158       this.pendingActions_.push({
159         perform: function() {
160           if (this.pendingUtterances_.length == 0)
161             return false;
162           if (MockFeedback.matchAndConsume_(
163                   text, {}, this.pendingUtterances_)) {
164             throw new Error('Got disallowed utterance "' + text + '".');
165           }
166           return true;
167         }.bind(this),
168         toString: function() { return 'Do not speak \'' + text + '\''; }
169       });
170     }.bind(this));
171     return this;
172   },
174   /**
175    * Adds an expectation for braille output.
176    * @param {string|RegExp} text
177    * @param {Object=} opt_props Additional properties to match in the
178    *     |NavBraille|
179    * @return {MockFeedback} |this| for chaining
180    */
181   expectBraille: function(text, opt_props) {
182     assertFalse(this.replaying_);
183     var props = opt_props || {};
184     this.pendingActions_.push({
185       perform: function() {
186         var match = MockFeedback.matchAndConsume_(
187             text, props, this.pendingBraille_);
188         if (match)
189           this.lastMatchedBraille_ = match;
190         return !!match;
191       }.bind(this),
192       toString: function() {
193         return 'Braille \'' + text + '\' ' + JSON.stringify(props);
194       }
195     });
196     return this;
197   },
199   /**
200    * Arranges for a callback to be invoked when all expectations that were
201    * added before this call have been met.  Callbacks are called in the
202    * order they are added.
203    * @param {Function} callback
204    * @return {MockFeedback} |this| for chaining
205    */
206   call: function(callback) {
207     assertFalse(this.replaying_);
208     this.pendingActions_.push({
209       perform: function() {
210         callback();
211         return true;
212       },
213       toString: function() {
214         return 'Callback';
215       }
216     });
217     return this;
218   },
220   /**
221    * Processes any feedback that has been received so far and treis to
222    * satisfy the registered expectations.  Any feedback that is received
223    * after this call (via the installed mock objects) is processed immediately.
224    * When all expectations are satisfied and registered callbacks called,
225    * the finish callbcak, if any, is called.
226    * This function may only be called once.
227    */
228   replay: function() {
229     assertFalse(this.replaying_);
230     this.replaying_ = true;
231     this.process_();
232   },
234   /**
235    * Returns the |NavBraille| that matched an expectation.  This is
236    * intended to be used by a callback to invoke braille commands that
237    * depend on display contents.
238    * @type {cvox.NavBraille}
239    */
240   get lastMatchedBraille() {
241     assertTrue(this.replaying_);
242     return this.lastMatchedBraille_;
243   },
245   /**
246    * @param {string} textString
247    * @param {cvox.QueueMode} queueMode
248    * @param {Object=} properties
249    * @private
250    */
251   addUtterance_: function(textString, queueMode, properties) {
252     var callback;
253     if (properties && (properties.startCallback || properties.endCallback)) {
254       var startCallback = properties.startCallback;
255       var endCallback = properties.endCallback;
256       callback = function() {
257         startCallback && startCallback();
258         endCallback && endCallback();
259       };
260     }
261     this.pendingUtterances_.push(
262         {text: textString,
263          callback: callback});
264     this.process_();
265   },
267   /** @private */
268   addBraille_: function(navBraille) {
269     this.pendingBraille_.push(navBraille);
270     this.process_();
271   },
273   /*** @private */
274   process_: function() {
275     if (!this.replaying_ || this.inProcess_)
276       return;
277     try {
278       this.inProcess_ = true;
279       while (this.pendingActions_.length > 0) {
280         var action = this.pendingActions_[0];
281         if (action.perform()) {
282           this.pendingActions_.shift();
283           if (this.logTimeoutId_) {
284             window.clearTimeout(this.logTimeoutId_);
285             this.logTimeoutId_ = 0;
286           }
287         } else {
288           break;
289         }
290       }
291       if (this.pendingActions_.length == 0) {
292         if (this.finishedCallback_) {
293           this.finishedCallback_();
294           this.finishedCallback_ = null;
295         }
296       } else {
297         // If there are pending actions and no matching feedback for a few
298         // seconds, log the pending state to ease debugging.
299         if (!this.logTimeoutId_) {
300           this.logTimeoutId_ = window.setTimeout(
301               this.logPendingState_.bind(this), 2000);
302         }
303       }
304     } finally {
305       this.inProcess_ = false;
306     }
307   },
309   /** @private */
310   logPendingState_: function() {
311     if (this.pendingActions_.length > 0)
312       console.log('Still waiting for ' + this.pendingActions_[0].toString());
313     function logPending(desc, list) {
314       if (list.length > 0)
315         console.log('Pending ' + desc + ':\n  ' +
316             list.map(function(i) {
317               var ret = '\'' + i.text + '\'';
318               if ('startIndex' in i)
319                 ret += ' startIndex=' + i.startIndex;
320               if ('endIndex' in i)
321                 ret += ' endIndex=' + i.endIndex;
322               return ret;
323             }).join('\n  ') + '\n  ');
324     }
325     logPending('speech utterances', this.pendingUtterances_);
326     logPending('braille', this.pendingBraille_);
327     this.logTimeoutId_ = 0;
328   },
332  * @param {string} text
333  * @param {Object} props
334  * @param {Array<{text: (string|RegExp), callback: (function|undefined)}>}
335  *     pending
336  * @return {Object}
337  * @private
338  */
339 MockFeedback.matchAndConsume_ = function(text, props, pending) {
340   for (var i = 0, candidate; candidate = pending[i]; ++i) {
341     var candidateText = candidate.text.toString();
342     if (text === candidateText ||
343         (text instanceof RegExp && text.test(candidateText))) {
344       var matched = true;
345       for (prop in props) {
346         if (candidate[prop] !== props[prop]) {
347           matched = false;
348           break;
349         }
350       }
351       if (matched)
352         break;
353     }
354   }
355   if (candidate) {
356     var consumed = pending.splice(0, i + 1);
357     consumed.forEach(function(item) {
358       if (item.callback)
359         item.callback();
360     });
361   }
362   return candidate;