gecko/browser/devtools/commandline/test/helpers.js
Joe Walker 3d539a8363 Bug 933727 - Split gcli.jsm and use JS directly from gcli project; r=mratcliffe
* * *
Bug 933727 - Update Developer Toolbar to use promises and split out GCLI; r=mratcliffe
* * *
Bug 933727 - Update GCLI tests to use Task.spawn; r=mratcliffe
* * *
Bug 933727 - Update the Inspector to use split out GCLI; r=pbrosset
* * *
Bug 933727 - Update style editor to use split out GCLI; r=harth
* * *
Bug 933727 - Update Debugger to use split out GCLI; r=past
* * *
Bug 933727 - Update Profiler/Tilt/Scratchpad to use split out GCLI; r=valueof

--HG--
rename : browser/devtools/commandline/test/browser_cmd_pref.js => browser/devtools/commandline/test/browser_cmd_pref1.js
rename : browser/devtools/commandline/test/browser_cmd_pref.js => browser/devtools/commandline/test/browser_cmd_pref2.js
rename : browser/devtools/commandline/test/browser_gcli_cli.js => browser/devtools/commandline/test/browser_gcli_cli1.js
rename : browser/devtools/commandline/test/browser_gcli_cli.js => browser/devtools/commandline/test/browser_gcli_cli2.js
rename : browser/devtools/commandline/test/browser_gcli_completion.js => browser/devtools/commandline/test/browser_gcli_completion1.js
rename : browser/devtools/commandline/test/browser_gcli_completion.js => browser/devtools/commandline/test/browser_gcli_completion2.js
2013-09-03 12:20:27 +01:00

1246 lines
38 KiB
JavaScript

/*
* Copyright 2012, Mozilla Foundation and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
// A copy of this code exists in firefox mochitests. They should be kept
// in sync. Hence the exports synonym for non AMD contexts.
this.EXPORTED_SYMBOLS = [ 'helpers' ];
var helpers = {};
this.helpers = helpers;
var TargetFactory = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.TargetFactory;
var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
var assert = { ok: ok, is: is, log: info };
var util = require('gcli/util/util');
var promise = require('gcli/util/promise');
var cli = require('gcli/cli');
var KeyEvent = require('gcli/util/util').KeyEvent;
var gcli = require('gcli/index');
/**
* See notes in helpers.checkOptions()
*/
var createFFDisplayAutomator = function(display) {
var automator = {
setInput: function(typed) {
return display.inputter.setInput(typed);
},
setCursor: function(cursor) {
return display.inputter.setCursor(cursor);
},
focus: function() {
return display.inputter.focus();
},
fakeKey: function(keyCode) {
var fakeEvent = {
keyCode: keyCode,
preventDefault: function() { },
timeStamp: new Date().getTime()
};
display.inputter.onKeyDown(fakeEvent);
if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
var input = display.inputter.element;
input.value = input.value.slice(0, -1);
}
return display.inputter.handleKeyUp(fakeEvent);
},
getInputState: function() {
return display.inputter.getInputState();
},
getCompleterTemplateData: function() {
return display.completer._getCompleterTemplateData();
},
getErrorMessage: function() {
return display.tooltip.errorEle.textContent;
}
};
Object.defineProperty(automator, 'focusManager', {
get: function() { return display.focusManager; },
enumerable: true
});
Object.defineProperty(automator, 'field', {
get: function() { return display.tooltip.field; },
enumerable: true
});
return automator;
};
/**
* Warning: For use with Firefox Mochitests only.
*
* Open a new tab at a URL and call a callback on load, and then tidy up when
* the callback finishes.
* The function will be passed a set of test options, and will usually return a
* promise to indicate that the tab can be cleared up. (To be formal, we call
* Promise.resolve() on the return value of the callback function)
*
* The options used by addTab include:
* - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest
* - tab: The new XUL tab element, as returned by gBrowser.addTab()
* - target: The debug target as defined by the devtools framework
* - browser: The XUL browser element for the given tab
* - window: Content window for the created tab. a.k.a 'content' in mochitest
* - isFirefox: Always true. Allows test sharing with GCLI
*
* Normally addTab will create an options object containing the values as
* described above. However these options can be customized by the third
* 'options' parameter. This has the ability to customize the value of
* chromeWindow or isFirefox, and to add new properties.
*
* @param url The URL for the new tab
* @param callback The function to call on page load
* @param options An optional set of options to customize the way the tests run
*/
helpers.addTab = function(url, callback, options) {
waitForExplicitFinish();
options = options || {};
options.chromeWindow = options.chromeWindow || window;
options.isFirefox = true;
var tabbrowser = options.chromeWindow.gBrowser;
options.tab = tabbrowser.addTab();
tabbrowser.selectedTab = options.tab;
options.browser = tabbrowser.getBrowserForTab(options.tab);
options.target = TargetFactory.forTab(options.tab);
var loaded = helpers.listenOnce(options.browser, "load", true).then(function(ev) {
options.document = options.browser.contentDocument;
options.window = options.document.defaultView;
var reply = callback.call(null, options);
return promise.resolve(reply).then(null, function(error) {
ok(false, error);
}).then(function() {
tabbrowser.removeTab(options.tab);
delete options.window;
delete options.document;
delete options.target;
delete options.browser;
delete options.tab;
delete options.chromeWindow;
delete options.isFirefox;
});
});
options.browser.contentWindow.location = url;
return loaded;
};
/**
* Open a new tab
* @param url Address of the page to open
* @param options Object to which we add properties describing the new tab. The
* following properties are added:
* - chromeWindow
* - tab
* - browser
* - target
* - document
* - window
* @return A promise which resolves to the options object when the 'load' event
* happens on the new tab
*/
helpers.openTab = function(url, options) {
waitForExplicitFinish();
options = options || {};
options.chromeWindow = options.chromeWindow || window;
options.isFirefox = true;
var tabbrowser = options.chromeWindow.gBrowser;
options.tab = tabbrowser.addTab();
tabbrowser.selectedTab = options.tab;
options.browser = tabbrowser.getBrowserForTab(options.tab);
options.target = TargetFactory.forTab(options.tab);
options.browser.contentWindow.location = url;
return helpers.listenOnce(options.browser, "load", true).then(function() {
options.document = options.browser.contentDocument;
options.window = options.document.defaultView;
return options;
});
};
/**
* Undo the effects of |helpers.openTab|
* @param options The options object passed to |helpers.openTab|
* @return A promise resolved (with undefined) when the tab is closed
*/
helpers.closeTab = function(options) {
options.chromeWindow.gBrowser.removeTab(options.tab);
delete options.window;
delete options.document;
delete options.target;
delete options.browser;
delete options.tab;
delete options.chromeWindow;
delete options.isFirefox;
return promise.resolve(undefined);
};
/**
* Open the developer toolbar in a tab
* @param options Object to which we add properties describing the developer
* toolbar. The following properties are added:
* - automator
* - requisition
* @return A promise which resolves to the options object when the 'load' event
* happens on the new tab
*/
helpers.openToolbar = function(options) {
return options.chromeWindow.DeveloperToolbar.show(true).then(function() {
var display = options.chromeWindow.DeveloperToolbar.display;
options.automator = createFFDisplayAutomator(display);
options.requisition = display.requisition;
});
};
/**
* Undo the effects of |helpers.openToolbar|
* @param options The options object passed to |helpers.openToolbar|
* @return A promise resolved (with undefined) when the toolbar is closed
*/
helpers.closeToolbar = function(options) {
return options.chromeWindow.DeveloperToolbar.hide().then(function() {
delete options.automator;
delete options.requisition;
});
};
/**
* A helper to work with Task.spawn so you can do:
* return Task.spawn(realTestFunc).then(finish, helpers.handleError);
*/
helpers.handleError = function(ex) {
console.error(ex);
ok(false, ex);
finish();
};
/**
* A helper for calling addEventListener and then removeEventListener as soon
* as the event is called, passing the results on as a promise
* @param element The DOM element to listen on
* @param event The name of the event to listen for
* @param useCapture Should we use the capturing phase?
* @return A promise resolved with the event object when the event first happens
*/
helpers.listenOnce = function(element, event, useCapture) {
var deferred = promise.defer();
var onEvent = function(ev) {
element.removeEventListener(event, onEvent, useCapture);
deferred.resolve(ev);
};
element.addEventListener(event, onEvent, useCapture);
return deferred.promise;
};
/**
* A wrapper for calling Services.obs.[add|remove]Observer using promises.
* @param topic The topic parameter to Services.obs.addObserver
* @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a
* default value of false
* @return a promise that resolves when the ObserverService first notifies us
* of the topic. The value of the promise is the first parameter to the observer
* function other parameters are dropped.
*/
helpers.observeOnce = function(topic, ownsWeak=false) {
let deferred = promise.defer();
let resolver = function(subject) {
Services.obs.removeObserver(resolver, topic);
deferred.resolve(subject);
};
Services.obs.addObserver(resolver, topic, ownsWeak);
return deferred.promise;
};
/**
* Takes a function that uses a callback as its last parameter, and returns a
* new function that returns a promise instead
*/
helpers.promiseify = function(functionWithLastParamCallback, scope) {
return function() {
let deferred = promise.defer();
let args = [].slice.call(arguments);
args.push(function(callbackParam) {
deferred.resolve(callbackParam);
});
try {
functionWithLastParamCallback.apply(scope, args);
}
catch (ex) {
deferred.resolve(ex);
}
return deferred.promise;
}
};
/**
* Warning: For use with Firefox Mochitests only.
*
* As addTab, but that also opens the developer toolbar. In addition a new
* 'automator' property is added to the options object with the display from GCLI
* in the developer toolbar
*/
helpers.addTabWithToolbar = function(url, callback, options) {
return helpers.addTab(url, function(innerOptions) {
var win = innerOptions.chromeWindow;
return win.DeveloperToolbar.show(true).then(function() {
var display = win.DeveloperToolbar.display;
innerOptions.automator = createFFDisplayAutomator(display);
innerOptions.requisition = display.requisition;
var reply = callback.call(null, innerOptions);
return promise.resolve(reply).then(null, function(error) {
ok(false, error);
console.error(error);
}).then(function() {
win.DeveloperToolbar.hide().then(function() {
delete innerOptions.automator;
});
});
});
}, options);
};
/**
* Warning: For use with Firefox Mochitests only.
*
* Run a set of test functions stored in the values of the 'exports' object
* functions stored under setup/shutdown will be run at the start/end of the
* sequence of tests.
* A test will be considered finished when its return value is resolved.
* @param options An object to be passed to the test functions
* @param tests An object containing named test functions
* @return a promise which will be resolved when all tests have been run and
* their return values resolved
*/
helpers.runTests = function(options, tests) {
var testNames = Object.keys(tests).filter(function(test) {
return test != "setup" && test != "shutdown";
});
var recover = function(error) {
ok(false, error);
console.error(error);
};
info("SETUP");
var setupDone = (tests.setup != null) ?
promise.resolve(tests.setup(options)) :
promise.resolve();
var testDone = setupDone.then(function() {
return util.promiseEach(testNames, function(testName) {
info(testName);
var action = tests[testName];
if (typeof action === "function") {
var reply = action.call(tests, options);
return promise.resolve(reply);
}
else if (Array.isArray(action)) {
return helpers.audit(options, action);
}
return promise.reject("test action '" + testName +
"' is not a function or helpers.audit() object");
});
}, recover);
return testDone.then(function() {
info("SHUTDOWN");
return (tests.shutdown != null) ?
promise.resolve(tests.shutdown(options)) :
promise.resolve();
}, recover);
};
///////////////////////////////////////////////////////////////////////////////
/**
* Ensure that the options object is setup correctly
* options should contain an automator object that looks like this:
* {
* getInputState: function() { ... },
* setCursor: function(cursor) { ... },
* getCompleterTemplateData: function() { ... },
* focus: function() { ... },
* getErrorMessage: function() { ... },
* fakeKey: function(keyCode) { ... },
* setInput: function(typed) { ... },
* focusManager: ...,
* field: ...,
* }
*/
function checkOptions(options) {
if (options == null) {
console.trace();
throw new Error('Missing options object');
}
if (options.requisition == null) {
console.trace();
throw new Error('options.requisition == null');
}
}
/**
* Various functions to return the actual state of the command line
*/
helpers._actual = {
input: function(options) {
return options.automator.getInputState().typed;
},
hints: function(options) {
return options.automator.getCompleterTemplateData().then(function(data) {
var emptyParams = data.emptyParameters.join('');
return (data.directTabText + emptyParams + data.arrowTabText)
.replace(/\u00a0/g, ' ')
.replace(/\u21E5/, '->')
.replace(/ $/, '');
});
},
markup: function(options) {
var cursor = helpers._actual.cursor(options);
var statusMarkup = options.requisition.getInputStatusMarkup(cursor);
return statusMarkup.map(function(s) {
return new Array(s.string.length + 1).join(s.status.toString()[0]);
}).join('');
},
cursor: function(options) {
return options.automator.getInputState().cursor.start;
},
current: function(options) {
var cursor = helpers._actual.cursor(options);
return options.requisition.getAssignmentAt(cursor).param.name;
},
status: function(options) {
return options.requisition.status.toString();
},
predictions: function(options) {
var cursor = helpers._actual.cursor(options);
var assignment = options.requisition.getAssignmentAt(cursor);
var context = options.requisition.executionContext;
return assignment.getPredictions(context).then(function(predictions) {
return predictions.map(function(prediction) {
return prediction.name;
});
});
},
unassigned: function(options) {
return options.requisition._unassigned.map(function(assignment) {
return assignment.arg.toString();
}.bind(this));
},
outputState: function(options) {
var outputData = options.automator.focusManager._shouldShowOutput();
return outputData.visible + ':' + outputData.reason;
},
tooltipState: function(options) {
var tooltipData = options.automator.focusManager._shouldShowTooltip();
return tooltipData.visible + ':' + tooltipData.reason;
},
options: function(options) {
if (options.automator.field.menu == null) {
return [];
}
return options.automator.field.menu.items.map(function(item) {
return item.name.textContent ? item.name.textContent : item.name;
});
},
message: function(options) {
return options.automator.getErrorMessage();
}
};
function shouldOutputUnquoted(value) {
var type = typeof value;
return value == null || type === 'boolean' || type === 'number';
}
function outputArray(array) {
return (array.length === 0) ?
'[ ]' :
'[ \'' + array.join('\', \'') + '\' ]';
}
helpers._createDebugCheck = function(options) {
checkOptions(options);
var requisition = options.requisition;
var command = requisition.commandAssignment.value;
var cursor = helpers._actual.cursor(options);
var input = helpers._actual.input(options);
var padding = new Array(input.length + 1).join(' ');
var hintsPromise = helpers._actual.hints(options);
var predictionsPromise = helpers._actual.predictions(options);
return promise.all(hintsPromise, predictionsPromise).then(function(values) {
var hints = values[0];
var predictions = values[1];
var output = '';
output += 'return helpers.audit(options, [\n';
output += ' {\n';
if (cursor === input.length) {
output += ' setup: \'' + input + '\',\n';
}
else {
output += ' name: \'' + input + ' (cursor=' + cursor + ')\',\n';
output += ' setup: function() {\n';
output += ' return helpers.setInput(options, \'' + input + '\', ' + cursor + ');\n';
output += ' },\n';
}
output += ' check: {\n';
output += ' input: \'' + input + '\',\n';
output += ' hints: ' + padding + '\'' + hints + '\',\n';
output += ' markup: \'' + helpers._actual.markup(options) + '\',\n';
output += ' cursor: ' + cursor + ',\n';
output += ' current: \'' + helpers._actual.current(options) + '\',\n';
output += ' status: \'' + helpers._actual.status(options) + '\',\n';
output += ' options: ' + outputArray(helpers._actual.options(options)) + ',\n';
output += ' message: \'' + helpers._actual.message(options) + '\',\n';
output += ' predictions: ' + outputArray(predictions) + ',\n';
output += ' unassigned: ' + outputArray(requisition._unassigned) + ',\n';
output += ' outputState: \'' + helpers._actual.outputState(options) + '\',\n';
output += ' tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
(command ? ',' : '') +'\n';
if (command) {
output += ' args: {\n';
output += ' command: { name: \'' + command.name + '\' },\n';
requisition.getAssignments().forEach(function(assignment) {
output += ' ' + assignment.param.name + ': { ';
if (typeof assignment.value === 'string') {
output += 'value: \'' + assignment.value + '\', ';
}
else if (shouldOutputUnquoted(assignment.value)) {
output += 'value: ' + assignment.value + ', ';
}
else {
output += '/*value:' + assignment.value + ',*/ ';
}
output += 'arg: \'' + assignment.arg + '\', ';
output += 'status: \'' + assignment.getStatus().toString() + '\', ';
output += 'message: \'' + assignment.message + '\'';
output += ' },\n';
});
output += ' }\n';
}
output += ' },\n';
output += ' exec: {\n';
output += ' output: \'\',\n';
output += ' type: \'string\',\n';
output += ' error: false\n';
output += ' }\n';
output += ' }\n';
output += ']);';
return output;
}.bind(this), util.errorHandler);
};
/**
* Simulate focusing the input field
*/
helpers.focusInput = function(options) {
checkOptions(options);
options.automator.focus();
};
/**
* Simulate pressing TAB in the input field
*/
helpers.pressTab = function(options) {
checkOptions(options);
return helpers.pressKey(options, KeyEvent.DOM_VK_TAB);
};
/**
* Simulate pressing RETURN in the input field
*/
helpers.pressReturn = function(options) {
checkOptions(options);
return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN);
};
/**
* Simulate pressing a key by keyCode in the input field
*/
helpers.pressKey = function(options, keyCode) {
checkOptions(options);
return options.automator.fakeKey(keyCode);
};
/**
* A list of special key presses and how to to them, for the benefit of
* helpers.setInput
*/
var ACTIONS = {
'<TAB>': function(options) {
return helpers.pressTab(options);
},
'<RETURN>': function(options) {
return helpers.pressReturn(options);
},
'<UP>': function(options) {
return helpers.pressKey(options, KeyEvent.DOM_VK_UP);
},
'<DOWN>': function(options) {
return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN);
},
'<BACKSPACE>': function(options) {
return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE);
}
};
/**
* Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into
* an array like [ 'blah', '<TAB>', 'foo', '<UP>' ].
* When using this RegExp, you also need to filter out the blank strings.
*/
var CHUNKER = /([^<]*)(<[A-Z]+>)/;
/**
* Alter the input to <code>typed</code> optionally leaving the cursor at
* <code>cursor</code>.
* @return A promise of the number of key-presses to respond
*/
helpers.setInput = function(options, typed, cursor) {
checkOptions(options);
var inputPromise;
var automator = options.automator;
// We try to measure average keypress time, but setInput can simulate
// several, so we try to keep track of how many
var chunkLen = 1;
// The easy case is a simple string without things like <TAB>
if (typed.indexOf('<') === -1) {
inputPromise = automator.setInput(typed);
}
else {
// Cut the input up into input strings separated by '<KEY>' tokens. The
// CHUNKS RegExp leaves blanks so we filter them out.
var chunks = typed.split(CHUNKER).filter(function(s) {
return s !== '';
});
chunkLen = chunks.length + 1;
// We're working on this in chunks so first clear the input
inputPromise = automator.setInput('').then(function() {
return util.promiseEach(chunks, function(chunk) {
if (chunk.charAt(0) === '<') {
var action = ACTIONS[chunk];
if (typeof action !== 'function') {
console.error('Known actions: ' + Object.keys(ACTIONS).join());
throw new Error('Key action not found "' + chunk + '"');
}
return action(options);
}
else {
return automator.setInput(automator.getInputState().typed + chunk);
}
});
});
}
return inputPromise.then(function() {
if (cursor != null) {
automator.setCursor({ start: cursor, end: cursor });
}
if (automator.focusManager) {
automator.focusManager.onInputChange();
}
// Firefox testing is noisy and distant, so logging helps
if (options.isFirefox) {
var cursorStr = (cursor == null ? '' : ', ' + cursor);
log('setInput("' + typed + '"' + cursorStr + ')');
}
return chunkLen;
});
};
/**
* Helper for helpers.audit() to ensure that all the 'check' properties match.
* See helpers.audit for more information.
* @param name The name to use in error messages
* @param checks See helpers.audit for a list of available checks
* @return A promise which resolves to undefined when the checks are complete
*/
helpers._check = function(options, name, checks) {
// A test method to check that all args are assigned in some way
var requisition = options.requisition;
requisition._args.forEach(function(arg) {
if (arg.assignment == null) {
assert.ok(false, 'No assignment for ' + arg);
}
});
if (checks == null) {
return promise.resolve();
}
var outstanding = [];
var suffix = name ? ' (for \'' + name + '\')' : '';
if (!options.isNoDom && 'input' in checks) {
assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
}
if (!options.isNoDom && 'cursor' in checks) {
assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
}
if (!options.isNoDom && 'current' in checks) {
assert.is(helpers._actual.current(options), checks.current, 'current' + suffix);
}
if ('status' in checks) {
assert.is(helpers._actual.status(options), checks.status, 'status' + suffix);
}
if (!options.isNoDom && 'markup' in checks) {
assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
}
if (!options.isNoDom && 'hints' in checks) {
var hintCheck = function(actualHints) {
assert.is(actualHints, checks.hints, 'hints' + suffix);
};
outstanding.push(helpers._actual.hints(options).then(hintCheck));
}
if (!options.isNoDom && 'predictions' in checks) {
var predictionsCheck = function(actualPredictions) {
helpers.arrayIs(actualPredictions,
checks.predictions,
'predictions' + suffix);
};
outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
}
if (!options.isNoDom && 'predictionsContains' in checks) {
var containsCheck = function(actualPredictions) {
checks.predictionsContains.forEach(function(prediction) {
var index = actualPredictions.indexOf(prediction);
assert.ok(index !== -1,
'predictionsContains:' + prediction + suffix);
});
};
outstanding.push(helpers._actual.predictions(options).then(containsCheck));
}
if ('unassigned' in checks) {
helpers.arrayIs(helpers._actual.unassigned(options),
checks.unassigned,
'unassigned' + suffix);
}
/* TODO: Fix this
if (!options.isNoDom && 'tooltipState' in checks) {
assert.is(helpers._actual.tooltipState(options),
checks.tooltipState,
'tooltipState' + suffix);
}
*/
if (!options.isNoDom && 'outputState' in checks) {
assert.is(helpers._actual.outputState(options),
checks.outputState,
'outputState' + suffix);
}
if (!options.isNoDom && 'options' in checks) {
helpers.arrayIs(helpers._actual.options(options),
checks.options,
'options' + suffix);
}
if (!options.isNoDom && 'error' in checks) {
assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
}
if (checks.args != null) {
Object.keys(checks.args).forEach(function(paramName) {
var check = checks.args[paramName];
// We allow an 'argument' called 'command' to be the command itself, but
// what if the command has a parameter called 'command' (for example, an
// 'exec' command)? We default to using the parameter because checking
// the command value is less useful
var assignment = requisition.getAssignment(paramName);
if (assignment == null && paramName === 'command') {
assignment = requisition.commandAssignment;
}
if (assignment == null) {
assert.ok(false, 'Unknown arg: ' + paramName + suffix);
return;
}
if ('value' in check) {
if (typeof check.value === 'function') {
try {
check.value(assignment.value);
}
catch (ex) {
assert.ok(false, '' + ex);
}
}
else {
assert.is(assignment.value,
check.value,
'arg.' + paramName + '.value' + suffix);
}
}
if ('name' in check) {
assert.is(assignment.value.name,
check.name,
'arg.' + paramName + '.name' + suffix);
}
if ('type' in check) {
assert.is(assignment.arg.type,
check.type,
'arg.' + paramName + '.type' + suffix);
}
if ('arg' in check) {
assert.is(assignment.arg.toString(),
check.arg,
'arg.' + paramName + '.arg' + suffix);
}
if ('status' in check) {
assert.is(assignment.getStatus().toString(),
check.status,
'arg.' + paramName + '.status' + suffix);
}
if (!options.isNoDom && 'message' in check) {
if (typeof check.message.test === 'function') {
assert.ok(check.message.test(assignment.message),
'arg.' + paramName + '.message' + suffix);
}
else {
assert.is(assignment.message,
check.message,
'arg.' + paramName + '.message' + suffix);
}
}
});
}
return promise.all(outstanding).then(function() {
// Ensure the promise resolves to nothing
return undefined;
});
};
/**
* Helper for helpers.audit() to ensure that all the 'exec' properties work.
* See helpers.audit for more information.
* @param name The name to use in error messages
* @param expected See helpers.audit for a list of available exec checks
* @return A promise which resolves to undefined when the checks are complete
*/
helpers._exec = function(options, name, expected) {
var requisition = options.requisition;
if (expected == null) {
return promise.resolve({});
}
var origLogErrors = cli.logErrors;
if (expected.error) {
cli.logErrors = false;
}
try {
return requisition.exec({ hidden: true }).then(function(output) {
if ('type' in expected) {
assert.is(output.type,
expected.type,
'output.type for: ' + name);
}
if ('error' in expected) {
assert.is(output.error,
expected.error,
'output.error for: ' + name);
}
if (!('output' in expected)) {
return { output: output };
}
var context = requisition.conversionContext;
var convertPromise;
if (options.isNoDom) {
convertPromise = output.convert('string', context);
}
else {
convertPromise = output.convert('dom', context).then(function(node) {
return node.textContent.trim();
});
}
return convertPromise.then(function(textOutput) {
var doTest = function(match, against) {
// Only log the real textContent if the test fails
if (against.match(match) != null) {
assert.ok(true, 'html output for \'' + name + '\' ' +
'should match /' + (match.source || match) + '/');
} else {
assert.ok(false, 'html output for \'' + name + '\' ' +
'should match /' + (match.source || match) + '/. ' +
'Actual textContent: "' + against + '"');
}
};
if (typeof expected.output === 'string') {
assert.is(textOutput,
expected.output,
'html output for ' + name);
}
else if (Array.isArray(expected.output)) {
expected.output.forEach(function(match) {
doTest(match, textOutput);
});
}
else {
doTest(expected.output, textOutput);
}
if (expected.error) {
cli.logErrors = origLogErrors;
}
return { output: output, text: textOutput };
});
}.bind(this)).then(function(data) {
if (expected.error) {
cli.logErrors = origLogErrors;
}
return data;
});
}
catch (ex) {
assert.ok(false, 'Failure executing \'' + name + '\': ' + ex);
util.errorHandler(ex);
if (expected.error) {
cli.logErrors = origLogErrors;
}
return promise.resolve({});
}
};
/**
* Helper to setup the test
*/
helpers._setup = function(options, name, audit) {
if (typeof audit.setup === 'string') {
return helpers.setInput(options, audit.setup);
}
if (typeof audit.setup === 'function') {
return promise.resolve(audit.setup.call(audit));
}
return promise.reject('\'setup\' property must be a string or a function. Is ' + audit.setup);
};
/**
* Helper to shutdown the test
*/
helpers._post = function(name, audit, data) {
if (typeof audit.post === 'function') {
return promise.resolve(audit.post.call(audit, data.output, data.text));
}
return promise.resolve(audit.post);
};
/*
* We do some basic response time stats so we can see if we're getting slow
*/
var totalResponseTime = 0;
var averageOver = 0;
var maxResponseTime = 0;
var maxResponseCulprit;
var start;
/**
* Restart the stats collection process
*/
helpers.resetResponseTimes = function() {
start = new Date().getTime();
totalResponseTime = 0;
averageOver = 0;
maxResponseTime = 0;
maxResponseCulprit = undefined;
};
/**
* Expose an average response time in milliseconds
*/
Object.defineProperty(helpers, 'averageResponseTime', {
get: function() {
return averageOver === 0 ?
undefined :
Math.round(100 * totalResponseTime / averageOver) / 100;
},
enumerable: true
});
/**
* Expose a maximum response time in milliseconds
*/
Object.defineProperty(helpers, 'maxResponseTime', {
get: function() { return Math.round(maxResponseTime * 100) / 100; },
enumerable: true
});
/**
* Expose the name of the test that provided the maximum response time
*/
Object.defineProperty(helpers, 'maxResponseCulprit', {
get: function() { return maxResponseCulprit; },
enumerable: true
});
/**
* Quick summary of the times
*/
Object.defineProperty(helpers, 'timingSummary', {
get: function() {
var elapsed = (new Date().getTime() - start) / 1000;
return 'Total ' + elapsed + 's, ' +
'ave response ' + helpers.averageResponseTime + 'ms, ' +
'max response ' + helpers.maxResponseTime + 'ms ' +
'from \'' + helpers.maxResponseCulprit + '\'';
},
enumerable: true
});
/**
* A way of turning a set of tests into something more declarative, this helps
* to allow tests to be asynchronous.
* @param audits An array of objects each of which contains:
* - setup: string/function to be called to set the test up.
* If audit is a string then it is passed to helpers.setInput().
* If audit is a function then it is executed. The tests will wait while
* tests that return promises complete.
* - name: For debugging purposes. If name is undefined, and 'setup'
* is a string then the setup value will be used automatically
* - skipIf: A function to define if the test should be skipped. Useful for
* excluding tests from certain environments (e.g. nodom, firefox, etc).
* The name of the test will be used in log messages noting the skip
* See helpers.reason for pre-defined skip functions. The skip function must
* be synchronous, and will be passed the test options object.
* - skipRemainingIf: A function to skip all the remaining audits in this set.
* See skipIf for details of how skip functions work.
* - check: Check data. Available checks:
* - input: The text displayed in the input field
* - cursor: The position of the start of the cursor
* - status: One of 'VALID', 'ERROR', 'INCOMPLETE'
* - hints: The hint text, i.e. a concatenation of the directTabText, the
* emptyParameters and the arrowTabText. The text as inserted into the UI
* will include NBSP and Unicode RARR characters, these should be
* represented using normal space and '->' for the arrow
* - markup: What state should the error markup be in. e.g. 'VVVIIIEEE'
* - args: Maps of checks to make against the arguments:
* - value: i.e. assignment.value (which ignores defaultValue)
* - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
* Care should be taken with this since it's something of an
* implementation detail
* - arg: The toString value of the argument
* - status: i.e. assignment.getStatus
* - message: i.e. assignment.message
* - name: For commands - checks assignment.value.name
* - exec: Object to indicate we should execute the command and check the
* results. Available checks:
* - output: A string, RegExp or array of RegExps to compare with the output
* If typeof output is a string then the output should be exactly equal
* to the given string. If the type of output is a RegExp or array of
* RegExps then the output should match all RegExps
* - post: Function to be called after the checks have been run
*/
helpers.audit = function(options, audits) {
checkOptions(options);
var skipReason = null;
return util.promiseEach(audits, function(audit) {
var name = audit.name;
if (name == null && typeof audit.setup === 'string') {
name = audit.setup;
}
if (assert.testLogging) {
log('- START \'' + name + '\' in ' + assert.currentTest);
}
if (audit.skipRemainingIf) {
var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ?
audit.skipRemainingIf(options) :
!!audit.skipRemainingIf;
if (skipRemainingIf) {
skipReason = audit.skipRemainingIf.name ?
'due to ' + audit.skipRemainingIf.name :
'';
assert.log('Skipped ' + name + ' ' + skipReason);
return promise.resolve(undefined);
}
}
if (audit.skipIf) {
var skip = (typeof audit.skipIf === 'function') ?
audit.skipIf(options) :
!!audit.skipIf;
if (skip) {
var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : '';
assert.log('Skipped ' + name + ' ' + reason);
return promise.resolve(undefined);
}
}
if (skipReason != null) {
assert.log('Skipped ' + name + ' ' + skipReason);
return promise.resolve(undefined);
}
var start = new Date().getTime();
var setupDone = helpers._setup(options, name, audit);
return setupDone.then(function(chunkLen) {
if (typeof chunkLen !== 'number') {
chunkLen = 1;
}
// Nasty hack to allow us to auto-skip tests where we're actually testing
// a key-sequence (i.e. targeting terminal.js) when there is no terminal
if (chunkLen === -1) {
assert.log('Skipped ' + name + ' ' + skipReason);
return promise.resolve(undefined);
}
if (assert.currentTest) {
var responseTime = (new Date().getTime() - start) / chunkLen;
totalResponseTime += responseTime;
if (responseTime > maxResponseTime) {
maxResponseTime = responseTime;
maxResponseCulprit = assert.currentTest + '/' + name;
}
averageOver++;
}
var checkDone = helpers._check(options, name, audit.check);
return checkDone.then(function() {
var execDone = helpers._exec(options, name, audit.exec);
return execDone.then(function(data) {
return helpers._post(name, audit, data).then(function() {
if (assert.testLogging) {
log('- END \'' + name + '\' in ' + assert.currentTest);
}
});
});
});
});
}).then(function() {
return options.automator.setInput('');
});
};
/**
* Compare 2 arrays.
*/
helpers.arrayIs = function(actual, expected, message) {
assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
assert.ok(Array.isArray(expected), 'expected is not an array: ' + message);
if (!Array.isArray(actual) || !Array.isArray(expected)) {
return;
}
assert.is(actual.length, expected.length, 'array length: ' + message);
for (var i = 0; i < actual.length && i < expected.length; i++) {
assert.is(actual[i], expected[i], 'member[' + i + ']: ' + message);
}
};
/**
* A quick helper to log to the correct place
*/
function log(message) {
if (typeof info === 'function') {
info(message);
}
else {
console.log(message);
}
}