You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by al...@apache.org on 2017/07/21 11:06:03 UTC

cordova-paramedic git commit: CB-13028 Run tests for browser platform on Sauce

Repository: cordova-paramedic
Updated Branches:
  refs/heads/master 76dc9e637 -> 0c9f8517c


CB-13028 Run tests for browser platform on Sauce


Project: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/commit/0c9f8517
Tree: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/tree/0c9f8517
Diff: http://git-wip-us.apache.org/repos/asf/cordova-paramedic/diff/0c9f8517

Branch: refs/heads/master
Commit: 0c9f8517c9087a61583ca5dc7b12d63949137169
Parents: 76dc9e6
Author: Alexander Sorokin <al...@akvelon.com>
Authored: Fri Jul 21 14:05:42 2017 +0300
Committer: Alexander Sorokin <al...@akvelon.com>
Committed: Fri Jul 21 14:05:42 2017 +0300

----------------------------------------------------------------------
 README.md                           |  16 +++-
 conf/pr/browser-chrome.config.json  |   8 ++
 conf/pr/browser-edge.config.json    |   8 ++
 conf/pr/browser-firefox.config.json |   8 ++
 conf/pr/browser-safari.config.json  |   8 ++
 lib/LocalServer.js                  |   6 +-
 lib/ParamedicConfig.js              |  28 +++++-
 lib/appium/AppiumRunner.js          |  79 ++--------------
 lib/appium/helpers/appPatcher.js    | 112 +++++++++++++++++++++++
 lib/appium/helpers/wdHelper.js      |   9 +-
 lib/paramedic.js                    | 149 ++++++++++++++++++++++---------
 main.js                             |   9 +-
 12 files changed, 312 insertions(+), 128 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 4fcf712..c915426 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,14 @@ Verbose mode. Display more information output
 cordova-paramedic --platform ios --plugin cordova-plugin-inappbrowser --verbose
 ```
 
+####--cli (optional)
+
+A path to Cordova CLI. Useful when you're testing against locally installed Cordova version.
+
+```
+cordova-paramedic --platform android --plugin cordova-plugin-device --cli ./cordova-cli/bin/cordova
+```
+
 ####--timeout (optional)
 
 Time in millisecs to wait for tests to pass|fail (defaults to 10 minutes).
@@ -148,11 +156,11 @@ cordova-paramedic --platform ios --plugin cordova-plugin-contacts --tccDbPath tc
 
 ####--shouldUseSauce (optional)
 
-Run tests on [Sauce Labs](https://saucelabs.com/). You'll need to specify Sauce Labs username and access key using either --sauceUser and --sauceKey arguments or SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables.
+Run tests on [Sauce Labs](https://saucelabs.com/). You'll need to specify Sauce Labs username and access key using either --sauceUser and --sauceKey arguments or `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables.
 
 ####--buildName (optional)
 
-Build name to show on Sauce Labs dashboard.
+Build name to show on Sauce Labs dashboard. If omitted, will use "Paramedic sauce test" and a timestamp.
 
 ####--sauceUser (optional)
 
@@ -168,11 +176,11 @@ cordova-paramedic --platform ios --plugin cordova-plugin-contacts --shouldUseSau
 
 ####--sauceDeviceName (optional)
 
-Name of the Sauce Labs emulator. For example, "iPhone Simulator". Please refer to the [Sauce Labs platforms list](https://saucelabs.com/platforms) to see available device names.
+Name of the Sauce Labs emulator or browser. For example, "iPhone Simulator" or "firefox". Please refer to the [Sauce Labs platforms list](https://saucelabs.com/platforms) to see available device names.
 
 ####--saucePlatformVersion (optional)
 
-Platform version of the Sauce Labs emulator. For example, "9.3". Please refer to the [Sauce Labs platforms list](https://saucelabs.com/platforms) to see available platform versions.
+Platform version of the Sauce Labs emulator OS, or version of the browser (if testing `browser` platform). For example, "9.3" or "54.0". Please refer to the [Sauce Labs platforms list](https://saucelabs.com/platforms) to see available platform versions.
 
 ####--sauceAppiumVersion (optional)
 

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/conf/pr/browser-chrome.config.json
----------------------------------------------------------------------
diff --git a/conf/pr/browser-chrome.config.json b/conf/pr/browser-chrome.config.json
new file mode 100644
index 0000000..89cf4d5
--- /dev/null
+++ b/conf/pr/browser-chrome.config.json
@@ -0,0 +1,8 @@
+{
+    "platform": "browser",
+    "action": "run",
+    "cleanUpAfterRun": true,
+    "verbose": true,
+    "sauceDeviceName": "chrome",
+    "saucePlatformVersion": "59.0"
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/conf/pr/browser-edge.config.json
----------------------------------------------------------------------
diff --git a/conf/pr/browser-edge.config.json b/conf/pr/browser-edge.config.json
new file mode 100644
index 0000000..07fe98e
--- /dev/null
+++ b/conf/pr/browser-edge.config.json
@@ -0,0 +1,8 @@
+{
+    "platform": "browser",
+    "action": "run",
+    "cleanUpAfterRun": true,
+    "verbose": true,
+    "sauceDeviceName": "MicrosoftEdge",
+    "saucePlatformVersion": "15.15063"
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/conf/pr/browser-firefox.config.json
----------------------------------------------------------------------
diff --git a/conf/pr/browser-firefox.config.json b/conf/pr/browser-firefox.config.json
new file mode 100644
index 0000000..769ef94
--- /dev/null
+++ b/conf/pr/browser-firefox.config.json
@@ -0,0 +1,8 @@
+{
+    "platform": "browser",
+    "action": "run",
+    "cleanUpAfterRun": true,
+    "verbose": true,
+    "sauceDeviceName": "firefox",
+    "saucePlatformVersion": "54.0"
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/conf/pr/browser-safari.config.json
----------------------------------------------------------------------
diff --git a/conf/pr/browser-safari.config.json b/conf/pr/browser-safari.config.json
new file mode 100644
index 0000000..9c90238
--- /dev/null
+++ b/conf/pr/browser-safari.config.json
@@ -0,0 +1,8 @@
+{
+    "platform": "browser",
+    "action": "run",
+    "cleanUpAfterRun": true,
+    "verbose": true,
+    "sauceDeviceName": "safari",
+    "saucePlatformVersion": "10.0"
+}

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/LocalServer.js
----------------------------------------------------------------------
diff --git a/lib/LocalServer.js b/lib/LocalServer.js
index 4c5fbc1..4f92dcc 100644
--- a/lib/LocalServer.js
+++ b/lib/LocalServer.js
@@ -46,7 +46,7 @@ function getRandomInt(min, max) {
     return Math.floor(Math.random() * (max - min)) + min;
 }
 
-LocalServer.startServer = function (ports, externalServerUrl, useTunnel) {
+LocalServer.startServer = function (ports, externalServerUrl, useTunnel, noListener) {
     logger.normal("local-server: scanning ports from " + ports.start + " to " + ports.end);
 
     return LocalServer.getAvailablePort(ports.start, ports.end)
@@ -55,7 +55,9 @@ LocalServer.startServer = function (ports, externalServerUrl, useTunnel) {
             logger.info("local-server: starting local medic server");
 
             var localServer = new LocalServer(port, externalServerUrl);
-            localServer.createSocketListener();
+            if (!noListener) {
+                localServer.createSocketListener();
+            }
 
             if (useTunnel) {
                 return localServer.createTunnel();

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/ParamedicConfig.js
----------------------------------------------------------------------
diff --git a/lib/ParamedicConfig.js b/lib/ParamedicConfig.js
index c6b2360..cdb955a 100644
--- a/lib/ParamedicConfig.js
+++ b/lib/ParamedicConfig.js
@@ -36,11 +36,23 @@ function ParamedicConfig(json) {
 }
 
 ParamedicConfig.prototype.getDefaultSauceDeviceName = function () {
-    return this.getPlatformId() === 'android' ? DEFAULT_SAUCE_DEVICE_NAME_ANDROID : DEFAULT_SAUCE_DEVICE_NAME_IOS;
+    if (this.getPlatformId() === util.ANDROID) {
+        return DEFAULT_SAUCE_DEVICE_NAME_ANDROID;
+    } else if (this.getPlatformId() === util.IOS) {
+        return DEFAULT_SAUCE_DEVICE_NAME_IOS;
+    } else {
+        throw new Error('Don\'t know a default device name for platform: ' + this.getPlatformId());
+    }
 };
 
 ParamedicConfig.prototype.getDefaultSaucePlatformVersion = function () {
-    return this.getPlatformId() === 'android' ? DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID : DEFAULT_SAUCE_PLATFORM_VERSION_IOS;
+    if (this.getPlatformId() === util.ANDROID) {
+        return DEFAULT_SAUCE_PLATFORM_VERSION_ANDROID;
+    } else if (this.getPlatformId() === util.IOS) {
+        return DEFAULT_SAUCE_PLATFORM_VERSION_IOS;
+    } else {
+        throw new Error('Don\'t know a default platform version for platform: ' + this.getPlatformId());
+    }
 };
 
 ParamedicConfig.parseFromArguments = function (argv) {
@@ -65,6 +77,7 @@ ParamedicConfig.parseFromArguments = function (argv) {
         sauceDeviceName:      argv.sauceDeviceName && argv.sauceDeviceName.toString(),
         saucePlatformVersion: argv.saucePlatformVersion && argv.saucePlatformVersion.toString(),
         sauceAppiumVersion:   argv.sauceAppiumVersion && argv.sauceAppiumVersion.toString(),
+        sauceTunnelId:        argv.sauceTunnelId,
         skipAppiumTests:      argv.skipAppium,
         skipMainTests:        argv.skipMainTests,
         ci:                   argv.ci,
@@ -210,6 +223,17 @@ ParamedicConfig.prototype.setSauceAppiumVersion = function (sauceAppiumVersion)
     this._config.sauceAppiumVersion = sauceAppiumVersion.toString();
 };
 
+ParamedicConfig.prototype.getSauceTunnelId = function () {
+    if (typeof this._config.sauceTunnelId === 'boolean') {
+        this._config.sauceTunnelId = undefined;
+    }
+    return this._config.sauceTunnelId;
+};
+
+ParamedicConfig.prototype.setSauceTunnelId = function (tid) {
+    this._config.sauceTunnelId = tid;
+};
+
 ParamedicConfig.prototype.runMainTests = function () {
     return !this._config.skipMainTests;
 };

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/appium/AppiumRunner.js
----------------------------------------------------------------------
diff --git a/lib/appium/AppiumRunner.js b/lib/appium/AppiumRunner.js
index 2157b8c..dca23a4 100644
--- a/lib/appium/AppiumRunner.js
+++ b/lib/appium/AppiumRunner.js
@@ -30,13 +30,13 @@ var logger           = require('../utils').logger;
 var wd               = require('wd');
 var wdHelper         = require('./helpers/wdHelper');
 var screenshotHelper = require('./helpers/screenshotHelper');
+var appPatcher       = require('./helpers/appPatcher.js');
 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');
@@ -99,75 +99,6 @@ 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);
@@ -431,12 +362,12 @@ AppiumRunner.prototype.prepareApp = function () {
 
         // set properties/CSP rules
         if (self.options.platform === 'ios') {
-            setPreference(fullAppPath, 'CameraUsesGeolocation', 'true');
+            appPatcher.setPreference(fullAppPath, 'CameraUsesGeolocation', 'true');
         } else if (self.options.platform === 'android') {
-            setPreference(fullAppPath, 'loadUrlTimeoutValue', 60000);
+            appPatcher.setPreference(fullAppPath, 'loadUrlTimeoutValue', 60000);
         }
-        addCspSource(fullAppPath, 'connect-src', 'http://*');
-        permitAccess(fullAppPath, '*');
+        appPatcher.addCspSource(fullAppPath, 'connect-src', 'http://*');
+        appPatcher.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(self.options.cli + ' plugin add cordova-save-image-gallery' + util.PARAMEDIC_COMMON_CLI_ARGS, fullAppPath);

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/appium/helpers/appPatcher.js
----------------------------------------------------------------------
diff --git a/lib/appium/helpers/appPatcher.js b/lib/appium/helpers/appPatcher.js
new file mode 100644
index 0000000..a3beca8
--- /dev/null
+++ b/lib/appium/helpers/appPatcher.js
@@ -0,0 +1,112 @@
+/* jshint node: true */
+/*
+ *
+ * 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.
+ *
+*/
+
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var util = require('../../utils').utilities;
+var logger = require('../../utils').logger;
+var ConfigParser = require('cordova-common').ConfigParser;
+
+function getConfigPath(appPath) {
+    return path.join(appPath, 'config.xml');
+}
+
+module.exports.permitAccess = function (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();
+    }
+};
+
+module.exports.addCspSource = function (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.');
+    }
+};
+
+module.exports.setPreference = function (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();
+};
+
+module.exports.monkeyPatch = function (file, regex, replacement) {
+    try {
+        var sedResult = shell.sed('-i', regex, replacement, file);
+        if (sedResult.indexOf(replacement) >= 0) {
+            return true;
+        } else {
+            return false;
+        }
+    } catch (err) {
+        logger.warn('cordova-paramedic: something went wrong while monkey patching ' + file + ':\n' + err.stack);
+    }
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/appium/helpers/wdHelper.js
----------------------------------------------------------------------
diff --git a/lib/appium/helpers/wdHelper.js b/lib/appium/helpers/wdHelper.js
index 912a69b..70a239d 100644
--- a/lib/appium/helpers/wdHelper.js
+++ b/lib/appium/helpers/wdHelper.js
@@ -192,8 +192,9 @@ module.exports.tapElementByXPath = function (xpath, driver) {
 };
 
 module.exports.pollForEvents = function (driver, platform, skipBuster, windowOffset) {
-    var isAndroid = platform === 'android';
-    var isIOS = platform === 'ios';
+    var isAndroid = platform === util.ANDROID;
+    var isBrowser = platform === util.BROWSER;
+    var isIOS = platform === util.IOS;
     if (!windowOffset) {
         windowOffset = 0;
     }
@@ -207,7 +208,7 @@ module.exports.pollForEvents = function (driver, platform, skipBuster, windowOff
         return driver.bustAlert(platform);
     })
     .then(function() {
-        if (isIOS) {
+        if (isIOS || isBrowser) {
             return driver;
         }
         // for some reason inappbrowser tests tend to leave an active window on android
@@ -237,7 +238,7 @@ module.exports.pollForEvents = function (driver, platform, skipBuster, windowOff
             return result;
         }
         if (!isAndroid) {
-            throw new Error('Cannot get the event cache: it doesn\'t exist in the app.');
+            throw new Error('Cannot get the event cache: it doesn\'t exist in the app. Got this instead: ' + result);
         }
         // no luck finding the event cache in this window, let's try next
         return module.exports.pollForEvents(driver, platform, skipBuster, windowOffset + 1);

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/lib/paramedic.js
----------------------------------------------------------------------
diff --git a/lib/paramedic.js b/lib/paramedic.js
index f91a992..6988646 100644
--- a/lib/paramedic.js
+++ b/lib/paramedic.js
@@ -36,6 +36,7 @@ var wd              = require('wd');
 var SauceLabs       = require('saucelabs');
 var randomstring    = require('randomstring');
 var AppiumRunner    = require('./appium/AppiumRunner');
+var appPatcher      = require('./appium/helpers/appPatcher');
 var ParamediciOSPermissions = require('./ParamediciOSPermissions');
 var ParamedicTargetChooser  = require('./ParamedicTargetChooser');
 var ParamedicAppUninstall   = require('./ParamedicAppUninstall');
@@ -74,7 +75,8 @@ ParamedicRunner.prototype.run = function () {
     })
     .then(function () {
         if (self.config.runMainTests()) {
-            return Server.startServer(self.config.getPorts(), self.config.getExternalServerUrl(), self.config.getUseTunnel());
+            var isBrowser = self.config.getPlatformId() === util.BROWSER;
+            return Server.startServer(self.config.getPorts(), self.config.getExternalServerUrl(), self.config.getUseTunnel(), isBrowser);
         }
     })
     .then(function (server) {
@@ -188,20 +190,23 @@ ParamedicRunner.prototype.installPlatform = 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...');
-            try {
-                // comment out the line where the gradle daemon is forced on
-                var gradleBuilderFile = path.join(self.tempFolder.name, 'platforms/android/cordova/lib/builders/GradleBuilder.js');
-                var sedResult = shell.sed('-i', 
-                    /args\.push\('\-Dorg\.gradle\.daemon=true'\);/, 
-                    '//args.push(\'-Dorg.gradle.daemon=true\');', 
-                    gradleBuilderFile);
-                if (sedResult.indexOf('//args.push(\'-Dorg.gradle.daemon=true\');') >= 0) {
-                    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?');
-                }
-            } catch (err) {
-                logger.warn('cordova-paramedic: something went wrong while patching Android platform:' + err.message);
+            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'\);/, '')) {
+                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');
+            if (appPatcher.monkeyPatch(cordovaRunFile, /return cordovaServe\.launchBrowser\(.*\)\;/, '')) {
+                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;
             }
         }
     });
@@ -340,7 +345,7 @@ ParamedicRunner.prototype.runLocalTests = function () {
 
         return util.TEST_PASSED; // if we're not waiting for a test result, just report tests as passed
     })
-    .then(function (result) {
+    .fin(function (result) {
         if (runProcess) {
             return Q.promise(function (resolve, reject) {
                 util.killProcess(runProcess.pid, function () {
@@ -466,14 +471,15 @@ ParamedicRunner.prototype.waitForTests = function () {
 };
 
 ParamedicRunner.prototype.getCommandForStartingTests = function () {
+    var self = this;
+    var cmd  = self.config.getCli() + ' ' + this.config.getAction() + ' ' + this.config.getPlatformId() + util.PARAMEDIC_COMMON_CLI_ARGS;
+
     function addConfigArgs(cmd) {
         if (self.config.getArgs()) {
             cmd += ' ' + self.config.getArgs();
         }
         return cmd;
     }
-    var self = this;
-    var cmd  = self.config.getCli() + ' ' + this.config.getAction() + ' ' + this.config.getPlatformId() + util.PARAMEDIC_COMMON_CLI_ARGS;
 
     if (self.config.getPlatformId() === util.BROWSER) {
         return addConfigArgs(cmd);
@@ -545,8 +551,9 @@ ParamedicRunner.prototype.cleanUpProject = function () {
 
 ParamedicRunner.prototype.checkSauceRequirements = function () {
     if (this.config.shouldUseSauce()) {
-        if (this.config.getPlatformId() !== util.ANDROID && this.config.getPlatformId() !== util.IOS) {
-            logger.warn('Saucelabs only supports Android and iOS, falling back to testing locally.');
+        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 ' +
@@ -582,8 +589,10 @@ ParamedicRunner.prototype.packageApp = function () {
         }
         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('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know how to package the app for platform: ' + this.config.getPlatformId());
     }
     return Q.resolve();
 };
@@ -640,7 +649,7 @@ ParamedicRunner.prototype.getPackageFolder = function () {
             packageFolder = path.join(this.tempFolder.name, 'platforms/ios/build/emulator/');
             break;
         default:
-            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\t know where the package foler is for platform: ' + this.config.getPlatformId());
     }
     return packageFolder;
 };
@@ -655,7 +664,7 @@ ParamedicRunner.prototype.getPackageName = function () {
             packageName = this.getBinaryName();
             break;
         default:
-            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know what the package name is for platform: ' + this.config.getPlatformId());
     }
     return packageName;
 };
@@ -670,7 +679,7 @@ ParamedicRunner.prototype.getBinaryDir = function () {
             binaryPath = path.join(this.tempFolder.name, 'platforms/ios/build/emulator/');
             break;
         default:
-            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know the binary folder for platform: ' + this.config.getPlatformId());
     }
     return binaryPath;
 };
@@ -699,7 +708,7 @@ ParamedicRunner.prototype.getBinaryName = function () {
             binaryName = 'HelloCordova.app';
             break;
         default:
-            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know the binary name for platform: ' + this.config.getPlatformId());
     }
     return binaryName;
 };
@@ -718,7 +727,7 @@ ParamedicRunner.prototype.getAppName = function () {
             appName += '.zip';
             break;
         default:
-            throw new Error('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know the app name for platform: ' + this.config.getPlatformId());
     }
     this.appName = appName;
     return appName;
@@ -774,30 +783,48 @@ ParamedicRunner.prototype.getSauceCaps = function () {
     this.sauceBuildName = this.sauceBuildName || this.config.getBuildName();
     var caps = {
         name: this.sauceBuildName,
-        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
     };
+    if (this.config.getSauceTunnelId()) {
+        caps.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.12';
+            if (!caps.tunnelIdentifier && process.env.TRAVIS_JOB_NUMBER) {
+                caps.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
+            } 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('Unsupported platform for sauce labs testing: ' + this.config.getPlatformId());
+            throw new Error('Don\'t know the Sauce caps for platform: ' + this.config.getPlatformId());
     }
     return caps;
 };
@@ -836,6 +863,7 @@ ParamedicRunner.prototype.runSauceTests = function () {
     var isTestPassed = false;
     var pollForResults;
     var driver;
+    var runProcess = null;
 
     if (!self.config.runMainTests()) {
         logger.normal('Skipping main tests...');
@@ -844,15 +872,37 @@ ParamedicRunner.prototype.runSauceTests = function () {
 
     logger.info('cordova-paramedic: running tests with sauce');
 
-    return this.buildApp()
-    .then(self.packageApp.bind(self))
-    .then(self.uploadApp.bind(self))
+    return Q().then(function () {
+        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) {
+                    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 () {
         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()) {
+        if (self.config.getUseTunnel() || self.config.getPlatformId() === util.BROWSER) {
             return driver;
         }
         return driver
@@ -891,6 +941,9 @@ ParamedicRunner.prototype.runSauceTests = function () {
                 skipBuster = true;
             }
         }
+        if (platform === util.BROWSER) {
+            skipBuster = true;
+        }
         if (!self.config.getUseTunnel()) {
             var polling = false;
             pollForResults = setInterval(function () {
@@ -917,13 +970,29 @@ ParamedicRunner.prototype.runSauceTests = function () {
         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);
+        logger.normal('cordova-paramedic: Tests failed to complete; ending appium session. The error is:\n' + error.stack);
     })
     .fin(function () {
         if (pollForResults) {
             clearInterval(pollForResults);
         }
-        return driver.quit();
+        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();
+                });
+            });
+        }
     })
     .then(function () {
         return isTestPassed;

http://git-wip-us.apache.org/repos/asf/cordova-paramedic/blob/0c9f8517/main.js
----------------------------------------------------------------------
diff --git a/main.js b/main.js
index 943c19b..f9a778c 100755
--- a/main.js
+++ b/main.js
@@ -50,9 +50,10 @@ var USAGE           = "Error missing args. \n" +
     "--logMins : (optional) Windows only - specifies number of minutes to get logs\n" +
     "--outputDir : (optional) path to save Junit results file & Device logs\n" +
     "--sauceAppiumVersion : (optional) Appium version to use when running on Saucelabs. For example, \"1.5.3\"\n" +
-    "--sauceDeviceName : (optional) Name of the SauceLabs emulator. For example, \"iPhone Simulator\"\n" +
+    "--sauceDeviceName : (optional) Name of the SauceLabs emulator/browser. For example, \"iPhone Simulator\" or \"firefox\"\n" +
     "--sauceKey : (optional) Saucelabs access key\n" +
-    "--saucePlatformVersion : (optional) Platform version of the SauceLabs emulator. For example, \"9.3\"\n" +
+    "--saucePlatformVersion : (optional) Version of the emulator OS or version of the browser. For example, \"9.3\" or \"54.0\"\n" +
+    "--sauceTunnelId : (optional) Tunnel identifier to use. Only usable if you have Sauce Connect up\n"
     "--sauceUser : (optional) Saucelabs username\n" +
     "--shouldUseSauce : (optional) run tests on Sauce Labs\n" +
     "--skipAppiumTests : (optional) Do not run Appium tests\n" +
@@ -136,6 +137,10 @@ if (argv.version) {
         paramedicConfig.setSauceAppiumVersion(argv.sauceAppiumVersion);
     }
 
+    if (argv.sauceTunnelId) {
+        paramedicConfig.setSauceTunnelId(argv.sauceTunnelId);
+    }
+
     if (argv.useTunnel) {
         if (argv.useTunnel === 'false') {
             argv.useTunnel = false;


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