blob: a748674fecfb0305ca64f2d9088bc2b32d2a0c9e [file] [log] [blame]
/**
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 cp = require('child_process');
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 ParamediciOSPermissions = require('./ParamediciOSPermissions');
var ParamedicTargetChooser = require('./ParamedicTargetChooser');
var ParamedicAppUninstall = require('./ParamedicAppUninstall');
//this will add custom promise chain methods to the driver prototype
require('./appium/helpers/wdHelper');
// Time to wait for initial device connection.
// If device has not connected within this interval the tests are stopped.
var INITIAL_CONNECTION_TIMEOUT = 540000; // 9mins
var applicationsToGrantPermission = [
'kTCCServiceAddressBook'
];
function ParamedicRunner(config, _callback) {
this.tempFolder = null;
this.pluginsManager = null;
this.config = config;
this.targetObj = undefined;
exec.setVerboseLevel(config.isVerbose());
}
ParamedicRunner.prototype.run = function () {
var self = this;
var isTestPassed = false;
self.checkConfig();
return Q().then(function () {
self.createTempProject();
shell.pushd(self.tempFolder.name);
return self.prepareProjectToRunTests();
})
.then(function () {
if (self.config.runMainTests()) {
var noListener = (self.config.getPlatformId() === util.BROWSER) && self.config.shouldUseSauce();
return Server.startServer(self.config.getPorts(), self.config.getExternalServerUrl(), self.config.getUseTunnel(), noListener);
}
})
.then(function (server) {
if (self.config.runMainTests()) {
self.server = server;
self.injectReporters();
self.subcribeForEvents();
var logUrl = self.server.getConnectionUrl(self.config.getPlatformId());
self.writeMedicJson(logUrl);
logger.normal('Start running tests at ' + (new Date()).toLocaleTimeString());
}
return self.runTests();
})
.timeout(self.config.getTimeout(), 'Timed out after waiting for ' + self.config.getTimeout() + ' ms.')
.fin(function (result) {
isTestPassed = result;
logger.normal('Completed tests at ' + (new Date()).toLocaleTimeString());
// if we do --justbuild or run on sauce,
// we should NOT do actions below
if (self.config.getAction() !== 'build' && !self.config.shouldUseSauce()) {
self.collectDeviceLogs();
return self.uninstallApp()
.fail(function () {
// do not fail if uninstall fails
})
.fin(function () {
self.killEmulatorProcess();
});
}
return self.displaySauceDetails(self.sauceBuildName);
})
.fin(function () {
self.cleanUpProject();
});
};
ParamedicRunner.prototype.checkConfig = function () {
this.checkSauceRequirements();
if (!this.config.runMainTests() && !this.config.runAppiumTests()) {
throw new Error('No tests to run: both --skipAppiumTests and --skipMainTests are used');
}
checkCli: {
if (this.config.getCli() !== 'cordova' && this.config.getCli() !== 'phonegap') {
if (path.isAbsolute(this.config.getCli())) {
break checkCli;
}
var cliAbsolutePath = path.resolve(this.config.getCli());
this.config.setCli(cliAbsolutePath);
}
}
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 + "(" + 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 () {
if(this.config.getPlatformId() === util.IOS) {
logger.info('cordova-paramedic: Setting required permissions.');
var tccDb = this.config.getTccDb();
if(tccDb) {
var appName = util.PARAMEDIC_DEFAULT_APP_NAME;
var paramediciOSPermissions = new ParamediciOSPermissions(appName, tccDb, this.targetObj);
paramediciOSPermissions.updatePermissions(applicationsToGrantPermission);
}
}
};
ParamedicRunner.prototype.injectReporters = function () {
var self = this;
var reporters = Reporters.getReporters(self.config.getOutputDir());
['jasmineStarted', 'specStarted', 'specDone',
'suiteStarted', 'suiteDone', 'jasmineDone'].forEach(function(route) {
reporters.forEach(function(reporter) {
if (reporter[route] instanceof Function)
self.server.on(route, reporter[route].bind(reporter));
});
});
};
ParamedicRunner.prototype.subcribeForEvents = function () {
this.server.on('deviceLog', function (data) {
logger.verbose('device|console.' + data.type + ': ' + data.msg[0]);
});
this.server.on('deviceInfo', function (data) {
logger.normal('cordova-paramedic: Device info: ' + JSON.stringify(data));
});
};
ParamedicRunner.prototype.writeMedicJson = function(logUrl) {
logger.normal('cordova-paramedic: writing medic log url to project ' + 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 () {
var plugins = self.config.getPlugins();
for (var i = 0; i < plugins.length; i++) {
if (plugins[i].indexOf('cordova-plugin-file-transfer') >= 0 && !self.config.getFileTransferServer() && !self.config.isCI()) {
return self.server.startFileTransferServer(self.tempFolder.name);
}
}
});
};
ParamedicRunner.prototype.runLocalTests = function () {
var self = this;
var runProcess = null;
// checking for Android platform here because in this case we still need to start an emulator
// will check again a bit lower
if (!self.config.runMainTests() && self.config.getPlatformId() !== util.ANDROID) {
logger.normal('Skipping main tests...');
return Q(util.TEST_PASSED);
}
return Q().then(function () {
return self.maybeRunFileTransferServer();
})
.then(function () {
return self.getCommandForStartingTests();
})
.then(function(command) {
self.setPermissions();
logger.normal('cordova-paramedic: running command ' + command);
if (self.config.getPlatformId() !== util.BROWSER) {
return execPromise(command);
}
// We do not wait for the child process to finish, server connects when deployed and ready.
runProcess = cp.exec(command, (error, stdout, stderr) => {
if (error) {
// This won't show up until the process completes:
console.log('[ERROR]: Running cordova run failed');
console.log(stdout);
console.log(stderr);
reject(error);
}
console.log(stdout);
console.log(stderr);
runProcess = null;
});
})
.then(function() {
// skipping here and not at the beginning because we need to
// start up the Android emulator for Appium tests to run on
if (!self.config.runMainTests()) {
logger.normal('Skipping main tests...');
return util.TEST_PASSED;
}
// skip tests if it was just build
if (self.shouldWaitForTestResult()) {
return Q.promise(function(resolve, reject) {
// reject if timed out
self.waitForConnection().catch(reject);
// resolve if got results
self.waitForTests().then(resolve);
});
}
return util.TEST_PASSED; // if we're not waiting for a test result, just report tests as passed
})
.fin(function (result) {
if (runProcess) {
return Q.Promise(function (resolve, reject) {
util.killProcess(runProcess.pid, function () {
resolve(result);
});
});
}
return result;
});
};
ParamedicRunner.prototype.runAppiumTests = function (useSauce) {
var platform = this.config.getPlatformId();
var self = this;
if (self.config.getAction() === 'build' || !self.config.runAppiumTests()) {
logger.normal('Skipping Appium tests...');
return Q(util.TEST_PASSED);
}
if (platform !== util.ANDROID && platform !== util.IOS) {
logger.warn('Unsupported platform for Appium test run: ' + platform);
// just skip Appium tests
return Q(util.TEST_PASSED);
}
if (!useSauce && (!self.targetObj || !self.targetObj.target)) {
throw new Error('Cannot determine device name for Appium');
}
logger.normal('Running Appium tests ' + (useSauce ? 'on Sauce Labs' : 'locally'));
var options = {
platform: self.config.getPlatformId(),
appPath: self.tempFolder.name,
pluginRepos: self.config.getPlugins().map(function (plugin) {
return path.join(self.tempFolder.name, 'plugins', path.basename(plugin));
}),
appiumDeviceName: self.targetObj && self.targetObj.target,
appiumPlatformVersion: null,
screenshotPath: path.join(process.cwd(), 'appium_screenshots'),
output: self.config.getOutputDir(),
verbose: self.config.isVerbose(),
sauce: useSauce,
browserify: self.config.isBrowserify,
cli: self.config.getCli(),
};
if (useSauce) {
options.sauceAppPath = 'sauce-storage:' + this.getAppName();
options.sauceUser = this.config.getSauceUser();
options.sauceKey = this.config.getSauceKey();
options.sauceCaps = this.getSauceCaps();
options.sauceCaps.name += '_Appium';
}
var appiumRunner = new AppiumRunner(options);
if (appiumRunner.options.testPaths && appiumRunner.options.testPaths.length === 0) {
logger.warn('Couldn\'t find Appium tests, skipping...');
return Q(util.TEST_PASSED);
}
return Q()
.then(function () {
return appiumRunner.prepareApp();
})
.then(function () {
if (useSauce) {
return self.packageApp()
.then(self.uploadApp.bind(self));
}
})
.then(function () {
return appiumRunner.runTests(useSauce);
});
};
ParamedicRunner.prototype.runTests = function () {
var isTestPassed = false;
var self = this;
if (this.config.shouldUseSauce()) {
return this.runSauceTests()
.then(function (result) {
isTestPassed = result;
return self.runAppiumTests(true);
})
.then(function (isAppiumTestPassed) {
return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED;
});
} else {
return this.runLocalTests()
.then(function (result) {
isTestPassed = result;
})
.then(self.runAppiumTests.bind(this))
.then(function (isAppiumTestPassed) {
return isTestPassed == util.TEST_PASSED && isAppiumTestPassed == util.TEST_PASSED;
});
}
};
ParamedicRunner.prototype.waitForTests = function () {
var self = this;
logger.info('cordova-paramedic: waiting for test results');
return Q.promise(function(resolve, reject) {
// time out if connection takes too long
var ERR_MSG = 'waitForTests: Seems like device not connected to local server in ' + INITIAL_CONNECTION_TIMEOUT / 1000 + ' secs';
setTimeout(function() {
if (!self.server.isDeviceConnected()) {
reject(new Error(ERR_MSG));
}
}, INITIAL_CONNECTION_TIMEOUT);
self.server.on('jasmineDone', function (data) {
logger.info('cordova-paramedic: tests have been completed');
var isTestPassed = (data.specResults.specFailed === 0);
resolve(isTestPassed);
});
self.server.on('disconnect', function () {
reject(new Error('device is disconnected before passing the tests'));
});
});
};
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;
}
if (self.config.getPlatformId() === util.BROWSER) {
return addConfigArgs(cmd);
}
var paramedicTargetChooser = new ParamedicTargetChooser(this.tempFolder.name, this.config);
if (self.config.getAction() === 'build' || (self.config.getPlatformId() === util.WINDOWS && self.config.getArgs().indexOf('appx=8.1-phone') < 0)) {
//The app is to be run as a store app or just build. So no need to choose a target.
return Q(addConfigArgs(cmd));
}
// For now we always trying to run test app on emulator
return Q().then(function () {
var configTarget = self.config.getTarget();
return paramedicTargetChooser.chooseTarget(/*useEmulator=*/true, /*preferredTarget=*/configTarget);
})
.then(function(targetObj){
self.targetObj = targetObj;
cmd += ' --target ' + self.targetObj.target;
// CB-11472 In case of iOS provide additional '--emulator' flag, otherwise
// 'cordova run ios --target' would hang waiting for device with name
// as specified in 'target' in case if any device is physically connected
if (self.config.getPlatformId() === util.IOS) {
cmd += ' --emulator';
}
return addConfigArgs(cmd);
});
};
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);
};
ParamedicRunner.prototype.waitForConnection = function () {
var self = this;
var ERR_MSG = 'waitForConnection: Seems like device not connected to local server in ' + INITIAL_CONNECTION_TIMEOUT / 1000 + ' secs';
return Q.promise(function(resolve, reject) {
setTimeout(function () {
if (!self.server.isDeviceConnected()) {
reject(new Error(ERR_MSG));
} else {
resolve();
}
}, INITIAL_CONNECTION_TIMEOUT);
});
};
ParamedicRunner.prototype.cleanUpProject = function () {
this.server && this.server.cleanUp();
if (this.config.shouldCleanUpAfterRun()) {
logger.info('cordova-paramedic: Deleting the application: ' + this.tempFolder.name);
shell.popd();
shell.rm('-rf', this.tempFolder.name);
}
};
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());
shell.exec(zipCommand, { silent: !self.config.isVerbose() }, 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.');
var paramedicKill = new ParamedicKill(this.config.getPlatformId());
paramedicKill.kill();
}
};
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);
};
ParamedicRunner.prototype.uninstallApp = function () {
logger.info('Uninstalling the app.');
var paramedicAppUninstall = new ParamedicAppUninstall(this.tempFolder.name, this.config.getPlatformId());
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 () {
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 () {
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) {
storedCWD = storedCWD || process.cwd();
var runner = new ParamedicRunner(paramedicConfig, null);
runner.storedCWD = storedCWD;
return runner.run();
};