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 that the next spoken utterances do *not* match
145 * the given arguments.
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.
151 * @param {...(string|RegExp)} var_args One or more utterance to add as
152 * negative expectations.
153 * @return {MockFeedback} |this| for chaining
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)
162 if (MockFeedback.matchAndConsume_(
163 text, {}, this.pendingUtterances_)) {
164 throw new Error('Got disallowed utterance "' + text + '".');
168 toString: function() { return 'Do not speak \'' + text + '\''; }
175 * Adds an expectation for braille output.
176 * @param {string|RegExp} text
177 * @param {Object=} opt_props Additional properties to match in the
179 * @return {MockFeedback} |this| for chaining
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_);
189 this.lastMatchedBraille_ = match;
192 toString: function() {
193 return 'Braille \'' + text + '\' ' + JSON.stringify(props);
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
206 call: function(callback) {
207 assertFalse(this.replaying_);
208 this.pendingActions_.push({
209 perform: function() {
213 toString: function() {
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.
229 assertFalse(this.replaying_);
230 this.replaying_ = true;
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}
240 get lastMatchedBraille() {
241 assertTrue(this.replaying_);
242 return this.lastMatchedBraille_;
246 * @param {string} textString
247 * @param {cvox.QueueMode} queueMode
248 * @param {Object=} properties
251 addUtterance_: function(textString, queueMode, properties) {
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();
261 this.pendingUtterances_.push(
263 callback: callback});
268 addBraille_: function(navBraille) {
269 this.pendingBraille_.push(navBraille);
274 process_: function() {
275 if (!this.replaying_ || this.inProcess_)
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;
291 if (this.pendingActions_.length == 0) {
292 if (this.finishedCallback_) {
293 this.finishedCallback_();
294 this.finishedCallback_ = null;
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);
305 this.inProcess_ = false;
310 logPendingState_: function() {
311 if (this.pendingActions_.length > 0)
312 console.log('Still waiting for ' + this.pendingActions_[0].toString());
313 function logPending(desc, list) {
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;
321 ret += ' endIndex=' + i.endIndex;
323 }).join('\n ') + '\n ');
325 logPending('speech utterances', this.pendingUtterances_);
326 logPending('braille', this.pendingBraille_);
327 this.logTimeoutId_ = 0;
332 * @param {string} text
333 * @param {Object} props
334 * @param {Array<{text: (string|RegExp), callback: (function|undefined)}>}
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))) {
345 for (prop in props) {
346 if (candidate[prop] !== props[prop]) {
356 var consumed = pending.splice(0, i + 1);
357 consumed.forEach(function(item) {