• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 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/**
7 * @fileoverview Stores the history of a ChromeVox session.
8 */
9
10goog.provide('cvox.History');
11
12goog.require('cvox.DomUtil');
13goog.require('cvox.NodeBreadcrumb');
14
15/**
16 * A history event is stored in the cvox.History object and contains all the
17 * information about a single ChromeVox event.
18 * @param {Object=} opt_json A simple initializer object.
19 * @constructor
20 */
21cvox.HistoryEvent = function(opt_json) {
22  /**
23   * The start time of this event, in msec since epoch.
24   * @type {number}
25   * @private
26   */
27  this.startTime_;
28
29  /**
30   * The end time of this event, in msec since epoch.
31   * @type {number}
32   * @private
33   */
34  this.endTime_;
35
36  /**
37   * The user command executed in this event.
38   * @type {string}
39   * @private
40   */
41  this.userCommand_;
42
43  /**
44   * An array of spoken output.
45   * @type {Array.<string>}
46   * @private
47   */
48  this.spoken_ = [];
49
50  /**
51   * The ChromeVox tag for the current node at the end of this event.
52   * @type {number}
53   * @private
54   */
55  this.cvTag_;
56
57  /**
58   * True if replayed.
59   * @type {boolean}
60   * @private
61   */
62  this.replayed_ = false;
63
64  if (opt_json) {
65    this.replayed_ = true;
66    this.userCommand_ = opt_json['cmd'];
67  } else {
68    this.startTime_ = new Date().getTime();
69  }
70};
71
72/**
73 * @param {string} functionName The name of the user command.
74 * @return {cvox.HistoryEvent} this for chaining.
75 */
76cvox.HistoryEvent.prototype.withUserCommand = function(functionName) {
77  if (this.userCommand_) {
78    window.console.error('Two user commands on ' + functionName, this);
79    return this;
80  }
81  this.userCommand_ = functionName;
82  return this;
83};
84
85/**
86 * @param {string} str The text spoken.
87 * @return {cvox.HistoryEvent} this for chaining.
88 */
89cvox.HistoryEvent.prototype.speak = function(str) {
90  this.spoken_.push(str);
91  return this;
92};
93
94/**
95 * Called when the event is done.  We can expect nothing else will be added to
96 * the event after this call.
97 * @return {cvox.HistoryEvent} this for chaining.
98 */
99cvox.HistoryEvent.prototype.done = function() {
100  this.endTime_ = new Date().getTime();
101
102  this.cvTag_ = cvox.NodeBreadcrumb.getInstance().tagCurrentNode();
103
104  window.console.log('User command done.', this);
105  return this;
106};
107
108/**
109 * Outputs the event as a simple object
110 * @return {Object} A object representation of the event.
111 */
112cvox.HistoryEvent.prototype.outputObject = function() {
113  return {
114    'start': this.startTime_,
115    'end': this.endTime_,
116    'cmd': this.userCommand_,
117    'spoken': this.spoken_
118  };
119};
120
121/**
122 * Outputs a HTML element that can be added to the DOM.
123 * @return {Element} The HTML element.
124 */
125cvox.HistoryEvent.prototype.outputHTML = function() {
126  var div = document.createElement('div');
127  div.className = 'cvoxHistoryEvent';
128  var dur = this.endTime_ - this.startTime_;
129  div.innerHTML = this.userCommand_ + ' (' + dur + 'ms)';
130  for (var i = 0; i < this.spoken_.length; i++) {
131    var sdiv = document.createElement('div');
132    sdiv.className = 'cvoxHistoryEventSpoken';
133    sdiv.innerHTML = this.spoken_[i].substr(0, 20);
134    if (this.spoken_[i].length > 20) {
135      sdiv.innerHTML += '...';
136    }
137    div.appendChild(sdiv);
138  }
139  return div;
140};
141
142/**
143 * Outputs Javascript to replay the command and assert the output.
144 * @return {string} The Javascript.
145 */
146cvox.HistoryEvent.prototype.outputJs = function() {
147  var o = 'this.waitForCalm(this.userCommand, \'' + this.userCommand_ + '\')';
148  if (this.spoken_.length > 0) {
149    o += '\n  .waitForCalm(this.assertSpoken, \'' +
150        cvox.DomUtil.collapseWhitespace(this.spoken_.join(' ')) + '\');\n';
151  } else {
152    o += ';\n';
153  }
154  return o;
155};
156
157
158/**
159 * @constructor
160 * @implements {cvox.TtsInterface}
161 */
162cvox.History = function() {
163  this.recording_ = false;
164
165  this.events_ = [];
166  this.markers_ = {};
167  this.currentEvent_ = null;
168
169  this.mainDiv_ = null;
170  this.listDiv_ = null;
171  this.styleDiv_ = null;
172
173  this.bigBoxDiv_ = null;
174
175  // NOTE(deboer): Currently we only ever have one cvox.History, but
176  // if we ever have more than one, we need multiple NodeBreadcrumbs as well.
177  this.nodeBreadcrumb_ = cvox.NodeBreadcrumb.getInstance();
178
179};
180goog.addSingletonGetter(cvox.History);
181
182/**
183 * Adds a list div to the DOM for debugging.
184 * @private
185 */
186cvox.History.prototype.addListDiv_ = function() {
187  this.mainDiv_ = document.createElement('div');
188  this.mainDiv_.style.position = 'fixed';
189  this.mainDiv_.style.bottom = '0';
190  this.mainDiv_.style.right = '0';
191  this.mainDiv_.style.zIndex = '999';
192
193  this.listDiv_ = document.createElement('div');
194  this.listDiv_.id = 'cvoxEventList';
195  this.mainDiv_.appendChild(this.listDiv_);
196
197  var buttonDiv = document.createElement('div');
198  var button = document.createElement('button');
199  button.onclick = cvox.History.sendToFeedback;
200  button.innerHTML = 'Create bug';
201  buttonDiv.appendChild(button);
202  this.mainDiv_.appendChild(buttonDiv);
203
204  var dumpDiv = document.createElement('div');
205  var dumpButton = document.createElement('button');
206  dumpButton.onclick = cvox.History.dumpJs;
207  dumpButton.innerHTML = 'Dump test case';
208  dumpDiv.appendChild(dumpButton);
209  this.mainDiv_.appendChild(dumpDiv);
210
211  document.body.appendChild(this.mainDiv_);
212
213  this.styleDiv_ = document.createElement('style');
214  this.styleDiv_.innerHTML =
215      '.cvoxHistoryEventSpoken { color: gray; font-size: 75% }';
216  document.body.appendChild(this.styleDiv_);
217};
218
219
220/**
221 * Removes the list div.
222 * @private
223 */
224cvox.History.prototype.removeListDiv_ = function() {
225  document.body.removeChild(this.mainDiv_);
226  document.body.removeChild(this.styleDiv_);
227  this.mainDiv_ = null;
228  this.listDiv_ = null;
229  this.styleDiv_ = null;
230};
231
232
233/**
234 * Adds a big text box in the middle of the screen
235 * @private
236 */
237cvox.History.prototype.addBigTextBox_ = function() {
238  var bigBoxDiv = document.createElement('div');
239  bigBoxDiv.style.position = 'fixed';
240  bigBoxDiv.style.top = '0';
241  bigBoxDiv.style.left = '0';
242  bigBoxDiv.style.zIndex = '999';
243
244  var textarea = document.createElement('textarea');
245  textarea.style.width = '500px';
246  textarea.style.height = '500px';
247  textarea.innerHTML = this.dumpJsOutput_();
248  bigBoxDiv.appendChild(textarea);
249
250  var buttons = document.createElement('div');
251  bigBoxDiv.appendChild(buttons);
252
253  function link(name, func) {
254    var linkElt = document.createElement('button');
255    linkElt.onclick = func;
256    linkElt.innerHTML = name;
257    buttons.appendChild(linkElt);
258  }
259  link('Close dialog', function() {
260    document.body.removeChild(bigBoxDiv);
261  });
262  link('Remove fluff', goog.bind(function() {
263    textarea.innerHTML = this.dumpJsOutput_(['stopSpeech', 'toggleKeyPrefix']);
264  }, this));
265  document.body.appendChild(bigBoxDiv);
266};
267
268
269
270/**
271 * Start recording and show the debugging list div.
272 */
273cvox.History.prototype.startRecording = function() {
274  this.recording_ = true;
275  this.addListDiv_();
276};
277
278
279/**
280 * Stop recording and clear the events array.
281 */
282cvox.History.prototype.stopRecording = function() {
283  this.recording_ = false;
284  this.removeListDiv_();
285  this.events_ = [];
286  this.currentEvent_ = null;
287};
288
289
290/**
291 * Called by ChromeVox when it enters a user command.
292 * @param {string} functionName The function name.
293 */
294cvox.History.prototype.enterUserCommand = function(functionName) {
295  if (!this.recording_) {
296    return;
297  }
298  if (this.currentEvent_) {
299    window.console.error(
300        'User command ' + functionName + ' overlaps current event',
301        this.currentEvent_);
302  }
303  this.currentEvent_ = new cvox.HistoryEvent()
304      .withUserCommand(functionName);
305  this.events_.push(this.currentEvent_);
306};
307
308
309/**
310 * Called by ChromeVox when it exits a user command.
311 * @param {string} functionName The function name, useful for debugging.
312 */
313cvox.History.prototype.exitUserCommand = function(functionName) {
314  if (!this.recording_ || !this.currentEvent_) {
315    return;
316  }
317  this.currentEvent_.done();
318  this.listDiv_.appendChild(this.currentEvent_.outputHTML());
319  this.currentEvent_ = null;
320};
321
322
323/** @override */
324cvox.History.prototype.speak = function(str, mode, props) {
325  if (!this.recording_) {
326    return this;
327  }
328  if (!this.currentEvent_) {
329    window.console.error('Speak called outside of a user command.');
330    return this;
331  }
332  this.currentEvent_.speak(str);
333  return this;
334};
335
336
337/** @override */
338cvox.History.prototype.isSpeaking = function() { return false; };
339/** @override */
340cvox.History.prototype.stop = function() { };
341/** @override */
342cvox.History.prototype.addCapturingEventListener = function(listener) { };
343/** @override */
344cvox.History.prototype.increaseOrDecreaseProperty =
345    function(propertyName, increase) { };
346/** @override */
347cvox.History.prototype.getDefaultProperty = function(property) { };
348
349
350cvox.History.dumpJs = function() {
351  var history = cvox.History.getInstance();
352  history.addBigTextBox_();
353  window.console.log(history.dumpJsOutput_());
354};
355
356
357/**
358 * @param {Array.<string>=} opt_skipCommands
359 * @return {string} A string of Javascript output.
360 * @private
361 */
362cvox.History.prototype.dumpJsOutput_ = function(opt_skipCommands) {
363  var skipMap = {};
364  if (opt_skipCommands) {
365    opt_skipCommands.forEach(function(e) { skipMap[e] = 1; });
366  }
367  // TODO: pretty print
368  return ['/*DOC: += ',
369          this.nodeBreadcrumb_.dumpWalkedDom().innerHTML, '*/\n']
370      .concat(this.events_
371          .filter(function(e) { return ! (e.userCommand_ in skipMap); })
372          .map(function(e) { return e.outputJs(); })).join('');
373};
374
375
376/**
377 * Send the history to Google Feedback.
378 */
379cvox.History.sendToFeedback = function() {
380  var history = cvox.History.getInstance();
381  var output = history.events_.map(function(e) {
382    return e.outputObject();
383  });
384
385  var feedbackScript = document.createElement('script');
386  feedbackScript.type = 'text/javascript';
387  feedbackScript.src = 'https://www.gstatic.com/feedback/api.js';
388
389  var runFeedbackScript = document.createElement('script');
390  runFeedbackScript.type = 'text/javascript';
391  runFeedbackScript.innerHTML =
392      'userfeedback.api.startFeedback(' +
393          '{ productId: \'76092\' }, ' +
394          '{ cvoxHistory: ' + cvox.ChromeVoxJSON.stringify(
395              cvox.ChromeVoxJSON.stringify(output)) + ' });';
396
397  feedbackScript.onload = function() {
398    document.body.appendChild(runFeedbackScript);
399  };
400
401  document.body.appendChild(feedbackScript);
402};
403
404
405// Add more events: key press, DOM
406