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 +}