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