Merge pull request #510 from subtleGradient/subtlegradient/run-the-browser-tests-on-saucelabs

Make it easy to run the browser tests on saucelabs.
This commit is contained in:
Ben Newman
2013-11-14 10:08:51 -08:00
10 changed files with 218 additions and 164 deletions
+1
View File
@@ -23,3 +23,4 @@ docs/js/examples
docs/downloads
examples/shared/*.js
test/the-files-to-test.generated.js
sauce_connect.log*
+3 -1
View File
@@ -6,6 +6,7 @@ var browserifyTask = require('./grunt/tasks/browserify');
var populistTask = require('./grunt/tasks/populist');
var webdriverPhantomJSTask = require('./grunt/tasks/webdriver-phantomjs');
var webdriverJasmineTasks = require('./grunt/tasks/webdriver-jasmine');
var sauceTunnelTask = require('./grunt/tasks/sauce-tunnel');
var npmTask = require('./grunt/tasks/npm');
var releaseTasks = require('./grunt/tasks/release');
@@ -46,6 +47,8 @@ module.exports = function(grunt) {
grunt.registerMultiTask('populist', populistTask);
grunt.registerTask('sauce-tunnel', sauceTunnelTask);
grunt.registerMultiTask('webdriver-jasmine', webdriverJasmineTasks);
grunt.registerMultiTask('npm', npmTask);
@@ -71,7 +74,6 @@ module.exports = function(grunt) {
grunt.registerTask('build:min', ['jsx:release', 'version-check', 'browserify:min']);
grunt.registerTask('build:addons-min', ['jsx:debug', 'browserify:addonsMin']);
grunt.registerTask('build:test', [
'jsx:jasmine',
'jsx:test',
'version-check',
'populist:test'
-10
View File
@@ -24,15 +24,6 @@ var debug = {
outputDir: "build/modules"
};
var jasmine = {
rootIDs: [
"all"
],
getConfig: getDebugConfig,
sourceDir: "vendor/jasmine",
outputDir: "build/jasmine"
};
var test = {
rootIDs: rootIDs.concat([
"test/all.js",
@@ -71,7 +62,6 @@ var release = {
module.exports = {
debug: debug,
jasmine: jasmine,
test: test,
release: release
};
+18 -22
View File
@@ -1,24 +1,22 @@
var pxlgif = Buffer('R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==', 'base64');
module.exports = function(grunt){
function printMiddleware(req, res, next) {
if (req._parsedUrl.pathname != '/print') return next();
if (req.query.message.indexOf('ok') === 0){
grunt.log.ok(req.query.message);
} else if (req.query.message.indexOf('not ok') === 0){
grunt.log.error(req.query.message);
} else {
grunt[req.query.type || 'log'].writeln('[%s][%s]', req.headers['user-agent'], Date.now(), req.query.message);
}
res.end(pxlgif);
}
function testResultLoggerMiddleware(req, res, next) {
if (!(req.body && req.body.data)) return next();
grunt.log.writeln('[%s][%s]', req.headers['user-agent'], Date.now(), req.body.data);
if (!(req.method == 'POST' && req._parsedUrl.pathname.indexOf('/reportTestResults') === 0)) return next();
var logType = 'writeln';
var message = req.body;
if (req.body.type && req.body.message){
if (req.body.type == 'error') logType = 'error';
else if (req.body.message.indexOf('ok') === 0) logType = 'ok';
else if (req.body.message.indexOf('not ok') === 0) logType = 'error';
message = req.body.message;
}
if (typeof message != 'string') message = JSON.stringify(message, null, 2);
grunt.log[logType]('[%s][%s]', req.headers['user-agent'], Date.now(), message);
res.write('<!doctype html><meta charset=utf-8>');
res.end('Got it, thanks!');
}
return {
server: {
options: {
@@ -26,17 +24,15 @@ module.exports = function(grunt){
hostname: '*',
port: 9999,
middleware: function(connect, options) {
connect.logger.token('user-agent', function(req, res){ return req.headers['user-agent']; });
connect.logger.token('timestamp', function(req, res){ return Date.now(); });
return [
connect.query(),
printMiddleware,
connect.logger({format:'[:user-agent][:timestamp] :method :url', stream:grunt.verbose}),
connect.bodyParser(),
connect.json(),
testResultLoggerMiddleware,
connect.logger({format:'[:user-agent][:timestamp] :method :url', stream:grunt.verbose}),
connect.static(options.base),
connect.directory(options.base)
];
+73 -3
View File
@@ -6,9 +6,8 @@ exports.local = {
remote: { protocol: 'http:', hostname: '127.0.0.1', port: 9515, path: '/' }
},
url: "http://127.0.0.1:9999/test/index.html",
onComplete: function(report){
var browser = this;
if (!report.passed){
onComplete: function(passed){
if (!passed){
grunt.fatal("tests failed");
}
},
@@ -16,3 +15,74 @@ exports.local = {
grunt.fatal(error);
}
}
if (grunt.option('debug')) exports.local.url += '?debug=' + grunt.option('debug');
exports.saucelabs = {
webdriver: {
remote: {
/* https://github.com/admc/wd/blob/master/README.md#named-parameters */
user: process.env.SAUCE_USERNAME,
pwd: process.env.SAUCE_ACCESS_KEY,
protocol: 'http:',
hostname: 'ondemand.saucelabs.com',
port: '80',
path: '/wd/hub'
}
},
desiredCapabilities: {
"build": process.env.TRAVIS_BUILD_NUMBER || 'dev' + Date.now(),
"tunnel-identifier": process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel',
"browserName": "chrome"
},
url: exports.local.url,
onStart: function(browser){
grunt.log.writeln("Starting WebDriver Test. Watch results here: http://saucelabs.com/tests/" + browser.sessionID);
},
onComplete: exports.local.onComplete,
onError: exports.local.onError
}
/* https://saucelabs.com/docs/platforms */
exports.saucelabs_ios =
exports.saucelabs_ios6_1 = sauceItUp({ browserName: 'iphone', version: '6.1', platform:'OS X 10.8' });
exports.saucelabs_ios6 = sauceItUp({ browserName: 'iphone', version: '6', platform:'OS X 10.8' });
exports.saucelabs_ios5_1 = sauceItUp({ browserName: 'iphone', version: '5.1', platform:'OS X 10.8' });
exports.saucelabs_ios5 = sauceItUp({ browserName: 'iphone', version: '5', platform:'OS X 10.6' });
exports.saucelabs_ios4 = sauceItUp({ browserName: 'iphone', version: '4', platform:'OS X 10.6' });
exports.saucelabs_ipad =
exports.saucelabs_ipad6_1 = sauceItUp({ browserName: 'ipad', version: '6.1', platform:'OS X 10.8' });
exports.saucelabs_ipad6 = sauceItUp({ browserName: 'ipad', version: '6', platform:'OS X 10.8' });
exports.saucelabs_ipad5_1 = sauceItUp({ browserName: 'ipad', version: '5.1', platform:'OS X 10.8' });
exports.saucelabs_ipad5 = sauceItUp({ browserName: 'ipad', version: '5', platform:'OS X 10.6' });
exports.saucelabs_ipad4 = sauceItUp({ browserName: 'ipad', version: '4', platform:'OS X 10.6' });
exports.saucelabs_android = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux' });
exports.saucelabs_android_tablet = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux', 'device-type':'tablet' });
exports.saucelabs_safari = sauceItUp({ browserName: 'safari' });
exports.saucelabs_chrome = sauceItUp({ browserName: 'chrome' });
exports.saucelabs_firefox = sauceItUp({ browserName: 'firefox' });
exports.saucelabs_ie =
exports.saucelabs_ie8 = sauceItUp({ browserName: 'internet explorer', version: 8 });
exports.saucelabs_ie9 = sauceItUp({ browserName: 'internet explorer', version: 9 });
exports.saucelabs_ie10 = sauceItUp({ browserName: 'internet explorer', version: 10 });
exports.saucelabs_ie11 = sauceItUp({ browserName: 'internet explorer', version: 11, platform:'Windows 8.1' });
function sauceItUp(desiredCapabilities){
desiredCapabilities["build"] = exports.saucelabs.desiredCapabilities["build"];
desiredCapabilities["tunnel-identifier"] = exports.saucelabs.desiredCapabilities["tunnel-identifier"];
return {
webdriver: exports.saucelabs.webdriver,
url: exports.saucelabs.url,
onStart: exports.saucelabs.onStart,
onComplete: exports.saucelabs.onComplete,
onError: exports.saucelabs.onError,
desiredCapabilities: desiredCapabilities,
};
}
+38
View File
@@ -0,0 +1,38 @@
var grunt = require('grunt');
var SauceTunnel = require('sauce-tunnel');
module.exports = function(){
var task = this;
var config = task.data;
var shouldStayAliveForever = task.flags.keepalive;
var SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY;
if (!SAUCE_ACCESS_KEY) grunt.fatal('Requires the environment variable SAUCE_ACCESS_KEY to be set');
var SAUCE_USERNAME = process.env.SAUCE_USERNAME;
if (!SAUCE_USERNAME) grunt.fatal('Requires the environment variable SAUCE_USERNAME to be set');
var IDENTIFIER = process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel';
var taskCompletedSuccessfully = task.async();
var stunnel = new SauceTunnel(SAUCE_USERNAME, SAUCE_ACCESS_KEY, IDENTIFIER, /*tunneled*/true, /*tunnelTimeout*/5);
process.on('exit', stunnel.stop.bind(stunnel, function(){}));
stunnel.on('log:error', grunt.log.error.bind(grunt.log));
stunnel.on('log:writeln', grunt.log.writeln.bind(grunt.log));
stunnel.on('verbose:ok', grunt.verbose.ok.bind(grunt.verbose));
stunnel.on('verbose:error', grunt.verbose.error.bind(grunt.verbose));
stunnel.on('verbose:debug', grunt.verbose.debug.bind(grunt.verbose));
stunnel.on('verbose:writeln', grunt.verbose.writeln.bind(grunt.verbose));
stunnel.openTunnel(function(isOpen){
if (shouldStayAliveForever && isOpen){
grunt.verbose.writeln('Keeping the sauce-tunnel open forever because you used the keepalive flag `' + task.nameArgs + '`');
return;
}
grunt.verbose.writeln('To keep the sauce-tunnel open forever, use the grunt task `' + task.nameArgs + ':keepalive`');
taskCompletedSuccessfully(isOpen);
});
};
+37 -10
View File
@@ -4,7 +4,14 @@ var wd = require('wd');
module.exports = function(){
var config = this.data;
var taskSucceeded = this.async();
grunt.verbose.write('webdriver remote', JSON.stringify(config.webdriver.remote));
var desiredCapabilities = {};
if (config.desiredCapabilities) Object.keys(config.desiredCapabilities).forEach(function(key){
if (config.desiredCapabilities[key] === undefined) return;
desiredCapabilities[key] = config.desiredCapabilities[key];
});
grunt.verbose.writeln("desiredCapabilities", JSON.stringify(desiredCapabilities));
var browser = wd.promiseChainRemote(config.webdriver.remote);
browser.on('status', function(info) {
@@ -15,30 +22,50 @@ module.exports = function(){
grunt.verbose.writeln(' > ' + meth, path, data || '');
});
var results = null;
// browser._debugPromise();
browser
.init(config.browser || {})
.init(desiredCapabilities)
.then(config.onStart && config.onStart.bind(config, browser))
.get(config.url)
.then(function(){return browser;})
.then(getJSReport)
.then(config.onComplete && config.onComplete.bind(browser), config.onError && config.onError.bind(browser))
.fail(grunt.verbose.writeln.bind(grunt.verbose))
.fin(function(){
.then(function(data){ results = data; })
.fail(function(error){
grunt.log.error(error);
return browser
.eval('document.documentElement.innerText || document.documentElement.textContent')
.then(grunt.verbose.writeln.bind(grunt.verbose))
.then(function(){throw error})
;
})
.finally(function(){
if (grunt.option('webdriver-keep-open')) return;
grunt.verbose.writeln('Closing the browser window. To keep it open, pass the --webdriver-keep-open flag to grunt.');
return browser.quit();
})
.done(
taskSucceeded.bind(null,true),
taskSucceeded.bind(null,false)
function(){
if (config.onComplete) config.onComplete(results);
taskSucceeded(true);
},
function(error){
if (config.onError) config.onError(error);
taskSucceeded(false);
}
)
;
}
function getJSReport(browser){
return browser
.waitForCondition("typeof window.jasmine != 'undefined'", 500)
.waitForCondition("typeof window.jasmine != 'undefined'", 5e3)
.fail(function(error){
throw Error("The test page didn't load properly. " + error);
})
.waitForCondition("typeof window.jasmine.getJSReport != 'undefined'", 10e3)
.waitForCondition("window.testImageURL.running <= 0", 5e3)
.eval("jasmine.getJSReport()")
.waitForCondition("window.postDataToURL.running <= 0", 30e3)
.eval("jasmine.getJSReport().passed")
;
}
+2 -1
View File
@@ -60,7 +60,8 @@
"jasmine-tapreporter": "~0.2.2",
"grunt-contrib-connect": "~0.5.0",
"es5-shim": "~2.1.0",
"wd": "~0.2.2"
"wd": "~0.2.2",
"sauce-tunnel": "~1.1.0"
},
"engines": {
"node": ">=0.10.0"
+46 -55
View File
@@ -1,30 +1,32 @@
console._error = console.error;
console._warn = console.warn;
console._log = console.log;
console.log = window.print = function(message){
console._log(message);
testImageURL('/print?type=log&message=' + encodeURIComponent(message) + '&_=' + Date.now().toString(36));
if (typeof console == 'undefined') console = {
log: function(){},
warn: function(){},
error: function(){}
};
console._log = console.log;
console.log = function(message){
console._log(message);
postDataToURL({type:'log', message:message}, '/reportTestResults');
}
console._error = console.error;
console.error = function(message){
console._error(message);
testImageURL('/print?type=error&message=' + encodeURIComponent(message) + '&_=' + Date.now().toString(36));
};
console.warn = function(message){
console._warn(message);
testImageURL('/print?type=warn&message=' + encodeURIComponent(message) + '&_=' + Date.now().toString(36));
};
postDataToURL({type:'error', message:message}, '/reportTestResults');
}
;(function(env){
env.addReporter(new jasmine.JSReporter());
env.addReporter(new TAPReporter(window.print));
if (location.search.substring(1).indexOf('debug') != -1){
env.addReporter(new TAPReporter(console.log.bind(console)));
}
function report(){
if (typeof jasmine.getJSReport != 'function') return setTimeout(report, 100);
postDataToURL(JSON.stringify(jasmine.getJSReport()), '/reportTestResults', function(error, event){
if (typeof jasmine.getJSReport != 'function') {
console.log("typeof jasmine.getJSReport != 'function'");
return setTimeout(report, 100);
}
postDataToURL(jasmine.getJSReport(), '/reportTestResults', function(error, results){
if (error) return console.error(error);
});
}
@@ -35,42 +37,31 @@ console.warn = function(message){
report();
};
function postDataToURL(data, url, callback){
var id = '$' + (+new Date()).toString(36);
var postReportingTarget = document.createElement('iframe');
postReportingTarget.id = postReportingTarget.name = 'postReportingTarget' + id;
var postReportingForm = document.createElement('form');
postReportingForm.method = 'POST';
postReportingForm.action = url;
postReportingForm.target = postReportingTarget.name;
var postReportingData = document.createElement('input');
postReportingData.type = 'hidden';
postReportingData.name = 'data';
postReportingData.value = data || '{"error":"unknown error in postReportingData"}';
postReportingForm.appendChild(postReportingData);
postReportingForm.appendChild(postReportingTarget);
postReportingForm.style.cssText = "visibility:hidden; position:absolute; bottom:100%; right:100%";
function done(error, event){
postReportingForm.parentNode.removeChild(postReportingForm);
callback(error, event);
}
postReportingTarget.onerror = function(error){
done(error);
}
postReportingTarget.onload = function(event){
done(null, event);
}
document.body.appendChild(postReportingForm);
setTimeout(function(){
postReportingForm.submit();
},0);
}
}(window.jasmine.getEnv()));
function createXMLHttpRequest(){
try{return new XMLHttpRequest();}
catch(e){}
try {return new ActiveXObject("Msxml2.XMLHTTP");}
catch (e) {}
try {return new ActiveXObject("Microsoft.XMLHTTP");}
catch (e) {}
}
function postDataToURL(data, url, callback) {
if (!callback) callback = postDataToURL.defaultCallback;
var request = createXMLHttpRequest();
if (!request) return callback(Error('XMLHttpRequest is unsupported'));
postDataToURL.running = (postDataToURL.running||0) + 1;
request.onreadystatechange = function(){
if (request.readyState != 4) return;
request.onreadystatechange = null;
postDataToURL.running = (postDataToURL.running||0) - 1;
callback(request.status == 200 ? null : request.status, request.responseText);
};
request.open('POST', url);
request.setRequestHeader('Content-Type', 'application/json');
request.send(JSON.stringify(data));
}
postDataToURL.defaultCallback = function(error){
// console.log('postDataToURL.defaultCallback', arguments)
}
-62
View File
@@ -1,62 +0,0 @@
testImageURL._recycle = function(img){
console.log('_recycle', img);
try {
img.src = '';
img.onload = img.onerror = null;
} catch(e){}
testImageURL._recycleBin.push(img);
}
testImageURL.getImage = function(callback){
// if (!testImageURL._recycleBin) testImageURL._recycleBin = [new Image(),new Image(),new Image(),new Image()];
// function get(){
// if (testImageURL._recycleBin.length === 0) return setTimeout(get, 100);
// callback(testImageURL._recycleBin.shift(), testImageURL._recycle);
// }
// get();
callback(new Image(), function recycle(){});
}
testImageURL.defaultCallback = function(error, event){}
function testImageURL(url, timeout, callback){
if (typeof timeout == 'function'){
callback = timeout;
timeout = testImageURL.timeout;
}
if (typeof callback != 'function') callback = testImageURL.defaultCallback;
testImageURL.getImage(function(img, done){
function callbackWrapper(error, event){
callbackWrapper = testImageURL.noop;
testImageURL.running = (testImageURL.running || 0) - 1;
clearTimeout(timer);
done(img);
img = url = timeout = null;
callback(error, event);
error = event = callback = null;
}
var timer = setTimeout(function(){callbackWrapper(Error('timeout'));}, timeout);
try {
img.onload = function(event){ callbackWrapper(null, event || window.event); };
img.onerror = function(error){ callbackWrapper(error); };
img.src = url;
testImageURL.running = (testImageURL.running || 0) + 1;
if (img.complete === true
|| img.readyState == 4
|| img.width > 0
|| img.height > 0
|| img.readyState == 'complete'
) callbackWrapper(null, null);
}
catch(error){
callbackWrapper(error);
}
});
}
testImageURL.noop = function(){};
testImageURL.timeout = 5000;