@testim/testim-cli
Version:
Command line interface for running Testing on you CI
403 lines (334 loc) • 13.1 kB
JavaScript
;
var q = require('q'),
fs = require('fs'),
path = require('path'),
merge = require('deepmerge'),
EventEmitter = require('events').EventEmitter,
safeExecute = require('./utils/safeExecute'),
RequestHandler = require('./utils/RequestHandler'),
ErrorHandler = require('./utils/ErrorHandler'),
Logger = require('./utils/Logger'),
sanitize = require('./helpers/sanitize'),
isMobileHelper = require('./helpers/isMobile'),
detectSeleniumBackend = require('./helpers/detectSeleniumBackend');
/**
* WebdriverIO v3
*/
module.exports = function WebdriverIO(args, modifier) {
var prototype = Object.create(Object.prototype),
eventHandler = new EventEmitter(),
internalEvents = ['init', 'command', 'error', 'result', 'end'],
fulFilledPromise = q(),
stacktrace = [],
commandList = [];
/**
* merge default options with given user options
*/
var options = merge({
protocol: 'http',
waitforTimeout: 500,
waitforInterval: 250,
coloredLogs: true,
logLevel: 'silent',
baseUrl: null
}, typeof args !== 'string' ? args : {});
/**
* define Selenium backend given on user options
*/
options = merge(detectSeleniumBackend(args), options);
/**
* only set globals we wouldn't get otherwise
*/
if(!process.env.WEBDRIVERIO_COLORED_LOGS) {
process.env.WEBDRIVERIO_COLORED_LOGS = options.coloredLogs;
}
var logger = new Logger(options, eventHandler),
requestHandler = new RequestHandler(options, eventHandler, logger);
/**
* assign instance to existing session
*/
if(typeof args === 'string') {
requestHandler.sessionID = args;
}
var desiredCapabilities = merge({
browserName: 'firefox',
version: '',
javascriptEnabled: true,
locationContextEnabled: true,
handlesAlerts: true,
rotatable: true,
platform: 'ANY'
}, options.desiredCapabilities || {});
/**
* set default logging prefs to enable log commands (mainly for chromedriver)
*/
if(typeof desiredCapabilities.loggingPrefs === 'undefined') {
desiredCapabilities.loggingPrefs = {
browser: 'ALL',
driver: 'ALL'
};
}
var isMobile = isMobileHelper(desiredCapabilities);
var resolve = function(result, isErrorHandled) {
if(typeof result === 'function') {
this.isExecuted = true;
result = result.call(this);
}
var resolveMethod = result instanceof Error ? 'reject' : 'resolve';
this.defer[resolveMethod](result);
/**
* By using finally in our next method we omit the duty to throw an exception an some
* point. To avoid propagating rejected promises until everything crashes silently we
* check if the last and current promise got rejected. If so we can throw the error.
*/
if(this.promise.isRejected() && !isErrorHandled) {
/**
* take screenshot only if screenshotPath is given
*/
if(typeof options.screenshotPath !== 'string') {
return throwException(result, stacktrace);
}
var screenshotPath = path.join(process.cwd(), options.screenshotPath);
/**
* take screenshot only if directory exists
*/
if(!fs.existsSync(screenshotPath)) {
return throwException(result, stacktrace);
}
var client = unit();
client.next(prototype.saveScreenshot, [
path.join(screenshotPath, 'ERROR_' + sanitize.caps(desiredCapabilities) + '_' + new Date().toJSON() + '.png')
], 'saveScreenshot');
var stack = stacktrace.slice();
return throwException.bind(null, result, stack);
}
return this.promise;
};
function throwException(e, stack) {
stack = stack.slice(0, -1).map(function(trace) {
return ' at ' + trace;
});
e.stack = e.type + ': ' + e.message + '\n' + stack.reverse().join('\n');
throw e;
}
/**
* WebdriverIO Monad
*/
function unit(lastPromise) {
var client = Object.create(prototype),
defer = q.defer(),
promise = defer.promise;
client.defer = defer;
client.promise = promise;
client.lastPromise = lastPromise || fulFilledPromise;
client.desiredCapabilities = desiredCapabilities;
client.requestHandler = requestHandler;
client.logger = logger;
client.options = options;
client.isMobile = isMobile;
client.commandList = commandList;
/**
* actual bind function
*/
client.next = function (func, args, name) {
var self = this;
/**
* use finally to propagate rejected promises up the chain
*/
return self.lastPromise.then(function() {
/**
* store command into command list so `getHistory` can return it
*/
commandList.push({
name: name,
args: args
});
return resolve.call(self, safeExecute(func, args));
}, function(e) {
/**
* reject pending commands in chain
*/
if(e.isPropagatedError) {
return self.defer.reject(e);
}
self.emit('error', {
message: e.message,
type: e.type,
stack: stacktrace
});
/**
* mark error as propagated so that error messages get only printed once
*/
e.isPropagatedError = true;
logger.printException(e.type || 'Error', e.message, stacktrace);
self.defer.reject(e);
});
};
client.finally = function(fn) {
var client = unit(this.promise.finally(function() {
return resolve.call(client, safeExecute(fn, []).bind(this));
}.bind(this)));
return client;
};
client.call = function(fn) {
var client = unit(this.promise.done(function() {
return resolve.call(client, safeExecute(fn, []).bind(this));
}.bind(this)));
return client;
};
client.then = function(onFulfilled, onRejected) {
var self = this;
if(typeof onFulfilled !== 'function' && typeof onRejected !== 'function') {
return this;
}
/**
* execute then function in context of the new instance
* but resolve result with this
*/
var client = unit(this.promise.then(function() {
return resolve.call(client, safeExecute(onFulfilled, arguments).bind(self));
}, function() {
return resolve.call(client, safeExecute(onRejected, arguments).bind(self), typeof onRejected === 'function');
}));
return client;
};
client.catch = function(onRejected) {
return this.then(undefined, onRejected);
};
client.inspect = function() {
return this.promise.inspect();
};
/**
* internal helper method to handle command results
*
* @param {Promise[]} promises list of promises
* @param {Boolean} option if true extract value property from selenium result
*/
client.unify = function(promises, option) {
option = option || {};
promises = Array.isArray(promises) ? promises : [promises];
return q.all(promises)
/**
* extract value property from result if desired
*/
.then(function(result) {
if(!option.extractValue || !Array.isArray(result)) {
return result;
}
return result.map(function(res) {
return res.value && typeof res.value === 'string' ? res.value.trim() : res.value;
});
/**
* sanitize result for better assertion
*/
}).then(function(result) {
if(Array.isArray(result) && result.length === 1) {
result = result[0];
}
if(option.lowercase && typeof result === 'string') {
result = result.toLowerCase();
}
return result;
});
};
client.addCommand = function(fnName, fn, forceOverwrite) {
if(client[fnName] && !forceOverwrite) {
throw new ErrorHandler.RuntimeError('Command "' + fnName + '" is already defined!');
}
return unit.lift.apply(null, arguments);
};
client.transferPromiseness = function(target, promise) {
/**
* transfer WebdriverIO commands
*/
var clientFunctions = Object.keys(prototype);
var promiseFunctions = ['then', 'catch', 'finally'];
var functionsToTranfer = clientFunctions.concat(promiseFunctions);
functionsToTranfer.forEach(function(fnName) {
if(typeof promise[fnName] === 'function') {
target[fnName] = promise[fnName].bind(promise);
}
});
};
if (typeof modifier === 'function') {
client = modifier(client, options);
}
return client;
}
/**
* enhance base monad prototype with methods
*/
unit.lift = function (name, func) {
prototype[name] = function () {
var nextPromise = this.promise,
args = Array.prototype.slice.apply(arguments),
callback;
/**
* commands executed inside commands don't have to wait
* on any promise
*/
if(this.isExecuted) {
nextPromise = this.lastPromise;
}
var client = unit(nextPromise);
/**
* catch stack to find information about where the command that causes
* the error was used (stack line 2) and only save it when it was not
* within WebdriverIO context
*/
var stack = new Error().stack,
lineInTest = stack.split('\n').slice(2, 3).join('\n'),
fileAndPosition = lineInTest.slice(lineInTest.indexOf('(') + 1, lineInTest.indexOf(')')),
atCommand = lineInTest.trim().slice(3).split(' ')[0];
atCommand = atCommand.slice(atCommand.lastIndexOf('.') + 1);
var trace = name + '(' + sanitize.args(args) + ') - ' + fileAndPosition.slice(fileAndPosition.lastIndexOf('/') + 1);
if(Object.keys(prototype).indexOf(atCommand) === -1 && atCommand !== 'exports') {
stacktrace = [trace];
} else {
/**
* save trace for nested commands
*/
stacktrace.push(trace);
}
/**
* queue command
*/
client.next(func, args, name, trace);
/**
* ensure backwards compatibility
* care about callbacks as last parameter for all commands
* but execute(Async) and selectExecute(Async)
*/
if(!name.match(/(selector)*(E|e)xecute(Async)*/) && !name.match(/waitUntil/) && typeof args[args.length - 1] === 'function') {
callback = args.pop();
return client.then(function(res) {
return callback.call(this, undefined, res);
}, function(err) {
return callback.call(this, err);
});
}
return client;
};
return unit;
};
/**
* register event emitter
*/
Object.keys(Object.getPrototypeOf(eventHandler)).forEach(function(eventCommand) {
prototype[eventCommand] = function() {
var args = Array.prototype.slice.apply(arguments);
/**
* custom commands needs to get emitted and registered in order
* to prevent race conditions
*/
if(internalEvents.indexOf(args[0]) === -1) {
return this.finally(function() {
eventHandler[eventCommand].apply(eventHandler, args);
});
}
eventHandler[eventCommand].apply(eventHandler, args);
return this;
};
});
return unit;
};