mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1136 lines
35 KiB
JavaScript
1136 lines
35 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
// Use as a JSM
|
|
// ------------
|
|
// helpers._createDebugCheck() can be useful at runtime. To use as a JSM, copy
|
|
// commandline/test/helpers.js to shared/helpers.jsm, and then import to
|
|
// DeveloperToolbar.jsm with:
|
|
// XPCOMUtils.defineLazyModuleGetter(this, "helpers",
|
|
// "resource:///modules/devtools/helpers.jsm");
|
|
// At the bottom of DeveloperToolbar.prototype._onload add this:
|
|
// var options = { display: this.display };
|
|
// this._input.onkeypress = function(ev) {
|
|
// helpers.setup(options);
|
|
// dump(helpers._createDebugCheck() + '\n\n');
|
|
// };
|
|
// Now GCLI will emit output on every keypress that both explains the state
|
|
// of GCLI and can be run as a test case.
|
|
|
|
this.EXPORTED_SYMBOLS = [ 'helpers' ];
|
|
var helpers = {};
|
|
this.helpers = helpers;
|
|
let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
|
|
Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
|
|
|
|
let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
|
|
let TargetFactory = (Cu.import("resource://gre/modules/devtools/Loader.jsm", {})).devtools.TargetFactory;
|
|
|
|
let promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
|
|
let assert = { ok: ok, is: is, log: info };
|
|
|
|
var util = require('util/util');
|
|
var cli = require('gcli/cli');
|
|
|
|
var converters = require('gcli/converters');
|
|
|
|
/**
|
|
* 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) {
|
|
var deferred = promise.defer();
|
|
|
|
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 onPageLoad = function() {
|
|
options.browser.removeEventListener("load", onPageLoad, true);
|
|
options.document = options.browser.contentDocument;
|
|
options.window = options.document.defaultView;
|
|
|
|
var cleanUp = 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;
|
|
|
|
deferred.resolve();
|
|
};
|
|
|
|
var reply = callback(options);
|
|
promise.resolve(reply).then(cleanUp, function(error) {
|
|
ok(false, error);
|
|
cleanUp();
|
|
});
|
|
};
|
|
|
|
options.browser.contentWindow.location = url;
|
|
options.browser.addEventListener("load", onPageLoad, true);
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
helpers.openTab = function(url, options) {
|
|
var deferred = promise.defer();
|
|
|
|
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;
|
|
|
|
var onPageLoad = function() {
|
|
options.browser.removeEventListener("load", onPageLoad, true);
|
|
options.document = options.browser.contentDocument;
|
|
options.window = options.document.defaultView;
|
|
|
|
deferred.resolve(options);
|
|
};
|
|
|
|
options.browser.addEventListener("load", onPageLoad, true);
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
helpers.openToolbar = function(options) {
|
|
var deferred = promise.defer();
|
|
|
|
options.chromeWindow.DeveloperToolbar.show(true, function() {
|
|
options.display = options.chromeWindow.DeveloperToolbar.display;
|
|
|
|
deferred.resolve(options);
|
|
});
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
helpers.closeToolbar = function(options) {
|
|
options.chromeWindow.DeveloperToolbar.hide();
|
|
delete options.display;
|
|
|
|
return promise.resolve(undefined);
|
|
};
|
|
|
|
helpers.handleError = function(ex) {
|
|
console.error(ex);
|
|
ok(false, ex);
|
|
finish();
|
|
};
|
|
|
|
/**
|
|
* Warning: For use with Firefox Mochitests only.
|
|
*
|
|
* As addTab, but that also opens the developer toolbar. In addition a new
|
|
* 'display' 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;
|
|
var deferred = promise.defer();
|
|
|
|
win.DeveloperToolbar.show(true, function() {
|
|
innerOptions.display = win.DeveloperToolbar.display;
|
|
|
|
var cleanUp = function() {
|
|
win.DeveloperToolbar.hide();
|
|
delete innerOptions.display;
|
|
deferred.resolve();
|
|
};
|
|
|
|
var reply = callback(innerOptions);
|
|
promise.resolve(reply).then(cleanUp, function(error) {
|
|
ok(false, error);
|
|
console.error(error);
|
|
cleanUp();
|
|
});
|
|
});
|
|
return deferred.promise;
|
|
}, 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);
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function checkOptions(options) {
|
|
if (options == null) {
|
|
console.trace();
|
|
throw new Error('Missing options object');
|
|
}
|
|
if (options.display == null) {
|
|
console.trace();
|
|
throw new Error('options object does not contain a display property');
|
|
}
|
|
if (options.display.requisition == null) {
|
|
console.trace();
|
|
throw new Error('display object does not contain a requisition');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Various functions to return the actual state of the command line
|
|
*/
|
|
helpers._actual = {
|
|
input: function(options) {
|
|
return options.display.inputter.element.value;
|
|
},
|
|
|
|
hints: function(options) {
|
|
var templateData = options.display.completer._getCompleterTemplateData();
|
|
var join = function(directTabText, emptyParameters, arrowTabText) {
|
|
return (directTabText + emptyParameters.join('') + arrowTabText)
|
|
.replace(/\u00a0/g, ' ')
|
|
.replace(/\u21E5/, '->')
|
|
.replace(/ $/, '');
|
|
};
|
|
|
|
var promisedJoin = promise.promised(join);
|
|
return promisedJoin(templateData.directTabText,
|
|
templateData.emptyParameters,
|
|
templateData.arrowTabText);
|
|
},
|
|
|
|
markup: function(options) {
|
|
var cursor = options.display.inputter.element.selectionStart;
|
|
var statusMarkup = options.display.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.display.inputter.element.selectionStart;
|
|
},
|
|
|
|
current: function(options) {
|
|
return options.display.requisition.getAssignmentAt(helpers._actual.cursor(options)).param.name;
|
|
},
|
|
|
|
status: function(options) {
|
|
return options.display.requisition.status.toString();
|
|
},
|
|
|
|
predictions: function(options) {
|
|
var cursor = options.display.inputter.element.selectionStart;
|
|
var assignment = options.display.requisition.getAssignmentAt(cursor);
|
|
return assignment.getPredictions().then(function(predictions) {
|
|
return predictions.map(function(prediction) {
|
|
return prediction.name;
|
|
});
|
|
});
|
|
},
|
|
|
|
unassigned: function(options) {
|
|
return options.display.requisition._unassigned.map(function(assignment) {
|
|
return assignment.arg.toString();
|
|
}.bind(this));
|
|
},
|
|
|
|
outputState: function(options) {
|
|
var outputData = options.display.focusManager._shouldShowOutput();
|
|
return outputData.visible + ':' + outputData.reason;
|
|
},
|
|
|
|
tooltipState: function(options) {
|
|
var tooltipData = options.display.focusManager._shouldShowTooltip();
|
|
return tooltipData.visible + ':' + tooltipData.reason;
|
|
},
|
|
|
|
options: function(options) {
|
|
if (options.display.tooltip.field.menu == null) {
|
|
return [];
|
|
}
|
|
return options.display.tooltip.field.menu.items.map(function(item) {
|
|
return item.name.textContent ? item.name.textContent : item.name;
|
|
});
|
|
},
|
|
|
|
message: function(options) {
|
|
return options.display.tooltip.errorEle.textContent;
|
|
}
|
|
};
|
|
|
|
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.display.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 += ' completed: true,\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.display.inputter.focus();
|
|
};
|
|
|
|
/**
|
|
* Simulate pressing TAB in the input field
|
|
*/
|
|
helpers.pressTab = function(options) {
|
|
checkOptions(options);
|
|
return helpers.pressKey(options, 9 /*KeyEvent.DOM_VK_TAB*/);
|
|
};
|
|
|
|
/**
|
|
* Simulate pressing RETURN in the input field
|
|
*/
|
|
helpers.pressReturn = function(options) {
|
|
checkOptions(options);
|
|
return helpers.pressKey(options, 13 /*KeyEvent.DOM_VK_RETURN*/);
|
|
};
|
|
|
|
/**
|
|
* Simulate pressing a key by keyCode in the input field
|
|
*/
|
|
helpers.pressKey = function(options, keyCode) {
|
|
checkOptions(options);
|
|
var fakeEvent = {
|
|
keyCode: keyCode,
|
|
preventDefault: function() { },
|
|
timeStamp: new Date().getTime()
|
|
};
|
|
options.display.inputter.onKeyDown(fakeEvent);
|
|
return options.display.inputter.handleKeyUp(fakeEvent);
|
|
};
|
|
|
|
/**
|
|
* 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, 38 /*KeyEvent.DOM_VK_UP*/);
|
|
},
|
|
'<DOWN>': function(options) {
|
|
return helpers.pressKey(options, 40 /*KeyEvent.DOM_VK_DOWN*/);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 inputter = options.display.inputter;
|
|
// 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 = inputter.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 = inputter.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 inputter.setInput(inputter.element.value + chunk);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return inputPromise.then(function() {
|
|
if (cursor != null) {
|
|
options.display.inputter.setCursor({ start: cursor, end: cursor });
|
|
}
|
|
else {
|
|
// This is a hack because jsdom appears to not handle cursor updates
|
|
// in the same way as most browsers.
|
|
if (options.isJsdom) {
|
|
options.display.inputter.setCursor({
|
|
start: typed.length,
|
|
end: typed.length
|
|
});
|
|
}
|
|
}
|
|
|
|
options.display.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) {
|
|
if (checks == null) {
|
|
return promise.resolve();
|
|
}
|
|
|
|
var outstanding = [];
|
|
var suffix = name ? ' (for \'' + name + '\')' : '';
|
|
|
|
if ('input' in checks) {
|
|
assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
|
|
}
|
|
|
|
if ('cursor' in checks) {
|
|
assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
|
|
}
|
|
|
|
if ('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 ('markup' in checks) {
|
|
assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
|
|
}
|
|
|
|
if ('hints' in checks) {
|
|
var hintCheck = function(actualHints) {
|
|
assert.is(actualHints, checks.hints, 'hints' + suffix);
|
|
};
|
|
outstanding.push(helpers._actual.hints(options).then(hintCheck));
|
|
}
|
|
|
|
if ('predictions' in checks) {
|
|
var predictionsCheck = function(actualPredictions) {
|
|
helpers.arrayIs(actualPredictions,
|
|
checks.predictions,
|
|
'predictions' + suffix);
|
|
};
|
|
outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
|
|
}
|
|
|
|
if ('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);
|
|
}
|
|
|
|
if ('tooltipState' in checks) {
|
|
if (options.isJsdom) {
|
|
assert.log('Skipped ' + name + '/tooltipState due to jsdom');
|
|
}
|
|
else {
|
|
assert.is(helpers._actual.tooltipState(options),
|
|
checks.tooltipState,
|
|
'tooltipState' + suffix);
|
|
}
|
|
}
|
|
|
|
if ('outputState' in checks) {
|
|
if (options.isJsdom) {
|
|
assert.log('Skipped ' + name + '/outputState due to jsdom');
|
|
}
|
|
else {
|
|
assert.is(helpers._actual.outputState(options),
|
|
checks.outputState,
|
|
'outputState' + suffix);
|
|
}
|
|
}
|
|
|
|
if ('options' in checks) {
|
|
helpers.arrayIs(helpers._actual.options(options),
|
|
checks.options,
|
|
'options' + suffix);
|
|
}
|
|
|
|
if ('error' in checks) {
|
|
assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
|
|
}
|
|
|
|
if (checks.args != null) {
|
|
var requisition = options.display.requisition;
|
|
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) {
|
|
if (options.isJsdom) {
|
|
assert.log('Skipped arg.' + paramName + '.name due to jsdom');
|
|
}
|
|
else {
|
|
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 ('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.display.requisition;
|
|
if (expected == null) {
|
|
return promise.resolve({});
|
|
}
|
|
|
|
var origLogErrors = cli.logErrors;
|
|
if (expected.error) {
|
|
cli.logErrors = false;
|
|
}
|
|
|
|
var completed = true;
|
|
|
|
try {
|
|
return requisition.exec({ hidden: true }).then(function(output) {
|
|
if ('completed' in expected) {
|
|
assert.is(completed,
|
|
expected.completed,
|
|
'output.completed for: ' + name);
|
|
}
|
|
|
|
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 (!options.window) {
|
|
assert.ok(false, 'Missing options.window in \'' + name + '\'. ' +
|
|
'Are you assming that helpers.audit is synchronous? ' +
|
|
'It returns a promise');
|
|
return { output: output };
|
|
}
|
|
|
|
if (!options.window.document.createElement) {
|
|
assert.log('skipping output tests (missing doc.createElement) for ' + name);
|
|
return { output: output };
|
|
}
|
|
|
|
if (!('output' in expected)) {
|
|
return { output: output };
|
|
}
|
|
|
|
var context = requisition.conversionContext;
|
|
return output.convert('dom', context).then(function(node) {
|
|
var actualOutput = node.textContent.trim();
|
|
|
|
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(actualOutput,
|
|
expected.output,
|
|
'html output for ' + name);
|
|
}
|
|
else if (Array.isArray(expected.output)) {
|
|
expected.output.forEach(function(match) {
|
|
doTest(match, actualOutput);
|
|
});
|
|
}
|
|
else {
|
|
doTest(expected.output, actualOutput);
|
|
}
|
|
|
|
if (expected.error) {
|
|
cli.logErrors = origLogErrors;
|
|
}
|
|
return { output: output, text: actualOutput };
|
|
});
|
|
}.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({});
|
|
}
|
|
finally {
|
|
completed = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Helper to setup the test
|
|
*/
|
|
helpers._setup = function(options, name, action) {
|
|
if (typeof action === 'string') {
|
|
return helpers.setInput(options, action);
|
|
}
|
|
|
|
if (typeof action === 'function') {
|
|
return promise.resolve(action());
|
|
}
|
|
|
|
return promise.reject('\'setup\' property must be a string or a function. Is ' + action);
|
|
};
|
|
|
|
/**
|
|
* Helper to shutdown the test
|
|
*/
|
|
helpers._post = function(name, action, data) {
|
|
if (typeof action === 'function') {
|
|
return promise.resolve(action(data.output, data.text));
|
|
}
|
|
return promise.resolve(action);
|
|
};
|
|
|
|
/*
|
|
* 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. jsdom, 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
|
|
* - completed: A boolean which declares that we should check to see if the
|
|
* command completed synchronously
|
|
* - 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.setup);
|
|
return setupDone.then(function(chunkLen) {
|
|
|
|
if (typeof chunkLen !== 'number') {
|
|
chunkLen = 1;
|
|
}
|
|
|
|
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.post, data).then(function() {
|
|
if (assert.testLogging) {
|
|
log('- END \'' + name + '\' in ' + assert.currentTest);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}).then(function() {
|
|
return options.display.inputter.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);
|
|
}
|
|
}
|
|
|
|
//});
|