browser/devtools/commandline/test/helpers.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/commandline/test/helpers.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1245 @@
     1.4 +/*
     1.5 + * Copyright 2012, Mozilla Foundation and contributors
     1.6 + *
     1.7 + * Licensed under the Apache License, Version 2.0 (the "License");
     1.8 + * you may not use this file except in compliance with the License.
     1.9 + * You may obtain a copy of the License at
    1.10 + *
    1.11 + * http://www.apache.org/licenses/LICENSE-2.0
    1.12 + *
    1.13 + * Unless required by applicable law or agreed to in writing, software
    1.14 + * distributed under the License is distributed on an "AS IS" BASIS,
    1.15 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    1.16 + * See the License for the specific language governing permissions and
    1.17 + * limitations under the License.
    1.18 + */
    1.19 +
    1.20 +'use strict';
    1.21 +
    1.22 +// A copy of this code exists in firefox mochitests. They should be kept
    1.23 +// in sync. Hence the exports synonym for non AMD contexts.
    1.24 +this.EXPORTED_SYMBOLS = [ 'helpers' ];
    1.25 +var helpers = {};
    1.26 +this.helpers = helpers;
    1.27 +
    1.28 +var TargetFactory = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.TargetFactory;
    1.29 +var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.30 +
    1.31 +var assert = { ok: ok, is: is, log: info };
    1.32 +var util = require('gcli/util/util');
    1.33 +var promise = require('gcli/util/promise');
    1.34 +var cli = require('gcli/cli');
    1.35 +var KeyEvent = require('gcli/util/util').KeyEvent;
    1.36 +var gcli = require('gcli/index');
    1.37 +
    1.38 +/**
    1.39 + * See notes in helpers.checkOptions()
    1.40 + */
    1.41 +var createFFDisplayAutomator = function(display) {
    1.42 +  var automator = {
    1.43 +    setInput: function(typed) {
    1.44 +      return display.inputter.setInput(typed);
    1.45 +    },
    1.46 +
    1.47 +    setCursor: function(cursor) {
    1.48 +      return display.inputter.setCursor(cursor);
    1.49 +    },
    1.50 +
    1.51 +    focus: function() {
    1.52 +      return display.inputter.focus();
    1.53 +    },
    1.54 +
    1.55 +    fakeKey: function(keyCode) {
    1.56 +      var fakeEvent = {
    1.57 +        keyCode: keyCode,
    1.58 +        preventDefault: function() { },
    1.59 +        timeStamp: new Date().getTime()
    1.60 +      };
    1.61 +
    1.62 +      display.inputter.onKeyDown(fakeEvent);
    1.63 +
    1.64 +      if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
    1.65 +        var input = display.inputter.element;
    1.66 +        input.value = input.value.slice(0, -1);
    1.67 +      }
    1.68 +
    1.69 +      return display.inputter.handleKeyUp(fakeEvent);
    1.70 +    },
    1.71 +
    1.72 +    getInputState: function() {
    1.73 +      return display.inputter.getInputState();
    1.74 +    },
    1.75 +
    1.76 +    getCompleterTemplateData: function() {
    1.77 +      return display.completer._getCompleterTemplateData();
    1.78 +    },
    1.79 +
    1.80 +    getErrorMessage: function() {
    1.81 +      return display.tooltip.errorEle.textContent;
    1.82 +    }
    1.83 +  };
    1.84 +
    1.85 +  Object.defineProperty(automator, 'focusManager', {
    1.86 +    get: function() { return display.focusManager; },
    1.87 +    enumerable: true
    1.88 +  });
    1.89 +
    1.90 +  Object.defineProperty(automator, 'field', {
    1.91 +    get: function() { return display.tooltip.field; },
    1.92 +    enumerable: true
    1.93 +  });
    1.94 +
    1.95 +  return automator;
    1.96 +};
    1.97 +
    1.98 +/**
    1.99 + * Warning: For use with Firefox Mochitests only.
   1.100 + *
   1.101 + * Open a new tab at a URL and call a callback on load, and then tidy up when
   1.102 + * the callback finishes.
   1.103 + * The function will be passed a set of test options, and will usually return a
   1.104 + * promise to indicate that the tab can be cleared up. (To be formal, we call
   1.105 + * Promise.resolve() on the return value of the callback function)
   1.106 + *
   1.107 + * The options used by addTab include:
   1.108 + * - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest
   1.109 + * - tab: The new XUL tab element, as returned by gBrowser.addTab()
   1.110 + * - target: The debug target as defined by the devtools framework
   1.111 + * - browser: The XUL browser element for the given tab
   1.112 + * - window: Content window for the created tab. a.k.a 'content' in mochitest
   1.113 + * - isFirefox: Always true. Allows test sharing with GCLI
   1.114 + *
   1.115 + * Normally addTab will create an options object containing the values as
   1.116 + * described above. However these options can be customized by the third
   1.117 + * 'options' parameter. This has the ability to customize the value of
   1.118 + * chromeWindow or isFirefox, and to add new properties.
   1.119 + *
   1.120 + * @param url The URL for the new tab
   1.121 + * @param callback The function to call on page load
   1.122 + * @param options An optional set of options to customize the way the tests run
   1.123 + */
   1.124 +helpers.addTab = function(url, callback, options) {
   1.125 +  waitForExplicitFinish();
   1.126 +
   1.127 +  options = options || {};
   1.128 +  options.chromeWindow = options.chromeWindow || window;
   1.129 +  options.isFirefox = true;
   1.130 +
   1.131 +  var tabbrowser = options.chromeWindow.gBrowser;
   1.132 +  options.tab = tabbrowser.addTab();
   1.133 +  tabbrowser.selectedTab = options.tab;
   1.134 +  options.browser = tabbrowser.getBrowserForTab(options.tab);
   1.135 +  options.target = TargetFactory.forTab(options.tab);
   1.136 +
   1.137 +  var loaded = helpers.listenOnce(options.browser, "load", true).then(function(ev) {
   1.138 +    options.document = options.browser.contentDocument;
   1.139 +    options.window = options.document.defaultView;
   1.140 +
   1.141 +    var reply = callback.call(null, options);
   1.142 +
   1.143 +    return promise.resolve(reply).then(null, function(error) {
   1.144 +      ok(false, error);
   1.145 +    }).then(function() {
   1.146 +      tabbrowser.removeTab(options.tab);
   1.147 +
   1.148 +      delete options.window;
   1.149 +      delete options.document;
   1.150 +
   1.151 +      delete options.target;
   1.152 +      delete options.browser;
   1.153 +      delete options.tab;
   1.154 +
   1.155 +      delete options.chromeWindow;
   1.156 +      delete options.isFirefox;
   1.157 +    });
   1.158 +  });
   1.159 +
   1.160 +  options.browser.contentWindow.location = url;
   1.161 +  return loaded;
   1.162 +};
   1.163 +
   1.164 +/**
   1.165 + * Open a new tab
   1.166 + * @param url Address of the page to open
   1.167 + * @param options Object to which we add properties describing the new tab. The
   1.168 + * following properties are added:
   1.169 + * - chromeWindow
   1.170 + * - tab
   1.171 + * - browser
   1.172 + * - target
   1.173 + * - document
   1.174 + * - window
   1.175 + * @return A promise which resolves to the options object when the 'load' event
   1.176 + * happens on the new tab
   1.177 + */
   1.178 +helpers.openTab = function(url, options) {
   1.179 +  waitForExplicitFinish();
   1.180 +
   1.181 +  options = options || {};
   1.182 +  options.chromeWindow = options.chromeWindow || window;
   1.183 +  options.isFirefox = true;
   1.184 +
   1.185 +  var tabbrowser = options.chromeWindow.gBrowser;
   1.186 +  options.tab = tabbrowser.addTab();
   1.187 +  tabbrowser.selectedTab = options.tab;
   1.188 +  options.browser = tabbrowser.getBrowserForTab(options.tab);
   1.189 +  options.target = TargetFactory.forTab(options.tab);
   1.190 +
   1.191 +  options.browser.contentWindow.location = url;
   1.192 +
   1.193 +  return helpers.listenOnce(options.browser, "load", true).then(function() {
   1.194 +    options.document = options.browser.contentDocument;
   1.195 +    options.window = options.document.defaultView;
   1.196 +    return options;
   1.197 +  });
   1.198 +};
   1.199 +
   1.200 +/**
   1.201 + * Undo the effects of |helpers.openTab|
   1.202 + * @param options The options object passed to |helpers.openTab|
   1.203 + * @return A promise resolved (with undefined) when the tab is closed
   1.204 + */
   1.205 +helpers.closeTab = function(options) {
   1.206 +  options.chromeWindow.gBrowser.removeTab(options.tab);
   1.207 +
   1.208 +  delete options.window;
   1.209 +  delete options.document;
   1.210 +
   1.211 +  delete options.target;
   1.212 +  delete options.browser;
   1.213 +  delete options.tab;
   1.214 +
   1.215 +  delete options.chromeWindow;
   1.216 +  delete options.isFirefox;
   1.217 +
   1.218 +  return promise.resolve(undefined);
   1.219 +};
   1.220 +
   1.221 +/**
   1.222 + * Open the developer toolbar in a tab
   1.223 + * @param options Object to which we add properties describing the developer
   1.224 + * toolbar. The following properties are added:
   1.225 + * - automator
   1.226 + * - requisition
   1.227 + * @return A promise which resolves to the options object when the 'load' event
   1.228 + * happens on the new tab
   1.229 + */
   1.230 +helpers.openToolbar = function(options) {
   1.231 +  return options.chromeWindow.DeveloperToolbar.show(true).then(function() {
   1.232 +    var display = options.chromeWindow.DeveloperToolbar.display;
   1.233 +    options.automator = createFFDisplayAutomator(display);
   1.234 +    options.requisition = display.requisition;
   1.235 +  });
   1.236 +};
   1.237 +
   1.238 +/**
   1.239 + * Undo the effects of |helpers.openToolbar|
   1.240 + * @param options The options object passed to |helpers.openToolbar|
   1.241 + * @return A promise resolved (with undefined) when the toolbar is closed
   1.242 + */
   1.243 +helpers.closeToolbar = function(options) {
   1.244 +  return options.chromeWindow.DeveloperToolbar.hide().then(function() {
   1.245 +    delete options.automator;
   1.246 +    delete options.requisition;
   1.247 +  });
   1.248 +};
   1.249 +
   1.250 +/**
   1.251 + * A helper to work with Task.spawn so you can do:
   1.252 + *   return Task.spawn(realTestFunc).then(finish, helpers.handleError);
   1.253 + */
   1.254 +helpers.handleError = function(ex) {
   1.255 +  console.error(ex);
   1.256 +  ok(false, ex);
   1.257 +  finish();
   1.258 +};
   1.259 +
   1.260 +/**
   1.261 + * A helper for calling addEventListener and then removeEventListener as soon
   1.262 + * as the event is called, passing the results on as a promise
   1.263 + * @param element The DOM element to listen on
   1.264 + * @param event The name of the event to listen for
   1.265 + * @param useCapture Should we use the capturing phase?
   1.266 + * @return A promise resolved with the event object when the event first happens
   1.267 + */
   1.268 +helpers.listenOnce = function(element, event, useCapture) {
   1.269 +  var deferred = promise.defer();
   1.270 +  var onEvent = function(ev) {
   1.271 +    element.removeEventListener(event, onEvent, useCapture);
   1.272 +    deferred.resolve(ev);
   1.273 +  };
   1.274 +  element.addEventListener(event, onEvent, useCapture);
   1.275 +  return deferred.promise;
   1.276 +};
   1.277 +
   1.278 +/**
   1.279 + * A wrapper for calling Services.obs.[add|remove]Observer using promises.
   1.280 + * @param topic The topic parameter to Services.obs.addObserver
   1.281 + * @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a
   1.282 + * default value of false
   1.283 + * @return a promise that resolves when the ObserverService first notifies us
   1.284 + * of the topic. The value of the promise is the first parameter to the observer
   1.285 + * function other parameters are dropped.
   1.286 + */
   1.287 +helpers.observeOnce = function(topic, ownsWeak=false) {
   1.288 +  let deferred = promise.defer();
   1.289 +  let resolver = function(subject) {
   1.290 +    Services.obs.removeObserver(resolver, topic);
   1.291 +    deferred.resolve(subject);
   1.292 +  };
   1.293 +  Services.obs.addObserver(resolver, topic, ownsWeak);
   1.294 +  return deferred.promise;
   1.295 +};
   1.296 +
   1.297 +/**
   1.298 + * Takes a function that uses a callback as its last parameter, and returns a
   1.299 + * new function that returns a promise instead
   1.300 + */
   1.301 +helpers.promiseify = function(functionWithLastParamCallback, scope) {
   1.302 +  return function() {
   1.303 +    let deferred = promise.defer();
   1.304 +
   1.305 +    let args = [].slice.call(arguments);
   1.306 +    args.push(function(callbackParam) {
   1.307 +      deferred.resolve(callbackParam);
   1.308 +    });
   1.309 +
   1.310 +    try {
   1.311 +      functionWithLastParamCallback.apply(scope, args);
   1.312 +    }
   1.313 +    catch (ex) {
   1.314 +      deferred.resolve(ex);
   1.315 +    }
   1.316 +
   1.317 +    return deferred.promise;
   1.318 +  }
   1.319 +};
   1.320 +
   1.321 +/**
   1.322 + * Warning: For use with Firefox Mochitests only.
   1.323 + *
   1.324 + * As addTab, but that also opens the developer toolbar. In addition a new
   1.325 + * 'automator' property is added to the options object with the display from GCLI
   1.326 + * in the developer toolbar
   1.327 + */
   1.328 +helpers.addTabWithToolbar = function(url, callback, options) {
   1.329 +  return helpers.addTab(url, function(innerOptions) {
   1.330 +    var win = innerOptions.chromeWindow;
   1.331 +
   1.332 +    return win.DeveloperToolbar.show(true).then(function() {
   1.333 +      var display = win.DeveloperToolbar.display;
   1.334 +      innerOptions.automator = createFFDisplayAutomator(display);
   1.335 +      innerOptions.requisition = display.requisition;
   1.336 +
   1.337 +      var reply = callback.call(null, innerOptions);
   1.338 +
   1.339 +      return promise.resolve(reply).then(null, function(error) {
   1.340 +        ok(false, error);
   1.341 +        console.error(error);
   1.342 +      }).then(function() {
   1.343 +        win.DeveloperToolbar.hide().then(function() {
   1.344 +          delete innerOptions.automator;
   1.345 +        });
   1.346 +      });
   1.347 +    });
   1.348 +  }, options);
   1.349 +};
   1.350 +
   1.351 +/**
   1.352 + * Warning: For use with Firefox Mochitests only.
   1.353 + *
   1.354 + * Run a set of test functions stored in the values of the 'exports' object
   1.355 + * functions stored under setup/shutdown will be run at the start/end of the
   1.356 + * sequence of tests.
   1.357 + * A test will be considered finished when its return value is resolved.
   1.358 + * @param options An object to be passed to the test functions
   1.359 + * @param tests An object containing named test functions
   1.360 + * @return a promise which will be resolved when all tests have been run and
   1.361 + * their return values resolved
   1.362 + */
   1.363 +helpers.runTests = function(options, tests) {
   1.364 +  var testNames = Object.keys(tests).filter(function(test) {
   1.365 +    return test != "setup" && test != "shutdown";
   1.366 +  });
   1.367 +
   1.368 +  var recover = function(error) {
   1.369 +    ok(false, error);
   1.370 +    console.error(error);
   1.371 +  };
   1.372 +
   1.373 +  info("SETUP");
   1.374 +  var setupDone = (tests.setup != null) ?
   1.375 +      promise.resolve(tests.setup(options)) :
   1.376 +      promise.resolve();
   1.377 +
   1.378 +  var testDone = setupDone.then(function() {
   1.379 +    return util.promiseEach(testNames, function(testName) {
   1.380 +      info(testName);
   1.381 +      var action = tests[testName];
   1.382 +
   1.383 +      if (typeof action === "function") {
   1.384 +        var reply = action.call(tests, options);
   1.385 +        return promise.resolve(reply);
   1.386 +      }
   1.387 +      else if (Array.isArray(action)) {
   1.388 +        return helpers.audit(options, action);
   1.389 +      }
   1.390 +
   1.391 +      return promise.reject("test action '" + testName +
   1.392 +                            "' is not a function or helpers.audit() object");
   1.393 +    });
   1.394 +  }, recover);
   1.395 +
   1.396 +  return testDone.then(function() {
   1.397 +    info("SHUTDOWN");
   1.398 +    return (tests.shutdown != null) ?
   1.399 +        promise.resolve(tests.shutdown(options)) :
   1.400 +        promise.resolve();
   1.401 +  }, recover);
   1.402 +};
   1.403 +
   1.404 +///////////////////////////////////////////////////////////////////////////////
   1.405 +
   1.406 +/**
   1.407 + * Ensure that the options object is setup correctly
   1.408 + * options should contain an automator object that looks like this:
   1.409 + * {
   1.410 + *   getInputState: function() { ... },
   1.411 + *   setCursor: function(cursor) { ... },
   1.412 + *   getCompleterTemplateData: function() { ... },
   1.413 + *   focus: function() { ... },
   1.414 + *   getErrorMessage: function() { ... },
   1.415 + *   fakeKey: function(keyCode) { ... },
   1.416 + *   setInput: function(typed) { ... },
   1.417 + *   focusManager: ...,
   1.418 + *   field: ...,
   1.419 + * }
   1.420 + */
   1.421 +function checkOptions(options) {
   1.422 +  if (options == null) {
   1.423 +    console.trace();
   1.424 +    throw new Error('Missing options object');
   1.425 +  }
   1.426 +  if (options.requisition == null) {
   1.427 +    console.trace();
   1.428 +    throw new Error('options.requisition == null');
   1.429 +  }
   1.430 +}
   1.431 +
   1.432 +/**
   1.433 + * Various functions to return the actual state of the command line
   1.434 + */
   1.435 +helpers._actual = {
   1.436 +  input: function(options) {
   1.437 +    return options.automator.getInputState().typed;
   1.438 +  },
   1.439 +
   1.440 +  hints: function(options) {
   1.441 +    return options.automator.getCompleterTemplateData().then(function(data) {
   1.442 +      var emptyParams = data.emptyParameters.join('');
   1.443 +      return (data.directTabText + emptyParams + data.arrowTabText)
   1.444 +                .replace(/\u00a0/g, ' ')
   1.445 +                .replace(/\u21E5/, '->')
   1.446 +                .replace(/ $/, '');
   1.447 +    });
   1.448 +  },
   1.449 +
   1.450 +  markup: function(options) {
   1.451 +    var cursor = helpers._actual.cursor(options);
   1.452 +    var statusMarkup = options.requisition.getInputStatusMarkup(cursor);
   1.453 +    return statusMarkup.map(function(s) {
   1.454 +      return new Array(s.string.length + 1).join(s.status.toString()[0]);
   1.455 +    }).join('');
   1.456 +  },
   1.457 +
   1.458 +  cursor: function(options) {
   1.459 +    return options.automator.getInputState().cursor.start;
   1.460 +  },
   1.461 +
   1.462 +  current: function(options) {
   1.463 +    var cursor = helpers._actual.cursor(options);
   1.464 +    return options.requisition.getAssignmentAt(cursor).param.name;
   1.465 +  },
   1.466 +
   1.467 +  status: function(options) {
   1.468 +    return options.requisition.status.toString();
   1.469 +  },
   1.470 +
   1.471 +  predictions: function(options) {
   1.472 +    var cursor = helpers._actual.cursor(options);
   1.473 +    var assignment = options.requisition.getAssignmentAt(cursor);
   1.474 +    var context = options.requisition.executionContext;
   1.475 +    return assignment.getPredictions(context).then(function(predictions) {
   1.476 +      return predictions.map(function(prediction) {
   1.477 +        return prediction.name;
   1.478 +      });
   1.479 +    });
   1.480 +  },
   1.481 +
   1.482 +  unassigned: function(options) {
   1.483 +    return options.requisition._unassigned.map(function(assignment) {
   1.484 +      return assignment.arg.toString();
   1.485 +    }.bind(this));
   1.486 +  },
   1.487 +
   1.488 +  outputState: function(options) {
   1.489 +    var outputData = options.automator.focusManager._shouldShowOutput();
   1.490 +    return outputData.visible + ':' + outputData.reason;
   1.491 +  },
   1.492 +
   1.493 +  tooltipState: function(options) {
   1.494 +    var tooltipData = options.automator.focusManager._shouldShowTooltip();
   1.495 +    return tooltipData.visible + ':' + tooltipData.reason;
   1.496 +  },
   1.497 +
   1.498 +  options: function(options) {
   1.499 +    if (options.automator.field.menu == null) {
   1.500 +      return [];
   1.501 +    }
   1.502 +    return options.automator.field.menu.items.map(function(item) {
   1.503 +      return item.name.textContent ? item.name.textContent : item.name;
   1.504 +    });
   1.505 +  },
   1.506 +
   1.507 +  message: function(options) {
   1.508 +    return options.automator.getErrorMessage();
   1.509 +  }
   1.510 +};
   1.511 +
   1.512 +function shouldOutputUnquoted(value) {
   1.513 +  var type = typeof value;
   1.514 +  return value == null || type === 'boolean' || type === 'number';
   1.515 +}
   1.516 +
   1.517 +function outputArray(array) {
   1.518 +  return (array.length === 0) ?
   1.519 +      '[ ]' :
   1.520 +      '[ \'' + array.join('\', \'') + '\' ]';
   1.521 +}
   1.522 +
   1.523 +helpers._createDebugCheck = function(options) {
   1.524 +  checkOptions(options);
   1.525 +  var requisition = options.requisition;
   1.526 +  var command = requisition.commandAssignment.value;
   1.527 +  var cursor = helpers._actual.cursor(options);
   1.528 +  var input = helpers._actual.input(options);
   1.529 +  var padding = new Array(input.length + 1).join(' ');
   1.530 +
   1.531 +  var hintsPromise = helpers._actual.hints(options);
   1.532 +  var predictionsPromise = helpers._actual.predictions(options);
   1.533 +
   1.534 +  return promise.all(hintsPromise, predictionsPromise).then(function(values) {
   1.535 +    var hints = values[0];
   1.536 +    var predictions = values[1];
   1.537 +    var output = '';
   1.538 +
   1.539 +    output += 'return helpers.audit(options, [\n';
   1.540 +    output += '  {\n';
   1.541 +
   1.542 +    if (cursor === input.length) {
   1.543 +      output += '    setup:    \'' + input + '\',\n';
   1.544 +    }
   1.545 +    else {
   1.546 +      output += '    name: \'' + input + ' (cursor=' + cursor + ')\',\n';
   1.547 +      output += '    setup: function() {\n';
   1.548 +      output += '      return helpers.setInput(options, \'' + input + '\', ' + cursor + ');\n';
   1.549 +      output += '    },\n';
   1.550 +    }
   1.551 +
   1.552 +    output += '    check: {\n';
   1.553 +
   1.554 +    output += '      input:  \'' + input + '\',\n';
   1.555 +    output += '      hints:  ' + padding + '\'' + hints + '\',\n';
   1.556 +    output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
   1.557 +    output += '      cursor: ' + cursor + ',\n';
   1.558 +    output += '      current: \'' + helpers._actual.current(options) + '\',\n';
   1.559 +    output += '      status: \'' + helpers._actual.status(options) + '\',\n';
   1.560 +    output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
   1.561 +    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
   1.562 +    output += '      predictions: ' + outputArray(predictions) + ',\n';
   1.563 +    output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
   1.564 +    output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
   1.565 +    output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
   1.566 +              (command ? ',' : '') +'\n';
   1.567 +
   1.568 +    if (command) {
   1.569 +      output += '      args: {\n';
   1.570 +      output += '        command: { name: \'' + command.name + '\' },\n';
   1.571 +
   1.572 +      requisition.getAssignments().forEach(function(assignment) {
   1.573 +        output += '        ' + assignment.param.name + ': { ';
   1.574 +
   1.575 +        if (typeof assignment.value === 'string') {
   1.576 +          output += 'value: \'' + assignment.value + '\', ';
   1.577 +        }
   1.578 +        else if (shouldOutputUnquoted(assignment.value)) {
   1.579 +          output += 'value: ' + assignment.value + ', ';
   1.580 +        }
   1.581 +        else {
   1.582 +          output += '/*value:' + assignment.value + ',*/ ';
   1.583 +        }
   1.584 +
   1.585 +        output += 'arg: \'' + assignment.arg + '\', ';
   1.586 +        output += 'status: \'' + assignment.getStatus().toString() + '\', ';
   1.587 +        output += 'message: \'' + assignment.message + '\'';
   1.588 +        output += ' },\n';
   1.589 +      });
   1.590 +
   1.591 +      output += '      }\n';
   1.592 +    }
   1.593 +
   1.594 +    output += '    },\n';
   1.595 +    output += '    exec: {\n';
   1.596 +    output += '      output: \'\',\n';
   1.597 +    output += '      type: \'string\',\n';
   1.598 +    output += '      error: false\n';
   1.599 +    output += '    }\n';
   1.600 +    output += '  }\n';
   1.601 +    output += ']);';
   1.602 +
   1.603 +    return output;
   1.604 +  }.bind(this), util.errorHandler);
   1.605 +};
   1.606 +
   1.607 +/**
   1.608 + * Simulate focusing the input field
   1.609 + */
   1.610 +helpers.focusInput = function(options) {
   1.611 +  checkOptions(options);
   1.612 +  options.automator.focus();
   1.613 +};
   1.614 +
   1.615 +/**
   1.616 + * Simulate pressing TAB in the input field
   1.617 + */
   1.618 +helpers.pressTab = function(options) {
   1.619 +  checkOptions(options);
   1.620 +  return helpers.pressKey(options, KeyEvent.DOM_VK_TAB);
   1.621 +};
   1.622 +
   1.623 +/**
   1.624 + * Simulate pressing RETURN in the input field
   1.625 + */
   1.626 +helpers.pressReturn = function(options) {
   1.627 +  checkOptions(options);
   1.628 +  return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN);
   1.629 +};
   1.630 +
   1.631 +/**
   1.632 + * Simulate pressing a key by keyCode in the input field
   1.633 + */
   1.634 +helpers.pressKey = function(options, keyCode) {
   1.635 +  checkOptions(options);
   1.636 +  return options.automator.fakeKey(keyCode);
   1.637 +};
   1.638 +
   1.639 +/**
   1.640 + * A list of special key presses and how to to them, for the benefit of
   1.641 + * helpers.setInput
   1.642 + */
   1.643 +var ACTIONS = {
   1.644 +  '<TAB>': function(options) {
   1.645 +    return helpers.pressTab(options);
   1.646 +  },
   1.647 +  '<RETURN>': function(options) {
   1.648 +    return helpers.pressReturn(options);
   1.649 +  },
   1.650 +  '<UP>': function(options) {
   1.651 +    return helpers.pressKey(options, KeyEvent.DOM_VK_UP);
   1.652 +  },
   1.653 +  '<DOWN>': function(options) {
   1.654 +    return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN);
   1.655 +  },
   1.656 +  '<BACKSPACE>': function(options) {
   1.657 +    return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE);
   1.658 +  }
   1.659 +};
   1.660 +
   1.661 +/**
   1.662 + * Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into
   1.663 + * an array like [ 'blah', '<TAB>', 'foo', '<UP>' ].
   1.664 + * When using this RegExp, you also need to filter out the blank strings.
   1.665 + */
   1.666 +var CHUNKER = /([^<]*)(<[A-Z]+>)/;
   1.667 +
   1.668 +/**
   1.669 + * Alter the input to <code>typed</code> optionally leaving the cursor at
   1.670 + * <code>cursor</code>.
   1.671 + * @return A promise of the number of key-presses to respond
   1.672 + */
   1.673 +helpers.setInput = function(options, typed, cursor) {
   1.674 +  checkOptions(options);
   1.675 +  var inputPromise;
   1.676 +  var automator = options.automator;
   1.677 +  // We try to measure average keypress time, but setInput can simulate
   1.678 +  // several, so we try to keep track of how many
   1.679 +  var chunkLen = 1;
   1.680 +
   1.681 +  // The easy case is a simple string without things like <TAB>
   1.682 +  if (typed.indexOf('<') === -1) {
   1.683 +    inputPromise = automator.setInput(typed);
   1.684 +  }
   1.685 +  else {
   1.686 +    // Cut the input up into input strings separated by '<KEY>' tokens. The
   1.687 +    // CHUNKS RegExp leaves blanks so we filter them out.
   1.688 +    var chunks = typed.split(CHUNKER).filter(function(s) {
   1.689 +      return s !== '';
   1.690 +    });
   1.691 +    chunkLen = chunks.length + 1;
   1.692 +
   1.693 +    // We're working on this in chunks so first clear the input
   1.694 +    inputPromise = automator.setInput('').then(function() {
   1.695 +      return util.promiseEach(chunks, function(chunk) {
   1.696 +        if (chunk.charAt(0) === '<') {
   1.697 +          var action = ACTIONS[chunk];
   1.698 +          if (typeof action !== 'function') {
   1.699 +            console.error('Known actions: ' + Object.keys(ACTIONS).join());
   1.700 +            throw new Error('Key action not found "' + chunk + '"');
   1.701 +          }
   1.702 +          return action(options);
   1.703 +        }
   1.704 +        else {
   1.705 +          return automator.setInput(automator.getInputState().typed + chunk);
   1.706 +        }
   1.707 +      });
   1.708 +    });
   1.709 +  }
   1.710 +
   1.711 +  return inputPromise.then(function() {
   1.712 +    if (cursor != null) {
   1.713 +      automator.setCursor({ start: cursor, end: cursor });
   1.714 +    }
   1.715 +
   1.716 +    if (automator.focusManager) {
   1.717 +      automator.focusManager.onInputChange();
   1.718 +    }
   1.719 +
   1.720 +    // Firefox testing is noisy and distant, so logging helps
   1.721 +    if (options.isFirefox) {
   1.722 +      var cursorStr = (cursor == null ? '' : ', ' + cursor);
   1.723 +      log('setInput("' + typed + '"' + cursorStr + ')');
   1.724 +    }
   1.725 +
   1.726 +    return chunkLen;
   1.727 +  });
   1.728 +};
   1.729 +
   1.730 +/**
   1.731 + * Helper for helpers.audit() to ensure that all the 'check' properties match.
   1.732 + * See helpers.audit for more information.
   1.733 + * @param name The name to use in error messages
   1.734 + * @param checks See helpers.audit for a list of available checks
   1.735 + * @return A promise which resolves to undefined when the checks are complete
   1.736 + */
   1.737 +helpers._check = function(options, name, checks) {
   1.738 +  // A test method to check that all args are assigned in some way
   1.739 +  var requisition = options.requisition;
   1.740 +  requisition._args.forEach(function(arg) {
   1.741 +    if (arg.assignment == null) {
   1.742 +      assert.ok(false, 'No assignment for ' + arg);
   1.743 +    }
   1.744 +  });
   1.745 +
   1.746 +  if (checks == null) {
   1.747 +    return promise.resolve();
   1.748 +  }
   1.749 +
   1.750 +  var outstanding = [];
   1.751 +  var suffix = name ? ' (for \'' + name + '\')' : '';
   1.752 +
   1.753 +  if (!options.isNoDom && 'input' in checks) {
   1.754 +    assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
   1.755 +  }
   1.756 +
   1.757 +  if (!options.isNoDom && 'cursor' in checks) {
   1.758 +    assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
   1.759 +  }
   1.760 +
   1.761 +  if (!options.isNoDom && 'current' in checks) {
   1.762 +    assert.is(helpers._actual.current(options), checks.current, 'current' + suffix);
   1.763 +  }
   1.764 +
   1.765 +  if ('status' in checks) {
   1.766 +    assert.is(helpers._actual.status(options), checks.status, 'status' + suffix);
   1.767 +  }
   1.768 +
   1.769 +  if (!options.isNoDom && 'markup' in checks) {
   1.770 +    assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
   1.771 +  }
   1.772 +
   1.773 +  if (!options.isNoDom && 'hints' in checks) {
   1.774 +    var hintCheck = function(actualHints) {
   1.775 +      assert.is(actualHints, checks.hints, 'hints' + suffix);
   1.776 +    };
   1.777 +    outstanding.push(helpers._actual.hints(options).then(hintCheck));
   1.778 +  }
   1.779 +
   1.780 +  if (!options.isNoDom && 'predictions' in checks) {
   1.781 +    var predictionsCheck = function(actualPredictions) {
   1.782 +      helpers.arrayIs(actualPredictions,
   1.783 +                       checks.predictions,
   1.784 +                       'predictions' + suffix);
   1.785 +    };
   1.786 +    outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
   1.787 +  }
   1.788 +
   1.789 +  if (!options.isNoDom && 'predictionsContains' in checks) {
   1.790 +    var containsCheck = function(actualPredictions) {
   1.791 +      checks.predictionsContains.forEach(function(prediction) {
   1.792 +        var index = actualPredictions.indexOf(prediction);
   1.793 +        assert.ok(index !== -1,
   1.794 +                  'predictionsContains:' + prediction + suffix);
   1.795 +      });
   1.796 +    };
   1.797 +    outstanding.push(helpers._actual.predictions(options).then(containsCheck));
   1.798 +  }
   1.799 +
   1.800 +  if ('unassigned' in checks) {
   1.801 +    helpers.arrayIs(helpers._actual.unassigned(options),
   1.802 +                     checks.unassigned,
   1.803 +                     'unassigned' + suffix);
   1.804 +  }
   1.805 +
   1.806 +  /* TODO: Fix this
   1.807 +  if (!options.isNoDom && 'tooltipState' in checks) {
   1.808 +    assert.is(helpers._actual.tooltipState(options),
   1.809 +              checks.tooltipState,
   1.810 +              'tooltipState' + suffix);
   1.811 +  }
   1.812 +  */
   1.813 +
   1.814 +  if (!options.isNoDom && 'outputState' in checks) {
   1.815 +    assert.is(helpers._actual.outputState(options),
   1.816 +              checks.outputState,
   1.817 +              'outputState' + suffix);
   1.818 +  }
   1.819 +
   1.820 +  if (!options.isNoDom && 'options' in checks) {
   1.821 +    helpers.arrayIs(helpers._actual.options(options),
   1.822 +                     checks.options,
   1.823 +                     'options' + suffix);
   1.824 +  }
   1.825 +
   1.826 +  if (!options.isNoDom && 'error' in checks) {
   1.827 +    assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
   1.828 +  }
   1.829 +
   1.830 +  if (checks.args != null) {
   1.831 +    Object.keys(checks.args).forEach(function(paramName) {
   1.832 +      var check = checks.args[paramName];
   1.833 +
   1.834 +      // We allow an 'argument' called 'command' to be the command itself, but
   1.835 +      // what if the command has a parameter called 'command' (for example, an
   1.836 +      // 'exec' command)? We default to using the parameter because checking
   1.837 +      // the command value is less useful
   1.838 +      var assignment = requisition.getAssignment(paramName);
   1.839 +      if (assignment == null && paramName === 'command') {
   1.840 +        assignment = requisition.commandAssignment;
   1.841 +      }
   1.842 +
   1.843 +      if (assignment == null) {
   1.844 +        assert.ok(false, 'Unknown arg: ' + paramName + suffix);
   1.845 +        return;
   1.846 +      }
   1.847 +
   1.848 +      if ('value' in check) {
   1.849 +        if (typeof check.value === 'function') {
   1.850 +          try {
   1.851 +            check.value(assignment.value);
   1.852 +          }
   1.853 +          catch (ex) {
   1.854 +            assert.ok(false, '' + ex);
   1.855 +          }
   1.856 +        }
   1.857 +        else {
   1.858 +          assert.is(assignment.value,
   1.859 +                    check.value,
   1.860 +                    'arg.' + paramName + '.value' + suffix);
   1.861 +        }
   1.862 +      }
   1.863 +
   1.864 +      if ('name' in check) {
   1.865 +        assert.is(assignment.value.name,
   1.866 +                  check.name,
   1.867 +                  'arg.' + paramName + '.name' + suffix);
   1.868 +      }
   1.869 +
   1.870 +      if ('type' in check) {
   1.871 +        assert.is(assignment.arg.type,
   1.872 +                  check.type,
   1.873 +                  'arg.' + paramName + '.type' + suffix);
   1.874 +      }
   1.875 +
   1.876 +      if ('arg' in check) {
   1.877 +        assert.is(assignment.arg.toString(),
   1.878 +                  check.arg,
   1.879 +                  'arg.' + paramName + '.arg' + suffix);
   1.880 +      }
   1.881 +
   1.882 +      if ('status' in check) {
   1.883 +        assert.is(assignment.getStatus().toString(),
   1.884 +                  check.status,
   1.885 +                  'arg.' + paramName + '.status' + suffix);
   1.886 +      }
   1.887 +
   1.888 +      if (!options.isNoDom && 'message' in check) {
   1.889 +        if (typeof check.message.test === 'function') {
   1.890 +          assert.ok(check.message.test(assignment.message),
   1.891 +                    'arg.' + paramName + '.message' + suffix);
   1.892 +        }
   1.893 +        else {
   1.894 +          assert.is(assignment.message,
   1.895 +                    check.message,
   1.896 +                    'arg.' + paramName + '.message' + suffix);
   1.897 +        }
   1.898 +      }
   1.899 +    });
   1.900 +  }
   1.901 +
   1.902 +  return promise.all(outstanding).then(function() {
   1.903 +    // Ensure the promise resolves to nothing
   1.904 +    return undefined;
   1.905 +  });
   1.906 +};
   1.907 +
   1.908 +/**
   1.909 + * Helper for helpers.audit() to ensure that all the 'exec' properties work.
   1.910 + * See helpers.audit for more information.
   1.911 + * @param name The name to use in error messages
   1.912 + * @param expected See helpers.audit for a list of available exec checks
   1.913 + * @return A promise which resolves to undefined when the checks are complete
   1.914 + */
   1.915 +helpers._exec = function(options, name, expected) {
   1.916 +  var requisition = options.requisition;
   1.917 +  if (expected == null) {
   1.918 +    return promise.resolve({});
   1.919 +  }
   1.920 +
   1.921 +  var origLogErrors = cli.logErrors;
   1.922 +  if (expected.error) {
   1.923 +    cli.logErrors = false;
   1.924 +  }
   1.925 +
   1.926 +  try {
   1.927 +    return requisition.exec({ hidden: true }).then(function(output) {
   1.928 +      if ('type' in expected) {
   1.929 +        assert.is(output.type,
   1.930 +                  expected.type,
   1.931 +                  'output.type for: ' + name);
   1.932 +      }
   1.933 +
   1.934 +      if ('error' in expected) {
   1.935 +        assert.is(output.error,
   1.936 +                  expected.error,
   1.937 +                  'output.error for: ' + name);
   1.938 +      }
   1.939 +
   1.940 +      if (!('output' in expected)) {
   1.941 +        return { output: output };
   1.942 +      }
   1.943 +
   1.944 +      var context = requisition.conversionContext;
   1.945 +      var convertPromise;
   1.946 +      if (options.isNoDom) {
   1.947 +        convertPromise = output.convert('string', context);
   1.948 +      }
   1.949 +      else {
   1.950 +        convertPromise = output.convert('dom', context).then(function(node) {
   1.951 +          return node.textContent.trim();
   1.952 +        });
   1.953 +      }
   1.954 +
   1.955 +      return convertPromise.then(function(textOutput) {
   1.956 +        var doTest = function(match, against) {
   1.957 +          // Only log the real textContent if the test fails
   1.958 +          if (against.match(match) != null) {
   1.959 +            assert.ok(true, 'html output for \'' + name + '\' ' +
   1.960 +                            'should match /' + (match.source || match) + '/');
   1.961 +          } else {
   1.962 +            assert.ok(false, 'html output for \'' + name + '\' ' +
   1.963 +                             'should match /' + (match.source || match) + '/. ' +
   1.964 +                             'Actual textContent: "' + against + '"');
   1.965 +          }
   1.966 +        };
   1.967 +
   1.968 +        if (typeof expected.output === 'string') {
   1.969 +          assert.is(textOutput,
   1.970 +                    expected.output,
   1.971 +                    'html output for ' + name);
   1.972 +        }
   1.973 +        else if (Array.isArray(expected.output)) {
   1.974 +          expected.output.forEach(function(match) {
   1.975 +            doTest(match, textOutput);
   1.976 +          });
   1.977 +        }
   1.978 +        else {
   1.979 +          doTest(expected.output, textOutput);
   1.980 +        }
   1.981 +
   1.982 +        if (expected.error) {
   1.983 +          cli.logErrors = origLogErrors;
   1.984 +        }
   1.985 +        return { output: output, text: textOutput };
   1.986 +      });
   1.987 +    }.bind(this)).then(function(data) {
   1.988 +      if (expected.error) {
   1.989 +        cli.logErrors = origLogErrors;
   1.990 +      }
   1.991 +
   1.992 +      return data;
   1.993 +    });
   1.994 +  }
   1.995 +  catch (ex) {
   1.996 +    assert.ok(false, 'Failure executing \'' + name + '\': ' + ex);
   1.997 +    util.errorHandler(ex);
   1.998 +
   1.999 +    if (expected.error) {
  1.1000 +      cli.logErrors = origLogErrors;
  1.1001 +    }
  1.1002 +    return promise.resolve({});
  1.1003 +  }
  1.1004 +};
  1.1005 +
  1.1006 +/**
  1.1007 + * Helper to setup the test
  1.1008 + */
  1.1009 +helpers._setup = function(options, name, audit) {
  1.1010 +  if (typeof audit.setup === 'string') {
  1.1011 +    return helpers.setInput(options, audit.setup);
  1.1012 +  }
  1.1013 +
  1.1014 +  if (typeof audit.setup === 'function') {
  1.1015 +    return promise.resolve(audit.setup.call(audit));
  1.1016 +  }
  1.1017 +
  1.1018 +  return promise.reject('\'setup\' property must be a string or a function. Is ' + audit.setup);
  1.1019 +};
  1.1020 +
  1.1021 +/**
  1.1022 + * Helper to shutdown the test
  1.1023 + */
  1.1024 +helpers._post = function(name, audit, data) {
  1.1025 +  if (typeof audit.post === 'function') {
  1.1026 +    return promise.resolve(audit.post.call(audit, data.output, data.text));
  1.1027 +  }
  1.1028 +  return promise.resolve(audit.post);
  1.1029 +};
  1.1030 +
  1.1031 +/*
  1.1032 + * We do some basic response time stats so we can see if we're getting slow
  1.1033 + */
  1.1034 +var totalResponseTime = 0;
  1.1035 +var averageOver = 0;
  1.1036 +var maxResponseTime = 0;
  1.1037 +var maxResponseCulprit;
  1.1038 +var start;
  1.1039 +
  1.1040 +/**
  1.1041 + * Restart the stats collection process
  1.1042 + */
  1.1043 +helpers.resetResponseTimes = function() {
  1.1044 +  start = new Date().getTime();
  1.1045 +  totalResponseTime = 0;
  1.1046 +  averageOver = 0;
  1.1047 +  maxResponseTime = 0;
  1.1048 +  maxResponseCulprit = undefined;
  1.1049 +};
  1.1050 +
  1.1051 +/**
  1.1052 + * Expose an average response time in milliseconds
  1.1053 + */
  1.1054 +Object.defineProperty(helpers, 'averageResponseTime', {
  1.1055 +  get: function() {
  1.1056 +    return averageOver === 0 ?
  1.1057 +        undefined :
  1.1058 +        Math.round(100 * totalResponseTime / averageOver) / 100;
  1.1059 +  },
  1.1060 +  enumerable: true
  1.1061 +});
  1.1062 +
  1.1063 +/**
  1.1064 + * Expose a maximum response time in milliseconds
  1.1065 + */
  1.1066 +Object.defineProperty(helpers, 'maxResponseTime', {
  1.1067 +  get: function() { return Math.round(maxResponseTime * 100) / 100; },
  1.1068 +  enumerable: true
  1.1069 +});
  1.1070 +
  1.1071 +/**
  1.1072 + * Expose the name of the test that provided the maximum response time
  1.1073 + */
  1.1074 +Object.defineProperty(helpers, 'maxResponseCulprit', {
  1.1075 +  get: function() { return maxResponseCulprit; },
  1.1076 +  enumerable: true
  1.1077 +});
  1.1078 +
  1.1079 +/**
  1.1080 + * Quick summary of the times
  1.1081 + */
  1.1082 +Object.defineProperty(helpers, 'timingSummary', {
  1.1083 +  get: function() {
  1.1084 +    var elapsed = (new Date().getTime() - start) / 1000;
  1.1085 +    return 'Total ' + elapsed + 's, ' +
  1.1086 +           'ave response ' + helpers.averageResponseTime + 'ms, ' +
  1.1087 +           'max response ' + helpers.maxResponseTime + 'ms ' +
  1.1088 +           'from \'' + helpers.maxResponseCulprit + '\'';
  1.1089 +  },
  1.1090 +  enumerable: true
  1.1091 +});
  1.1092 +
  1.1093 +/**
  1.1094 + * A way of turning a set of tests into something more declarative, this helps
  1.1095 + * to allow tests to be asynchronous.
  1.1096 + * @param audits An array of objects each of which contains:
  1.1097 + * - setup: string/function to be called to set the test up.
  1.1098 + *     If audit is a string then it is passed to helpers.setInput().
  1.1099 + *     If audit is a function then it is executed. The tests will wait while
  1.1100 + *     tests that return promises complete.
  1.1101 + * - name: For debugging purposes. If name is undefined, and 'setup'
  1.1102 + *     is a string then the setup value will be used automatically
  1.1103 + * - skipIf: A function to define if the test should be skipped. Useful for
  1.1104 + *     excluding tests from certain environments (e.g. nodom, firefox, etc).
  1.1105 + *     The name of the test will be used in log messages noting the skip
  1.1106 + *     See helpers.reason for pre-defined skip functions. The skip function must
  1.1107 + *     be synchronous, and will be passed the test options object.
  1.1108 + * - skipRemainingIf: A function to skip all the remaining audits in this set.
  1.1109 + *     See skipIf for details of how skip functions work.
  1.1110 + * - check: Check data. Available checks:
  1.1111 + *   - input: The text displayed in the input field
  1.1112 + *   - cursor: The position of the start of the cursor
  1.1113 + *   - status: One of 'VALID', 'ERROR', 'INCOMPLETE'
  1.1114 + *   - hints: The hint text, i.e. a concatenation of the directTabText, the
  1.1115 + *       emptyParameters and the arrowTabText. The text as inserted into the UI
  1.1116 + *       will include NBSP and Unicode RARR characters, these should be
  1.1117 + *       represented using normal space and '->' for the arrow
  1.1118 + *   - markup: What state should the error markup be in. e.g. 'VVVIIIEEE'
  1.1119 + *   - args: Maps of checks to make against the arguments:
  1.1120 + *     - value: i.e. assignment.value (which ignores defaultValue)
  1.1121 + *     - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
  1.1122 + *             Care should be taken with this since it's something of an
  1.1123 + *             implementation detail
  1.1124 + *     - arg: The toString value of the argument
  1.1125 + *     - status: i.e. assignment.getStatus
  1.1126 + *     - message: i.e. assignment.message
  1.1127 + *     - name: For commands - checks assignment.value.name
  1.1128 + * - exec: Object to indicate we should execute the command and check the
  1.1129 + *     results. Available checks:
  1.1130 + *   - output: A string, RegExp or array of RegExps to compare with the output
  1.1131 + *       If typeof output is a string then the output should be exactly equal
  1.1132 + *       to the given string. If the type of output is a RegExp or array of
  1.1133 + *       RegExps then the output should match all RegExps
  1.1134 + * - post: Function to be called after the checks have been run
  1.1135 + */
  1.1136 +helpers.audit = function(options, audits) {
  1.1137 +  checkOptions(options);
  1.1138 +  var skipReason = null;
  1.1139 +  return util.promiseEach(audits, function(audit) {
  1.1140 +    var name = audit.name;
  1.1141 +    if (name == null && typeof audit.setup === 'string') {
  1.1142 +      name = audit.setup;
  1.1143 +    }
  1.1144 +
  1.1145 +    if (assert.testLogging) {
  1.1146 +      log('- START \'' + name + '\' in ' + assert.currentTest);
  1.1147 +    }
  1.1148 +
  1.1149 +    if (audit.skipRemainingIf) {
  1.1150 +      var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ?
  1.1151 +          audit.skipRemainingIf(options) :
  1.1152 +          !!audit.skipRemainingIf;
  1.1153 +      if (skipRemainingIf) {
  1.1154 +        skipReason = audit.skipRemainingIf.name ?
  1.1155 +            'due to ' + audit.skipRemainingIf.name :
  1.1156 +            '';
  1.1157 +        assert.log('Skipped ' + name + ' ' + skipReason);
  1.1158 +        return promise.resolve(undefined);
  1.1159 +      }
  1.1160 +    }
  1.1161 +
  1.1162 +    if (audit.skipIf) {
  1.1163 +      var skip = (typeof audit.skipIf === 'function') ?
  1.1164 +          audit.skipIf(options) :
  1.1165 +          !!audit.skipIf;
  1.1166 +      if (skip) {
  1.1167 +        var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : '';
  1.1168 +        assert.log('Skipped ' + name + ' ' + reason);
  1.1169 +        return promise.resolve(undefined);
  1.1170 +      }
  1.1171 +    }
  1.1172 +
  1.1173 +    if (skipReason != null) {
  1.1174 +      assert.log('Skipped ' + name + ' ' + skipReason);
  1.1175 +      return promise.resolve(undefined);
  1.1176 +    }
  1.1177 +
  1.1178 +    var start = new Date().getTime();
  1.1179 +
  1.1180 +    var setupDone = helpers._setup(options, name, audit);
  1.1181 +    return setupDone.then(function(chunkLen) {
  1.1182 +      if (typeof chunkLen !== 'number') {
  1.1183 +        chunkLen = 1;
  1.1184 +      }
  1.1185 +
  1.1186 +      // Nasty hack to allow us to auto-skip tests where we're actually testing
  1.1187 +      // a key-sequence (i.e. targeting terminal.js) when there is no terminal
  1.1188 +      if (chunkLen === -1) {
  1.1189 +        assert.log('Skipped ' + name + ' ' + skipReason);
  1.1190 +        return promise.resolve(undefined);
  1.1191 +      }
  1.1192 +
  1.1193 +      if (assert.currentTest) {
  1.1194 +        var responseTime = (new Date().getTime() - start) / chunkLen;
  1.1195 +        totalResponseTime += responseTime;
  1.1196 +        if (responseTime > maxResponseTime) {
  1.1197 +          maxResponseTime = responseTime;
  1.1198 +          maxResponseCulprit = assert.currentTest + '/' + name;
  1.1199 +        }
  1.1200 +        averageOver++;
  1.1201 +      }
  1.1202 +
  1.1203 +      var checkDone = helpers._check(options, name, audit.check);
  1.1204 +      return checkDone.then(function() {
  1.1205 +        var execDone = helpers._exec(options, name, audit.exec);
  1.1206 +        return execDone.then(function(data) {
  1.1207 +          return helpers._post(name, audit, data).then(function() {
  1.1208 +            if (assert.testLogging) {
  1.1209 +              log('- END \'' + name + '\' in ' + assert.currentTest);
  1.1210 +            }
  1.1211 +          });
  1.1212 +        });
  1.1213 +      });
  1.1214 +    });
  1.1215 +  }).then(function() {
  1.1216 +    return options.automator.setInput('');
  1.1217 +  });
  1.1218 +};
  1.1219 +
  1.1220 +/**
  1.1221 + * Compare 2 arrays.
  1.1222 + */
  1.1223 +helpers.arrayIs = function(actual, expected, message) {
  1.1224 +  assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
  1.1225 +  assert.ok(Array.isArray(expected), 'expected is not an array: ' + message);
  1.1226 +
  1.1227 +  if (!Array.isArray(actual) || !Array.isArray(expected)) {
  1.1228 +    return;
  1.1229 +  }
  1.1230 +
  1.1231 +  assert.is(actual.length, expected.length, 'array length: ' + message);
  1.1232 +
  1.1233 +  for (var i = 0; i < actual.length && i < expected.length; i++) {
  1.1234 +    assert.is(actual[i], expected[i], 'member[' + i + ']: ' + message);
  1.1235 +  }
  1.1236 +};
  1.1237 +
  1.1238 +/**
  1.1239 + * A quick helper to log to the correct place
  1.1240 + */
  1.1241 +function log(message) {
  1.1242 +  if (typeof info === 'function') {
  1.1243 +    info(message);
  1.1244 +  }
  1.1245 +  else {
  1.1246 +    console.log(message);
  1.1247 +  }
  1.1248 +}

mercurial