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

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

mercurial