354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
/**
|
|
* Adapts Jasmine-Node tests to work better with WebDriverJS. Borrows
|
|
* heavily from the mocha WebDriverJS adapter at
|
|
* https://code.google.com/p/selenium/source/browse/javascript/node/selenium-webdriver/testing/index.js
|
|
*/
|
|
|
|
var WebElement; // Equal to webdriver.WebElement
|
|
var idleEventName = 'idle'; // Equal to webdriver.promise.ControlFlow.EventType.IDLE
|
|
var maybePromise = require('./maybePromise');
|
|
|
|
/**
|
|
* Validates that the parameter is a function.
|
|
* @param {Object} functionToValidate The function to validate.
|
|
* @throws {Error}
|
|
* @return {Object} The original parameter.
|
|
*/
|
|
function validateFunction(functionToValidate) {
|
|
if (functionToValidate && typeof functionToValidate === 'function') {
|
|
return functionToValidate;
|
|
} else {
|
|
throw Error(functionToValidate + ' is not a function');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that the parameter is a number.
|
|
* @param {Object} numberToValidate The number to validate.
|
|
* @throws {Error}
|
|
* @return {Object} The original number.
|
|
*/
|
|
function validateNumber(numberToValidate) {
|
|
if (!isNaN(numberToValidate)) {
|
|
return numberToValidate;
|
|
} else {
|
|
throw Error(numberToValidate + ' is not a number');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that the parameter is a string.
|
|
* @param {Object} stringToValidate The string to validate.
|
|
* @throws {Error}
|
|
* @return {Object} The original string.
|
|
*/
|
|
function validateString(stringtoValidate) {
|
|
if (typeof stringtoValidate == 'string' || stringtoValidate instanceof String) {
|
|
return stringtoValidate;
|
|
} else {
|
|
throw Error(stringtoValidate + ' is not a string');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls a function once the scheduler is idle. If the scheduler does not support the idle API,
|
|
* calls the function immediately. See scheduler.md#idle-api for details.
|
|
*
|
|
* @param {Object} scheduler The scheduler to wait for.
|
|
* @param {!Function} fn The function to call.
|
|
*/
|
|
function callWhenIdle(scheduler, fn) {
|
|
if (!scheduler.once || !scheduler.isIdle || scheduler.isIdle()) {
|
|
fn();
|
|
} else {
|
|
scheduler.once(idleEventName, function() { fn(); });
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Wraps a function so it runs inside a scheduler's `execute()` block.
|
|
*
|
|
* In the most common case, this means wrapping in a `webdriver.promise.ControlFlow` instance
|
|
* to wait for the control flow to complete one task before starting the next. See scheduler.md
|
|
* for details.
|
|
*
|
|
* @param {!Object} scheduler See scheduler.md for details.
|
|
* @param {!Function} newPromise Makes a new promise using whatever implementation the scheduler
|
|
* prefers.
|
|
* @param {!Function} globalFn The function to wrap.
|
|
* @param {!string} fnName The name of the function being wrapped (e.g. `'it'`).
|
|
* @return {!Function} The new function.
|
|
*/
|
|
function wrapInScheduler(scheduler, newPromise, globalFn, fnName) {
|
|
return function() {
|
|
var driverError = new Error();
|
|
driverError.stack = driverError.stack.replace(/ +at.+jasminewd.+\n/, '');
|
|
|
|
function asyncTestFn(fn, description) {
|
|
description = description ? ('("' + description + '")') : '';
|
|
return function(done) {
|
|
var async = fn.length > 0;
|
|
var testFn = fn.bind(this);
|
|
|
|
scheduler.execute(function schedulerExecute() {
|
|
return newPromise(function(fulfill, reject) {
|
|
function wrappedReject(err) {
|
|
if(err instanceof Error)
|
|
reject(err);
|
|
else
|
|
reject(new Error(err));
|
|
}
|
|
if (async) {
|
|
// If testFn is async (it expects a done callback), resolve the promise of this
|
|
// test whenever that callback says to. Any promises returned from testFn are
|
|
// ignored.
|
|
var proxyDone = fulfill;
|
|
proxyDone.fail = wrappedReject;
|
|
testFn(proxyDone);
|
|
} else {
|
|
// Without a callback, testFn can return a promise, or it will
|
|
// be assumed to have completed synchronously.
|
|
var ret = testFn();
|
|
if (maybePromise.isPromise(ret)) {
|
|
ret.then(fulfill, wrappedReject);
|
|
} else {
|
|
fulfill(ret);
|
|
}
|
|
}
|
|
});
|
|
}, 'Run ' + fnName + description + ' in control flow').then(
|
|
callWhenIdle.bind(null, scheduler, done), function(err) {
|
|
if (!err) {
|
|
err = new Error('Unknown Error');
|
|
err.stack = '';
|
|
}
|
|
err.stack = err.stack + '\nFrom asynchronous test: \n' + driverError.stack;
|
|
callWhenIdle(scheduler, done.fail.bind(done, err));
|
|
}
|
|
);
|
|
};
|
|
}
|
|
|
|
var description, func, timeout;
|
|
switch (fnName) {
|
|
case 'it':
|
|
case 'fit':
|
|
description = validateString(arguments[0]);
|
|
if (!arguments[1]) {
|
|
return globalFn(description);
|
|
}
|
|
func = validateFunction(arguments[1]);
|
|
if (!arguments[2]) {
|
|
return globalFn(description, asyncTestFn(func, description));
|
|
} else {
|
|
timeout = validateNumber(arguments[2]);
|
|
return globalFn(description, asyncTestFn(func, description), timeout);
|
|
}
|
|
break;
|
|
case 'beforeEach':
|
|
case 'afterEach':
|
|
case 'beforeAll':
|
|
case 'afterAll':
|
|
func = validateFunction(arguments[0]);
|
|
if (!arguments[1]) {
|
|
globalFn(asyncTestFn(func));
|
|
} else {
|
|
timeout = validateNumber(arguments[1]);
|
|
globalFn(asyncTestFn(func), timeout);
|
|
}
|
|
break;
|
|
default:
|
|
throw Error('invalid function: ' + fnName);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize the JasmineWd adapter with a particlar scheduler, generally a webdriver control flow.
|
|
*
|
|
* @param {Object=} scheduler The scheduler to wrap tests in. See scheduler.md for details.
|
|
* Defaults to a mock scheduler that calls functions immediately.
|
|
* @param {Object=} webdriver The result of `require('selenium-webdriver')`. Passed in here rather
|
|
* than required by jasminewd directly so that jasminewd can't end up up with a different version
|
|
* of `selenium-webdriver` than your tests use. If not specified, jasminewd will still work, but
|
|
* it won't check for `WebElement` instances in expect() statements and could cause control flow
|
|
* problems if your tests are using an old version of `selenium-webdriver` (e.g. version 2.53.0).
|
|
*/
|
|
function initJasmineWd(scheduler, webdriver) {
|
|
if (jasmine.JasmineWdInitialized) {
|
|
throw Error('JasmineWd already initialized when init() was called');
|
|
}
|
|
jasmine.JasmineWdInitialized = true;
|
|
|
|
|
|
// Pull information from webdriver instance
|
|
if (webdriver) {
|
|
WebElement = webdriver.WebElement || WebElement;
|
|
idleEventName = (
|
|
webdriver.promise &&
|
|
webdriver.promise.ControlFlow &&
|
|
webdriver.promise.ControlFlow.EventType &&
|
|
webdriver.promise.ControlFlow.EventType.IDLE
|
|
) || idleEventname;
|
|
}
|
|
|
|
// Default to mock scheduler
|
|
if (!scheduler) {
|
|
scheduler = { execute: function(fn) {
|
|
return Promise.resolve().then(fn);
|
|
} };
|
|
}
|
|
|
|
// Figure out how we're getting new promises
|
|
var newPromise;
|
|
if (typeof scheduler.promise == 'function') {
|
|
newPromise = scheduler.promise.bind(scheduler);
|
|
} else if (webdriver && webdriver.promise && webdriver.promise.ControlFlow &&
|
|
(scheduler instanceof webdriver.promise.ControlFlow) &&
|
|
(webdriver.promise.USE_PROMISE_MANAGER !== false)) {
|
|
newPromise = function(resolver) {
|
|
return new webdriver.promise.Promise(resolver, scheduler);
|
|
};
|
|
} else {
|
|
newPromise = function(resolver) {
|
|
return new Promise(resolver);
|
|
};
|
|
}
|
|
|
|
// Wrap functions
|
|
global.it = wrapInScheduler(scheduler, newPromise, global.it, 'it');
|
|
global.fit = wrapInScheduler(scheduler, newPromise, global.fit, 'fit');
|
|
global.beforeEach = wrapInScheduler(scheduler, newPromise, global.beforeEach, 'beforeEach');
|
|
global.afterEach = wrapInScheduler(scheduler, newPromise, global.afterEach, 'afterEach');
|
|
global.beforeAll = wrapInScheduler(scheduler, newPromise, global.beforeAll, 'beforeAll');
|
|
global.afterAll = wrapInScheduler(scheduler, newPromise, global.afterAll, 'afterAll');
|
|
|
|
// Reset API
|
|
if (scheduler.reset) {
|
|
// On timeout, the flow should be reset. This will prevent webdriver tasks
|
|
// from overflowing into the next test and causing it to fail or timeout
|
|
// as well. This is done in the reporter instead of an afterEach block
|
|
// to ensure that it runs after any afterEach() blocks with webdriver tasks
|
|
// get to complete first.
|
|
jasmine.getEnv().addReporter(new OnTimeoutReporter(function() {
|
|
console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.');
|
|
scheduler.reset();
|
|
}));
|
|
}
|
|
}
|
|
|
|
var originalExpect = global.expect;
|
|
global.expect = function(actual) {
|
|
if (WebElement && (actual instanceof WebElement)) {
|
|
throw Error('expect called with WebElement argument, expected a Promise. ' +
|
|
'Did you mean to use .getText()?');
|
|
}
|
|
return originalExpect(actual);
|
|
};
|
|
|
|
/**
|
|
* Creates a matcher wrapper that resolves any promises given for actual and
|
|
* expected values, as well as the `pass` property of the result.
|
|
*
|
|
* Wrapped matchers will return either `undefined` or a promise which resolves
|
|
* when the matcher is complete, depending on if the matcher had to resolve any
|
|
* promises.
|
|
*/
|
|
jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
|
|
return function() {
|
|
var expected = Array.prototype.slice.call(arguments, 0),
|
|
expectation = this,
|
|
matchError = new Error("Failed expectation");
|
|
|
|
matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, '');
|
|
|
|
// Return either undefined or a promise of undefined
|
|
return maybePromise(expectation.actual, function(actual) {
|
|
return maybePromise.all(expected, function(expected) {
|
|
return compare(actual, expected);
|
|
});
|
|
});
|
|
|
|
function compare(actual, expected) {
|
|
var args = expected.slice(0);
|
|
args.unshift(actual);
|
|
|
|
var matcher = matcherFactory(expectation.util, expectation.customEqualityTesters);
|
|
var matcherCompare = matcher.compare;
|
|
|
|
if (expectation.isNot) {
|
|
matcherCompare = matcher.negativeCompare || defaultNegativeCompare;
|
|
}
|
|
|
|
var result = matcherCompare.apply(null, args);
|
|
|
|
return maybePromise(result.pass, compareDone);
|
|
|
|
// compareDone always returns undefined
|
|
function compareDone(pass) {
|
|
var message = '';
|
|
|
|
if (!pass) {
|
|
if (!result.message) {
|
|
args.unshift(expectation.isNot);
|
|
args.unshift(name);
|
|
message = expectation.util.buildFailureMessage.apply(null, args);
|
|
} else {
|
|
if (Object.prototype.toString.apply(result.message) === '[object Function]') {
|
|
message = result.message(expectation.isNot);
|
|
} else {
|
|
message = result.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expected.length == 1) {
|
|
expected = expected[0];
|
|
}
|
|
var res = {
|
|
matcherName: name,
|
|
passed: pass,
|
|
message: message,
|
|
actual: actual,
|
|
expected: expected,
|
|
error: matchError
|
|
};
|
|
expectation.addExpectationResult(pass, res);
|
|
}
|
|
|
|
function defaultNegativeCompare() {
|
|
var result = matcher.compare.apply(null, args);
|
|
result.pass = maybePromise(result.pass, function(pass) {
|
|
return !pass;
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// Re-add core matchers so they are wrapped.
|
|
jasmine.Expectation.addCoreMatchers(jasmine.matchers);
|
|
|
|
/**
|
|
* A Jasmine reporter which does nothing but execute the input function
|
|
* on a timeout failure.
|
|
*/
|
|
var OnTimeoutReporter = function(fn) {
|
|
this.callback = fn;
|
|
};
|
|
|
|
OnTimeoutReporter.prototype.specDone = function(result) {
|
|
if (result.status === 'failed') {
|
|
for (var i = 0; i < result.failedExpectations.length; i++) {
|
|
var failureMessage = result.failedExpectations[i].message;
|
|
|
|
if (failureMessage.match(/Timeout/)) {
|
|
this.callback();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports.init = initJasmineWd;
|