/**
* @module digdug/CrossBrowserTestingTunnel
*/
var fs = require('fs');
var os = require('os');
var pathUtil = require('path');
var request = require('dojo/request');
var Tunnel = require('./Tunnel');
var util = require('./util');
var Promise = require('dojo/Promise');
var CBT_VERSION = '0.0.27';
/**
* A CrossBrowserTesting tunnel.
*
* This tunnel requires some non-standard configuration options (vs the other tunnels):
*
* 1. The capabilities must include the username, API key, browser_api_name, and os_api_name properties
* 2. The Intern proxyUrl must use 'local' instead of 'localhost'
*
* An Intern config using this tunnel might be look like:
*
* ```js
* define({
* proxyUrl: 'http://local:9000',
*
* tunnel: 'CrossBrowserTesting',
*
* environments: [
* {
* browserName: 'chrome',
* os_api_name: 'Win10',
* browser_api_name: 'Chrome52'
* }
* ]
*
* // Other Intern config options...
* });
* ```
*
* @constructor module:digdug/CrossBrowserTestingTunnel
* @extends module:digdug/Tunnel
*/
function CrossBrowserTestingTunnel() {
this.apiKey = process.env.CBT_APIKEY;
this.username = process.env.CBT_USERNAME;
this.cbtVersion = CBT_VERSION;
Tunnel.apply(this, arguments);
}
var _super = Tunnel.prototype;
CrossBrowserTestingTunnel.prototype = util.mixin(Object.create(_super), /** @lends module:digdug/CrossBrowserTestingTunnel# */ {
constructor: CrossBrowserTestingTunnel,
/**
* The CrossBrowserTesting API key. This will be initialized with the value of the `CBT_APIKEY` environment
* variable.
*
* @type {string}
* @default the value of the CBT_APIKEY environment variable
*/
apiKey: null,
/**
* The CrossBrowserTesting username. This will be initialized with the value of the `CBT_USERNAME` environment
* variable.
*
* @type {string}
* @default the value of the CBT_USERNAME environment variable
*/
username: null,
/**
* The URL of a service that provides a list of environments supported by CrossBrowserTesting.
*/
environmentUrl: 'https://crossbrowsertesting.com/api/v3/selenium/browsers?format=json',
executable: 'node',
hostname: 'hub.crossbrowsertesting.com',
port: 80,
get auth() {
return (this.username || '') + ':' + (this.apiKey || '');
},
get extraCapabilities() {
return {
username: this.username,
password: this.apiKey
};
},
get isDownloaded() {
try {
require('cbt_tunnels');
return true;
}
catch (error) {
return false;
}
},
download: function (forceDownload) {
if (!forceDownload && this.isDownloaded) {
return Promise.resolve();
}
var cbtVersion = this.cbtVersion;
return new Promise(function (resolve, reject) {
var child_process = require('child_process');
child_process.exec('npm install cbt_tunnels@' + cbtVersion, function (error, stdout, stderr) {
if (error) {
console.error(stderr);
reject(error);
}
resolve();
});
});
},
_makeArgs: function (readyFile) {
return [
'node_modules/.bin/cbt_tunnels',
'--authkey', this.apiKey,
'--username', this.username,
'--ready', readyFile
];
},
sendJobState: function (jobId, data) {
var payload = JSON.stringify({
action: 'set_score',
score: (data.status || data.success) ? 'pass' : 'fail'
});
return request.put('https://crossbrowsertesting.com/api/v3/selenium/' + jobId, {
data: payload,
handleAs: 'text',
headers: {
'Content-Length': Buffer.byteLength(payload, 'utf8'),
'Content-Type': 'application/json'
},
user: this.username,
password: this.apiKey,
proxy: this.proxy
}).then(function (response) {
if (response.data) {
var data = JSON.parse(response.data);
if (data.status) {
throw new Error('Could not save test status (' + data.message + ')');
}
if (response.statusCode !== 200) {
throw new Error('Server reported ' + response.statusCode + ' with: ' + response.data);
}
}
else {
throw new Error('Server reported ' + response.statusCode + ' with no other data.');
}
});
},
_start: function () {
var readyFile = pathUtil.join(os.tmpdir(), 'CrossBrowserTesting-' + Date.now());
var child = this._makeChild(readyFile);
var childProcess = child.process;
var dfd = child.deferred;
var stdout = [];
// Polling API is used because we are only watching for one file, so efficiency is not a big deal, and the
// `fs.watch` API has extra restrictions which are best avoided
fs.watchFile(readyFile, { persistent: false, interval: 1007 }, function (current, previous) {
if (Number(current.mtime) === Number(previous.mtime)) {
// readyFile hasn't been modified, so ignore the event
return;
}
fs.unwatchFile(readyFile);
readHandle.remove();
exitHandle.remove();
stdout = null;
dfd.resolve();
});
// The cbt tunnel outputs its startup error messages on stdout. Capture any data on stdout and display it if the
// process exits early.
var readHandle = util.on(childProcess.stdout, 'data', function (data) {
stdout.push(data);
});
var exitHandle = util.on(childProcess, 'exit', function () {
process.stderr.write(stdout.join(''));
});
this._handles.push(readHandle);
this._handles.push(exitHandle);
return child;
},
/**
* Attempt to normalize a TestingBot described environment with the standard Selenium capabilities
*
* TestingBot returns a list of environments that looks like:
*
* {
* "selenium_name": "Chrome36",
* "name": "googlechrome",
* "platform": "CAPITAN",
* "version":"36"
* }
*
* @param {Object} environment a TestingBot environment descriptor
* @returns a normalized descriptor
* @private
*/
_normalizeEnvironment: function (environment) {
var platform = environment.api_name;
return environment.browsers.map(function (browser) {
var browserName = browser.type.toLowerCase();
return {
platform: platform,
browserName: browserName,
version: browser.version,
descriptor: environment,
intern: {
browserName: browserName,
version: browser.version,
browser_api_name: browser.api_name,
os_api_name: platform
}
};
});
}
});
module.exports = CrossBrowserTestingTunnel;