You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ja...@apache.org on 2019/03/27 17:52:24 UTC

[cordova-paramedic] branch master updated: Extract classes from paramedic.js (App, SauceLabs) (#86)

This is an automated email from the ASF dual-hosted git repository.

janpio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-paramedic.git


The following commit(s) were added to refs/heads/master by this push:
     new a1e2493  Extract classes from paramedic.js (App, SauceLabs) (#86)
a1e2493 is described below

commit a1e2493595f0a715571210a76747a8a932fb5806
Author: Jan Piotrowski <pi...@gmail.com>
AuthorDate: Wed Mar 27 18:52:19 2019 +0100

    Extract classes from paramedic.js (App, SauceLabs) (#86)
    
    This PR extracts two big parts of `paramedic.js` into their own classes: `ParamedicApp` which handles the creation of the app, and `ParamedicSauceLabs` which does all the Sauce Labs things.
    
    Along the way I renamed `ParamedicLog` to `ParamedicLogCollector`, resorted some requires and expanded our npm scripts list to enable better testing of paramedic.
    
    The easiest way to review this PR is probably by looking at the individual PRs that show how I first extracted the code without changing it, then fixed all the issues that popped up because of the new data locations etc.
    
    closes #74
---
 lib/ParamedicApp.js                               | 143 +++++
 lib/{ParamedicLog.js => ParamedicLogCollector.js} |  16 +-
 lib/ParamedicSauceLabs.js                         | 551 ++++++++++++++++++
 lib/paramedic.js                                  | 664 +---------------------
 package.json                                      |  15 +-
 5 files changed, 744 insertions(+), 645 deletions(-)

diff --git a/lib/ParamedicApp.js b/lib/ParamedicApp.js
new file mode 100644
index 0000000..2983683
--- /dev/null
+++ b/lib/ParamedicApp.js
@@ -0,0 +1,143 @@
+#!/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 Q               = require('q');
+var tmp             = require('tmp');
+var shell           = require('shelljs');
+var path            = require('path');
+var exec            = require('./utils').exec;
+var execPromise     = require('./utils').execPromise;
+var util            = require('./utils').utilities;
+var logger          = require('./utils').logger;
+var PluginsManager  = require('./PluginsManager');
+var appPatcher      = require('./appium/helpers/appPatcher');
+
+
+
+function ParamedicApp(config, storedCWD, runner) {
+    this.config = config;
+    this.storedCWD = storedCWD;
+    this.runner = runner;
+
+    this.tempFolder = null;
+}
+module.exports = ParamedicApp;
+
+ParamedicApp.prototype.createTempProject = function () {
+    this.tempFolder = tmp.dirSync();
+    tmp.setGracefulCleanup();
+    logger.info('cordova-paramedic: creating temp project at ' + this.tempFolder.name);
+    exec(this.config.getCli() + ' create ' + this.tempFolder.name + util.PARAMEDIC_COMMON_CLI_ARGS);
+    return this.tempFolder;
+};
+
+ParamedicApp.prototype.prepareProjectToRunTests = function () {
+    var self = this;
+
+    this.installPlugins();
+    this.setUpStartPage();
+    return this.installPlatform()
+    .then(function () {
+        return self.checkPlatformRequirements();
+    });
+};
+
+ParamedicApp.prototype.installPlugins = function () {
+    logger.info('cordova-paramedic: installing plugins');
+    var pluginsManager = new PluginsManager(this.tempFolder.name, this.storedCWD, this.config);
+    pluginsManager.installPlugins(this.config.getPlugins());
+    pluginsManager.installTestsForExistingPlugins();
+
+    var additionalPlugins = ['cordova-plugin-test-framework', path.join(__dirname, '../paramedic-plugin')];
+    if (this.config.shouldUseSauce() && !this.config.getUseTunnel()) {
+        additionalPlugins.push(path.join(__dirname, '../event-cache-plugin'));
+    }
+    if (this.config.getPlatformId() === util.WINDOWS) {
+        additionalPlugins.push(path.join(__dirname, '../debug-mode-plugin'));
+    }
+    if (this.config.getPlatformId() === util.IOS) {
+        additionalPlugins.push(path.join(__dirname, '../ios-geolocation-permissions-plugin'));
+    }
+    if (this.config.isCI()) {
+        additionalPlugins.push(path.join(__dirname, '../ci-plugin'));
+    }
+
+    pluginsManager.installPlugins(additionalPlugins);
+};
+
+ParamedicApp.prototype.setUpStartPage = function () {
+    logger.normal('cordova-paramedic: setting app start page to test page');
+    shell.sed('-i', 'src="index.html"', 'src="cdvtests/index.html"', 'config.xml');
+};
+
+ParamedicApp.prototype.installPlatform = function () {
+    var self = this;
+    var platform = this.config.getPlatform();
+    var platformId = this.config.getPlatformId();
+    logger.info('cordova-paramedic: adding platform ' + platform + " (with: " + util.PARAMEDIC_COMMON_CLI_ARGS + util.PARAMEDIC_PLATFORM_ADD_ARGS + ")");
+
+    return execPromise(this.config.getCli() + ' platform add ' + platform + util.PARAMEDIC_COMMON_CLI_ARGS + util.PARAMEDIC_PLATFORM_ADD_ARGS)
+    .then(function () {
+        logger.info('cordova-paramedic: successfully finished adding platform ' + platform);
+        if (platformId === util.ANDROID && self.config.isCI()) {
+            logger.info('cordova-paramedic: monkey patching Android platform to disable gradle daemon...');
+            var gradleBuilderFile = path.join(self.tempFolder.name, 'platforms/android/cordova/lib/builders/GradleBuilder.js');
+            // remove the line where the gradle daemon is forced on
+            if (appPatcher.monkeyPatch(gradleBuilderFile, /args\.push\('\-Dorg\.gradle\.daemon=true'\);/, '//args.push(\'-Dorg.gradle.daemon=true\');')) {
+                logger.info('cordova-paramedic: success!');
+            } else {
+                logger.info('cordova-paramedic: couldn\'t apply the patch. It must be good news: does cordova-android not hard-code gradle daemon anymore?');
+            }
+        } else if (platformId === util.BROWSER && self.config.shouldUseSauce()) {
+            logger.info('cordova-paramedic: I like patching stuff, so...');
+            logger.info('cordova-paramedic: monkey patching browser platform to disable browser pop-up.');
+            var cordovaRunFile = path.join(self.tempFolder.name, 'platforms/browser/cordova/run');
+            // we need to supply some replacement string so this method can properly return a result
+            if (appPatcher.monkeyPatch(cordovaRunFile, /return cordovaServe\.launchBrowser\(.*\)\;/, '// no pop-up please')) {
+                logger.info('cordova-paramedic: success!');
+                self.runner.browserPatched = true;
+            } else {
+                cordovaRunFile = path.join(self.tempFolder.name, 'platforms/browser/cordova/lib/run.js');
+                if (appPatcher.monkeyPatch(cordovaRunFile, /return server\.launchBrowser\(\{'target'\: args\.target\, 'url'\: projectUrl\}\)\;/, '// no pop-up please')) {
+                    logger.info('cordova-paramedic: success!');
+                    self.runner.browserPatched = true;
+                } else {
+                    logger.info('cordova-paramedic: couldn\'t apply the patch. Not a big deal, though: things should work anyway.');
+                    self.runner.browserPatched = false;
+                }
+            }
+        }
+    });
+};
+
+ParamedicApp.prototype.checkPlatformRequirements = function () {
+    var platformId = this.config.getPlatformId();
+
+    if (platformId === util.BROWSER) {
+        return Q();
+    }
+
+    logger.normal('cordova-paramedic: checking requirements for platform ' + platformId);
+    return execPromise(this.config.getCli() + ' requirements ' + platformId + util.PARAMEDIC_COMMON_CLI_ARGS)
+    .then(function () {
+        logger.info('cordova-paramedic: successfully finished checking requirements for platform ' + platformId);
+    });
+};
\ No newline at end of file
diff --git a/lib/ParamedicLog.js b/lib/ParamedicLogCollector.js
similarity index 87%
rename from lib/ParamedicLog.js
rename to lib/ParamedicLogCollector.js
index c676049..2123c81 100644
--- a/lib/ParamedicLog.js
+++ b/lib/ParamedicLogCollector.js
@@ -30,14 +30,14 @@ var util     = require('./utils').utilities;
 var logger   = require('./utils').logger;
 var exec     = require('./utils').exec;
 
-function ParamedicLog(platform, appPath, outputDir, targetObj) {
+function ParamedicLogCollector(platform, appPath, outputDir, targetObj) {
     this.platform = platform;
     this.appPath = appPath;
     this.outputDir = outputDir;
     this.targetObj = targetObj;
 }
 
-ParamedicLog.prototype.logIOS = function (appPath) {
+ParamedicLogCollector.prototype.logIOS = function (appPath) {
     if (!this.targetObj) {
         logger.warn('It looks like there is no target to get logs from.');
         return;
@@ -54,7 +54,7 @@ ParamedicLog.prototype.logIOS = function (appPath) {
     }
 };
 
-ParamedicLog.prototype.logWindows = function (appPath, logMins) {
+ParamedicLogCollector.prototype.logWindows = function (appPath, logMins) {
     var logScriptPath = path.join(appPath, 'platforms', 'windows', 'cordova', 'log.bat');
     if (fs.existsSync(logScriptPath)) {
         var mins = util.DEFAULT_LOG_TIME;
@@ -66,7 +66,7 @@ ParamedicLog.prototype.logWindows = function (appPath, logMins) {
     }
 };
 
-ParamedicLog.prototype.logAndroid = function () {
+ParamedicLogCollector.prototype.logAndroid = function () {
     if (!this.targetObj) {
         logger.warn('It looks like there is no target to get logs from.');
         return;
@@ -81,7 +81,7 @@ ParamedicLog.prototype.logAndroid = function () {
     this.generateLogs(logCommand);
 };
 
-ParamedicLog.prototype.generateLogs = function (logCommand) {
+ParamedicLogCollector.prototype.generateLogs = function (logCommand) {
     var logFile = this.getLogFileName();
     logger.info('Running Command: ' + logCommand);
 
@@ -100,11 +100,11 @@ ParamedicLog.prototype.generateLogs = function (logCommand) {
     }
 };
 
-ParamedicLog.prototype.getLogFileName = function () {
+ParamedicLogCollector.prototype.getLogFileName = function () {
     return path.join(this.outputDir, this.platform + '_logs.txt');
 };
 
-ParamedicLog.prototype.collectLogs = function (logMins) {
+ParamedicLogCollector.prototype.collectLogs = function (logMins) {
     shelljs.config.fatal  = false;
     shelljs.config.silent = false;
 
@@ -124,4 +124,4 @@ ParamedicLog.prototype.collectLogs = function (logMins) {
     }
 };
 
-module.exports = ParamedicLog;
+module.exports = ParamedicLogCollector;
diff --git a/lib/ParamedicSauceLabs.js b/lib/ParamedicSauceLabs.js
new file mode 100644
index 0000000..bce1be1
--- /dev/null
+++ b/lib/ParamedicSauceLabs.js
@@ -0,0 +1,551 @@
+#!/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 path            = require('path');
+var cp              = require('child_process');
+var Q               = require('q');
+var shell           = require('shelljs');
+var randomstring    = require('randomstring');
+var fs              = require('fs');
+var wd              = require('wd');
+var SauceLabs       = require('saucelabs');
+var sauceConnectLauncher    = require('sauce-connect-launcher');
+
+var exec            = require('./utils').exec;
+var execPromise     = require('./utils').execPromise;
+var logger          = require('./utils').logger;
+var util            = require('./utils').utilities;
+var appPatcher      = require('./appium/helpers/appPatcher');
+
+function ParamedicSauceLabs(config, runner) {
+    this.config = config;
+    this.runner = runner;
+}
+module.exports = ParamedicSauceLabs;
+
+ParamedicSauceLabs.prototype.checkSauceRequirements = function () {
+    var platformId = this.config.getPlatformId();
+    if (platformId !== util.ANDROID && platformId !== util.IOS && platformId !== util.BROWSER) {
+        logger.warn('Saucelabs only supports Android and iOS (and browser), falling back to testing locally.');
+        this.config.setShouldUseSauce(false);
+    } else if (!this.config.getSauceKey()) {
+        throw new Error('Saucelabs key not set. Please set it via environmental variable ' +
+            util.SAUCE_KEY_ENV_VAR + ' or pass it with the --sauceKey parameter.');
+    } else if (!this.config.getSauceUser()) {
+        throw new Error('Saucelabs user not set. Please set it via environmental variable ' +
+            util.SAUCE_USER_ENV_VAR + ' or pass it with the --sauceUser parameter.');
+    } else if (!this.runner.shouldWaitForTestResult()) {
+        // don't throw, just silently disable Sauce
+        this.config.setShouldUseSauce(false);
+    }
+};
+
+ParamedicSauceLabs.prototype.packageApp = function () {
+    var self = this;
+    switch (this.config.getPlatformId()) {
+        case util.IOS: {
+            return Q.Promise(function (resolve, reject) {
+                var zipCommand = 'zip -r ' + self.getPackageName() + ' ' + self.getBinaryName();
+                shell.pushd(self.getPackageFolder());
+                shell.rm('-rf', self.getPackageName());
+                console.log('Running command: ' + zipCommand + ' in dir: ' + shell.pwd());
+                exec(zipCommand, function (code, stdout, stderr) {
+                    shell.popd();
+                    if (code) {
+                        reject('zip command returned with error code ' + code);
+                    } else {
+                        resolve();
+                    }
+                });
+            });
+        }
+        case util.ANDROID:
+            break; // don't need to zip the app for Android
+        case util.BROWSER:
+            break; // don't need to bundle the app on Browser platform at all
+        default:
+            throw new Error('Don\'t know how to package the app for platform: ' + this.config.getPlatformId());
+    }
+    return Q.resolve();
+};
+
+ParamedicSauceLabs.prototype.uploadApp = function () {
+    logger.normal('cordova-paramedic: uploading ' + this.getAppName() + ' to Sauce Storage');
+
+    var sauceUser = this.config.getSauceUser();
+    var key       = this.config.getSauceKey();
+
+    var uploadURI     = encodeURI('https://saucelabs.com/rest/v1/storage/' + sauceUser + '/' + this.getAppName() + '?overwrite=true');
+    var filePath      = this.getPackagedPath();
+    var uploadCommand =
+        'curl -u ' + sauceUser + ':' + key +
+        ' -X POST -H "Content-Type: application/octet-stream" ' +
+        uploadURI + ' --data-binary "@' + filePath + '"';
+
+    return execPromise(uploadCommand);
+};
+
+ParamedicSauceLabs.prototype.getPackagedPath = function () {
+    return path.join(this.getPackageFolder(), this.getPackageName());
+};
+
+ParamedicSauceLabs.prototype.getPackageFolder = function () {
+    var packageDirs = this.getPackageFolders();
+    var foundDir = null;
+    packageDirs.forEach (function (dir) {
+        if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
+            foundDir = dir;
+            return;
+        }
+    });
+    if (foundDir != null) {
+        return foundDir;
+    }
+    throw new Error ('Couldn\'t locate a built app directory. Looked here: ' + packageDirs);
+};
+
+ParamedicSauceLabs.prototype.getPackageFolders = function () {
+    var packageFolders;
+    switch (this.config.getPlatformId()) {
+        case util.ANDROID:
+            packageFolders =  [ path.join(this.runner.tempFolder.name, 'platforms/android/app/build/outputs/apk/debug'),
+                                path.join(this.runner.tempFolder.name, 'platforms/android/build/outputs/apk') ];
+            break;
+        case util.IOS:
+            packageFolders = [ path.join(this.runner.tempFolder.name, 'platforms/ios/build/emulator') ];
+            break;
+        default:
+            throw new Error('Don\t know where the package foler is for platform: ' + this.config.getPlatformId());
+    }
+    return packageFolders;
+};
+
+ParamedicSauceLabs.prototype.getPackageName = function () {
+    var packageName;
+    switch (this.config.getPlatformId()) {
+        case util.IOS:
+            packageName = 'HelloCordova.zip';
+            break;
+        case util.ANDROID:
+            packageName = this.getBinaryName();
+            break;
+        default:
+            throw new Error('Don\'t know what the package name is for platform: ' + this.config.getPlatformId());
+    }
+    return packageName;
+};
+
+ParamedicSauceLabs.prototype.getBinaryName = function () {
+    var binaryName;
+    switch (this.config.getPlatformId()) {
+        case util.ANDROID:
+            shell.pushd(this.getPackageFolder());
+            var apks = shell.ls('*debug.apk');
+            if (apks.length > 0) {
+                binaryName = apks.reduce(function (previous, current) {
+                    // if there is any apk for x86, take it
+                    if (current.indexOf('x86') >= 0) {
+                        return current;
+                    }
+                    // if not, just take the first one
+                    return previous;
+                });
+            } else {
+                throw new Error('Couldn\'t locate built apk');
+            }
+            shell.popd();
+            break;
+        case util.IOS:
+            binaryName = 'HelloCordova.app';
+            break;
+        default:
+            throw new Error('Don\'t know the binary name for platform: ' + this.config.getPlatformId());
+    }
+    return binaryName;
+};
+
+// Returns a name of the file at the SauceLabs storage
+ParamedicSauceLabs.prototype.getAppName = function () {
+    if (this.appName) {
+        // exit if we did this before
+        return this.appName;
+    }
+    var appName = randomstring.generate();
+    switch (this.config.getPlatformId()) {
+        case util.ANDROID:
+            appName += '.apk';
+            break;
+        case util.IOS:
+            appName += '.zip';
+            break;
+        default:
+            throw new Error('Don\'t know the app name for platform: ' + this.config.getPlatformId());
+    }
+    this.appName = appName; // save for additional function calls
+    return appName;
+};
+
+ParamedicSauceLabs.prototype.displaySauceDetails = function (buildName) {
+    if (!this.config.shouldUseSauce()) {
+        return Q();
+    }
+    if (!buildName) {
+        buildName = this.config.getBuildName();
+    }
+
+    var self = this;
+    var d = Q.defer();
+
+    logger.normal('Getting saucelabs jobs details...\n');
+
+    var sauce = new SauceLabs({
+        username: self.config.getSauceUser(),
+        password: self.config.getSauceKey()
+    });
+
+    sauce.getJobs(function (err, jobs) {
+        var found = false;
+        for (var job in jobs) {
+            if (jobs.hasOwnProperty(job) && jobs[job].name && jobs[job].name.indexOf(buildName) === 0) {
+                var jobUrl = 'https://saucelabs.com/beta/tests/' + jobs[job].id;
+                logger.normal('============================================================================================');
+                logger.normal('Job name: ' + jobs[job].name);
+                logger.normal('Job ID: ' + jobs[job].id);
+                logger.normal('Job URL: ' + jobUrl);
+                logger.normal('Video: ' + jobs[job].video_url);
+                logger.normal('Appium logs: ' + jobs[job].log_url);
+                if (self.config.getPlatformId() === util.ANDROID) {
+                    logger.normal('Logcat logs: ' + 'https://saucelabs.com/jobs/' + jobs[job].id + '/logcat.log');
+                }
+                logger.normal('============================================================================================');
+                logger.normal('');
+                found = true;
+            }
+        }
+
+        if (!found) {
+            logger.warn('Can not find saucelabs job. Logs and video will be unavailable.');
+        }
+        d.resolve();
+    });
+    return d.promise;
+};
+
+ParamedicSauceLabs.prototype.getSauceCaps = function () {
+    this.runner.sauceBuildName = this.runner.sauceBuildName || this.config.getBuildName();
+    var caps = {
+        name: this.runner.sauceBuildName,
+        idleTimeout: '100', // in seconds
+        maxDuration: util.SAUCE_MAX_DURATION,
+        tunnelIdentifier: this.config.getSauceTunnelId(),
+    };
+
+    switch(this.config.getPlatformId()) {
+        case util.ANDROID:
+            caps.platformName = 'Android';
+            caps.appPackage = 'io.cordova.hellocordova';
+            caps.appActivity = 'io.cordova.hellocordova.MainActivity';
+            caps.app = 'sauce-storage:' + this.getAppName();
+            caps.deviceType = 'phone';
+            caps.deviceOrientation = 'portrait';
+            caps.appiumVersion = this.config.getSauceAppiumVersion();
+            caps.deviceName = this.config.getSauceDeviceName();
+            caps.platformVersion = this.config.getSaucePlatformVersion();
+            break;
+        case util.IOS:
+            caps.platformName = 'iOS';
+            caps.autoAcceptAlerts = true;
+            caps.waitForAppScript = 'true;';
+            caps.app = 'sauce-storage:' + this.getAppName();
+            caps.deviceType = 'phone';
+            caps.deviceOrientation = 'portrait';
+            caps.appiumVersion = this.config.getSauceAppiumVersion();
+            caps.deviceName = this.config.getSauceDeviceName();
+            caps.platformVersion = this.config.getSaucePlatformVersion();
+            break;
+        case util.BROWSER:
+            caps.browserName = this.config.getSauceDeviceName() || 'chrome';
+            caps.version = this.config.getSaucePlatformVersion() || '45.0';
+            caps.platform = caps.browserName.indexOf('Edge') > 0 ? 'Windows 10' : 'macOS 10.13';
+            // setting from env.var here and not in the config
+            // because for any other platform we don't need to put the sauce connect up 
+            // unless the tunnel id is explicitly passed (means that user wants it anyway)
+            if (!caps.tunnelIdentifier && process.env[util.SAUCE_TUNNEL_ID_ENV_VAR]) {
+                caps.tunnelIdentifier = process.env[util.SAUCE_TUNNEL_ID_ENV_VAR];
+            } else if (!caps.tunnelIdentifier) {
+                throw new Error('Testing browser platform on Sauce Labs requires Sauce Connect tunnel. Please specify tunnel identifier via --sauceTunnelId');
+            }
+            break;
+        default:
+            throw new Error('Don\'t know the Sauce caps for platform: ' + this.config.getPlatformId());
+    }
+    return caps;
+};
+
+ParamedicSauceLabs.prototype.connectWebdriver = function () {
+    var user = this.config.getSauceUser();
+    var key = this.config.getSauceKey();
+    var caps = this.getSauceCaps();
+
+    logger.normal('cordova-paramedic: connecting webdriver');
+    var spamDots = setInterval(function () {
+        process.stdout.write('.');
+    }, 1000);
+
+    wd.configureHttp({
+        timeout: util.WD_TIMEOUT,
+        retryDelay: util.WD_RETRY_DELAY,
+        retries: util.WD_RETRIES
+    });
+
+    var driver = wd.promiseChainRemote(util.SAUCE_HOST, util.SAUCE_PORT, user, key);
+    return driver
+        .init(caps)
+        .then(function () {
+            clearInterval(spamDots);
+            process.stdout.write('\n');
+        }, function (error) {
+            clearInterval(spamDots);
+            process.stdout.write('\n');
+            throw(error);
+        });
+};
+
+ParamedicSauceLabs.prototype.connectSauceConnect = function () {
+    var self = this;
+    var isBrowser = self.config.getPlatformId() === util.BROWSER;
+
+    // on platforms other than browser, only run sauce connect if user explicitly asks for it
+    if (!isBrowser && !self.config.getSauceTunnelId()) {
+        return Q();
+    }
+    // on browser, run sauce connect in any case
+    if (isBrowser && !self.config.getSauceTunnelId()) {
+        self.config.setSauceTunnelId(process.env[util.SAUCE_TUNNEL_ID_ENV_VAR] || self.config.getBuildName());
+    }
+
+    return Q.Promise(function (resolve, reject) {
+        logger.info('cordova-paramedic: Starting Sauce Connect...');
+        sauceConnectLauncher({
+            username: self.config.getSauceUser(),
+            accessKey: self.config.getSauceKey(),
+            tunnelIdentifier: self.config.getSauceTunnelId(),
+            connectRetries: util.SAUCE_CONNECT_CONNECTION_RETRIES,
+            connectRetryTimeout: util.SAUCE_CONNECT_CONNECTION_TIMEOUT,
+            downloadRetries: util.SAUCE_CONNECT_DOWNLOAD_RETRIES,
+            downloadRetryTimeout: util.SAUCE_CONNECT_DOWNLOAD_TIMEOUT,
+        }, function (err, sauceConnectProcess) {
+            if (err) {
+                reject(err);
+            }
+            self.sauceConnectProcess = sauceConnectProcess;
+            logger.info('cordova-paramedic: Sauce Connect ready');
+            resolve();
+        });
+    });
+};
+
+ParamedicSauceLabs.prototype.runSauceTests = function () {
+    var self = this;
+    var isTestPassed = false;
+    var pollForResults;
+    var driver;
+    var runProcess = null;
+
+    if (!self.config.runMainTests()) {
+        logger.normal('Skipping main tests...');
+        return Q(util.TEST_PASSED);
+    }
+
+    logger.info('cordova-paramedic: running tests with sauce');
+
+    return Q().then(function () {
+        // Build + "Upload" app
+        if (self.config.getPlatformId() === util.BROWSER) {
+            // for browser, we need to serve the app for Sauce Connect
+            // we do it by just running "cordova run" and ignoring the chrome instance that pops up
+            return Q()
+                .then(function() {
+                    appPatcher.addCspSource(self.runner.tempFolder.name, 'connect-src', 'http://*');
+                    appPatcher.permitAccess(self.runner.tempFolder.name, '*');
+                    return self.runner.getCommandForStartingTests();
+                })
+                .then(function (command) {
+                    console.log('$ ' + command);
+                    runProcess = cp.exec(command, function onExit() {
+                        // a precaution not to try to kill some other process
+                        runProcess = null;
+                    });
+                });
+        } else {
+            return self.buildApp()
+                .then(self.packageApp.bind(self))
+                .then(self.uploadApp.bind(self));
+        }
+    })
+    .then(function () {
+        return self.connectSauceConnect();
+    })
+    .then(function () {
+        driver = self.connectWebdriver();
+        if (self.config.getPlatformId() === util.BROWSER) {
+            return driver.get('http://localhost:8000/cdvtests/index.html');
+        }
+        return driver;
+    })
+    .then(function () {
+        if (self.config.getUseTunnel() || self.config.getPlatformId() === util.BROWSER) {
+            return driver;
+        }
+        return driver
+        .getWebviewContext()
+        .then(function (webview) {
+            return driver.context(webview);
+        });
+    })
+    .then(function () {
+        var isWkWebview = false;
+        var plugins = self.config.getPlugins();
+        for (var plugin in plugins) {
+            if (plugins[plugin].indexOf('wkwebview') >= 0) {
+                isWkWebview = true;
+            }
+        }
+        if (isWkWebview) {
+            logger.normal('cordova-paramedic: navigating to a test page');
+            return driver
+                .sleep(1000)
+                .elementByXPath('//*[text() = "Auto Tests"]')
+                .click();
+        }
+        return driver;
+    })
+    .then(function () {
+        logger.normal('cordova-paramedic: connecting to app');
+
+        var platform = self.config.getPlatformId();
+        var plugins = self.config.getPlugins();
+
+        var skipBuster = false;
+        // skip permission buster for splashscreen and inappbrowser plugins
+        // it hangs the test run on Android 7 for some reason
+        for (var i = 0; i < plugins.length; i++) {
+            if (plugins[i].indexOf('cordova-plugin-splashscreen') >= 0 || plugins[i].indexOf('cordova-plugin-inappbrowser') >= 0) {
+                skipBuster = true;
+            }
+        }
+        // always skip buster for browser platform
+        if (platform === util.BROWSER) {
+            skipBuster = true;
+        }
+
+        if (!self.config.getUseTunnel()) {
+            var polling = false;
+            pollForResults = setInterval(function () {
+                if (!polling) {
+                    polling = true;
+                    driver.pollForEvents(platform, skipBuster)
+                    .then(function (events) {
+                        for (var i = 0; i < events.length; i++) {
+                            self.runner.server.emit(events[i].eventName, events[i].eventObject);
+                        }
+                        polling = false;
+                    })
+                    .fail(function (error) {
+                        logger.warn('appium: ' + error);
+                        polling = false;
+                    });
+                }
+            }, 2500);
+        }
+
+        return self.runner.waitForTests();
+    })
+    .then(function (result) {
+        logger.normal('cordova-paramedic: Tests finished');
+        isTestPassed = result;
+    }, function (error) {
+        logger.normal('cordova-paramedic: Tests failed to complete; ending appium session. The error is:\n' + error.stack);
+    })
+    .fin(function () {
+        if (pollForResults) {
+            clearInterval(pollForResults);
+        }
+        if (driver && typeof driver.quit === 'function') {
+            return driver.quit();
+        }
+    })
+    .fin(function () {
+        if (self.config.getPlatformId() === util.BROWSER && !self.runner.browserPatched) {
+            // we need to kill chrome
+            self.runner.killEmulatorProcess();
+        }
+        if (runProcess) {
+            // as well as we need to kill the spawned node process serving our app
+            return Q.Promise(function (resolve, reject) {
+                util.killProcess(runProcess.pid, function () {
+                    resolve();
+                });
+            });
+        }
+    })
+    .fin(function () {
+        if (self.sauceConnectProcess) {
+            logger.info('cordova-paramedic: Closing Sauce Connect process...');
+            return Q.Promise(function (resolve, reject) {
+                self.sauceConnectProcess.close(function () {
+                    logger.info('cordova-paramedic: Successfully closed Sauce Connect process');
+                    resolve();
+                });
+            });
+        }
+    })
+    .then(function () {
+        return isTestPassed;
+    });
+};
+
+ParamedicSauceLabs.prototype.buildApp = function () {
+    var self = this;
+    var command = this.getCommandForBuilding();
+
+    logger.normal('cordova-paramedic: running command ' + command);
+
+    return execPromise(command)
+    .then(function(output) {
+        if (output.indexOf ('BUILD FAILED') >= 0) {
+            throw new Error('Unable to build the project.');
+        }
+    }, 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 the project.');
+    });
+};
+
+ParamedicSauceLabs.prototype.getCommandForBuilding = function () {
+    var browserifyArg = this.config.isBrowserify() ? ' --browserify' : '';
+    var cmd = this.config.getCli() + ' build ' + this.config.getPlatformId() + browserifyArg + util.PARAMEDIC_COMMON_CLI_ARGS;
+
+    return cmd;
+};
diff --git a/lib/paramedic.js b/lib/paramedic.js
index 5b97657..3095c0c 100644
--- a/lib/paramedic.js
+++ b/lib/paramedic.js
@@ -22,25 +22,21 @@ var exec            = require('./utils').exec;
 var execPromise     = require('./utils').execPromise;
 var shell           = require('shelljs');
 var Server          = require('./LocalServer');
-var tmp             = require('tmp');
 var path            = require('path');
 var Q               = require('q');
 var fs              = require('fs');
+
 var logger          = require('./utils').logger;
 var util            = require('./utils').utilities;
-var PluginsManager  = require('./PluginsManager');
 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 appPatcher      = require('./appium/helpers/appPatcher');
-var sauceConnectLauncher    = require('sauce-connect-launcher');
+var ParamedicLogCollector   = require('./ParamedicLogCollector');
 var ParamediciOSPermissions = require('./ParamediciOSPermissions');
 var ParamedicTargetChooser  = require('./ParamedicTargetChooser');
 var ParamedicAppUninstall   = require('./ParamedicAppUninstall');
+var ParamedicApp   = require('./ParamedicApp');
+var ParamedicSauceLabs      = require('./ParamedicSauceLabs');
 
 //this will add custom promise chain methods to the driver prototype
 require('./appium/helpers/wdHelper');
@@ -49,18 +45,17 @@ require('./appium/helpers/wdHelper');
 // If device has not connected within this interval the tests are stopped.
 var INITIAL_CONNECTION_TIMEOUT = 540000; // 9mins
 
-var applicationsToGrantPermission = [
-    'kTCCServiceAddressBook'
-];
+Q.longStackSupport = true;
 
 function ParamedicRunner(config, _callback) {
     this.tempFolder = null;
-    this.pluginsManager = null;
 
     this.config = config;
     this.targetObj = undefined;
 
     exec.setVerboseLevel(config.isVerbose());
+
+    this.paramedicSauceLabs = null;
 }
 
 ParamedicRunner.prototype.run = function () {
@@ -71,9 +66,10 @@ ParamedicRunner.prototype.run = function () {
 
     return Q().then(function () {
         // create project and prepare (install plugins, setup test startpage, install platform, check platform requirements)
-        self.createTempProject();
+        var paramedicApp = new ParamedicApp(self.config, self.storedCWD, self);
+        self.tempFolder = paramedicApp.createTempProject();
         shell.pushd(self.tempFolder.name);
-        return self.prepareProjectToRunTests();
+        return paramedicApp.prepareProjectToRunTests();
     })
     .then(function () {
         if (self.config.runMainTests()) {
@@ -99,6 +95,11 @@ ParamedicRunner.prototype.run = function () {
         return self.runTests();
     })
     .timeout(self.config.getTimeout(), 'Timed out after waiting for ' + self.config.getTimeout() + ' ms.')
+    .catch(function (error) {
+        logger.error(error);
+        console.log(error.stack);
+        throw new Error(error);
+    })
     .fin(function (result) {
         isTestPassed = result;
         logger.normal('Completed tests at ' + (new Date()).toLocaleTimeString());
@@ -115,7 +116,7 @@ ParamedicRunner.prototype.run = function () {
                     self.killEmulatorProcess();
                 });
         }
-        return self.displaySauceDetails(self.sauceBuildName);
+        return self.paramedicSauceLabs.displaySauceDetails(self.sauceBuildName);
     })
     .fin(function () {
         self.cleanUpProject();
@@ -123,7 +124,10 @@ ParamedicRunner.prototype.run = function () {
 };
 
 ParamedicRunner.prototype.checkConfig = function () {
-    this.checkSauceRequirements();
+    if (this.config.shouldUseSauce()) {
+        this.paramedicSauceLabs = new ParamedicSauceLabs(this.config, this);
+        this.paramedicSauceLabs.checkSauceRequirements();
+    }
     if (!this.config.runMainTests() && !this.config.runAppiumTests()) {
         throw new Error('No tests to run: both --skipAppiumTests and --skipMainTests are used');
     }
@@ -139,107 +143,10 @@ ParamedicRunner.prototype.checkConfig = function () {
     logger.info('cordova-paramedic: Will use the following cli: ' + this.config.getCli());
 };
 
-ParamedicRunner.prototype.createTempProject = function () {
-    this.tempFolder = tmp.dirSync();
-    tmp.setGracefulCleanup();
-    logger.info('cordova-paramedic: creating temp project at ' + this.tempFolder.name);
-    exec(this.config.getCli() + ' create ' + this.tempFolder.name + util.PARAMEDIC_COMMON_CLI_ARGS);
-};
-
-ParamedicRunner.prototype.prepareProjectToRunTests = function () {
-    var self = this;
-
-    this.installPlugins();
-    this.setUpStartPage();
-    return this.installPlatform()
-    .then(function () {
-        return self.checkPlatformRequirements();
-    });
-};
-
-ParamedicRunner.prototype.installPlugins = function () {
-    logger.info('cordova-paramedic: installing plugins');
-    this.pluginsManager = new PluginsManager(this.tempFolder.name, this.storedCWD, this.config);
-    this.pluginsManager.installPlugins(this.config.getPlugins());
-    this.pluginsManager.installTestsForExistingPlugins();
-
-    var additionalPlugins = ['cordova-plugin-test-framework', path.join(__dirname, '../paramedic-plugin')];
-    if (this.config.shouldUseSauce() && !this.config.getUseTunnel()) {
-        additionalPlugins.push(path.join(__dirname, '../event-cache-plugin'));
-    }
-    if (this.config.getPlatformId() === util.WINDOWS) {
-        additionalPlugins.push(path.join(__dirname, '../debug-mode-plugin'));
-    }
-    if (this.config.getPlatformId() === util.IOS) {
-        additionalPlugins.push(path.join(__dirname, '../ios-geolocation-permissions-plugin'));
-    }
-    if (this.config.isCI()) {
-        additionalPlugins.push(path.join(__dirname, '../ci-plugin'));
-    }
-
-    this.pluginsManager.installPlugins(additionalPlugins);
-};
-
-ParamedicRunner.prototype.setUpStartPage = function () {
-    logger.normal('cordova-paramedic: setting app start page to test page');
-    shell.sed('-i', 'src="index.html"', 'src="cdvtests/index.html"', 'config.xml');
-};
-
-ParamedicRunner.prototype.installPlatform = function () {
-    var self = this;
-    var platform = this.config.getPlatform();
-    var platformId = this.config.getPlatformId();
-    logger.info('cordova-paramedic: adding platform ' + platform + " (with: " + util.PARAMEDIC_COMMON_CLI_ARGS + util.PARAMEDIC_PLATFORM_ADD_ARGS + ")");
-
-    return execPromise(this.config.getCli() + ' platform add ' + platform + util.PARAMEDIC_COMMON_CLI_ARGS + util.PARAMEDIC_PLATFORM_ADD_ARGS)
-    .then(function () {
-        logger.info('cordova-paramedic: successfully finished adding platform ' + platform);
-        if (platformId === util.ANDROID && self.config.isCI()) {
-            logger.info('cordova-paramedic: monkey patching Android platform to disable gradle daemon...');
-            var gradleBuilderFile = path.join(self.tempFolder.name, 'platforms/android/cordova/lib/builders/GradleBuilder.js');
-            // remove the line where the gradle daemon is forced on
-            if (appPatcher.monkeyPatch(gradleBuilderFile, /args\.push\('\-Dorg\.gradle\.daemon=true'\);/, '//args.push(\'-Dorg.gradle.daemon=true\');')) {
-                logger.info('cordova-paramedic: success!');
-            } else {
-                logger.info('cordova-paramedic: couldn\'t apply the patch. It must be good news: does cordova-android not hard-code gradle daemon anymore?');
-            }
-        } else if (platformId === util.BROWSER && self.config.shouldUseSauce()) {
-            logger.info('cordova-paramedic: I like patching stuff, so...');
-            logger.info('cordova-paramedic: monkey patching browser platform to disable browser pop-up.');
-            var cordovaRunFile = path.join(self.tempFolder.name, 'platforms/browser/cordova/run');
-            // we need to supply some replacement string so this method can properly return a result
-            if (appPatcher.monkeyPatch(cordovaRunFile, /return cordovaServe\.launchBrowser\(.*\)\;/, '// no pop-up please')) {
-                logger.info('cordova-paramedic: success!');
-                self.browserPatched = true;
-            } else {
-                cordovaRunFile = path.join(self.tempFolder.name, 'platforms/browser/cordova/lib/run.js');
-                if (appPatcher.monkeyPatch(cordovaRunFile, /return server\.launchBrowser\(\{'target'\: args\.target\, 'url'\: projectUrl\}\)\;/, '// no pop-up please')) {
-                    logger.info('cordova-paramedic: success!');
-                    self.browserPatched = true;
-                } else {
-                    logger.info('cordova-paramedic: couldn\'t apply the patch. Not a big deal, though: things should work anyway.');
-                    self.browserPatched = false;
-                }
-            }
-        }
-    });
-};
-
-ParamedicRunner.prototype.checkPlatformRequirements = function () {
-    var platformId = this.config.getPlatformId();
-
-    if (platformId === util.BROWSER) {
-        return Q();
-    }
-
-    logger.normal('cordova-paramedic: checking requirements for platform ' + platformId);
-    return execPromise(this.config.getCli() + ' requirements ' + platformId + util.PARAMEDIC_COMMON_CLI_ARGS)
-    .then(function () {
-        logger.info('cordova-paramedic: successfully finished checking requirements for platform ' + platformId);
-    });
-};
-
 ParamedicRunner.prototype.setPermissions = function () {
+    var applicationsToGrantPermission = [
+        'kTCCServiceAddressBook'
+    ];
     if(this.config.getPlatformId() === util.IOS) {
         logger.info('cordova-paramedic: Setting required permissions.');
         var tccDb = this.config.getTccDb();
@@ -280,27 +187,6 @@ ParamedicRunner.prototype.writeMedicJson = function(logUrl) {
     fs.writeFileSync(path.join('www','medic.json'), JSON.stringify({logurl:logUrl}));
 };
 
-ParamedicRunner.prototype.buildApp = function () {
-    var self = this;
-    var command = this.getCommandForBuilding();
-
-    logger.normal('cordova-paramedic: running command ' + command);
-
-    return execPromise(command)
-    .then(function(output) {
-        if (output.indexOf ('BUILD FAILED') >= 0) {
-            throw new Error('Unable to build the project.');
-        }
-    }, 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 the project.');
-    });
-};
-
 ParamedicRunner.prototype.maybeRunFileTransferServer = function () {
     var self = this;
     return Q().then(function () {
@@ -417,10 +303,10 @@ ParamedicRunner.prototype.runAppiumTests = function (useSauce) {
         cli: self.config.getCli(),
     };
     if (useSauce) {
-        options.sauceAppPath = 'sauce-storage:' + this.getAppName();
+        options.sauceAppPath = 'sauce-storage:' + this.paramedicSauceLabs.getAppName();
         options.sauceUser = this.config.getSauceUser();
         options.sauceKey = this.config.getSauceKey();
-        options.sauceCaps = this.getSauceCaps();
+        options.sauceCaps = this.paramedicSauceLabs.getSauceCaps();
         options.sauceCaps.name += '_Appium';
     }
 
@@ -435,8 +321,8 @@ ParamedicRunner.prototype.runAppiumTests = function (useSauce) {
     })
     .then(function () {
         if (useSauce) {
-            return self.packageApp()
-            .then(self.uploadApp.bind(self));
+            return self.paramedicSauceLabs.packageApp()
+            .then(self.paramedicSauceLabs.uploadApp.bind(self));
         }
     })
     .then(function () {
@@ -449,7 +335,7 @@ ParamedicRunner.prototype.runTests = function () {
     var self = this;
     // Sauce Labs
     if (this.config.shouldUseSauce()) {
-        return this.runSauceTests()
+        return this.paramedicSauceLabs.runSauceTests()
         .then(function (result) {
             isTestPassed = result;
             return self.runAppiumTests(true);
@@ -539,13 +425,6 @@ ParamedicRunner.prototype.getCommandForStartingTests = function () {
     });
 };
 
-ParamedicRunner.prototype.getCommandForBuilding = function () {
-    var browserifyArg = this.config.isBrowserify() ? ' --browserify' : '';
-    var cmd = this.config.getCli() + ' build ' + this.config.getPlatformId() + browserifyArg + util.PARAMEDIC_COMMON_CLI_ARGS;
-
-    return cmd;
-};
-
 ParamedicRunner.prototype.shouldWaitForTestResult = function () {
     var action = this.config.getAction();
     return (action.indexOf('run') === 0) || (action.indexOf('emulate') === 0);
@@ -576,74 +455,6 @@ ParamedicRunner.prototype.cleanUpProject = function () {
     }
 };
 
-ParamedicRunner.prototype.checkSauceRequirements = function () {
-    if (this.config.shouldUseSauce()) {
-        var platformId = this.config.getPlatformId();
-        if (platformId !== util.ANDROID && platformId !== util.IOS && platformId !== util.BROWSER) {
-            logger.warn('Saucelabs only supports Android and iOS (and browser), falling back to testing locally.');
-            this.config.setShouldUseSauce(false);
-        } else if (!this.config.getSauceKey()) {
-            throw new Error('Saucelabs key not set. Please set it via environmental variable ' +
-                util.SAUCE_KEY_ENV_VAR + ' or pass it with the --sauceKey parameter.');
-        } else if (!this.config.getSauceUser()) {
-            throw new Error('Saucelabs user not set. Please set it via environmental variable ' +
-                util.SAUCE_USER_ENV_VAR + ' or pass it with the --sauceUser parameter.');
-        } else if (!this.shouldWaitForTestResult()) {
-            // don't throw, just silently disable Sauce
-            this.config.setShouldUseSauce(false);
-        }
-    }
-};
-
-ParamedicRunner.prototype.packageApp = function () {
-    var self = this;
-    switch (this.config.getPlatformId()) {
-        case util.IOS: {
-            return Q.Promise(function (resolve, reject) {
-                var zipCommand = 'zip -r ' + self.getPackageName() + ' ' + self.getBinaryName();
-                shell.pushd(self.getPackageFolder());
-                shell.rm('-rf', self.getPackageName());
-                console.log('Running command: ' + zipCommand + ' in dir: ' + shell.pwd());
-                exec(zipCommand, function (code, stdout, stderr) {
-                    shell.popd();
-                    if (code) {
-                        reject('zip command returned with error code ' + code);
-                    } else {
-                        resolve();
-                    }
-                });
-            });
-        }
-        case util.ANDROID:
-            break; // don't need to zip the app for Android
-        case util.BROWSER:
-            break; // don't need to bundle the app on Browser platform at all
-        default:
-            throw new Error('Don\'t know how to package the app for platform: ' + this.config.getPlatformId());
-    }
-    return Q.resolve();
-};
-
-ParamedicRunner.prototype.uploadApp = function () {
-    logger.normal('cordova-paramedic: uploading ' + this.getAppName() + ' to Sauce Storage');
-
-    var sauceUser = this.config.getSauceUser();
-    var key       = this.config.getSauceKey();
-
-    var uploadURI     = encodeURI('https://saucelabs.com/rest/v1/storage/' + sauceUser + '/' + this.getAppName() + '?overwrite=true');
-    var filePath      = this.getPackagedPath();
-    var uploadCommand =
-        'curl -u ' + sauceUser + ':' + key +
-        ' -X POST -H "Content-Type: application/octet-stream" ' +
-        uploadURI + ' --data-binary "@' + filePath + '"';
-
-    return execPromise(uploadCommand);
-};
-
-ParamedicRunner.prototype.getPackagedPath = function () {
-    return path.join(this.getPackageFolder(), this.getPackageName());
-};
-
 ParamedicRunner.prototype.killEmulatorProcess = function () {
     if(this.config.shouldCleanUpAfterRun()){
         logger.info('cordova-paramedic: Killing the emulator process.');
@@ -656,8 +467,8 @@ ParamedicRunner.prototype.collectDeviceLogs = function () {
     logger.info('Collecting logs for the devices.');
     var outputDir    = this.config.getOutputDir()? this.config.getOutputDir(): this.tempFolder.name;
     var logMins      = this.config.getLogMins()? this.config.getLogMins(): util.DEFAULT_LOG_TIME;
-    var paramedicLog = new ParamedicLog(this.config.getPlatformId(), this.tempFolder.name, outputDir, this.targetObj);
-    paramedicLog.collectLogs(logMins);
+    var paramedicLogCollector = new ParamedicLogCollector(this.config.getPlatformId(), this.tempFolder.name, outputDir, this.targetObj);
+    paramedicLogCollector.collectLogs(logMins);
 };
 
 ParamedicRunner.prototype.uninstallApp = function () {
@@ -666,421 +477,6 @@ ParamedicRunner.prototype.uninstallApp = function () {
     return paramedicAppUninstall.uninstallApp(this.targetObj,util.PARAMEDIC_DEFAULT_APP_NAME);
 };
 
-ParamedicRunner.prototype.getPackageFolder = function () {
-    var packageDirs = this.getPackageFolders();
-    var foundDir = null;
-    packageDirs.forEach (function (dir) {
-        if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
-            foundDir = dir;
-            return;
-        }
-    });
-    if (foundDir != null) {
-        return foundDir;
-    }
-    throw new Error ('Couldn\'t locate a built app directory. Looked here: ' + packageDirs);
-};
-
-ParamedicRunner.prototype.getPackageFolders = function () {
-    var packageFolders;
-    switch (this.config.getPlatformId()) {
-        case util.ANDROID:
-            packageFolders =  [ path.join(this.tempFolder.name, 'platforms/android/app/build/outputs/apk/debug'),
-                                path.join(this.tempFolder.name, 'platforms/android/build/outputs/apk') ];
-            break;
-        case util.IOS:
-            packageFolders = [ path.join(this.tempFolder.name, 'platforms/ios/build/emulator') ];
-            break;
-        default:
-            throw new Error('Don\t know where the package foler is for platform: ' + this.config.getPlatformId());
-    }
-    return packageFolders;
-};
-
-ParamedicRunner.prototype.getPackageName = function () {
-    var packageName;
-    switch (this.config.getPlatformId()) {
-        case util.IOS:
-            packageName = 'HelloCordova.zip';
-            break;
-        case util.ANDROID:
-            packageName = this.getBinaryName();
-            break;
-        default:
-            throw new Error('Don\'t know what the package name is for platform: ' + this.config.getPlatformId());
-    }
-    return packageName;
-};
-
-ParamedicRunner.prototype.getBinaryName = function () {
-    var binaryName;
-    switch (this.config.getPlatformId()) {
-        case util.ANDROID:
-            shell.pushd(this.getPackageFolder());
-            var apks = shell.ls('*debug.apk');
-            if (apks.length > 0) {
-                binaryName = apks.reduce(function (previous, current) {
-                    // if there is any apk for x86, take it
-                    if (current.indexOf('x86') >= 0) {
-                        return current;
-                    }
-                    // if not, just take the first one
-                    return previous;
-                });
-            } else {
-                throw new Error('Couldn\'t locate built apk');
-            }
-            shell.popd();
-            break;
-        case util.IOS:
-            binaryName = 'HelloCordova.app';
-            break;
-        default:
-            throw new Error('Don\'t know the binary name for platform: ' + this.config.getPlatformId());
-    }
-    return binaryName;
-};
-
-// Returns a name of the file at the SauceLabs storage
-ParamedicRunner.prototype.getAppName = function () {
-    if (this.appName) {
-        return this.appName;
-    }
-    var appName = randomstring.generate();
-    switch (this.config.getPlatformId()) {
-        case util.ANDROID:
-            appName += '.apk';
-            break;
-        case util.IOS:
-            appName += '.zip';
-            break;
-        default:
-            throw new Error('Don\'t know the app name for platform: ' + this.config.getPlatformId());
-    }
-    this.appName = appName;
-    return appName;
-};
-
-ParamedicRunner.prototype.displaySauceDetails = function (buildName) {
-    if (!this.config.shouldUseSauce()) {
-        return Q();
-    }
-    if (!buildName) {
-        buildName = this.config.getBuildName();
-    }
-
-    var self = this;
-    var d = Q.defer();
-
-    logger.normal('Getting saucelabs jobs details...\n');
-
-    var sauce = new SauceLabs({
-        username: self.config.getSauceUser(),
-        password: self.config.getSauceKey()
-    });
-
-    sauce.getJobs(function (err, jobs) {
-        var found = false;
-        for (var job in jobs) {
-            if (jobs.hasOwnProperty(job) && jobs[job].name && jobs[job].name.indexOf(buildName) === 0) {
-                var jobUrl = 'https://saucelabs.com/beta/tests/' + jobs[job].id;
-                logger.normal('============================================================================================');
-                logger.normal('Job name: ' + jobs[job].name);
-                logger.normal('Job ID: ' + jobs[job].id);
-                logger.normal('Job URL: ' + jobUrl);
-                logger.normal('Video: ' + jobs[job].video_url);
-                logger.normal('Appium logs: ' + jobs[job].log_url);
-                if (self.config.getPlatformId() === util.ANDROID) {
-                    logger.normal('Logcat logs: ' + 'https://saucelabs.com/jobs/' + jobs[job].id + '/logcat.log');
-                }
-                logger.normal('============================================================================================');
-                logger.normal('');
-                found = true;
-            }
-        }
-
-        if (!found) {
-            logger.warn('Can not find saucelabs job. Logs and video will be unavailable.');
-        }
-        d.resolve();
-    });
-    return d.promise;
-};
-
-ParamedicRunner.prototype.getSauceCaps = function () {
-    this.sauceBuildName = this.sauceBuildName || this.config.getBuildName();
-    var caps = {
-        name: this.sauceBuildName,
-        idleTimeout: '100', // in seconds
-        maxDuration: util.SAUCE_MAX_DURATION,
-        tunnelIdentifier: this.config.getSauceTunnelId(),
-    };
-
-    switch(this.config.getPlatformId()) {
-        case util.ANDROID:
-            caps.platformName = 'Android';
-            caps.appPackage = 'io.cordova.hellocordova';
-            caps.appActivity = 'io.cordova.hellocordova.MainActivity';
-            caps.app = 'sauce-storage:' + this.getAppName();
-            caps.deviceType = 'phone';
-            caps.deviceOrientation = 'portrait';
-            caps.appiumVersion = this.config.getSauceAppiumVersion();
-            caps.deviceName = this.config.getSauceDeviceName();
-            caps.platformVersion = this.config.getSaucePlatformVersion();
-            break;
-        case util.IOS:
-            caps.platformName = 'iOS';
-            caps.autoAcceptAlerts = true;
-            caps.waitForAppScript = 'true;';
-            caps.app = 'sauce-storage:' + this.getAppName();
-            caps.deviceType = 'phone';
-            caps.deviceOrientation = 'portrait';
-            caps.appiumVersion = this.config.getSauceAppiumVersion();
-            caps.deviceName = this.config.getSauceDeviceName();
-            caps.platformVersion = this.config.getSaucePlatformVersion();
-            break;
-        case util.BROWSER:
-            caps.browserName = this.config.getSauceDeviceName() || 'chrome';
-            caps.version = this.config.getSaucePlatformVersion() || '45.0';
-            caps.platform = caps.browserName.indexOf('Edge') > 0 ? 'Windows 10' : 'macOS 10.13';
-            // setting from env.var here and not in the config
-            // because for any other platform we don't need to put the sauce connect up 
-            // unless the tunnel id is explicitly passed (means that user wants it anyway)
-            if (!caps.tunnelIdentifier && process.env[util.SAUCE_TUNNEL_ID_ENV_VAR]) {
-                caps.tunnelIdentifier = process.env[util.SAUCE_TUNNEL_ID_ENV_VAR];
-            } else if (!caps.tunnelIdentifier) {
-                throw new Error('Testing browser platform on Sauce Labs requires Sauce Connect tunnel. Please specify tunnel identifier via --sauceTunnelId');
-            }
-            break;
-        default:
-            throw new Error('Don\'t know the Sauce caps for platform: ' + 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');
-    var spamDots = setInterval(function () {
-        process.stdout.write('.');
-    }, 1000);
-
-    wd.configureHttp({
-        timeout: util.WD_TIMEOUT,
-        retryDelay: util.WD_RETRY_DELAY,
-        retries: util.WD_RETRIES
-    });
-
-    var driver = wd.promiseChainRemote(util.SAUCE_HOST, util.SAUCE_PORT, user, key);
-    return driver
-        .init(caps)
-        .then(function () {
-            clearInterval(spamDots);
-            process.stdout.write('\n');
-        }, function (error) {
-            clearInterval(spamDots);
-            process.stdout.write('\n');
-            throw(error);
-        });
-};
-
-ParamedicRunner.prototype.connectSauceConnect = function () {
-    var self = this;
-    var isBrowser = self.config.getPlatformId() === util.BROWSER;
-
-    // on platforms other than browser, only run sauce connect if user explicitly asks for it
-    if (!isBrowser && !self.config.getSauceTunnelId()) {
-        return Q();
-    }
-    // on browser, run sauce connect in any case
-    if (isBrowser && !self.config.getSauceTunnelId()) {
-        self.config.setSauceTunnelId(process.env[util.SAUCE_TUNNEL_ID_ENV_VAR] || self.config.getBuildName());
-    }
-
-    return Q.Promise(function (resolve, reject) {
-        logger.info('cordova-paramedic: Starting Sauce Connect...');
-        sauceConnectLauncher({
-            username: self.config.getSauceUser(),
-            accessKey: self.config.getSauceKey(),
-            tunnelIdentifier: self.config.getSauceTunnelId(),
-            connectRetries: util.SAUCE_CONNECT_CONNECTION_RETRIES,
-            connectRetryTimeout: util.SAUCE_CONNECT_CONNECTION_TIMEOUT,
-            downloadRetries: util.SAUCE_CONNECT_DOWNLOAD_RETRIES,
-            downloadRetryTimeout: util.SAUCE_CONNECT_DOWNLOAD_TIMEOUT,
-        }, function (err, sauceConnectProcess) {
-            if (err) {
-                reject(err);
-            }
-            self.sauceConnectProcess = sauceConnectProcess;
-            logger.info('cordova-paramedic: Sauce Connect ready');
-            resolve();
-        });
-    });
-};
-
-ParamedicRunner.prototype.runSauceTests = function () {
-    var self = this;
-    var isTestPassed = false;
-    var pollForResults;
-    var driver;
-    var runProcess = null;
-
-    if (!self.config.runMainTests()) {
-        logger.normal('Skipping main tests...');
-        return Q(util.TEST_PASSED);
-    }
-
-    logger.info('cordova-paramedic: running tests with sauce');
-
-    return Q().then(function () {
-        // Build + "Upload" app
-        if (self.config.getPlatformId() === util.BROWSER) {
-            // for browser, we need to serve the app for Sauce Connect
-            // we do it by just running "cordova run" and ignoring the chrome instance that pops up
-            return Q()
-                .then(function() {
-                    appPatcher.addCspSource(self.tempFolder.name, 'connect-src', 'http://*');
-                    appPatcher.permitAccess(self.tempFolder.name, '*');
-                    return self.getCommandForStartingTests();
-                })
-                .then(function (command) {
-                    console.log('$ ' + command);
-                    runProcess = cp.exec(command, function onExit() {
-                        // a precaution not to try to kill some other process
-                        runProcess = null;
-                    });
-                });
-        } else {
-            return self.buildApp()
-                .then(self.packageApp.bind(self))
-                .then(self.uploadApp.bind(self));
-        }
-    })
-    .then(function () {
-        return self.connectSauceConnect();
-    })
-    .then(function () {
-        driver = self.connectWebdriver();
-        if (self.config.getPlatformId() === util.BROWSER) {
-            return driver.get('http://localhost:8000/cdvtests/index.html');
-        }
-        return driver;
-    })
-    .then(function () {
-        if (self.config.getUseTunnel() || self.config.getPlatformId() === util.BROWSER) {
-            return driver;
-        }
-        return driver
-        .getWebviewContext()
-        .then(function (webview) {
-            return driver.context(webview);
-        });
-    })
-    .then(function () {
-        var isWkWebview = false;
-        var plugins = self.config.getPlugins();
-        for (var plugin in plugins) {
-            if (plugins[plugin].indexOf('wkwebview') >= 0) {
-                isWkWebview = true;
-            }
-        }
-        if (isWkWebview) {
-            logger.normal('cordova-paramedic: navigating to a test page');
-            return driver
-                .sleep(1000)
-                .elementByXPath('//*[text() = "Auto Tests"]')
-                .click();
-        }
-        return driver;
-    })
-    .then(function () {
-        logger.normal('cordova-paramedic: connecting to app');
-
-        var platform = self.config.getPlatformId();
-        var plugins = self.config.getPlugins();
-
-        var skipBuster = false;
-        // skip permission buster for splashscreen and inappbrowser plugins
-        // it hangs the test run on Android 7 for some reason
-        for (var i = 0; i < plugins.length; i++) {
-            if (plugins[i].indexOf('cordova-plugin-splashscreen') >= 0 || plugins[i].indexOf('cordova-plugin-inappbrowser') >= 0) {
-                skipBuster = true;
-            }
-        }
-        // always skip buster for browser platform
-        if (platform === util.BROWSER) {
-            skipBuster = true;
-        }
-
-        if (!self.config.getUseTunnel()) {
-            var polling = false;
-            pollForResults = setInterval(function () {
-                if (!polling) {
-                    polling = true;
-                    driver.pollForEvents(platform, skipBuster)
-                    .then(function (events) {
-                        for (var i = 0; i < events.length; i++) {
-                            self.server.emit(events[i].eventName, events[i].eventObject);
-                        }
-                        polling = false;
-                    })
-                    .fail(function (error) {
-                        logger.warn('appium: ' + error);
-                        polling = false;
-                    });
-                }
-            }, 2500);
-        }
-
-        return self.waitForTests();
-    })
-    .then(function (result) {
-        logger.normal('cordova-paramedic: Tests finished');
-        isTestPassed = result;
-    }, function (error) {
-        logger.normal('cordova-paramedic: Tests failed to complete; ending appium session. The error is:\n' + error.stack);
-    })
-    .fin(function () {
-        if (pollForResults) {
-            clearInterval(pollForResults);
-        }
-        if (driver && typeof driver.quit === 'function') {
-            return driver.quit();
-        }
-    })
-    .fin(function () {
-        if (self.config.getPlatformId() === util.BROWSER && !self.browserPatched) {
-            // we need to kill chrome
-            self.killEmulatorProcess();
-        }
-        if (runProcess) {
-            // as well as we need to kill the spawned node process serving our app
-            return Q.Promise(function (resolve, reject) {
-                util.killProcess(runProcess.pid, function () {
-                    resolve();
-                });
-            });
-        }
-    })
-    .fin(function () {
-        if (self.sauceConnectProcess) {
-            logger.info('cordova-paramedic: Closing Sauce Connect process...');
-            return Q.Promise(function (resolve, reject) {
-                self.sauceConnectProcess.close(function () {
-                    logger.info('cordova-paramedic: Successfully closed Sauce Connect process');
-                    resolve();
-                });
-            });
-        }
-    })
-    .then(function () {
-        return isTestPassed;
-    });
-};
-
 var storedCWD = null;
 
 exports.run = function(paramedicConfig) {
diff --git a/package.json b/package.json
index d50e19e..02078e2 100644
--- a/package.json
+++ b/package.json
@@ -15,17 +15,26 @@
     "url": "git://github.com/apache/cordova-paramedic.git"
   },
   "scripts": {
-    "test": "npm run jshint & npm run test-ios",
+    "test": "npm run jshint & npm run test-local && npm run test-saucelabs",
+    "test-on-windows": "npm run jshint & npm run test-local-on-windows && npm run test-saucelabs-on-windows",
     "jshint": "node node_modules/jshint/bin/jshint lib/",
-    "test-appveyor": "npm run test-browser",
     "test-travis": "npm run jshint & npm run test-ios",
+    "test-appveyor": "npm run test-browser",
+    "test-local": "npm run test-browser && npm run test-android && npm run test-ios",
+    "test-local-on-windows": "npm run test-browser && npm run test-android",
     "test-android": "node main.js --platform android --plugin ./spec/testable-plugin/",
     "test-ios": "node main.js --platform ios --plugin ./spec/testable-plugin/ --args=--buildFlag='-UseModernBuildSystem=0' --verbose",
     "test-windows": "node main.js --platform windows --plugin ./spec/testable-plugin/",
-    "test-browser": "node main.js --platform browser --plugin ./spec/testable-plugin/"
+    "test-browser": "node main.js --platform browser --plugin ./spec/testable-plugin/",
+    "test-saucelabs": "npm run test-saucelabs-browser && npm run test-saucelabs-ios && npm run test-saucelabs-android",
+    "test-saucelabs-on-windows": "npm run test-saucelabs-browser && npm run test-saucelabs-android",
+    "test-saucelabs-browser": "node main.js --config ./pr/browser-chrome --plugin ./spec/testable-plugin/ --shouldUseSauce",
+    "test-saucelabs-ios": "cordova-paramedic: installing node main.js --config ./pr/ios-10.0 --plugin ./spec/testable-plugin/ --shouldUseSauce",
+    "test-saucelabs-android": "cordova-paramedic: installing node main.js --config ./pr/android-7.0 --plugin ./spec/testable-plugin/ --shouldUseSauce"
   },
   "keywords": [
     "cordova",
+    "paramedic",
     "medic",
     "test"
   ],


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org