min-wd
Version:
Minimal WebDriver that pipes stdin to browsers
353 lines (326 loc) • 8.71 kB
JavaScript
/*
* min-webdriver
*
* Copyright (c) 2014 Maximilian Antoni <mail@maxantoni.de>
*
* @license MIT
*/
;
var through = require('through2');
var listen = require('listen');
var sourceMap = require('source-mapper');
var request = require('./request');
var sauceLabs = require('./saucelabs');
function close(context, callback) {
request(context, 'DELETE', '/window', null, function () {
request(context, 'DELETE', '', null, function () {
callback();
});
});
}
function handleError(context, err, callback) {
if (context.closeOnError) {
close(context, function () {
callback(err);
});
} else {
callback(err);
}
}
function pollLogs(context, callback) {
var endpoint = context.asyncPolling ? '/execute_async' : '/execute';
var timeout = context.asyncPolling ? 0 : context.pollingInterval;
var script;
if (context.asyncPolling) {
script = 'window._webdriver_poll(arguments[0]);';
} else {
script = 'return window._webdriver_manualPoll();';
}
request(context, 'POST', endpoint, {
script : script,
args : []
}, function (err, res) {
if (err) {
handleError(context, err, callback);
return;
}
var match = res.value.match(/^WEBDRIVER_EXIT\(([0-9]+)\)$/m);
if (match) {
context.out.write(res.value.substring(0, match.index));
var localCallback = function () {
var passed = match[1] === '0';
function done() {
if (passed) {
callback();
} else {
callback(new Error('Build failed: ' + match[1]));
}
}
if (context.sauceLabs) {
sauceLabs.updateJob(res.sessionId, {
name : context.sauceJobName,
passed : passed,
build : process.env[context.BUILD_VAR]
}, done);
} else {
done();
}
};
if (context.closeOnSuccess) {
close(context, localCallback);
} else {
localCallback();
}
return;
}
if (context.out.write(res.value)) {
setTimeout(function () {
pollLogs(context, callback);
}, timeout);
} else {
context.out.once('drain', function () {
setTimeout(function () {
pollLogs(context, callback);
}, timeout);
});
}
});
}
/*
* This hack works around the following issues:
*
* https://github.com/mantoni/mochify.js/issues/110
* https://bugs.chromium.org/p/chromedriver/issues/detail?id=402
* https://github.com/sinonjs/sinon/issues/912
*
* Apparently the Chrome webdriver has a buffer limit somewhere around 1 MB.
* Injecting scripts that are below a certain size works reliably, so we have
* to slice the actual script into chunks, merge the parts in the browser and
* then inject a script tag there.
*/
var MAX_SCRIPT_CHUNK = 700 * 1000;
function streamChunk(context, script, callback) {
var execute = false;
var nextScript = '';
if (script.length > MAX_SCRIPT_CHUNK) {
nextScript = script.substring(MAX_SCRIPT_CHUNK);
script = script.substring(0, MAX_SCRIPT_CHUNK);
} else {
execute = true;
}
request(context, 'POST', '/execute', {
script: 'window._webdriver_receive(arguments[0], arguments[1])',
args: [script, execute]
}, function (err) {
if (err) {
handleError(context, err, callback);
} else if (nextScript) {
streamChunk(context, nextScript, callback);
} else {
setTimeout(function () {
pollLogs(context, callback);
}, 10);
}
});
}
function execute(context, script, callback) {
request(context, 'POST', '/execute', {
script: 'var script = "";'
+ 'window._webdriver_receive = function (chunk, execute) {'
+ 'script += chunk;'
+ 'if (execute) {'
+ ' var s = document.createElement("script");'
+ ' s.textContent = script;'
+ ' document.body.appendChild(s);'
+ '}};',
args: []
}, function (err) {
if (err) {
handleError(context, err, callback);
} else {
streamChunk(context, script, callback);
}
});
}
function openUrl(context, script, callback) {
var browser = context.browser;
var parts = [browser.name];
if (browser.version) {
parts.push(browser.version);
}
if (browser.platformName) {
parts.push(browser.platformName);
}
if (browser.platformVersion) {
parts.push(browser.platformVersion);
}
var title = parts.join(' ');
context.out.write('# ' + title + ':\n');
var x = sourceMap.extract(script);
if (x.map) {
var sm = sourceMap.stream(x.map);
sm.pipe(context.out);
context.out = sm;
}
var url = browser.url || context.url;
if (url) {
request(context, 'POST', '/url', {
url : url
}, function (err) {
if (err) {
handleError(context, err, callback);
return;
}
execute(context, x.js, callback);
});
} else {
execute(context, x.js, callback);
}
}
var optional_caps = [
'platformName',
'platformVersion',
'deviceName'
];
function connectBrowser(context, callback) {
var caps = {
browserName : context.browser.name,
version : context.browser.version,
platform : context.browser.platform,
javascriptEnabled : true
};
if (context.sauceLabs) {
caps.username = process.env.SAUCE_USERNAME;
caps.accessKey = process.env.SAUCE_ACCESS_KEY;
}
optional_caps.forEach(function (key) {
if (context.browser[key]) {
caps[key] = context.browser[key];
}
});
if (context.browser.capabilities) {
Object.keys(context.browser.capabilities).forEach(function (key) {
caps[key] = context.browser.capabilities[key];
});
}
var json = {
desiredCapabilities : caps
};
request(context, 'POST', '/session', json, function (err, res) {
if (err) {
callback(err);
return;
}
context.basePath = context.basePath + '/session/' + res.sessionId;
if (!context.asyncPolling || context.timeout === 0) {
callback(null);
return;
}
request(context, 'POST', '/timeouts/async_script', {
ms : context.timeout || 10000
}, function (err) {
if (err) {
handleError(context, err, callback);
} else {
callback(null);
}
});
});
}
function createContext(options, browser, out) {
return {
hostname : options.hostname,
port : options.port,
url : options.url,
asyncPolling : options.asyncPolling,
pollingInterval : options.pollingInterval,
timeout : options.timeout,
basePath : '/wd/hub',
browser : browser,
BUILD_VAR : options.BUILD_VAR,
sauceJobName : options.sauceJobName,
out : out,
sauceLabs : options.sauceLabs,
closeOnError : options.closeOnError,
closeOnSuccess : options.closeOnSuccess
};
}
function pipe(streams, out) {
if (!streams.length) {
out.end();
return;
}
var stream = streams.shift();
stream.on('data', function (data) {
out.write(data);
});
stream.on('end', function () {
pipe(streams, out);
});
stream.resume();
}
function run(options, out, runner, callback) {
var listener = listen();
var streams = options.browsers.map(function (browser) {
var stream = through();
stream.pause();
var cb = listener(function () {
stream.end();
});
var context = createContext(options, browser, stream);
connectBrowser(context, function (err) {
if (err) {
cb(err);
return;
}
runner(context, cb);
});
return stream;
});
pipe(streams, out);
listener.then(callback);
}
function createRunner(input) {
var requests = [];
var script = '';
input.on('data', function (chunk) {
script += chunk;
});
input.on('end', function () {
if (!script) {
requests.forEach(function (request) {
close(request.context, request.callback);
});
} else {
requests.forEach(function (request) {
openUrl(request.context, script, request.callback);
});
}
requests = null;
});
return function (context, callback) {
if (requests) {
requests.push({ context : context, callback : callback });
} else {
if (!script) {
close(context, callback);
return;
}
openUrl(context, script, callback);
}
};
}
module.exports = function (input, options, callback) {
var error;
var out = through(function (chunk, enc, next) {
this.push(chunk);
next();
}, function (next) {
next();
callback(error);
});
run(options, out, createRunner(input), function (err) {
error = err;
});
return out;
};