Skip to content

Commit

Permalink
Use proxy when opening Android browsers (closes #25) (#30)
Browse files Browse the repository at this point in the history
* Use proxy when opening Android browsers (closes #25)

* Avoid simultaneous API requests
  • Loading branch information
AndreyBelym authored and AlexanderMoskovkin committed Apr 18, 2018
1 parent aaad6ed commit 5b72e49
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 59 deletions.
2 changes: 1 addition & 1 deletion Gulpfile.js
Expand Up @@ -64,7 +64,7 @@ gulp.task('test-testcafe', ['build'], function () {
var testCafeCmd = path.join(__dirname, 'node_modules/.bin/testcafe');

var testCafeOpts = [
'browserstack:chrome',
'browserstack:chrome,browserstack:Google Pixel,browserstack:iPhone SE',
'test/testcafe/**/*.js',
'-s', '.screenshots'
];
Expand Down
54 changes: 54 additions & 0 deletions src/api-request.js
@@ -0,0 +1,54 @@
import Promise from 'pinkie';
import request from 'request-promise';
import delay from './utils/delay';


const BUILD_ID = process.env['BROWSERSTACK_BUILD_ID'];
const PROJECT_NAME = process.env['BROWSERSTACK_PROJECT_NAME'];

const AUTH_FAILED_ERROR = 'Authentication failed. Please assign the correct username and access key ' +
'to the BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables.';

const API_REQUEST_DELAY = 100;

let apiRequestPromise = Promise.resolve(null);


export default function (apiPath, params) {
if (!process.env['BROWSERSTACK_USERNAME'] || !process.env['BROWSERSTACK_ACCESS_KEY'])
throw new Error(AUTH_FAILED_ERROR);

var url = apiPath.url;

var opts = {
auth: {
user: process.env['BROWSERSTACK_USERNAME'],
pass: process.env['BROWSERSTACK_ACCESS_KEY'],
},

qs: Object.assign({},
BUILD_ID && { build: BUILD_ID },
PROJECT_NAME && { project: PROJECT_NAME },
params
),

method: apiPath.method || 'GET',
json: !apiPath.binaryStream
};

if (apiPath.binaryStream)
opts.encoding = null;

const currentRequestPromise = apiRequestPromise
.then(() => request(url, opts))
.catch(error => {
if (error.statusCode === 401)
throw new Error(AUTH_FAILED_ERROR);

throw error;
});

apiRequestPromise = currentRequestPromise.then(() => delay(API_REQUEST_DELAY));

return currentRequestPromise;
}
47 changes: 47 additions & 0 deletions src/browser-proxy.js
@@ -0,0 +1,47 @@
import http from 'http';
import { parse as parseUrl } from 'url';
import Promise from 'pinkie';


module.exports = class BrowserProxy {
constructor (targetHost, targetPort, { proxyPort, responseDelay } = {}) {
this.targetHost = targetHost;
this.targetPort = targetPort;
this.proxyPort = proxyPort;
this.responseDelay = responseDelay || 0;

this.server = http.createServer((...args) => this._onBrowserRequest(...args));

this.server.on('connection', socket => socket.unref());
}

_onBrowserRequest (req, res) {
setTimeout(() => {
const parsedRequestUrl = parseUrl(req.url);
const destinationUrl = 'http://' + this.targetHost + ':' + this.targetPort + parsedRequestUrl.path;

res.statusCode = 302;

res.setHeader('location', destinationUrl);
res.end();
}, this.responseDelay);
}

async init () {
return new Promise((resolve, reject) => {
this.server.listen(this.proxyPort, err => {
if (err)
reject(err);
else {
this.proxyPort = this.server.address().port;

resolve();
}
});
});
}

dispose () {
this.server.close();
}
};
116 changes: 58 additions & 58 deletions src/index.js
@@ -1,13 +1,14 @@
import { parse as parseUrl } from 'url';
import Promise from 'pinkie';
import request from 'request-promise';
import parseCapabilities from 'desired-capabilities';
import { Local as BrowserstackConnector } from 'browserstack-local';
import jimp from 'jimp';
import OS from 'os-family';
import nodeUrl from 'url';
import apiRequest from './api-request';
import BrowserProxy from './browser-proxy';
import delay from './utils/delay';

const BUILD_ID = process.env['BROWSERSTACK_BUILD_ID'];
const PROJECT_NAME = process.env['BROWSERSTACK_PROJECT_NAME'];

const TESTS_TIMEOUT = process.env['BROWSERSTACK_TEST_TIMEOUT'] || 1800;
const BROWSERSTACK_CONNECTOR_DELAY = 10000;
Expand All @@ -16,8 +17,7 @@ const MINIMAL_WORKER_TIME = 30000;
const TESTCAFE_CLOSING_TIMEOUT = 10000;
const TOO_SMALL_TIME_FOR_WAITING = MINIMAL_WORKER_TIME - TESTCAFE_CLOSING_TIMEOUT;

const AUTH_FAILED_ERROR = 'Authentication failed. Please assign the correct username and access key ' +
'to the BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables.';
const ANDROID_PROXY_RESPONSE_DELAY = 500;

const PROXY_AUTH_RE = /^([^:]*)(?::(.*))?$/;

Expand Down Expand Up @@ -46,10 +46,6 @@ const identity = x => x;

const capitalize = str => str[0].toUpperCase() + str.slice(1);

function delay (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

function copyOptions (source, destination, transfromFunc = identity) {
Object
.keys(source)
Expand Down Expand Up @@ -77,7 +73,7 @@ function createBrowserStackConnector (accessKey) {
return new Promise((resolve, reject) => {
var connector = new BrowserstackConnector();
var parallelRuns = process.env['BROWSERSTACK_PARALLEL_RUNS'];

var opts = {
key: accessKey,
logfile: OS.win ? 'NUL' : '/dev/null',
Expand Down Expand Up @@ -121,47 +117,16 @@ function destroyBrowserStackConnector (connector) {
});
}

function doRequest (apiPath, params) {
if (!process.env['BROWSERSTACK_USERNAME'] || !process.env['BROWSERSTACK_ACCESS_KEY'])
throw new Error(AUTH_FAILED_ERROR);

var url = apiPath.url;

var opts = {
auth: {
user: process.env['BROWSERSTACK_USERNAME'],
pass: process.env['BROWSERSTACK_ACCESS_KEY'],
},

qs: Object.assign({},
BUILD_ID && { build: BUILD_ID },
PROJECT_NAME && { project: PROJECT_NAME },
params
),

method: apiPath.method || 'GET',
json: !apiPath.binaryStream
};

if (apiPath.binaryStream)
opts.encoding = null;

return request(url, opts)
.catch(error => {
if (error.statusCode === 401)
throw new Error(AUTH_FAILED_ERROR);

throw error;
});
}

export default {
// Multiple browsers support
isMultiBrowser: true,
connectorPromise: Promise.resolve(null),
workers: {},
platformsInfo: [],
browserNames: [],
isMultiBrowser: true,

connectorPromise: Promise.resolve(null),
browserProxyPromise: Promise.resolve(null),

workers: {},
platformsInfo: [],
browserNames: [],

_getConnector () {
this.connectorPromise = this.connectorPromise
Expand All @@ -187,8 +152,35 @@ export default {
return this.connectorPromise;
},

_getBrowserProxy (host, port) {
this.browserProxyPromise = this.browserProxyPromise
.then(async browserProxy => {
if (!browserProxy) {
browserProxy = new BrowserProxy(host, port, { responseDelay: ANDROID_PROXY_RESPONSE_DELAY });

await browserProxy.init();
}

return browserProxy;
});

return this.browserProxyPromise;
},

_disposeBrowserProxy () {
this.browserProxyPromise = this.browserProxyPromise
.then(async browserProxy => {
if (browserProxy)
await browserProxy.dispose();

return null;
});

return this.browserProxyPromise;
},

async _getDeviceList () {
this.platformsInfo = await doRequest(BROWSERSTACK_API_PATHS.browserList);
this.platformsInfo = await apiRequest(BROWSERSTACK_API_PATHS.browserList);

this.platformsInfo.reverse();
},
Expand Down Expand Up @@ -255,13 +247,20 @@ export default {
var capabilities = this._generateCapabilities(browserName);
var connector = await this._getConnector();

capabilities.timeout = TESTS_TIMEOUT;
capabilities.url = pageUrl;
capabilities.name = `TestCafe test run ${id}`;
capabilities.localIdentifier = connector.localIdentifierFlag;
if (capabilities.os.toLowerCase() === 'android') {
const parsedPageUrl = parseUrl(pageUrl);
const browserProxy = await this._getBrowserProxy(parsedPageUrl.hostname, parsedPageUrl.port);

pageUrl = 'http://' + browserProxy.targetHost + ':' + browserProxy.proxyPort + parsedPageUrl.path;
}

capabilities.timeout = TESTS_TIMEOUT;
capabilities.url = pageUrl;
capabilities.name = `TestCafe test run ${id}`;
capabilities.localIdentifier = connector.localIdentifierFlag;
capabilities['browserstack.local'] = true;

this.workers[id] = await doRequest(BROWSERSTACK_API_PATHS.newWorker, capabilities);
this.workers[id] = await apiRequest(BROWSERSTACK_API_PATHS.newWorker, capabilities);
this.workers[id].started = Date.now();
},

Expand All @@ -271,12 +270,12 @@ export default {

if (workerTime < MINIMAL_WORKER_TIME) {
if (workerTime < TOO_SMALL_TIME_FOR_WAITING)
await doRequest(BROWSERSTACK_API_PATHS.deleteWorker(workerId));
await apiRequest(BROWSERSTACK_API_PATHS.deleteWorker(workerId));

await delay(MINIMAL_WORKER_TIME - workerTime);
}

await doRequest(BROWSERSTACK_API_PATHS.deleteWorker(workerId));
await apiRequest(BROWSERSTACK_API_PATHS.deleteWorker(workerId));
},


Expand All @@ -290,6 +289,7 @@ export default {

async dispose () {
await this._disposeConnector();
await this._disposeBrowserProxy();
},


Expand All @@ -310,7 +310,7 @@ export default {

async takeScreenshot (id, screenshotPath) {
return new Promise(async (resolve, reject) => {
var buffer = await doRequest(BROWSERSTACK_API_PATHS.screenshot(this.workers[id].id));
var buffer = await apiRequest(BROWSERSTACK_API_PATHS.screenshot(this.workers[id].id));

jimp
.read(buffer)
Expand Down
6 changes: 6 additions & 0 deletions src/utils/delay.js
@@ -0,0 +1,6 @@
import Promise from 'pinkie';


export default function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

0 comments on commit 5b72e49

Please sign in to comment.