browser/devtools/commandline/test/helpers.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /*
michael@0 2 * Copyright 2012, Mozilla Foundation and contributors
michael@0 3 *
michael@0 4 * Licensed under the Apache License, Version 2.0 (the "License");
michael@0 5 * you may not use this file except in compliance with the License.
michael@0 6 * You may obtain a copy of the License at
michael@0 7 *
michael@0 8 * http://www.apache.org/licenses/LICENSE-2.0
michael@0 9 *
michael@0 10 * Unless required by applicable law or agreed to in writing, software
michael@0 11 * distributed under the License is distributed on an "AS IS" BASIS,
michael@0 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0 13 * See the License for the specific language governing permissions and
michael@0 14 * limitations under the License.
michael@0 15 */
michael@0 16
michael@0 17 'use strict';
michael@0 18
michael@0 19 // A copy of this code exists in firefox mochitests. They should be kept
michael@0 20 // in sync. Hence the exports synonym for non AMD contexts.
michael@0 21 this.EXPORTED_SYMBOLS = [ 'helpers' ];
michael@0 22 var helpers = {};
michael@0 23 this.helpers = helpers;
michael@0 24
michael@0 25 var TargetFactory = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.TargetFactory;
michael@0 26 var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
michael@0 27
michael@0 28 var assert = { ok: ok, is: is, log: info };
michael@0 29 var util = require('gcli/util/util');
michael@0 30 var promise = require('gcli/util/promise');
michael@0 31 var cli = require('gcli/cli');
michael@0 32 var KeyEvent = require('gcli/util/util').KeyEvent;
michael@0 33 var gcli = require('gcli/index');
michael@0 34
michael@0 35 /**
michael@0 36 * See notes in helpers.checkOptions()
michael@0 37 */
michael@0 38 var createFFDisplayAutomator = function(display) {
michael@0 39 var automator = {
michael@0 40 setInput: function(typed) {
michael@0 41 return display.inputter.setInput(typed);
michael@0 42 },
michael@0 43
michael@0 44 setCursor: function(cursor) {
michael@0 45 return display.inputter.setCursor(cursor);
michael@0 46 },
michael@0 47
michael@0 48 focus: function() {
michael@0 49 return display.inputter.focus();
michael@0 50 },
michael@0 51
michael@0 52 fakeKey: function(keyCode) {
michael@0 53 var fakeEvent = {
michael@0 54 keyCode: keyCode,
michael@0 55 preventDefault: function() { },
michael@0 56 timeStamp: new Date().getTime()
michael@0 57 };
michael@0 58
michael@0 59 display.inputter.onKeyDown(fakeEvent);
michael@0 60
michael@0 61 if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
michael@0 62 var input = display.inputter.element;
michael@0 63 input.value = input.value.slice(0, -1);
michael@0 64 }
michael@0 65
michael@0 66 return display.inputter.handleKeyUp(fakeEvent);
michael@0 67 },
michael@0 68
michael@0 69 getInputState: function() {
michael@0 70 return display.inputter.getInputState();
michael@0 71 },
michael@0 72
michael@0 73 getCompleterTemplateData: function() {
michael@0 74 return display.completer._getCompleterTemplateData();
michael@0 75 },
michael@0 76
michael@0 77 getErrorMessage: function() {
michael@0 78 return display.tooltip.errorEle.textContent;
michael@0 79 }
michael@0 80 };
michael@0 81
michael@0 82 Object.defineProperty(automator, 'focusManager', {
michael@0 83 get: function() { return display.focusManager; },
michael@0 84 enumerable: true
michael@0 85 });
michael@0 86
michael@0 87 Object.defineProperty(automator, 'field', {
michael@0 88 get: function() { return display.tooltip.field; },
michael@0 89 enumerable: true
michael@0 90 });
michael@0 91
michael@0 92 return automator;
michael@0 93 };
michael@0 94
michael@0 95 /**
michael@0 96 * Warning: For use with Firefox Mochitests only.
michael@0 97 *
michael@0 98 * Open a new tab at a URL and call a callback on load, and then tidy up when
michael@0 99 * the callback finishes.
michael@0 100 * The function will be passed a set of test options, and will usually return a
michael@0 101 * promise to indicate that the tab can be cleared up. (To be formal, we call
michael@0 102 * Promise.resolve() on the return value of the callback function)
michael@0 103 *
michael@0 104 * The options used by addTab include:
michael@0 105 * - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest
michael@0 106 * - tab: The new XUL tab element, as returned by gBrowser.addTab()
michael@0 107 * - target: The debug target as defined by the devtools framework
michael@0 108 * - browser: The XUL browser element for the given tab
michael@0 109 * - window: Content window for the created tab. a.k.a 'content' in mochitest
michael@0 110 * - isFirefox: Always true. Allows test sharing with GCLI
michael@0 111 *
michael@0 112 * Normally addTab will create an options object containing the values as
michael@0 113 * described above. However these options can be customized by the third
michael@0 114 * 'options' parameter. This has the ability to customize the value of
michael@0 115 * chromeWindow or isFirefox, and to add new properties.
michael@0 116 *
michael@0 117 * @param url The URL for the new tab
michael@0 118 * @param callback The function to call on page load
michael@0 119 * @param options An optional set of options to customize the way the tests run
michael@0 120 */
michael@0 121 helpers.addTab = function(url, callback, options) {
michael@0 122 waitForExplicitFinish();
michael@0 123
michael@0 124 options = options || {};
michael@0 125 options.chromeWindow = options.chromeWindow || window;
michael@0 126 options.isFirefox = true;
michael@0 127
michael@0 128 var tabbrowser = options.chromeWindow.gBrowser;
michael@0 129 options.tab = tabbrowser.addTab();
michael@0 130 tabbrowser.selectedTab = options.tab;
michael@0 131 options.browser = tabbrowser.getBrowserForTab(options.tab);
michael@0 132 options.target = TargetFactory.forTab(options.tab);
michael@0 133
michael@0 134 var loaded = helpers.listenOnce(options.browser, "load", true).then(function(ev) {
michael@0 135 options.document = options.browser.contentDocument;
michael@0 136 options.window = options.document.defaultView;
michael@0 137
michael@0 138 var reply = callback.call(null, options);
michael@0 139
michael@0 140 return promise.resolve(reply).then(null, function(error) {
michael@0 141 ok(false, error);
michael@0 142 }).then(function() {
michael@0 143 tabbrowser.removeTab(options.tab);
michael@0 144
michael@0 145 delete options.window;
michael@0 146 delete options.document;
michael@0 147
michael@0 148 delete options.target;
michael@0 149 delete options.browser;
michael@0 150 delete options.tab;
michael@0 151
michael@0 152 delete options.chromeWindow;
michael@0 153 delete options.isFirefox;
michael@0 154 });
michael@0 155 });
michael@0 156
michael@0 157 options.browser.contentWindow.location = url;
michael@0 158 return loaded;
michael@0 159 };
michael@0 160
michael@0 161 /**
michael@0 162 * Open a new tab
michael@0 163 * @param url Address of the page to open
michael@0 164 * @param options Object to which we add properties describing the new tab. The
michael@0 165 * following properties are added:
michael@0 166 * - chromeWindow
michael@0 167 * - tab
michael@0 168 * - browser
michael@0 169 * - target
michael@0 170 * - document
michael@0 171 * - window
michael@0 172 * @return A promise which resolves to the options object when the 'load' event
michael@0 173 * happens on the new tab
michael@0 174 */
michael@0 175 helpers.openTab = function(url, options) {
michael@0 176 waitForExplicitFinish();
michael@0 177
michael@0 178 options = options || {};
michael@0 179 options.chromeWindow = options.chromeWindow || window;
michael@0 180 options.isFirefox = true;
michael@0 181
michael@0 182 var tabbrowser = options.chromeWindow.gBrowser;
michael@0 183 options.tab = tabbrowser.addTab();
michael@0 184 tabbrowser.selectedTab = options.tab;
michael@0 185 options.browser = tabbrowser.getBrowserForTab(options.tab);
michael@0 186 options.target = TargetFactory.forTab(options.tab);
michael@0 187
michael@0 188 options.browser.contentWindow.location = url;
michael@0 189
michael@0 190 return helpers.listenOnce(options.browser, "load", true).then(function() {
michael@0 191 options.document = options.browser.contentDocument;
michael@0 192 options.window = options.document.defaultView;
michael@0 193 return options;
michael@0 194 });
michael@0 195 };
michael@0 196
michael@0 197 /**
michael@0 198 * Undo the effects of |helpers.openTab|
michael@0 199 * @param options The options object passed to |helpers.openTab|
michael@0 200 * @return A promise resolved (with undefined) when the tab is closed
michael@0 201 */
michael@0 202 helpers.closeTab = function(options) {
michael@0 203 options.chromeWindow.gBrowser.removeTab(options.tab);
michael@0 204
michael@0 205 delete options.window;
michael@0 206 delete options.document;
michael@0 207
michael@0 208 delete options.target;
michael@0 209 delete options.browser;
michael@0 210 delete options.tab;
michael@0 211
michael@0 212 delete options.chromeWindow;
michael@0 213 delete options.isFirefox;
michael@0 214
michael@0 215 return promise.resolve(undefined);
michael@0 216 };
michael@0 217
michael@0 218 /**
michael@0 219 * Open the developer toolbar in a tab
michael@0 220 * @param options Object to which we add properties describing the developer
michael@0 221 * toolbar. The following properties are added:
michael@0 222 * - automator
michael@0 223 * - requisition
michael@0 224 * @return A promise which resolves to the options object when the 'load' event
michael@0 225 * happens on the new tab
michael@0 226 */
michael@0 227 helpers.openToolbar = function(options) {
michael@0 228 return options.chromeWindow.DeveloperToolbar.show(true).then(function() {
michael@0 229 var display = options.chromeWindow.DeveloperToolbar.display;
michael@0 230 options.automator = createFFDisplayAutomator(display);
michael@0 231 options.requisition = display.requisition;
michael@0 232 });
michael@0 233 };
michael@0 234
michael@0 235 /**
michael@0 236 * Undo the effects of |helpers.openToolbar|
michael@0 237 * @param options The options object passed to |helpers.openToolbar|
michael@0 238 * @return A promise resolved (with undefined) when the toolbar is closed
michael@0 239 */
michael@0 240 helpers.closeToolbar = function(options) {
michael@0 241 return options.chromeWindow.DeveloperToolbar.hide().then(function() {
michael@0 242 delete options.automator;
michael@0 243 delete options.requisition;
michael@0 244 });
michael@0 245 };
michael@0 246
michael@0 247 /**
michael@0 248 * A helper to work with Task.spawn so you can do:
michael@0 249 * return Task.spawn(realTestFunc).then(finish, helpers.handleError);
michael@0 250 */
michael@0 251 helpers.handleError = function(ex) {
michael@0 252 console.error(ex);
michael@0 253 ok(false, ex);
michael@0 254 finish();
michael@0 255 };
michael@0 256
michael@0 257 /**
michael@0 258 * A helper for calling addEventListener and then removeEventListener as soon
michael@0 259 * as the event is called, passing the results on as a promise
michael@0 260 * @param element The DOM element to listen on
michael@0 261 * @param event The name of the event to listen for
michael@0 262 * @param useCapture Should we use the capturing phase?
michael@0 263 * @return A promise resolved with the event object when the event first happens
michael@0 264 */
michael@0 265 helpers.listenOnce = function(element, event, useCapture) {
michael@0 266 var deferred = promise.defer();
michael@0 267 var onEvent = function(ev) {
michael@0 268 element.removeEventListener(event, onEvent, useCapture);
michael@0 269 deferred.resolve(ev);
michael@0 270 };
michael@0 271 element.addEventListener(event, onEvent, useCapture);
michael@0 272 return deferred.promise;
michael@0 273 };
michael@0 274
michael@0 275 /**
michael@0 276 * A wrapper for calling Services.obs.[add|remove]Observer using promises.
michael@0 277 * @param topic The topic parameter to Services.obs.addObserver
michael@0 278 * @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a
michael@0 279 * default value of false
michael@0 280 * @return a promise that resolves when the ObserverService first notifies us
michael@0 281 * of the topic. The value of the promise is the first parameter to the observer
michael@0 282 * function other parameters are dropped.
michael@0 283 */
michael@0 284 helpers.observeOnce = function(topic, ownsWeak=false) {
michael@0 285 let deferred = promise.defer();
michael@0 286 let resolver = function(subject) {
michael@0 287 Services.obs.removeObserver(resolver, topic);
michael@0 288 deferred.resolve(subject);
michael@0 289 };
michael@0 290 Services.obs.addObserver(resolver, topic, ownsWeak);
michael@0 291 return deferred.promise;
michael@0 292 };
michael@0 293
michael@0 294 /**
michael@0 295 * Takes a function that uses a callback as its last parameter, and returns a
michael@0 296 * new function that returns a promise instead
michael@0 297 */
michael@0 298 helpers.promiseify = function(functionWithLastParamCallback, scope) {
michael@0 299 return function() {
michael@0 300 let deferred = promise.defer();
michael@0 301
michael@0 302 let args = [].slice.call(arguments);
michael@0 303 args.push(function(callbackParam) {
michael@0 304 deferred.resolve(callbackParam);
michael@0 305 });
michael@0 306
michael@0 307 try {
michael@0 308 functionWithLastParamCallback.apply(scope, args);
michael@0 309 }
michael@0 310 catch (ex) {
michael@0 311 deferred.resolve(ex);
michael@0 312 }
michael@0 313
michael@0 314 return deferred.promise;
michael@0 315 }
michael@0 316 };
michael@0 317
michael@0 318 /**
michael@0 319 * Warning: For use with Firefox Mochitests only.
michael@0 320 *
michael@0 321 * As addTab, but that also opens the developer toolbar. In addition a new
michael@0 322 * 'automator' property is added to the options object with the display from GCLI
michael@0 323 * in the developer toolbar
michael@0 324 */
michael@0 325 helpers.addTabWithToolbar = function(url, callback, options) {
michael@0 326 return helpers.addTab(url, function(innerOptions) {
michael@0 327 var win = innerOptions.chromeWindow;
michael@0 328
michael@0 329 return win.DeveloperToolbar.show(true).then(function() {
michael@0 330 var display = win.DeveloperToolbar.display;
michael@0 331 innerOptions.automator = createFFDisplayAutomator(display);
michael@0 332 innerOptions.requisition = display.requisition;
michael@0 333
michael@0 334 var reply = callback.call(null, innerOptions);
michael@0 335
michael@0 336 return promise.resolve(reply).then(null, function(error) {
michael@0 337 ok(false, error);
michael@0 338 console.error(error);
michael@0 339 }).then(function() {
michael@0 340 win.DeveloperToolbar.hide().then(function() {
michael@0 341 delete innerOptions.automator;
michael@0 342 });
michael@0 343 });
michael@0 344 });
michael@0 345 }, options);
michael@0 346 };
michael@0 347
michael@0 348 /**
michael@0 349 * Warning: For use with Firefox Mochitests only.
michael@0 350 *
michael@0 351 * Run a set of test functions stored in the values of the 'exports' object
michael@0 352 * functions stored under setup/shutdown will be run at the start/end of the
michael@0 353 * sequence of tests.
michael@0 354 * A test will be considered finished when its return value is resolved.
michael@0 355 * @param options An object to be passed to the test functions
michael@0 356 * @param tests An object containing named test functions
michael@0 357 * @return a promise which will be resolved when all tests have been run and
michael@0 358 * their return values resolved
michael@0 359 */
michael@0 360 helpers.runTests = function(options, tests) {
michael@0 361 var testNames = Object.keys(tests).filter(function(test) {
michael@0 362 return test != "setup" && test != "shutdown";
michael@0 363 });
michael@0 364
michael@0 365 var recover = function(error) {
michael@0 366 ok(false, error);
michael@0 367 console.error(error);
michael@0 368 };
michael@0 369
michael@0 370 info("SETUP");
michael@0 371 var setupDone = (tests.setup != null) ?
michael@0 372 promise.resolve(tests.setup(options)) :
michael@0 373 promise.resolve();
michael@0 374
michael@0 375 var testDone = setupDone.then(function() {
michael@0 376 return util.promiseEach(testNames, function(testName) {
michael@0 377 info(testName);
michael@0 378 var action = tests[testName];
michael@0 379
michael@0 380 if (typeof action === "function") {
michael@0 381 var reply = action.call(tests, options);
michael@0 382 return promise.resolve(reply);
michael@0 383 }
michael@0 384 else if (Array.isArray(action)) {
michael@0 385 return helpers.audit(options, action);
michael@0 386 }
michael@0 387
michael@0 388 return promise.reject("test action '" + testName +
michael@0 389 "' is not a function or helpers.audit() object");
michael@0 390 });
michael@0 391 }, recover);
michael@0 392
michael@0 393 return testDone.then(function() {
michael@0 394 info("SHUTDOWN");
michael@0 395 return (tests.shutdown != null) ?
michael@0 396 promise.resolve(tests.shutdown(options)) :
michael@0 397 promise.resolve();
michael@0 398 }, recover);
michael@0 399 };
michael@0 400
michael@0 401 ///////////////////////////////////////////////////////////////////////////////
michael@0 402
michael@0 403 /**
michael@0 404 * Ensure that the options object is setup correctly
michael@0 405 * options should contain an automator object that looks like this:
michael@0 406 * {
michael@0 407 * getInputState: function() { ... },
michael@0 408 * setCursor: function(cursor) { ... },
michael@0 409 * getCompleterTemplateData: function() { ... },
michael@0 410 * focus: function() { ... },
michael@0 411 * getErrorMessage: function() { ... },
michael@0 412 * fakeKey: function(keyCode) { ... },
michael@0 413 * setInput: function(typed) { ... },
michael@0 414 * focusManager: ...,
michael@0 415 * field: ...,
michael@0 416 * }
michael@0 417 */
michael@0 418 function checkOptions(options) {
michael@0 419 if (options == null) {
michael@0 420 console.trace();
michael@0 421 throw new Error('Missing options object');
michael@0 422 }
michael@0 423 if (options.requisition == null) {
michael@0 424 console.trace();
michael@0 425 throw new Error('options.requisition == null');
michael@0 426 }
michael@0 427 }
michael@0 428
michael@0 429 /**
michael@0 430 * Various functions to return the actual state of the command line
michael@0 431 */
michael@0 432 helpers._actual = {
michael@0 433 input: function(options) {
michael@0 434 return options.automator.getInputState().typed;
michael@0 435 },
michael@0 436
michael@0 437 hints: function(options) {
michael@0 438 return options.automator.getCompleterTemplateData().then(function(data) {
michael@0 439 var emptyParams = data.emptyParameters.join('');
michael@0 440 return (data.directTabText + emptyParams + data.arrowTabText)
michael@0 441 .replace(/\u00a0/g, ' ')
michael@0 442 .replace(/\u21E5/, '->')
michael@0 443 .replace(/ $/, '');
michael@0 444 });
michael@0 445 },
michael@0 446
michael@0 447 markup: function(options) {
michael@0 448 var cursor = helpers._actual.cursor(options);
michael@0 449 var statusMarkup = options.requisition.getInputStatusMarkup(cursor);
michael@0 450 return statusMarkup.map(function(s) {
michael@0 451 return new Array(s.string.length + 1).join(s.status.toString()[0]);
michael@0 452 }).join('');
michael@0 453 },
michael@0 454
michael@0 455 cursor: function(options) {
michael@0 456 return options.automator.getInputState().cursor.start;
michael@0 457 },
michael@0 458
michael@0 459 current: function(options) {
michael@0 460 var cursor = helpers._actual.cursor(options);
michael@0 461 return options.requisition.getAssignmentAt(cursor).param.name;
michael@0 462 },
michael@0 463
michael@0 464 status: function(options) {
michael@0 465 return options.requisition.status.toString();
michael@0 466 },
michael@0 467
michael@0 468 predictions: function(options) {
michael@0 469 var cursor = helpers._actual.cursor(options);
michael@0 470 var assignment = options.requisition.getAssignmentAt(cursor);
michael@0 471 var context = options.requisition.executionContext;
michael@0 472 return assignment.getPredictions(context).then(function(predictions) {
michael@0 473 return predictions.map(function(prediction) {
michael@0 474 return prediction.name;
michael@0 475 });
michael@0 476 });
michael@0 477 },
michael@0 478
michael@0 479 unassigned: function(options) {
michael@0 480 return options.requisition._unassigned.map(function(assignment) {
michael@0 481 return assignment.arg.toString();
michael@0 482 }.bind(this));
michael@0 483 },
michael@0 484
michael@0 485 outputState: function(options) {
michael@0 486 var outputData = options.automator.focusManager._shouldShowOutput();
michael@0 487 return outputData.visible + ':' + outputData.reason;
michael@0 488 },
michael@0 489
michael@0 490 tooltipState: function(options) {
michael@0 491 var tooltipData = options.automator.focusManager._shouldShowTooltip();
michael@0 492 return tooltipData.visible + ':' + tooltipData.reason;
michael@0 493 },
michael@0 494
michael@0 495 options: function(options) {
michael@0 496 if (options.automator.field.menu == null) {
michael@0 497 return [];
michael@0 498 }
michael@0 499 return options.automator.field.menu.items.map(function(item) {
michael@0 500 return item.name.textContent ? item.name.textContent : item.name;
michael@0 501 });
michael@0 502 },
michael@0 503
michael@0 504 message: function(options) {
michael@0 505 return options.automator.getErrorMessage();
michael@0 506 }
michael@0 507 };
michael@0 508
michael@0 509 function shouldOutputUnquoted(value) {
michael@0 510 var type = typeof value;
michael@0 511 return value == null || type === 'boolean' || type === 'number';
michael@0 512 }
michael@0 513
michael@0 514 function outputArray(array) {
michael@0 515 return (array.length === 0) ?
michael@0 516 '[ ]' :
michael@0 517 '[ \'' + array.join('\', \'') + '\' ]';
michael@0 518 }
michael@0 519
michael@0 520 helpers._createDebugCheck = function(options) {
michael@0 521 checkOptions(options);
michael@0 522 var requisition = options.requisition;
michael@0 523 var command = requisition.commandAssignment.value;
michael@0 524 var cursor = helpers._actual.cursor(options);
michael@0 525 var input = helpers._actual.input(options);
michael@0 526 var padding = new Array(input.length + 1).join(' ');
michael@0 527
michael@0 528 var hintsPromise = helpers._actual.hints(options);
michael@0 529 var predictionsPromise = helpers._actual.predictions(options);
michael@0 530
michael@0 531 return promise.all(hintsPromise, predictionsPromise).then(function(values) {
michael@0 532 var hints = values[0];
michael@0 533 var predictions = values[1];
michael@0 534 var output = '';
michael@0 535
michael@0 536 output += 'return helpers.audit(options, [\n';
michael@0 537 output += ' {\n';
michael@0 538
michael@0 539 if (cursor === input.length) {
michael@0 540 output += ' setup: \'' + input + '\',\n';
michael@0 541 }
michael@0 542 else {
michael@0 543 output += ' name: \'' + input + ' (cursor=' + cursor + ')\',\n';
michael@0 544 output += ' setup: function() {\n';
michael@0 545 output += ' return helpers.setInput(options, \'' + input + '\', ' + cursor + ');\n';
michael@0 546 output += ' },\n';
michael@0 547 }
michael@0 548
michael@0 549 output += ' check: {\n';
michael@0 550
michael@0 551 output += ' input: \'' + input + '\',\n';
michael@0 552 output += ' hints: ' + padding + '\'' + hints + '\',\n';
michael@0 553 output += ' markup: \'' + helpers._actual.markup(options) + '\',\n';
michael@0 554 output += ' cursor: ' + cursor + ',\n';
michael@0 555 output += ' current: \'' + helpers._actual.current(options) + '\',\n';
michael@0 556 output += ' status: \'' + helpers._actual.status(options) + '\',\n';
michael@0 557 output += ' options: ' + outputArray(helpers._actual.options(options)) + ',\n';
michael@0 558 output += ' message: \'' + helpers._actual.message(options) + '\',\n';
michael@0 559 output += ' predictions: ' + outputArray(predictions) + ',\n';
michael@0 560 output += ' unassigned: ' + outputArray(requisition._unassigned) + ',\n';
michael@0 561 output += ' outputState: \'' + helpers._actual.outputState(options) + '\',\n';
michael@0 562 output += ' tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
michael@0 563 (command ? ',' : '') +'\n';
michael@0 564
michael@0 565 if (command) {
michael@0 566 output += ' args: {\n';
michael@0 567 output += ' command: { name: \'' + command.name + '\' },\n';
michael@0 568
michael@0 569 requisition.getAssignments().forEach(function(assignment) {
michael@0 570 output += ' ' + assignment.param.name + ': { ';
michael@0 571
michael@0 572 if (typeof assignment.value === 'string') {
michael@0 573 output += 'value: \'' + assignment.value + '\', ';
michael@0 574 }
michael@0 575 else if (shouldOutputUnquoted(assignment.value)) {
michael@0 576 output += 'value: ' + assignment.value + ', ';
michael@0 577 }
michael@0 578 else {
michael@0 579 output += '/*value:' + assignment.value + ',*/ ';
michael@0 580 }
michael@0 581
michael@0 582 output += 'arg: \'' + assignment.arg + '\', ';
michael@0 583 output += 'status: \'' + assignment.getStatus().toString() + '\', ';
michael@0 584 output += 'message: \'' + assignment.message + '\'';
michael@0 585 output += ' },\n';
michael@0 586 });
michael@0 587
michael@0 588 output += ' }\n';
michael@0 589 }
michael@0 590
michael@0 591 output += ' },\n';
michael@0 592 output += ' exec: {\n';
michael@0 593 output += ' output: \'\',\n';
michael@0 594 output += ' type: \'string\',\n';
michael@0 595 output += ' error: false\n';
michael@0 596 output += ' }\n';
michael@0 597 output += ' }\n';
michael@0 598 output += ']);';
michael@0 599
michael@0 600 return output;
michael@0 601 }.bind(this), util.errorHandler);
michael@0 602 };
michael@0 603
michael@0 604 /**
michael@0 605 * Simulate focusing the input field
michael@0 606 */
michael@0 607 helpers.focusInput = function(options) {
michael@0 608 checkOptions(options);
michael@0 609 options.automator.focus();
michael@0 610 };
michael@0 611
michael@0 612 /**
michael@0 613 * Simulate pressing TAB in the input field
michael@0 614 */
michael@0 615 helpers.pressTab = function(options) {
michael@0 616 checkOptions(options);
michael@0 617 return helpers.pressKey(options, KeyEvent.DOM_VK_TAB);
michael@0 618 };
michael@0 619
michael@0 620 /**
michael@0 621 * Simulate pressing RETURN in the input field
michael@0 622 */
michael@0 623 helpers.pressReturn = function(options) {
michael@0 624 checkOptions(options);
michael@0 625 return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN);
michael@0 626 };
michael@0 627
michael@0 628 /**
michael@0 629 * Simulate pressing a key by keyCode in the input field
michael@0 630 */
michael@0 631 helpers.pressKey = function(options, keyCode) {
michael@0 632 checkOptions(options);
michael@0 633 return options.automator.fakeKey(keyCode);
michael@0 634 };
michael@0 635
michael@0 636 /**
michael@0 637 * A list of special key presses and how to to them, for the benefit of
michael@0 638 * helpers.setInput
michael@0 639 */
michael@0 640 var ACTIONS = {
michael@0 641 '<TAB>': function(options) {
michael@0 642 return helpers.pressTab(options);
michael@0 643 },
michael@0 644 '<RETURN>': function(options) {
michael@0 645 return helpers.pressReturn(options);
michael@0 646 },
michael@0 647 '<UP>': function(options) {
michael@0 648 return helpers.pressKey(options, KeyEvent.DOM_VK_UP);
michael@0 649 },
michael@0 650 '<DOWN>': function(options) {
michael@0 651 return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN);
michael@0 652 },
michael@0 653 '<BACKSPACE>': function(options) {
michael@0 654 return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE);
michael@0 655 }
michael@0 656 };
michael@0 657
michael@0 658 /**
michael@0 659 * Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into
michael@0 660 * an array like [ 'blah', '<TAB>', 'foo', '<UP>' ].
michael@0 661 * When using this RegExp, you also need to filter out the blank strings.
michael@0 662 */
michael@0 663 var CHUNKER = /([^<]*)(<[A-Z]+>)/;
michael@0 664
michael@0 665 /**
michael@0 666 * Alter the input to <code>typed</code> optionally leaving the cursor at
michael@0 667 * <code>cursor</code>.
michael@0 668 * @return A promise of the number of key-presses to respond
michael@0 669 */
michael@0 670 helpers.setInput = function(options, typed, cursor) {
michael@0 671 checkOptions(options);
michael@0 672 var inputPromise;
michael@0 673 var automator = options.automator;
michael@0 674 // We try to measure average keypress time, but setInput can simulate
michael@0 675 // several, so we try to keep track of how many
michael@0 676 var chunkLen = 1;
michael@0 677
michael@0 678 // The easy case is a simple string without things like <TAB>
michael@0 679 if (typed.indexOf('<') === -1) {
michael@0 680 inputPromise = automator.setInput(typed);
michael@0 681 }
michael@0 682 else {
michael@0 683 // Cut the input up into input strings separated by '<KEY>' tokens. The
michael@0 684 // CHUNKS RegExp leaves blanks so we filter them out.
michael@0 685 var chunks = typed.split(CHUNKER).filter(function(s) {
michael@0 686 return s !== '';
michael@0 687 });
michael@0 688 chunkLen = chunks.length + 1;
michael@0 689
michael@0 690 // We're working on this in chunks so first clear the input
michael@0 691 inputPromise = automator.setInput('').then(function() {
michael@0 692 return util.promiseEach(chunks, function(chunk) {
michael@0 693 if (chunk.charAt(0) === '<') {
michael@0 694 var action = ACTIONS[chunk];
michael@0 695 if (typeof action !== 'function') {
michael@0 696 console.error('Known actions: ' + Object.keys(ACTIONS).join());
michael@0 697 throw new Error('Key action not found "' + chunk + '"');
michael@0 698 }
michael@0 699 return action(options);
michael@0 700 }
michael@0 701 else {
michael@0 702 return automator.setInput(automator.getInputState().typed + chunk);
michael@0 703 }
michael@0 704 });
michael@0 705 });
michael@0 706 }
michael@0 707
michael@0 708 return inputPromise.then(function() {
michael@0 709 if (cursor != null) {
michael@0 710 automator.setCursor({ start: cursor, end: cursor });
michael@0 711 }
michael@0 712
michael@0 713 if (automator.focusManager) {
michael@0 714 automator.focusManager.onInputChange();
michael@0 715 }
michael@0 716
michael@0 717 // Firefox testing is noisy and distant, so logging helps
michael@0 718 if (options.isFirefox) {
michael@0 719 var cursorStr = (cursor == null ? '' : ', ' + cursor);
michael@0 720 log('setInput("' + typed + '"' + cursorStr + ')');
michael@0 721 }
michael@0 722
michael@0 723 return chunkLen;
michael@0 724 });
michael@0 725 };
michael@0 726
michael@0 727 /**
michael@0 728 * Helper for helpers.audit() to ensure that all the 'check' properties match.
michael@0 729 * See helpers.audit for more information.
michael@0 730 * @param name The name to use in error messages
michael@0 731 * @param checks See helpers.audit for a list of available checks
michael@0 732 * @return A promise which resolves to undefined when the checks are complete
michael@0 733 */
michael@0 734 helpers._check = function(options, name, checks) {
michael@0 735 // A test method to check that all args are assigned in some way
michael@0 736 var requisition = options.requisition;
michael@0 737 requisition._args.forEach(function(arg) {
michael@0 738 if (arg.assignment == null) {
michael@0 739 assert.ok(false, 'No assignment for ' + arg);
michael@0 740 }
michael@0 741 });
michael@0 742
michael@0 743 if (checks == null) {
michael@0 744 return promise.resolve();
michael@0 745 }
michael@0 746
michael@0 747 var outstanding = [];
michael@0 748 var suffix = name ? ' (for \'' + name + '\')' : '';
michael@0 749
michael@0 750 if (!options.isNoDom && 'input' in checks) {
michael@0 751 assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
michael@0 752 }
michael@0 753
michael@0 754 if (!options.isNoDom && 'cursor' in checks) {
michael@0 755 assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
michael@0 756 }
michael@0 757
michael@0 758 if (!options.isNoDom && 'current' in checks) {
michael@0 759 assert.is(helpers._actual.current(options), checks.current, 'current' + suffix);
michael@0 760 }
michael@0 761
michael@0 762 if ('status' in checks) {
michael@0 763 assert.is(helpers._actual.status(options), checks.status, 'status' + suffix);
michael@0 764 }
michael@0 765
michael@0 766 if (!options.isNoDom && 'markup' in checks) {
michael@0 767 assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
michael@0 768 }
michael@0 769
michael@0 770 if (!options.isNoDom && 'hints' in checks) {
michael@0 771 var hintCheck = function(actualHints) {
michael@0 772 assert.is(actualHints, checks.hints, 'hints' + suffix);
michael@0 773 };
michael@0 774 outstanding.push(helpers._actual.hints(options).then(hintCheck));
michael@0 775 }
michael@0 776
michael@0 777 if (!options.isNoDom && 'predictions' in checks) {
michael@0 778 var predictionsCheck = function(actualPredictions) {
michael@0 779 helpers.arrayIs(actualPredictions,
michael@0 780 checks.predictions,
michael@0 781 'predictions' + suffix);
michael@0 782 };
michael@0 783 outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
michael@0 784 }
michael@0 785
michael@0 786 if (!options.isNoDom && 'predictionsContains' in checks) {
michael@0 787 var containsCheck = function(actualPredictions) {
michael@0 788 checks.predictionsContains.forEach(function(prediction) {
michael@0 789 var index = actualPredictions.indexOf(prediction);
michael@0 790 assert.ok(index !== -1,
michael@0 791 'predictionsContains:' + prediction + suffix);
michael@0 792 });
michael@0 793 };
michael@0 794 outstanding.push(helpers._actual.predictions(options).then(containsCheck));
michael@0 795 }
michael@0 796
michael@0 797 if ('unassigned' in checks) {
michael@0 798 helpers.arrayIs(helpers._actual.unassigned(options),
michael@0 799 checks.unassigned,
michael@0 800 'unassigned' + suffix);
michael@0 801 }
michael@0 802
michael@0 803 /* TODO: Fix this
michael@0 804 if (!options.isNoDom && 'tooltipState' in checks) {
michael@0 805 assert.is(helpers._actual.tooltipState(options),
michael@0 806 checks.tooltipState,
michael@0 807 'tooltipState' + suffix);
michael@0 808 }
michael@0 809 */
michael@0 810
michael@0 811 if (!options.isNoDom && 'outputState' in checks) {
michael@0 812 assert.is(helpers._actual.outputState(options),
michael@0 813 checks.outputState,
michael@0 814 'outputState' + suffix);
michael@0 815 }
michael@0 816
michael@0 817 if (!options.isNoDom && 'options' in checks) {
michael@0 818 helpers.arrayIs(helpers._actual.options(options),
michael@0 819 checks.options,
michael@0 820 'options' + suffix);
michael@0 821 }
michael@0 822
michael@0 823 if (!options.isNoDom && 'error' in checks) {
michael@0 824 assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
michael@0 825 }
michael@0 826
michael@0 827 if (checks.args != null) {
michael@0 828 Object.keys(checks.args).forEach(function(paramName) {
michael@0 829 var check = checks.args[paramName];
michael@0 830
michael@0 831 // We allow an 'argument' called 'command' to be the command itself, but
michael@0 832 // what if the command has a parameter called 'command' (for example, an
michael@0 833 // 'exec' command)? We default to using the parameter because checking
michael@0 834 // the command value is less useful
michael@0 835 var assignment = requisition.getAssignment(paramName);
michael@0 836 if (assignment == null && paramName === 'command') {
michael@0 837 assignment = requisition.commandAssignment;
michael@0 838 }
michael@0 839
michael@0 840 if (assignment == null) {
michael@0 841 assert.ok(false, 'Unknown arg: ' + paramName + suffix);
michael@0 842 return;
michael@0 843 }
michael@0 844
michael@0 845 if ('value' in check) {
michael@0 846 if (typeof check.value === 'function') {
michael@0 847 try {
michael@0 848 check.value(assignment.value);
michael@0 849 }
michael@0 850 catch (ex) {
michael@0 851 assert.ok(false, '' + ex);
michael@0 852 }
michael@0 853 }
michael@0 854 else {
michael@0 855 assert.is(assignment.value,
michael@0 856 check.value,
michael@0 857 'arg.' + paramName + '.value' + suffix);
michael@0 858 }
michael@0 859 }
michael@0 860
michael@0 861 if ('name' in check) {
michael@0 862 assert.is(assignment.value.name,
michael@0 863 check.name,
michael@0 864 'arg.' + paramName + '.name' + suffix);
michael@0 865 }
michael@0 866
michael@0 867 if ('type' in check) {
michael@0 868 assert.is(assignment.arg.type,
michael@0 869 check.type,
michael@0 870 'arg.' + paramName + '.type' + suffix);
michael@0 871 }
michael@0 872
michael@0 873 if ('arg' in check) {
michael@0 874 assert.is(assignment.arg.toString(),
michael@0 875 check.arg,
michael@0 876 'arg.' + paramName + '.arg' + suffix);
michael@0 877 }
michael@0 878
michael@0 879 if ('status' in check) {
michael@0 880 assert.is(assignment.getStatus().toString(),
michael@0 881 check.status,
michael@0 882 'arg.' + paramName + '.status' + suffix);
michael@0 883 }
michael@0 884
michael@0 885 if (!options.isNoDom && 'message' in check) {
michael@0 886 if (typeof check.message.test === 'function') {
michael@0 887 assert.ok(check.message.test(assignment.message),
michael@0 888 'arg.' + paramName + '.message' + suffix);
michael@0 889 }
michael@0 890 else {
michael@0 891 assert.is(assignment.message,
michael@0 892 check.message,
michael@0 893 'arg.' + paramName + '.message' + suffix);
michael@0 894 }
michael@0 895 }
michael@0 896 });
michael@0 897 }
michael@0 898
michael@0 899 return promise.all(outstanding).then(function() {
michael@0 900 // Ensure the promise resolves to nothing
michael@0 901 return undefined;
michael@0 902 });
michael@0 903 };
michael@0 904
michael@0 905 /**
michael@0 906 * Helper for helpers.audit() to ensure that all the 'exec' properties work.
michael@0 907 * See helpers.audit for more information.
michael@0 908 * @param name The name to use in error messages
michael@0 909 * @param expected See helpers.audit for a list of available exec checks
michael@0 910 * @return A promise which resolves to undefined when the checks are complete
michael@0 911 */
michael@0 912 helpers._exec = function(options, name, expected) {
michael@0 913 var requisition = options.requisition;
michael@0 914 if (expected == null) {
michael@0 915 return promise.resolve({});
michael@0 916 }
michael@0 917
michael@0 918 var origLogErrors = cli.logErrors;
michael@0 919 if (expected.error) {
michael@0 920 cli.logErrors = false;
michael@0 921 }
michael@0 922
michael@0 923 try {
michael@0 924 return requisition.exec({ hidden: true }).then(function(output) {
michael@0 925 if ('type' in expected) {
michael@0 926 assert.is(output.type,
michael@0 927 expected.type,
michael@0 928 'output.type for: ' + name);
michael@0 929 }
michael@0 930
michael@0 931 if ('error' in expected) {
michael@0 932 assert.is(output.error,
michael@0 933 expected.error,
michael@0 934 'output.error for: ' + name);
michael@0 935 }
michael@0 936
michael@0 937 if (!('output' in expected)) {
michael@0 938 return { output: output };
michael@0 939 }
michael@0 940
michael@0 941 var context = requisition.conversionContext;
michael@0 942 var convertPromise;
michael@0 943 if (options.isNoDom) {
michael@0 944 convertPromise = output.convert('string', context);
michael@0 945 }
michael@0 946 else {
michael@0 947 convertPromise = output.convert('dom', context).then(function(node) {
michael@0 948 return node.textContent.trim();
michael@0 949 });
michael@0 950 }
michael@0 951
michael@0 952 return convertPromise.then(function(textOutput) {
michael@0 953 var doTest = function(match, against) {
michael@0 954 // Only log the real textContent if the test fails
michael@0 955 if (against.match(match) != null) {
michael@0 956 assert.ok(true, 'html output for \'' + name + '\' ' +
michael@0 957 'should match /' + (match.source || match) + '/');
michael@0 958 } else {
michael@0 959 assert.ok(false, 'html output for \'' + name + '\' ' +
michael@0 960 'should match /' + (match.source || match) + '/. ' +
michael@0 961 'Actual textContent: "' + against + '"');
michael@0 962 }
michael@0 963 };
michael@0 964
michael@0 965 if (typeof expected.output === 'string') {
michael@0 966 assert.is(textOutput,
michael@0 967 expected.output,
michael@0 968 'html output for ' + name);
michael@0 969 }
michael@0 970 else if (Array.isArray(expected.output)) {
michael@0 971 expected.output.forEach(function(match) {
michael@0 972 doTest(match, textOutput);
michael@0 973 });
michael@0 974 }
michael@0 975 else {
michael@0 976 doTest(expected.output, textOutput);
michael@0 977 }
michael@0 978
michael@0 979 if (expected.error) {
michael@0 980 cli.logErrors = origLogErrors;
michael@0 981 }
michael@0 982 return { output: output, text: textOutput };
michael@0 983 });
michael@0 984 }.bind(this)).then(function(data) {
michael@0 985 if (expected.error) {
michael@0 986 cli.logErrors = origLogErrors;
michael@0 987 }
michael@0 988
michael@0 989 return data;
michael@0 990 });
michael@0 991 }
michael@0 992 catch (ex) {
michael@0 993 assert.ok(false, 'Failure executing \'' + name + '\': ' + ex);
michael@0 994 util.errorHandler(ex);
michael@0 995
michael@0 996 if (expected.error) {
michael@0 997 cli.logErrors = origLogErrors;
michael@0 998 }
michael@0 999 return promise.resolve({});
michael@0 1000 }
michael@0 1001 };
michael@0 1002
michael@0 1003 /**
michael@0 1004 * Helper to setup the test
michael@0 1005 */
michael@0 1006 helpers._setup = function(options, name, audit) {
michael@0 1007 if (typeof audit.setup === 'string') {
michael@0 1008 return helpers.setInput(options, audit.setup);
michael@0 1009 }
michael@0 1010
michael@0 1011 if (typeof audit.setup === 'function') {
michael@0 1012 return promise.resolve(audit.setup.call(audit));
michael@0 1013 }
michael@0 1014
michael@0 1015 return promise.reject('\'setup\' property must be a string or a function. Is ' + audit.setup);
michael@0 1016 };
michael@0 1017
michael@0 1018 /**
michael@0 1019 * Helper to shutdown the test
michael@0 1020 */
michael@0 1021 helpers._post = function(name, audit, data) {
michael@0 1022 if (typeof audit.post === 'function') {
michael@0 1023 return promise.resolve(audit.post.call(audit, data.output, data.text));
michael@0 1024 }
michael@0 1025 return promise.resolve(audit.post);
michael@0 1026 };
michael@0 1027
michael@0 1028 /*
michael@0 1029 * We do some basic response time stats so we can see if we're getting slow
michael@0 1030 */
michael@0 1031 var totalResponseTime = 0;
michael@0 1032 var averageOver = 0;
michael@0 1033 var maxResponseTime = 0;
michael@0 1034 var maxResponseCulprit;
michael@0 1035 var start;
michael@0 1036
michael@0 1037 /**
michael@0 1038 * Restart the stats collection process
michael@0 1039 */
michael@0 1040 helpers.resetResponseTimes = function() {
michael@0 1041 start = new Date().getTime();
michael@0 1042 totalResponseTime = 0;
michael@0 1043 averageOver = 0;
michael@0 1044 maxResponseTime = 0;
michael@0 1045 maxResponseCulprit = undefined;
michael@0 1046 };
michael@0 1047
michael@0 1048 /**
michael@0 1049 * Expose an average response time in milliseconds
michael@0 1050 */
michael@0 1051 Object.defineProperty(helpers, 'averageResponseTime', {
michael@0 1052 get: function() {
michael@0 1053 return averageOver === 0 ?
michael@0 1054 undefined :
michael@0 1055 Math.round(100 * totalResponseTime / averageOver) / 100;
michael@0 1056 },
michael@0 1057 enumerable: true
michael@0 1058 });
michael@0 1059
michael@0 1060 /**
michael@0 1061 * Expose a maximum response time in milliseconds
michael@0 1062 */
michael@0 1063 Object.defineProperty(helpers, 'maxResponseTime', {
michael@0 1064 get: function() { return Math.round(maxResponseTime * 100) / 100; },
michael@0 1065 enumerable: true
michael@0 1066 });
michael@0 1067
michael@0 1068 /**
michael@0 1069 * Expose the name of the test that provided the maximum response time
michael@0 1070 */
michael@0 1071 Object.defineProperty(helpers, 'maxResponseCulprit', {
michael@0 1072 get: function() { return maxResponseCulprit; },
michael@0 1073 enumerable: true
michael@0 1074 });
michael@0 1075
michael@0 1076 /**
michael@0 1077 * Quick summary of the times
michael@0 1078 */
michael@0 1079 Object.defineProperty(helpers, 'timingSummary', {
michael@0 1080 get: function() {
michael@0 1081 var elapsed = (new Date().getTime() - start) / 1000;
michael@0 1082 return 'Total ' + elapsed + 's, ' +
michael@0 1083 'ave response ' + helpers.averageResponseTime + 'ms, ' +
michael@0 1084 'max response ' + helpers.maxResponseTime + 'ms ' +
michael@0 1085 'from \'' + helpers.maxResponseCulprit + '\'';
michael@0 1086 },
michael@0 1087 enumerable: true
michael@0 1088 });
michael@0 1089
michael@0 1090 /**
michael@0 1091 * A way of turning a set of tests into something more declarative, this helps
michael@0 1092 * to allow tests to be asynchronous.
michael@0 1093 * @param audits An array of objects each of which contains:
michael@0 1094 * - setup: string/function to be called to set the test up.
michael@0 1095 * If audit is a string then it is passed to helpers.setInput().
michael@0 1096 * If audit is a function then it is executed. The tests will wait while
michael@0 1097 * tests that return promises complete.
michael@0 1098 * - name: For debugging purposes. If name is undefined, and 'setup'
michael@0 1099 * is a string then the setup value will be used automatically
michael@0 1100 * - skipIf: A function to define if the test should be skipped. Useful for
michael@0 1101 * excluding tests from certain environments (e.g. nodom, firefox, etc).
michael@0 1102 * The name of the test will be used in log messages noting the skip
michael@0 1103 * See helpers.reason for pre-defined skip functions. The skip function must
michael@0 1104 * be synchronous, and will be passed the test options object.
michael@0 1105 * - skipRemainingIf: A function to skip all the remaining audits in this set.
michael@0 1106 * See skipIf for details of how skip functions work.
michael@0 1107 * - check: Check data. Available checks:
michael@0 1108 * - input: The text displayed in the input field
michael@0 1109 * - cursor: The position of the start of the cursor
michael@0 1110 * - status: One of 'VALID', 'ERROR', 'INCOMPLETE'
michael@0 1111 * - hints: The hint text, i.e. a concatenation of the directTabText, the
michael@0 1112 * emptyParameters and the arrowTabText. The text as inserted into the UI
michael@0 1113 * will include NBSP and Unicode RARR characters, these should be
michael@0 1114 * represented using normal space and '->' for the arrow
michael@0 1115 * - markup: What state should the error markup be in. e.g. 'VVVIIIEEE'
michael@0 1116 * - args: Maps of checks to make against the arguments:
michael@0 1117 * - value: i.e. assignment.value (which ignores defaultValue)
michael@0 1118 * - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
michael@0 1119 * Care should be taken with this since it's something of an
michael@0 1120 * implementation detail
michael@0 1121 * - arg: The toString value of the argument
michael@0 1122 * - status: i.e. assignment.getStatus
michael@0 1123 * - message: i.e. assignment.message
michael@0 1124 * - name: For commands - checks assignment.value.name
michael@0 1125 * - exec: Object to indicate we should execute the command and check the
michael@0 1126 * results. Available checks:
michael@0 1127 * - output: A string, RegExp or array of RegExps to compare with the output
michael@0 1128 * If typeof output is a string then the output should be exactly equal
michael@0 1129 * to the given string. If the type of output is a RegExp or array of
michael@0 1130 * RegExps then the output should match all RegExps
michael@0 1131 * - post: Function to be called after the checks have been run
michael@0 1132 */
michael@0 1133 helpers.audit = function(options, audits) {
michael@0 1134 checkOptions(options);
michael@0 1135 var skipReason = null;
michael@0 1136 return util.promiseEach(audits, function(audit) {
michael@0 1137 var name = audit.name;
michael@0 1138 if (name == null && typeof audit.setup === 'string') {
michael@0 1139 name = audit.setup;
michael@0 1140 }
michael@0 1141
michael@0 1142 if (assert.testLogging) {
michael@0 1143 log('- START \'' + name + '\' in ' + assert.currentTest);
michael@0 1144 }
michael@0 1145
michael@0 1146 if (audit.skipRemainingIf) {
michael@0 1147 var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ?
michael@0 1148 audit.skipRemainingIf(options) :
michael@0 1149 !!audit.skipRemainingIf;
michael@0 1150 if (skipRemainingIf) {
michael@0 1151 skipReason = audit.skipRemainingIf.name ?
michael@0 1152 'due to ' + audit.skipRemainingIf.name :
michael@0 1153 '';
michael@0 1154 assert.log('Skipped ' + name + ' ' + skipReason);
michael@0 1155 return promise.resolve(undefined);
michael@0 1156 }
michael@0 1157 }
michael@0 1158
michael@0 1159 if (audit.skipIf) {
michael@0 1160 var skip = (typeof audit.skipIf === 'function') ?
michael@0 1161 audit.skipIf(options) :
michael@0 1162 !!audit.skipIf;
michael@0 1163 if (skip) {
michael@0 1164 var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : '';
michael@0 1165 assert.log('Skipped ' + name + ' ' + reason);
michael@0 1166 return promise.resolve(undefined);
michael@0 1167 }
michael@0 1168 }
michael@0 1169
michael@0 1170 if (skipReason != null) {
michael@0 1171 assert.log('Skipped ' + name + ' ' + skipReason);
michael@0 1172 return promise.resolve(undefined);
michael@0 1173 }
michael@0 1174
michael@0 1175 var start = new Date().getTime();
michael@0 1176
michael@0 1177 var setupDone = helpers._setup(options, name, audit);
michael@0 1178 return setupDone.then(function(chunkLen) {
michael@0 1179 if (typeof chunkLen !== 'number') {
michael@0 1180 chunkLen = 1;
michael@0 1181 }
michael@0 1182
michael@0 1183 // Nasty hack to allow us to auto-skip tests where we're actually testing
michael@0 1184 // a key-sequence (i.e. targeting terminal.js) when there is no terminal
michael@0 1185 if (chunkLen === -1) {
michael@0 1186 assert.log('Skipped ' + name + ' ' + skipReason);
michael@0 1187 return promise.resolve(undefined);
michael@0 1188 }
michael@0 1189
michael@0 1190 if (assert.currentTest) {
michael@0 1191 var responseTime = (new Date().getTime() - start) / chunkLen;
michael@0 1192 totalResponseTime += responseTime;
michael@0 1193 if (responseTime > maxResponseTime) {
michael@0 1194 maxResponseTime = responseTime;
michael@0 1195 maxResponseCulprit = assert.currentTest + '/' + name;
michael@0 1196 }
michael@0 1197 averageOver++;
michael@0 1198 }
michael@0 1199
michael@0 1200 var checkDone = helpers._check(options, name, audit.check);
michael@0 1201 return checkDone.then(function() {
michael@0 1202 var execDone = helpers._exec(options, name, audit.exec);
michael@0 1203 return execDone.then(function(data) {
michael@0 1204 return helpers._post(name, audit, data).then(function() {
michael@0 1205 if (assert.testLogging) {
michael@0 1206 log('- END \'' + name + '\' in ' + assert.currentTest);
michael@0 1207 }
michael@0 1208 });
michael@0 1209 });
michael@0 1210 });
michael@0 1211 });
michael@0 1212 }).then(function() {
michael@0 1213 return options.automator.setInput('');
michael@0 1214 });
michael@0 1215 };
michael@0 1216
michael@0 1217 /**
michael@0 1218 * Compare 2 arrays.
michael@0 1219 */
michael@0 1220 helpers.arrayIs = function(actual, expected, message) {
michael@0 1221 assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
michael@0 1222 assert.ok(Array.isArray(expected), 'expected is not an array: ' + message);
michael@0 1223
michael@0 1224 if (!Array.isArray(actual) || !Array.isArray(expected)) {
michael@0 1225 return;
michael@0 1226 }
michael@0 1227
michael@0 1228 assert.is(actual.length, expected.length, 'array length: ' + message);
michael@0 1229
michael@0 1230 for (var i = 0; i < actual.length && i < expected.length; i++) {
michael@0 1231 assert.is(actual[i], expected[i], 'member[' + i + ']: ' + message);
michael@0 1232 }
michael@0 1233 };
michael@0 1234
michael@0 1235 /**
michael@0 1236 * A quick helper to log to the correct place
michael@0 1237 */
michael@0 1238 function log(message) {
michael@0 1239 if (typeof info === 'function') {
michael@0 1240 info(message);
michael@0 1241 }
michael@0 1242 else {
michael@0 1243 console.log(message);
michael@0 1244 }
michael@0 1245 }

mercurial