Repository: cordova-paramedic Updated Branches: refs/heads/master 09d861aef -> afad9b3d0
CB-11546 Appium tests support Project: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/commit/afad9b3d Tree: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/tree/afad9b3d Diff: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/diff/afad9b3d Branch: refs/heads/master Commit: afad9b3d06c77cdf94e49fe57bd8c38478a7a529 Parents: 09d861a Author: Alexander Sorokin <[email protected]> Authored: Thu Jul 7 19:31:39 2016 +0300 Committer: Alexander Sorokin <[email protected]> Committed: Thu Jul 7 19:31:39 2016 +0300 ---------------------------------------------------------------------- lib/ParamedicConfig.js | 87 ++++-- lib/ParamedicLog.js | 10 +- lib/Reporters.js | 22 +- lib/appium/.jshintrc | 11 + lib/appium/AppiumRunner.js | 473 +++++++++++++++++++++++++++++++++ lib/appium/cordova_logo_thumb.jpg | Bin 0 -> 1932 bytes lib/appium/helpers/wdHelper.js | 103 +++++-- lib/paramedic.js | 299 ++++++++++++++------- lib/utils/utilities.js | 23 +- main.js | 43 ++- package.json | 8 + 11 files changed, 930 insertions(+), 149 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/ParamedicConfig.js ---------------------------------------------------------------------- diff --git a/lib/ParamedicConfig.js b/lib/ParamedicConfig.js index 51a1114..b68345c 100644 --- a/lib/ParamedicConfig.js +++ b/lib/ParamedicConfig.js @@ -19,7 +19,13 @@ var DEFAULT_START_PORT = 8008; var DEFAULT_END_PORT = 8018; -var DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes in msec - this will become a param +var DEFAULT_TIMEOUT = 60 * 60 * 1000; // 60 minutes in msec - this will become a param +var DEFAULT_SAUCE_DEVICE_NAME_ANDROID = 'Android Emulator'; +var DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID = '4.4'; +var DEFAULT_SAUCE_DEVICE_NAME_IOS = 'iPhone Simulator'; +var DEFAULT_SAUCE_PLATFORM_VERSION_IOS = '9.3'; +var DEFAULT_SAUCE_APPIUM_VERSION = '1.5.3'; +var DEFAULT_BUILD_NAME = 'Paramedic sauce test'; var util = require('./utils').utilities; @@ -27,25 +33,36 @@ function ParamedicConfig(json) { this._config = json; } +ParamedicConfig.prototype.getDefaultSauceDeviceName = function () { + return this._config.platform === 'android' ? DEFAULT_SAUCE_DEVICE_NAME_ANDROID : DEFAULT_SAUCE_DEVICE_NAME_IOS; +}; + +ParamedicConfig.prototype.getDefaultSaucePlatformVersion = function () { + return this._config.platform === 'android' ? DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID : DEFAULT_SAUCE_PLATFORM_VERSION_IOS; +}; + ParamedicConfig.parseFromArguments = function (argv) { return new ParamedicConfig({ - platform: argv.platform, - action: !!argv.justbuild ? 'build' : 'run', - args: (!!argv.browserify ? '--browserify ' : ''), - plugins: Array.isArray(argv.plugin) ? argv.plugin : [argv.plugin], - useTunnel: !!argv.useTunnel, - verbose: !!argv.verbose, - startPort: argv.startport || argv.port, - endPort: argv.endport || argv.port, - externalServerUrl: argv.externalServerUrl, - outputDir: !!argv.outputDir? argv.outputDir: null, - logMins: !!argv.logMins? argv.logMins: null, - tccDb: !!argv.tccDbPath? argv.tccDb: null, - cleanUpAfterRun: !!argv.cleanUpAfterRun? true: false, - shouldUseSauce: !!argv.shouldUseSauce || false, - buildName: argv.buildName || 'Paramedic sauce test', - sauceUser: argv.sauceUser || process.env[util.SAUCE_USER_ENV_VAR], - sauceKey: argv.sauceKey || process.env[util.SAUCE_KEY_ENV_VAR] + platform: argv.platform, + action: !!argv.justbuild ? 'build' : 'run', + args: (!!argv.browserify ? '--browserify ' : ''), + plugins: Array.isArray(argv.plugin) ? argv.plugin : [argv.plugin], + useTunnel: !!argv.useTunnel, + verbose: !!argv.verbose, + startPort: argv.startport || argv.port, + endPort: argv.endport || argv.port, + externalServerUrl: argv.externalServerUrl, + outputDir: !!argv.outputDir? argv.outputDir: null, + logMins: !!argv.logMins? argv.logMins: null, + tccDb: !!argv.tccDbPath? argv.tccDb: null, + cleanUpAfterRun: !!argv.cleanUpAfterRun? true: false, + shouldUseSauce: !!argv.shouldUseSauce || false, + buildName: argv.buildName, + sauceUser: argv.sauceUser, + sauceKey: argv.sauceKey, + sauceDeviceName: argv.sauceDeviceName && argv.sauceDeviceName.toString(), + saucePlatformVersion: argv.saucePlatformVersion && argv.saucePlatformVersion.toString(), + sauceAppiumVersion: argv.sauceAppiumVersion && argv.sauceAppiumVersion.toString() }); }; @@ -126,15 +143,19 @@ ParamedicConfig.prototype.setShouldUseSauce = function (sus) { }; ParamedicConfig.prototype.getBuildName = function () { - return this._config.buildName; + return this._config.buildName || DEFAULT_BUILD_NAME; }; ParamedicConfig.prototype.setBuildName = function (buildName) { this._config.buildName = buildName; }; +ParamedicConfig.prototype.getDefaultBuildName = function () { + return DEFAULT_BUILD_NAME; +}; + ParamedicConfig.prototype.getSauceUser = function () { - return this._config.sauceUser; + return this._config.sauceUser || process.env[util.SAUCE_USER_ENV_VAR]; }; ParamedicConfig.prototype.setSauceUser = function (sauceUser) { @@ -142,13 +163,37 @@ ParamedicConfig.prototype.setSauceUser = function (sauceUser) { }; ParamedicConfig.prototype.getSauceKey = function () { - return this._config.sauceKey; + return this._config.sauceKey || process.env[util.SAUCE_KEY_ENV_VAR]; }; ParamedicConfig.prototype.setSauceKey = function (sauceKey) { this._config.sauceKey = sauceKey; }; +ParamedicConfig.prototype.getSauceDeviceName = function () { + return this._config.sauceDeviceName || this.getDefaultSauceDeviceName(); +}; + +ParamedicConfig.prototype.setSauceDeviceName = function (sauceDeviceName) { + this._config.sauceDeviceName = sauceDeviceName.toString(); +}; + +ParamedicConfig.prototype.getSaucePlatformVersion = function () { + return this._config.saucePlatformVersion || this.getDefaultSaucePlatformVersion(); +}; + +ParamedicConfig.prototype.setSaucePlatformVersion = function (saucePlatformVersion) { + this._config.saucePlatformVersion = saucePlatformVersion.toString(); +}; + +ParamedicConfig.prototype.getSauceAppiumVersion = function () { + return this._config.sauceAppiumVersion || DEFAULT_SAUCE_APPIUM_VERSION; +}; + +ParamedicConfig.prototype.setSauceAppiumVersion = function (sauceAppiumVersion) { + this._config.sauceAppiumVersion = sauceAppiumVersion.toString(); +}; + ParamedicConfig.prototype.isBrowserify = function () { return this._config.browserify; }; http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/ParamedicLog.js ---------------------------------------------------------------------- diff --git a/lib/ParamedicLog.js b/lib/ParamedicLog.js index 14dea4b..3180d09 100644 --- a/lib/ParamedicLog.js +++ b/lib/ParamedicLog.js @@ -37,6 +37,10 @@ function ParamedicLog(platform, appPath, outputDir, targetObj) { } ParamedicLog.prototype.logIOS = function (appPath) { + if (!this.targetObj) { + logger.warn('It looks like there is no target to get logs from.'); + return; + } var simId = this.targetObj.simId; if (simId) { @@ -62,8 +66,12 @@ ParamedicLog.prototype.logWindows = function (appPath, logMins) { }; ParamedicLog.prototype.logAndroid = function () { - var logCommand = 'adb -s ' + this.targetObj.target + ' logcat -d -v time'; + if (!this.targetObj) { + logger.warn('It looks like there is no target to get logs from.'); + return; + } + var logCommand = 'adb -s ' + this.targetObj.target + ' logcat -d -v time'; var numDevices = util.countAndroidDevices(); if (numDevices != 1) { logger.error('there must be exactly one emulator/device attached'); http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/Reporters.js ---------------------------------------------------------------------- diff --git a/lib/Reporters.js b/lib/Reporters.js index e204c45..2191e26 100644 --- a/lib/Reporters.js +++ b/lib/Reporters.js @@ -20,7 +20,7 @@ var JasmineSpecReporter = require('jasmine-spec-reporter'); var jasmineReporters = require('jasmine-reporters'); -module.exports = function(outputDir) { +module.exports.getReporters = function(outputDir) { var reporters = [new JasmineSpecReporter({displayPendingSummary: false, displaySuiteNumber: true})]; if (outputDir) { @@ -29,3 +29,23 @@ module.exports = function(outputDir) { return reporters; }; + +module.exports.ParamedicReporter = ParamedicReporter; + +function ParamedicReporter(callback) { + this.allDoneCallback = callback; + this.failed = false; +} + +ParamedicReporter.prototype = { + specDone: function (spec) { + if (spec.status === 'failed') { + this.failed = true; + } + }, + jasmineDone: function () { + if (this.allDoneCallback instanceof Function) { + this.allDoneCallback(!this.failed); + } + } +}; http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/.jshintrc ---------------------------------------------------------------------- diff --git a/lib/appium/.jshintrc b/lib/appium/.jshintrc new file mode 100644 index 0000000..6997763 --- /dev/null +++ b/lib/appium/.jshintrc @@ -0,0 +1,11 @@ +{ + "node": true, + "bitwise": true, + "undef": true, + "trailing": true, + "quotmark": true, + "indent": 4, + "unused": "vars", + "latedef": "nofunc", + "-W030": false +} http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/AppiumRunner.js ---------------------------------------------------------------------- diff --git a/lib/appium/AppiumRunner.js b/lib/appium/AppiumRunner.js new file mode 100644 index 0000000..d126dd7 --- /dev/null +++ b/lib/appium/AppiumRunner.js @@ -0,0 +1,473 @@ +#!/usr/bin/env node + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* jshint node: true */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var util = require('../utils').utilities; +var logger = require('../utils').logger; +var wd = require('wd'); +var wdHelper = require('./helpers/wdHelper'); +var screenshotHelper = require('./helpers/screenshotHelper'); +var kill = require('tree-kill'); +var child_process = require('child_process'); +var expectTelnet = require('expect-telnet'); +var shell = require('shelljs'); +var Jasmine = require('jasmine'); +var unorm = require('unorm'); +var Q = require('q'); +var ConfigParser = require('cordova-common').ConfigParser; +var Reporters = require('../Reporters'); +var execPromise = require('../utils').execPromise; +var Reporters = require('../Reporters'); + +var KILL_SIGNAL = 'SIGINT'; +var SMALL_BUFFER_SIZE = 1024 * 1024; +var BIG_BUFFER_SIZE = 50 * 1024 * 1024; +var APPIUM_SERVER_PATH = getAppiumServerPath(); + +function AppiumRunner(options) { + this.options = options; + this.prepareOptions(); + this.createScreenshotDir(); + this.findTests(); + this.setGlobals(); +} + +function getAppiumServerPath() { + return path.resolve(process.cwd(), 'cordova-paramedic/node_modules/appium/build/lib/main.js'); +} + +function getFullAppPath(appPath) { + var fullPath = appPath; + if (!path.isAbsolute(appPath)) { + fullPath = path.join(__dirname, '../..', appPath); + } + return fullPath; +} + +function getPackagePath(options) { + if (options.sauce) { + return options.sauceAppPath; + } + + var fullAppPath = getFullAppPath(options.appPath); + + switch (options.platform) { + case 'android': + var packagePath = path.join(fullAppPath, '/platforms/android/build/outputs/apk/android-debug.apk'); + if (fs.existsSync(packagePath)) { + return packagePath; + } + throw new Error('Could not find apk'); + case 'ios': + var searchDir = options.device ? + path.join(fullAppPath, '/platforms/ios/build/device/') : + path.join(fullAppPath, '/platforms/ios/build/emulator/'); + var fileMask = options.device ? '*.ipa' : '*.app'; + var files = shell.ls(searchDir + fileMask); + logger.normal('paramedic-appium: Looking for app package in ' + searchDir); + if (files && files.length > 0) { + logger.normal('paramedic-appium: Found app package: ' + files[0]); + return files[0]; + } + throw new Error('Could not find the app package'); + } +} + +function getPluginDirs(appPath) { + return shell.ls(path.join(appPath, '/plugins/cordova-plugin-*')); +} + +function getConfigPath(appPath) { + return path.join(appPath, 'config.xml'); +} + +function addCspSource(appPath, directive, source) { + var cspInclFile = path.join(appPath, 'www/csp-incl.js'); + var indexFile = path.join(appPath, 'www/index.html'); + var cspFile = fs.existsSync(cspInclFile) ? cspInclFile : indexFile; + var cspContent = fs.readFileSync(cspFile, util.DEFAULT_ENCODING); + var cspTagOpening = '<meta http-equiv="Content-Security-Policy" content=\''; + var cspRule = directive + ' ' + source; + var cspRuleReg = new RegExp(directive + '[^;"]+' + source.replace('*', '\\*')); + + logger.normal('paramedic-appium: Adding CSP source "' + source + '" to directive "' + directive + '"'); + + if (cspContent.match(cspRuleReg)) { + logger.normal('paramedic-appium: It\'s already there.'); + } else if (util.contains(cspContent, directive)) { + // if the directive is there, just add the source to it + cspContent = cspContent.replace(directive, cspRule); + fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING); + } else if (cspContent.match(/content=".*?default-src.+?"/)) { + // needed directive is not there but there is default-src directive + // creating needed directive and copying default-src sources to it + var defaultSrcReg = /(content=".*?default-src)(.+?);/; + cspContent = cspContent.replace(defaultSrcReg, '$1$2; ' + cspRule + '$2;'); + fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING); + } else if (util.contains(cspContent, cspTagOpening)) { + // needed directive is not there and there is no default-src directive + // but the CSP tag is till present + // just adding needed directive to a start of CSP tag content + cspContent = cspContent.replace(cspTagOpening, cspTagOpening + directive + ' ' + source + '; '); + fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING); + } else { + // no CSP tag, skipping + logger.normal('paramedic-appium: WARNING: No CSP tag found.'); + } +} + +function setPreference(appPath, preference, value) { + var configFile = getConfigPath(appPath); + var config = new ConfigParser(configFile); + + logger.normal('paramedic-appium: Setting "' + preference + '" preference to "' + value + '"'); + config.setGlobalPreference(preference, value); + config.write(); +} + +function permitAccess(appPath, origin) { + var configFile = getConfigPath(appPath); + var config = new ConfigParser(configFile); + var accesses = config.getAccesses(); + var accessPresent = false; + + logger.normal('paramedic-appium: Adding a whitelist "access" rule for origin: ' + origin); + accesses.forEach(function (access) { + if (access.origin == origin) { + accessPresent = true; + } + }); + + if (accessPresent) { + logger.normal('paramedic-appium: It is already in place'); + } else { + config.addElement('access', { origin: origin }); + config.write(); + } +} + +function runCommand(command, appPath) { + if (appPath) { + shell.pushd(appPath); + } + shell.exec(command); + if (appPath) { + shell.popd(); + } +} + +function isFailFastError(error) { + if (error && error.message) { + return error.message.indexOf('Could not find a connected') > -1 || + error.message.indexOf('Bad app') > -1; + } + return false; +} + +function killProcess(procObj, killSignal, callback) { + if (procObj && procObj.alive) { + procObj.alive = false; + setTimeout(function () { + kill(procObj.process.pid, killSignal, callback); + }, 1000); + } else { + callback(); + } +} + +function installAppiumServer() { + logger.normal('paramedic-appium: Installing Appium server...'); + shell.pushd(path.join(__dirname, '../..')); + return execPromise('npm install appium').then(function () { + shell.popd(); + }); +} + +AppiumRunner.prototype.createScreenshotDir = function () { + util.mkdirSync(this.options.screenshotPath); +}; + +AppiumRunner.prototype.prepareOptions = function () { + if (!this.options.hasOwnProperty('device')) { + this.options.device = false; + } + if (this.options.platform === 'ios' && this.options.appiumDeviceName) { + this.options.appiumDeviceName = this.options.appiumDeviceName.replace('-', ' '); + } +}; + +AppiumRunner.prototype.cleanUp = function (callback) { + var self = this; + + killProcess(self.appium, KILL_SIGNAL, function () { + killProcess(self.iosProxy, KILL_SIGNAL, function () { + callback(); + }); + }); +}; + +AppiumRunner.prototype.startTests = function () { + var jasmine = new Jasmine(); + var self = this; + var d = Q.defer(); + + function exitGracefully(e) { + if (self.exiting) { + return; + } + if (!!e) { + logger.normal('paramedic-appium: ' + e); + } + logger.normal('paramedic-appium: Uncaught exception! Killing server and exiting in 2 seconds...'); + self.exiting = true; + self.cleanUp(function () { + setTimeout(function () { + d.reject(e.stack); + }, 2000); + }); + } + + process.on('uncaughtException', function(err) { + exitGracefully(err); + }); + + logger.normal('paramedic-appium: Running tests from:'); + self.options.testPaths.forEach(function (testPath) { + logger.normal('paramedic-appium: ' + testPath); + }); + + jasmine.loadConfig({ + spec_dir: '', + spec_files: self.options.testPaths + }); + + // don't use default reporter, it exits the process before + // we would get the chance to kill appium server + //jasmine.configureDefaultReporter({ showColors: false }); + + var outputDir = self.options.output || process.cwd(); + var reporters = Reporters.getReporters(outputDir); + var paramedicReporter = new Reporters.ParamedicReporter(function (passed) { + self.passed = passed; + self.cleanUp(d.resolve); + }); + + reporters.forEach(function (reporter) { + jasmine.addReporter(reporter); + }); + jasmine.addReporter(paramedicReporter); + + try { + // Launch the tests! + jasmine.execute(); + } catch (e) { + exitGracefully(e); + } + + return d.promise; +}; + +AppiumRunner.prototype.startIosProxy = function () { + var self = this; + var iosProxyCommand; + self.iosProxy = { + alive: false, + process: null + }; + + if (this.options.platform === 'ios' && this.options.device && this.options.udid) { + iosProxyCommand = 'ios_webkit_debug_proxy -c ' + this.options.udid + ':27753'; + logger.normal('paramedic-appium: Running:'); + logger.normal('paramedic-appium: ' + iosProxyCommand); + self.iosProxy.alive = true; + self.iosProxy.process = child_process.exec(iosProxyCommand, { maxBuffer: BIG_BUFFER_SIZE }, function () { + self.iosProxy.alive = false; + logger.normal('paramedic-appium: iOS proxy process exited.'); + }); + } +}; + +AppiumRunner.prototype.startAppiumServer = function () { + var d = Q.defer(); + var self = this; + var appiumServerCommand; + var additionalArgs = ''; + self.appium = { + alive: false, + process: null + }; + + // compose a command to run the Appium server + switch (self.options.platform) { + case 'android': + break; + case 'ios': + if (self.options.udid) { + additionalArgs += ' --udid ' + self.options.udid; + } + break; + default: + throw new Error('Unsupported platform: ' + self.options.platform); + } + if (self.options.logFile) { + additionalArgs += ' --log ' + self.options.logFile; + } + + appiumServerCommand = 'node ' + APPIUM_SERVER_PATH + additionalArgs; + + // run the Appium server + logger.normal('paramedic-appium: Running:'); + logger.normal('paramedic-appium: ' + appiumServerCommand); + self.appium.alive = true; + self.appium.process = child_process.exec(appiumServerCommand, { maxBuffer: BIG_BUFFER_SIZE }, function (error) { + logger.normal('paramedic-appium: Appium process exited.'); + if (self.appium.alive && error) { + logger.normal('paramedic-appium: Error running appium server: ' + error); + if (isFailFastError(error)) { + self.cleanUp(d.reject); + } else { + logger.normal('paramedic-appium: Another instance already running? Will try to run tests on it.'); + d.resolve(); + } + } + self.appium.alive = false; + }); + + // Wait for the Appium server to start up + self.appium.process.stdout.on('data', function (data) { + if (data.indexOf('Appium REST http interface listener started') > -1) { + d.resolve(); + } + }); + + return d.promise; +}; + +AppiumRunner.prototype.findTests = function () { + var self = this; + + if (!self.options.pluginRepos) { + self.options.pluginRepos = getPluginDirs(self.options.appPath); + } + + // looking for the tests + self.options.testPaths = []; + var searchPaths = []; + self.options.pluginRepos.forEach(function (pluginRepo) { + searchPaths.push(path.join(pluginRepo, 'appium-tests', self.options.platform)); + searchPaths.push(path.join(pluginRepo, 'appium-tests', 'common')); + }); + searchPaths.forEach(function (searchPath) { + if (fs.existsSync(searchPath)) { + logger.normal('paramedic-appium: Found tests in: ' + searchPath); + if (path.isAbsolute(searchPath)) { + searchPath = path.relative(process.cwd(), searchPath); + } + self.options.testPaths.push(path.join(searchPath, '*.spec.js')); + } + }); +}; + +AppiumRunner.prototype.setGlobals = function () { + // setting up the global variables so the tests could use them + global.WD = wd; + global.WD_HELPER = wdHelper; + global.SCREENSHOT_HELPER = screenshotHelper; + global.ET = expectTelnet; + global.SHELL = shell; + global.DEVICE = this.options.device; + global.DEVICE_NAME = this.options.appiumDeviceName; + global.PLATFORM = this.options.platform; + global.PLATFORM_VERSION = this.options.appiumPlatformVersion; + global.SCREENSHOT_PATH = this.options.screenshotPath; + global.UNORM = unorm; + global.UDID = this.options.udid; + global.VERBOSE = this.options.verbose; + global.USE_SAUCE = this.options.sauce; + global.SAUCE_USER = this.options.sauceUser; + global.SAUCE_KEY = this.options.sauceKey; + global.SAUCE_CAPS = this.options.sauceCaps; + global.VERBOSE = this.options.verbose; + global.SAUCE_SERVER_HOST = util.SAUCE_HOST; + global.SAUCE_SERVER_PORT = util.SAUCE_PORT; +}; + +AppiumRunner.prototype.prepareApp = function () { + var self = this; + var d = Q.defer(); + var fullAppPath = getFullAppPath(self.options.appPath); + var deviceString = self.options.device ? ' --device' : ''; + var buildCommand = 'cordova build ' + self.options.platform + deviceString; + + // remove medic.json and (re)build + shell.rm(path.join(fullAppPath, 'www', 'medic.json')); + fs.stat(fullAppPath, function (error, stats) { + // check if the app exists + if (error || !stats.isDirectory()) { + d.reject('The app directory doesn\'t exist: ' + fullAppPath); + } + + // set properties/CSP rules + if (self.options.platform === 'ios') { + setPreference(fullAppPath, 'CameraUsesGeolocation', 'true'); + } else if (self.options.platform === 'android') { + setPreference(fullAppPath, 'loadUrlTimeoutValue', 60000); + } + addCspSource(fullAppPath, 'connect-src', 'http://*'); + permitAccess(fullAppPath, '*'); + // add cordova-save-image-gallery plugin from npm to enable + // Appium tests for camera plugin to save test image to the gallery + runCommand('cordova plugin add cordova-save-image-gallery', fullAppPath); + + // rebuild the app + logger.normal('paramedic-appium: Building the app...'); + child_process.exec(buildCommand, { cwd: fullAppPath, maxBuffer: SMALL_BUFFER_SIZE }, function (error) { + if (error) { + d.reject('Couldn\'t build the app: ' + error); + } else { + global.PACKAGE_PATH = getPackagePath(self.options); + d.resolve(); + } + }); + }); + return d.promise; +}; + +AppiumRunner.prototype.runTests = function (useSauce) { + var self = this; + + return Q().then(function () { + if (!useSauce) { + self.startIosProxy(); + return installAppiumServer() + .then(self.startAppiumServer.bind(self)); + } + }) + .then(self.startTests.bind(self)) + .then(function () { return self.passed; }); +}; + +module.exports = AppiumRunner; http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/cordova_logo_thumb.jpg ---------------------------------------------------------------------- diff --git a/lib/appium/cordova_logo_thumb.jpg b/lib/appium/cordova_logo_thumb.jpg new file mode 100644 index 0000000..5bc04bf Binary files /dev/null and b/lib/appium/cordova_logo_thumb.jpg differ http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/appium/helpers/wdHelper.js ---------------------------------------------------------------------- diff --git a/lib/appium/helpers/wdHelper.js b/lib/appium/helpers/wdHelper.js index ce32daa..61505d5 100644 --- a/lib/appium/helpers/wdHelper.js +++ b/lib/appium/helpers/wdHelper.js @@ -35,6 +35,9 @@ var wd = global.WD || require('wd'); module.exports.getDriver = function (platform) { var normalizedPlatform; + var driverConfig = {}; + var serverConfig = {}; + var driver; switch (platform.toLowerCase()) { case 'android': normalizedPlatform = 'Android'; @@ -46,24 +49,37 @@ module.exports.getDriver = function (platform) { throw 'Unknown platform: ' + platform; } - var serverConfig = { - host: APPIUM_SERVER_HOST, - port: APPIUM_SERVER_PORT - }; - - var driverConfig = { - browserName: '', - platformName: normalizedPlatform, - platformVersion: global.PLATFORM_VERSION || '', - deviceName: global.DEVICE_NAME || '', - app: global.PACKAGE_PATH, - autoAcceptAlerts: true, - }; - if (global.UDID) { - driverConfig.udid = global.UDID; + if (global.USE_SAUCE) { + serverConfig = { + host: global.SAUCE_SERVER_HOST, + port: global.SAUCE_SERVER_PORT + }; + + driverConfig = global.SAUCE_CAPS; + + driver = global.WD.promiseChainRemote(serverConfig.host, serverConfig.port, global.SAUCE_USER, global.SAUCE_KEY); + } else { + serverConfig = { + host: APPIUM_SERVER_HOST, + port: APPIUM_SERVER_PORT + }; + + driverConfig = { + browserName: '', + platformName: normalizedPlatform, + platformVersion: global.PLATFORM_VERSION || '', + deviceName: global.DEVICE_NAME || '', + app: global.PACKAGE_PATH, + autoAcceptAlerts: true, + }; + + if (global.UDID) { + driverConfig.udid = global.UDID; + } + + driver = global.WD.promiseChainRemote(serverConfig); } - var driver = global.WD.promiseChainRemote(serverConfig); module.exports.configureLogging(driver); return driver @@ -122,6 +138,9 @@ module.exports.injectLibraries = function (driver) { }; module.exports.configureLogging = function (driver) { + if (!global.VERBOSE) { + return; + } driver.on('status', function (info) { console.log(info); }); @@ -200,20 +219,54 @@ module.exports.pollForEvents = function (driver, isAndroid, windowOffset) { }); }; +module.exports.addFillerImage = function (driver) { + var bitmap = fs.readFileSync(path.join(__dirname, '../cordova_logo_thumb.jpg')); + var base64str = new Buffer(bitmap).toString('base64'); + + return driver.executeAsync(function (b64str, cb) { + if (window.imageSaver) { + window.imageSaver.saveBase64Image( { + data: b64str + }, function (fpath) { + cb(fpath); + }, function (err) { + cb('ERROR: ' + err); + }); + } else { + cb(); + } + }, [base64str]); +}; + +module.exports.deleteFillerImage = function (driver, testImagePath) { + if (!testImagePath) { + return driver; + } + return driver.executeAsync(function (testImagePath, cb) { + if (window.imageSaver) { + window.imageSaver.removeImage({ + data: testImagePath + }, function () { + cb(); + }, function (err) { + cb('ERROR: ' + err); + }); + } else { + cb(); + } + }, [testImagePath]); +}; + wd.addPromiseChainMethod('getWebviewContext', function (retries) { return module.exports.getWebviewContext(this, retries); }); -wd.addPromiseChainMethod('injectLibraries', function () { - return module.exports.tapElementByXPath(this); -}); - wd.addPromiseChainMethod('waitForDeviceReady', function () { return module.exports.waitForDeviceReady(this); }); wd.addPromiseChainMethod('injectLibraries', function () { - return module.exports.tapElementByXPath(this); + return module.exports.injectLibraries(this); }); wd.addPromiseChainMethod('tapElementByXPath', function (xpath) { @@ -223,3 +276,11 @@ wd.addPromiseChainMethod('tapElementByXPath', function (xpath) { wd.addPromiseChainMethod('pollForEvents', function (isAndroid) { return module.exports.pollForEvents(this, isAndroid); }); + +wd.addPromiseChainMethod('addFillerImage', function () { + return module.exports.addFillerImage(this); +}); + +wd.addPromiseChainMethod('deleteFillerImage', function (testImagePath) { + return module.exports.deleteFillerImage(this, testImagePath); +}); http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/paramedic.js ---------------------------------------------------------------------- diff --git a/lib/paramedic.js b/lib/paramedic.js index b2c83a1..ee1a084 100644 --- a/lib/paramedic.js +++ b/lib/paramedic.js @@ -28,11 +28,13 @@ var fs = require('fs'); var logger = require('./utils').logger; var util = require('./utils').utilities; var PluginsManager = require('./PluginsManager'); -var getReporters = require('./Reporters'); +var Reporters = require('./Reporters'); var ParamedicKill = require('./ParamedicKill'); var ParamedicLog = require('./ParamedicLog'); var wd = require('wd'); var SauceLabs = require('saucelabs'); +var randomstring = require('randomstring'); +var AppiumRunner = require('./appium/AppiumRunner'); var ParamediciOSPermissions = require('./ParamediciOSPermissions'); var ParamedicTargetChooser = require('./ParamedicTargetChooser'); var ParamedicAppUninstall = require('./ParamedicAppUninstall'); @@ -44,9 +46,6 @@ require('./appium/helpers/wdHelper'); // If device has not connected within this interval the tests are stopped. var INITIAL_CONNECTION_TIMEOUT = 300000; // 5mins -var SAUCE_HOST = 'ondemand.saucelabs.com'; -var SAUCE_PORT = 80; - var applicationsToGrantPermission = [ 'kTCCServiceAddressBook' ]; @@ -63,6 +62,7 @@ function ParamedicRunner(config, _callback) { ParamedicRunner.prototype.run = function () { var self = this; + var isTestPassed = false; this.checkSauceRequirements(); @@ -84,7 +84,9 @@ ParamedicRunner.prototype.run = function () { logger.normal('Start running tests at ' + (new Date()).toLocaleTimeString()); return self.runTests(); }) + .timeout(self.config.getTimeout(), 'Timed out after waiting for ' + self.config.getTimeout() + ' ms.') .fin(function (result) { + isTestPassed = result; logger.normal('Completed tests at ' + (new Date()).toLocaleTimeString()); // if we do --justbuild or run on sauce, // we should NOT do actions below @@ -94,7 +96,7 @@ ParamedicRunner.prototype.run = function () { self.killEmulatorProcess(); } self.cleanUpProject(); - return result; + return self.displaySauceDetails(); }); }; @@ -164,7 +166,7 @@ ParamedicRunner.prototype.setPermissions = function () { ParamedicRunner.prototype.injectReporters = function () { var self = this; - var reporters = getReporters(self.config.getOutputDir()); + var reporters = Reporters.getReporters(self.config.getOutputDir()); ['jasmineStarted', 'specStarted', 'specDone', 'suiteStarted', 'suiteDone', 'jasmineDone'].forEach(function(route) { @@ -190,48 +192,124 @@ ParamedicRunner.prototype.writeMedicConnectionUrl = function(url) { fs.writeFileSync(path.join('www','medic.json'), JSON.stringify({logurl:url})); }; -ParamedicRunner.prototype.runTests = function () { +ParamedicRunner.prototype.buildApp = function () { var self = this; - if (this.config.shouldUseSauce()) { - var command = this.getCommandForBuilding(); + var command = this.getCommandForBuilding(); + + logger.normal('cordova-paramedic: running command ' + command); + + return execPromise(command) + .fail(function(output) { + // this trace is automatically available in verbose mode + // so we check for this flag to not trace twice + if (!self.config.verbose) { + logger.normal(output); + } + throw new Error('Unable to build project.'); + }); +}; + +ParamedicRunner.prototype.runLocalTests = function () { + var self = this; + + return self.getCommandForStartingTests() + .then(function(command) { + self.setPermissions(); logger.normal('cordova-paramedic: running command ' + command); - return execPromise(command) - .then(self.runSauceTests.bind(self), function(output) { - // this trace is automatically available in verbose mode - // so we check for this flag to not trace twice - if (!self.config.verbose) { - logger.normal(output); - } - logger.normal('cordova-paramedic: unable to build project; command log is available above'); - throw new Error('Command "' + command + '" failed.'); + return execPromise(command); + }) + .then(function() { + // skip tests if it was just build + if (self.shouldWaitForTestResult()) { + return Q.promise(function(resolve, reject) { + // reject if timed out + self.waitForConnection().catch(reject); + // resolve if got results + self.waitForTests().then(resolve); + }); + } + }, function(output) { + // this trace is automatically available in verbose mode + // so we check for this flag to not trace twice + if (!self.config.verbose) { + logger.normal(output); + } + throw new Error('Unable to run tests.'); + }); +}; + +ParamedicRunner.prototype.runAppiumTests = function (useSauce) { + var platform = this.config.getPlatformId(); + var self = this; + if (platform !== 'android' && platform !== 'ios') { + logger.warn('Unsupported platform for Appium test run: ' + platform); + // just skip Appium tests + return Q(util.TEST_PASSED); + } + if (!useSauce && (!self.targetObj || !self.targetObj.target)) { + throw new Error('Cannot determine device name for Appium'); + } + + logger.normal('Running Appium tests ' + useSauce ? 'on Sauce Labs' : 'locally'); + + var options = { + platform: self.config.getPlatformId(), + appPath: self.tempFolder.name, + appiumDeviceName: self.targetObj && self.targetObj.target, + appiumPlatformVersion: null, + screenshotPath: path.join(process.cwd(), 'appium_screenshots'), + output: self.config.getOutputDir(), + verbose: self.config.isVerbose(), + sauce: useSauce + }; + if (useSauce) { + options.sauceAppPath = 'sauce-storage:' + this.getAppName(); + options.sauceUser = this.config.getSauceUser(); + options.sauceKey = this.config.getSauceKey(); + options.sauceCaps = this.getSauceCaps(); + options.sauceCaps.name += '_Appium'; + } + + var appiumRunner = new AppiumRunner(options); + if (appiumRunner.options.testPaths && appiumRunner.options.testPaths.length === 0) { + logger.warn('Couldn\'t find Appium tests, skipping...'); + return Q(util.TEST_PASSED); + } + return Q() + .then(function () { + return appiumRunner.prepareApp(); + }) + .then(function () { + if (useSauce) { + return self.uploadApp.bind(self); + } + }) + .then(function () { + return appiumRunner.runTests(useSauce); + }); +}; + +ParamedicRunner.prototype.runTests = function () { + var isTestPassed = false; + var self = this; + if (this.config.shouldUseSauce()) { + return this.runSauceTests() + .then(function (result) { + isTestPassed = result; + return self.runAppiumTests(true); + }) + .then(function (isAppiumTestPassed) { + return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED; }); } else { - return self.getCommandForStartingTests() - .then(function(command) { - self.setPermissions(); - logger.normal('cordova-paramedic: running command ' + command); - - return execPromise(command); + return this.runLocalTests() + .then(function (result) { + isTestPassed = result; }) - .then(function() { - // skip tests if it was just build - if (self.shouldWaitForTestResult()) { - return Q.promise(function(resolve, reject) { - // reject if timed out - self.waitForConnection().catch(reject); - // resolve if got results - self.waitForTests().then(resolve); - }); - } - }, function(output) { - // this trace is automatically available in verbose mode - // so we check for this flag to not trace twice - if (!self.config.verbose) { - logger.normal(output); - } - logger.normal('cordova-paramedic: unable to run tests; command log is available above'); - throw new Error('Command "' + command + '" failed.'); + .then(self.runAppiumTests.bind(this)) + .then(function (isAppiumTestPassed) { + return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED; }); } }; @@ -418,7 +496,6 @@ ParamedicRunner.prototype.uninstallApp = function () { paramedicAppUninstall.uninstallApp(this.targetObj,util.PARAMEDIC_DEFAULT_APP_NAME); }; - ParamedicRunner.prototype.getPackageFolder = function () { var packageFolder; switch (this.config.getPlatformId()) { @@ -479,35 +556,49 @@ ParamedicRunner.prototype.getBinaryName = function () { return binaryName; }; +// Returns a name of the file at the SauceLabs storage ParamedicRunner.prototype.getAppName = function () { - var appName; + if (this.appName) { + return this.appName; + } + var appName = randomstring.generate(); switch (this.config.getPlatformId()) { case 'android': - appName = 'mobilespec.apk'; + appName += '.apk'; break; case 'ios': - appName = 'HelloCordova.zip'; + appName += '.zip'; break; default: throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId()); } + this.appName = appName; return appName; }; -ParamedicRunner.prototype.getSauceDetails = function () { +ParamedicRunner.prototype.displaySauceDetails = function () { + if (!this.config.shouldUseSauce()) { + return Q(); + } + var self = this; var d = Q.defer(); - logger.normal('Getting saucelabs job details...\n'); + logger.normal('Getting saucelabs jobs details...\n'); var sauce = new SauceLabs({ username: self.config.getSauceUser(), password: self.config.getSauceKey() }); + if (self.config.getBuildName() === self.config.getDefaultBuildName()) { + logger.warn('Build name is not specified, showing all sauce jobs with default name...'); + } + sauce.getJobs(function (err, jobs) { + var found = false; for (var job in jobs) { - if (jobs.hasOwnProperty(job) && jobs[job].name === self.config.getBuildName()) { + if (jobs.hasOwnProperty(job) && jobs[job].name && jobs[job].name.indexOf(self.config.getBuildName()) === 0) { var jobUrl = 'https://saucelabs.com/beta/tests/' + jobs[job].id; logger.normal('============================================================================================'); logger.normal('Job name: ' + jobs[job].name); @@ -520,18 +611,66 @@ ParamedicRunner.prototype.getSauceDetails = function () { } logger.normal('============================================================================================'); logger.normal(''); - d.resolve(); - break; + found = true; } } - if (d.promise.inspect().state !== 'fulfilled') { + + if (!found) { logger.warn('Can not find saucelabs job. Logs and video will be unavailable.'); - d.resolve(); } + d.resolve(); }); return d.promise; }; +ParamedicRunner.prototype.getSauceCaps = function () { + var caps = { + name: this.config.getBuildName(), + browserName: '', + appiumVersion: this.config.getSauceAppiumVersion(), + deviceOrientation: 'portrait', + deviceType: 'phone', + idleTimeout: '100', // in seconds + app: 'sauce-storage:' + this.getAppName(), + deviceName: this.config.getSauceDeviceName(), + platformVersion: this.config.getSaucePlatformVersion(), + maxDuration: util.SAUCE_MAX_DURATION + }; + + switch(this.config.getPlatformId()) { + case 'android': + caps.platformName = 'Android'; + caps.appPackage = 'io.cordova.hellocordova'; + caps.appActivity = 'io.cordova.hellocordova.MainActivity'; + break; + case 'ios': + caps.platformName = 'iOS'; + caps.autoAcceptAlerts = true; + caps.waitForAppScript = 'true;'; + break; + default: + throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId()); + } + return caps; +}; + +ParamedicRunner.prototype.connectWebdriver = function () { + var user = this.config.getSauceUser(); + var key = this.config.getSauceKey(); + var caps = this.getSauceCaps(); + + logger.normal('cordova-paramedic: connecting webdriver'); + + wd.configureHttp({ + timeout: 3 * 60 * 1000, + retryDelay: 15000, + retries: 5 + }); + + var driver = wd.promiseChainRemote(util.SAUCE_HOST, util.SAUCE_PORT, user, key); + return driver.init(caps); +}; + ParamedicRunner.prototype.runSauceTests = function () { logger.info('cordova-paramedic: running sauce tests'); var self = this; @@ -539,52 +678,12 @@ ParamedicRunner.prototype.runSauceTests = function () { var pollForResults; var driver; - return self.packageApp() + return this.buildApp() + .then(self.packageApp.bind(self)) .then(self.uploadApp.bind(self)) - .then(function() { - logger.normal('cordova-paramedic: app uploaded; starting tests'); - - var user = self.config.getSauceUser(); - var key = self.config.getSauceKey(); - - var caps = { - name: self.config.getBuildName(), - browserName: '', - appiumVersion: '1.5.2', - deviceOrientation: 'portrait', - deviceType: 'phone', - idleTimeout: '100', // in seconds - app: 'sauce-storage:' + self.getAppName() - }; - - switch(self.config.getPlatformId()) { - case 'android': - caps.deviceName = 'Android Emulator'; - caps.platformVersion = '4.4'; - caps.platformName = 'Android'; - caps.appPackage = 'io.cordova.hellocordova'; - caps.appActivity = 'io.cordova.hellocordova.MainActivity'; - break; - case 'ios': - caps.deviceName = 'iPhone Simulator'; - caps.platformVersion = '9.3'; - caps.platformName = 'iOS'; - caps.autoAcceptAlerts = true; - break; - default: - throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId()); - } - - logger.normal('cordova-paramedic: connecting webdriver'); - - wd.configureHttp({ - timeout: 3 * 60 * 1000, - retryDelay: 15000, - retries: 5 - }); - - driver = wd.promiseChainRemote(SAUCE_HOST, SAUCE_PORT, user, key); - return driver.init(caps); + .then(function () { + driver = self.connectWebdriver(); + return driver; }) .then(function () { if (self.config.getUseTunnel()) { @@ -628,7 +727,6 @@ ParamedicRunner.prototype.runSauceTests = function () { } return driver.quit(); }) - .fin(self.getSauceDetails.bind(self)) .then(function () { return isTestPassed; }); @@ -643,6 +741,5 @@ exports.run = function(paramedicConfig) { var runner = new ParamedicRunner(paramedicConfig, null); runner.storedCWD = storedCWD; - return runner.run() - .timeout(paramedicConfig.getTimeout(), 'This test seems to be blocked :: timeout exceeded. Exiting ...'); + return runner.run(); }; http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/lib/utils/utilities.js ---------------------------------------------------------------------- diff --git a/lib/utils/utilities.js b/lib/utils/utilities.js index a2a8c82..87736ba 100644 --- a/lib/utils/utilities.js +++ b/lib/utils/utilities.js @@ -123,12 +123,24 @@ function doesFileExist(filePath) { return fileExists; } +function mkdirSync(path) { + try { + fs.mkdirSync(path); + } catch(e) { + if ( e.code != 'EEXIST' ) throw e; + } +} + function getSqlite3InsertionCommand(destinationTCCFile, service, appName) { return util.format('sqlite3 %s "insert into access' + '(service, client, client_type, allowed, prompt_count, csreq) values(\'%s\', \'%s\', ' + '0,1,1,NULL)"', destinationTCCFile, service, appName); } +function contains(collection, item) { + return collection.indexOf(item) !== (-1); +} + module.exports = { ANDROID: 'android', IOS: 'ios', @@ -136,10 +148,17 @@ module.exports = { PARAMEDIC_DEFAULT_APP_NAME: 'io.cordova.hellocordova', SAUCE_USER_ENV_VAR: 'SAUCE_USER', SAUCE_KEY_ENV_VAR: 'SAUCE_ACCESS_KEY', + SAUCE_HOST: 'ondemand.saucelabs.com', + SAUCE_PORT: 80, + SAUCE_MAX_DURATION: 5400, // in seconds + DEFAULT_ENCODING: 'utf-8', DEFAULT_LOG_TIME: 15, DEFAULT_LOG_TIME_ADDITIONAL: 2, + TEST_PASSED: 1, + TEST_FAILED: 0, + secToMin: secToMin, isWindows: isWindows, countAndroidDevices: countAndroidDevices, @@ -147,5 +166,7 @@ module.exports = { doesFileExist: doesFileExist, getSqlite3InsertionCommand: getSqlite3InsertionCommand, getSimulatorModelId: getSimulatorModelId, - getSimulatorId: getSimulatorId + getSimulatorId: getSimulatorId, + contains: contains, + mkdirSync: mkdirSync }; http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/main.js ---------------------------------------------------------------------- diff --git a/main.js b/main.js index 15b0b49..65c763d 100755 --- a/main.js +++ b/main.js @@ -1,5 +1,24 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + var parseArgs = require('minimist'); var path = require('path'); var paramedic = require('./lib/paramedic'); @@ -27,11 +46,14 @@ var USAGE = "Error missing args. \n" + "--outputDir: (optional) path to save Junit results file & Device logs\n" + "--cleanUpAfterRun: (optional) cleans up the application after the run\n" + "--logMins: (optional) Windows only - specifies number of minutes to get logs\n" + - "--tccDb: (optional) iOS only - specifies the path for the TCC.db file to be copied." + + "--tccDb: (optional) iOS only - specifies the path for the TCC.db file to be copied.\n" + "--shouldUseSauce: (optional) run tests on Saucelabs\n" + "--buildName: (optional) Build name to show in Saucelabs dashboard\n" + "--sauceUser: (optional) Saucelabs username\n" + - "--sauceKey: (optional) Saucelabs access key"; + "--sauceKey: (optional) Saucelabs access key\n" + + "--sauceDeviceName: (optional) Name of the SauceLabs emulator. For example, \"iPhone Simulator\"\n" + + "--saucePlatformVersion: (optional) Platform version of the SauceLabs emulator. For example, \"9.3\"" + + "--sauceAppiumVersion: (optional) Appium version to use when running on Saucelabs. For example, \"1.5.3\""; var argv = parseArgs(process.argv.slice(2)); var pathToParamedicConfig = argv.config && path.resolve(argv.config); @@ -86,6 +108,18 @@ if (pathToParamedicConfig || // --config paramedicConfig.setSauceKey(argv.sauceKey); } + if (argv.sauceDeviceName) { + paramedicConfig.setSauceDeviceName(argv.sauceDeviceName); + } + + if (argv.saucePlatformVersion) { + paramedicConfig.setSaucePlatformVersion(argv.saucePlatformVersion); + } + + if (argv.sauceAppiumVersion) { + paramedicConfig.setSauceAppiumVersion(argv.sauceAppiumVersion); + } + if (argv.useTunnel) { if (argv.useTunnel === 'false') { argv.useTunnel = false; @@ -103,7 +137,10 @@ if (pathToParamedicConfig || // --config process.exit(1); }) .done(function(isTestPassed) { - process.exit(isTestPassed ? 0 : 1); + var exitCode = isTestPassed ? 0 : 1; + + console.log('Finished with exit code ' + exitCode); + process.exit(exitCode); }); } else { http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/afad9b3d/package.json ---------------------------------------------------------------------- diff --git a/package.json b/package.json index 41ce5b3..508e80d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "bin": { "cordova-paramedic": "./main.js" }, + "engines" : { + "node" : ">=0.11.2" + }, "repository": { "type": "git", "url": "git://github.com/apache/cordova-paramedic.git" @@ -29,17 +32,22 @@ "author": "Jesse MacFadyen", "dependencies": { "cordova-common": "^1.1.0", + "expect-telnet": "^0.5.2", + "jasmine": "^2.4.1", "jasmine-reporters": "^2.1.1", "jasmine-spec-reporter": "^2.4.0", "localtunnel": "~1.5.0", "minimist": "~1.1.0", "path-extra": "^3.0.0", "q": "^1.4.1", + "randomstring": "^1.1.5", "saucelabs": "^1.2.0", "shelljs": "~0.3.0", "socket.io": "^1.4.5", "tcp-port-used": "^0.1.2", "tmp": "0.0.25", + "tree-kill": "^1.1.0", + "unorm": "^1.4.1", "wd": "^0.4.0" }, "devDependencies": { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
