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
) {