123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- /**
- * 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;
|