'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _keys = require('babel-runtime/core-js/object/keys'); var _keys2 = _interopRequireDefault(_keys); var _assign = require('babel-runtime/core-js/object/assign'); var _assign2 = _interopRequireDefault(_assign); var _promise = require('babel-runtime/core-js/promise'); var _promise2 = _interopRequireDefault(_promise); var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); var _create = require('babel-runtime/core-js/object/create'); var _create2 = _interopRequireDefault(_create); var _q = require('q'); var _q2 = _interopRequireDefault(_q); var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _deepmerge = require('deepmerge'); var _deepmerge2 = _interopRequireDefault(_deepmerge); var _mkdirp = require('mkdirp'); var _mkdirp2 = _interopRequireDefault(_mkdirp); var _events = require('events'); var _events2 = _interopRequireDefault(_events); var _safeBuffer = require('safe-buffer'); var _RequestHandler = require('./utils/RequestHandler'); var _RequestHandler2 = _interopRequireDefault(_RequestHandler); var _ErrorHandler = require('./utils/ErrorHandler'); var _Logger = require('./utils/Logger'); var _Logger2 = _interopRequireDefault(_Logger); var _safeExecute = require('./helpers/safeExecute'); var _safeExecute2 = _interopRequireDefault(_safeExecute); var _sanitize = require('./helpers/sanitize'); var _sanitize2 = _interopRequireDefault(_sanitize); var _mobileDetector2 = require('./helpers/mobileDetector'); var _mobileDetector3 = _interopRequireDefault(_mobileDetector2); var _detectSeleniumBackend = require('./helpers/detectSeleniumBackend'); var _detectSeleniumBackend2 = _interopRequireDefault(_detectSeleniumBackend); var _errorHandler = require('./helpers/errorHandler'); var _errorHandler2 = _interopRequireDefault(_errorHandler); var _hasElementResultHelper = require('./helpers/hasElementResultHelper'); var _hasElementResultHelper2 = _interopRequireDefault(_hasElementResultHelper); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var INTERNAL_EVENTS = ['init', 'command', 'error', 'result', 'end', 'screenshot']; var PROMISE_FUNCTIONS = ['then', 'catch', 'finally']; var EventEmitter = _events2.default.EventEmitter; /** * WebdriverIO v4 */ var WebdriverIO = function WebdriverIO(args, modifier) { var prototype = (0, _create2.default)(Object.prototype); var rawCommands = {}; var eventHandler = new EventEmitter(); var fulFilledPromise = (0, _q2.default)(); var stacktrace = []; var commandList = []; var EVENTHANDLER_FUNCTIONS = (0, _getPrototypeOf2.default)(eventHandler); /** * merge default options with given user options */ var options = (0, _deepmerge2.default)({ protocol: 'http', waitforTimeout: 1000, waitforInterval: 500, coloredLogs: true, logLevel: 'silent', baseUrl: null, onError: [], connectionRetryTimeout: 90000, connectionRetryCount: 3 }, typeof args !== 'string' ? args : {}); /** * define Selenium backend given on user options */ options = (0, _deepmerge2.default)((0, _detectSeleniumBackend2.default)(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 _Logger2.default(options, eventHandler); var requestHandler = new _RequestHandler2.default(options, eventHandler, logger); /** * assign instance to existing session */ if (typeof args === 'string') { requestHandler.sessionID = args; } /** * sanitize error handler */ if (!Array.isArray(options.onError)) { options.onError = [options.onError]; } options.onError = options.onError.filter(function (fn) { return typeof fn === 'function'; }); var desiredCapabilities = (0, _deepmerge2.default)({ javascriptEnabled: true, locationContextEnabled: true, handlesAlerts: true, rotatable: true }, options.desiredCapabilities || {}); var _mobileDetector = (0, _mobileDetector3.default)(desiredCapabilities), isMobile = _mobileDetector.isMobile, isIOS = _mobileDetector.isIOS, isAndroid = _mobileDetector.isAndroid; /** * if no caps are specified fall back to firefox */ if (!desiredCapabilities.browserName && !isMobile) { desiredCapabilities.browserName = 'firefox'; } if (!isMobile && typeof desiredCapabilities.loggingPrefs === 'undefined') { desiredCapabilities.loggingPrefs = { browser: 'ALL', driver: 'ALL' }; } var resolve = function resolve(result, onFulfilled, onRejected, context) { if (typeof result === 'function') { this.isExecuted = true; result = result.call(this); } /** * run error handler if command fails */ if (result instanceof Error) { var _result = result; this.defer.resolve(_promise2.default.all(_errorHandler2.default.map(function (fn) { return fn.call(context, result); })).then(function (res) { var handlerResponses = res.filter(function (r) { return typeof r !== 'undefined'; }); /** * if no handler was triggered trough actual error */ if (handlerResponses.length === 0) { return callErrorHandlerAndReject.call(context, _result, onRejected); } return onFulfilled.call(context, handlerResponses[0]); }, function (e) { return callErrorHandlerAndReject.call(context, e, onRejected); })); } else { this.defer.resolve(result); } return this.promise; }; /** * middleware to call on error handler in wdio mode */ var callErrorHandlerAndReject = function callErrorHandlerAndReject(err, onRejected) { var _this = this; /** * only call error handler if there is any and if error has bubbled up */ if (!this || this.depth !== 0 || options.onError.length === 0) { return reject.call(this, err, onRejected); } return new _promise2.default(function (resolve, reject) { return _promise2.default.all(options.onError.map(function (fn) { if (!global.wdioSync) { return fn.call(_this, err); } return new _promise2.default(function (resolve) { return global.wdioSync(fn, resolve).call(_this, err); }); })).then(resolve, reject); }).then(function () { return reject.call(_this, err, onRejected); }); }; /** * By using finally in our next method we omit the duty to throw an exception at 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. */ var reject = function reject(err, onRejected) { if (!options.isWDIO && !options.screenshotOnReject && typeof onRejected === 'function') { delete err.screenshot; return onRejected(err); } var onRejectedSafe = function onRejectedSafe(err) { if (typeof onRejected === 'function') { onRejected(err); } }; if (!this && !options.screenshotOnReject) { onRejectedSafe(err); throw err; } if (this && this.depth !== 0) { onRejectedSafe(err); return this.promise; } var shouldTakeScreenshot = options.screenshotOnReject || typeof options.screenshotPath === 'string'; if (!shouldTakeScreenshot || err.shotTaken || insideCommand('screenshot', this)) { return fail(err, onRejected); } err.shotTaken = true; return takeScreenshot(err).catch(function (e) { return logger.log('\tFailed to take screenshot on reject:', e); }).then(function () { return fail(err, onRejected); }); }; function insideCommand(command, unit) { var commands = unit && unit.commandList; return commands && commands[commands.length - 1].name === command; } function takeScreenshot(err) { var client = unit(); var failDate = new Date(); if (typeof options.screenshotOnReject === 'object') { client.requestHandler = createRequestHandlerDecorator(options.screenshotOnReject); } // don't take a new screenshot if we already got one from Selenium var getScreenshot = typeof err.screenshot === 'string' ? function () { return err.screenshot; } : function () { return rawCommands.screenshot.call(client).then(function (res) { return res.value; }); }; return _q2.default.fcall(getScreenshot).then(function (screenshot) { if (options.screenshotOnReject) { err.screenshot = screenshot; } if (typeof options.screenshotPath === 'string') { var filename = saveScreenshotSync(screenshot, failDate); client.emit('screenshot', { data: screenshot, filename: filename }); } }); } function createRequestHandlerDecorator(opts) { return (0, _create2.default)(requestHandler, { defaultOptions: { value: (0, _assign2.default)({}, requestHandler.defaultOptions, opts) } }); } function saveScreenshotSync(screenshot, date) { var screenshotPath = _path2.default.isAbsolute(options.screenshotPath) ? options.screenshotPath : _path2.default.join(process.cwd(), options.screenshotPath); /** * create directory if not existing */ try { _fs2.default.statSync(screenshotPath); } catch (e) { _mkdirp2.default.sync(screenshotPath); } var capId = _sanitize2.default.caps(desiredCapabilities); var timestamp = date.toJSON().replace(/:/g, '-'); var filename = 'ERROR_' + capId + '_' + timestamp + '.png'; var filePath = _path2.default.join(screenshotPath, filename); _fs2.default.writeFileSync(filePath, new _safeBuffer.Buffer(screenshot, 'base64')); logger.log('\tSaved screenshot: ' + filename); return filename; } function fail(e, onRejected) { if (!e.stack) { e = new Error(e); } var message = e.seleniumStack && e.seleniumStack.message || e.message; var stack = stacktrace.slice().map(function (trace) { return ' at ' + trace; }); e.stack = e.name + ': ' + message + '\n' + stack.reverse().join('\n'); /** * the waitUntil command can execute a lot of functions until it resolves * to keep the stacktrace sane we just shrink down the stacktrack and * only keep waitUntil in it */ if (e.name === 'WaitUntilTimeoutError') { stack = e.stack.split('\n'); stack.splice(1, stack.length - 2); e.stack = stack.join('\n'); } /** * ToDo useful feature for standalone mode: * option that if true causes script to throw exception if command fails: * * process.nextTick(() => { * throw e * }) */ if (typeof onRejected !== 'function') { throw e; } return onRejected(e); } /** * WebdriverIO Monad */ function unit(lastPromise) { var client = (0, _create2.default)(prototype); var defer = _q2.default.defer(); var 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.commandList = commandList; client.isMobile = isMobile; client.isIOS = isIOS; client.isAndroid = isAndroid; /** * actual bind function */ client.next = function (func, args, name) { var _this2 = this; /** * use finally to propagate rejected promises up the chain */ return this.lastPromise.then(function (val) { /** * store command into command list so `getHistory` can return it */ var timestamp = Date.now(); commandList.push({ name: name, args: args, timestamp: timestamp }); /** * allow user to leave out selector argument if they have already queried an element before */ var lastResult = val || _this2.lastResult; if ((0, _hasElementResultHelper2.default)(lastResult) && args.length < func.length && func.toString().indexOf('function ' + name + '(selector') === 0) { var isWaitFor = name.match(/^waitFor/); var isWaitForWithSelector = isWaitFor && typeof args[0] === 'string'; /** * Throw error if someone tries to call an action on a chained element call, * otherwise the context would be ignored and a different element could * selected. * Ignore this behavior for all waitFor calls as we want to refetch elements * in case they haven't been found. Also not throw an error for isExisting * and isVisible since they use that information to return their result. */ if (!isWaitFor && !name.match(/^is(Existing|Visible)/) && lastResult.state === 'failure') { var message = lastResult.message; /** * add selector parameter if no existing */ if (message.match(/using the given search parameters\.$/)) { message = message.slice(0, -1) + ' ("' + lastResult.selector + '").'; } var error = new Error(message); return resolve.call(_this2, error, null, null, { depth: 0 }); } /** * propagate last element result if last result has a selector and we've * called a waitFor command without selector * * e.g. when calling `waitForExist` via $('#notExisting').waitForExist() */ if (lastResult.selector && isWaitFor && !isWaitForWithSelector) { _this2.lastResult = null; args.unshift(lastResult.selector); /** * All non waitFor commands get null as selector which will be replaced by * the actual web element id in the element(s) protocol commands * * e.g. when calling `getText` via $('#existing').getText() */ } else if (!isWaitForWithSelector) { args.unshift(null); } } return resolve.call(_this2, (0, _safeExecute2.default)(func, args)); }, function (e) { /** * this will get reached only in standalone mode if the command * fails and doesn't get followed by a then or catch method */ return resolve.call(_this2, e, null, null, { depth: 0 }); }); }; client.finally = function (fn) { var _this3 = this; var client = unit(this.promise.finally(function () { return resolve.call(client, (0, _safeExecute2.default)(fn, []).bind(_this3)); })); return client; }; client.call = function (fn) { var _this4 = this; var client = unit(this.promise.done(function () { return resolve.call(client, (0, _safeExecute2.default)(fn, []).bind(_this4)); })); return client; }; client.then = function (onFulfilled, onRejected) { var _this5 = 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 () { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } /** * store result in commandList */ if (commandList.length) { commandList[commandList.length - 1].result = args[0]; } /** * resolve command */ return resolve.call(client, (0, _safeExecute2.default)(onFulfilled, args).bind(_this5)); }, function (e) { var result = (0, _safeExecute2.default)(onRejected, [e]).bind(_this5); /** * handle error once command was bubbled up the command chain */ if (_this5.depth === 0) { result = e; } return resolve.call(client, result, onFulfilled, onRejected, _this5); })); 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) { var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; promises = Array.isArray(promises) ? promises : [promises]; return _promise2.default.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; }); /** * 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; }); }; /** * addCommand handler for async or standalone mode * @param {String} fnName function name * @param {Function} fn function * @param {Boolean} forceOverwrite if true it can overwrite existing commands */ client.addCommand = function (fnName, fn, forceOverwrite) { if (typeof fn === 'string') { var namespace = arguments[0]; fnName = arguments[1]; fn = arguments[2]; forceOverwrite = arguments[3]; switch (typeof client[namespace]) { case 'function': throw new _ErrorHandler.RuntimeError('Command namespace "' + namespace + '" is used internally, and can\'t be overwritten!'); case 'object': if (client[namespace][fnName] && !forceOverwrite) { throw new _ErrorHandler.RuntimeError('Command "' + fnName + '" is already defined!'); } break; } return unit.lift(namespace, fnName, fn); } if (client[fnName] && !forceOverwrite) { throw new _ErrorHandler.RuntimeError('Command "' + fnName + '" is already defined!'); } return unit.lift(fnName, fn); }; client.getPrototype = function () { return prototype; }; client.transferPromiseness = function (target, promise) { /** * transfer WebdriverIO commands */ var clientFunctions = (0, _keys2.default)(prototype); var functionsToTranfer = clientFunctions.concat(PROMISE_FUNCTIONS); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)(functionsToTranfer), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var fnName = _step.value; if (typeof promise[fnName] === 'function') { target[fnName] = promise[fnName].bind(promise); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } }; if (typeof modifier === 'function') { client = modifier(client, options); } return client; } /** * enhance base monad prototype with methods */ unit.lift = function (name, func) { var commandGroup = prototype; if (typeof func === 'string') { var namespace = arguments[0]; name = arguments[1]; func = arguments[2]; if (!prototype[namespace]) { prototype[namespace] = {}; } commandGroup = prototype[namespace]; } rawCommands[name] = func; commandGroup[name] = function () { var nextPromise = this.promise; /** * 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; var lineInTest = stack.split('\n').slice(2, 3).join('\n'); var fileAndPosition = lineInTest.slice(lineInTest.indexOf('(') + 1, lineInTest.indexOf(')')); var atCommand = lineInTest.trim().slice(3).split(' ')[0]; atCommand = atCommand.slice(atCommand.lastIndexOf('.') + 1); for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } var trace = name + '(' + _sanitize2.default.args(args) + ') - ' + fileAndPosition.slice(fileAndPosition.lastIndexOf('/') + 1); if ((0, _keys2.default)(prototype).indexOf(atCommand) === -1 && atCommand !== 'exports') { stacktrace = [trace]; } else { /** * save trace for nested commands */ stacktrace.push(trace); } /** * determine execution depth: * This little tweak helps us to determine whether the command was executed * by the test script or by another command. With that we can make sure * that errors are getting thrown once they bubbled up the command chain. */ client.depth = stack.split('\n').filter(function (line) { return !!line.match(/\/lib\/(commands|protocol)\/(\w+)\.js/); }).length; /** * queue command */ client.name = name; client.lastResult = this.lastResult; client.next(func, args, name); return client; }; return unit; }; /** * register event emitter */ var _loop = function _loop(eventCommand) { prototype[eventCommand] = function () { for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } /** * custom commands needs to get emitted and registered in order * to prevent race conditions */ if (INTERNAL_EVENTS.indexOf(args[0]) === -1) { return this.finally(function () { return eventHandler[eventCommand].apply(eventHandler, args); }); } eventHandler[eventCommand].apply(eventHandler, args); return this; }; }; for (var eventCommand in EVENTHANDLER_FUNCTIONS) { _loop(eventCommand); } return unit; }; exports.default = WebdriverIO; module.exports = exports['default'];