diff --git a/Gruntfile.js b/Gruntfile.js index d51c8a1..bf781e5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,6 +37,13 @@ module.exports = function(grunt) { base: './', keepalive: true } + }, + ci: { + options: { + port: 8080, + base: './', + keepalive: false + } } }, uglify: { @@ -65,7 +72,7 @@ module.exports = function(grunt) { if (arguments.length) { selenium[arg1].apply(null, arguments); } else { - selenium.tests(); + selenium.tests().onValue(done); } }); @@ -81,6 +88,6 @@ module.exports = function(grunt) { grunt.registerTask('server', ['connect']); grunt.registerTask('build', ['concat', 'uglify']); grunt.registerTask('default', ['jshint', 'concat', 'qunit', 'uglify']); - grunt.registerTask('travis', ['jshint', 'concat','qunit', 'uglify', 'webdriver']); + grunt.registerTask('travis', ['jshint', 'concat','qunit', 'uglify', 'connect:ci', 'webdriver']); }; diff --git a/package.json b/package.json index 56460df..a6d1a0d 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,9 @@ "grunt-contrib-jshint": "*", "grunt-contrib-qunit": "*", "grunt-contrib-watch": "~0.5.1", - "googleapis": "~0.4.3", - "jwt-sign": "~0.1.0", "base64-arraybuffer": ">= 0.1.0", "png-js": ">= 0.1.1", - "sync-webdriver": ">=0.1.1", - "express": "~3.2.3", - "baconjs": "~0.3.15", + "baconjs": "0.7.11", "wd": "~0.2.7", "grunt-contrib-connect": "~0.6.0" }, diff --git a/tests/selenium.js b/tests/selenium.js index af29b5c..85c777a 100644 --- a/tests/selenium.js +++ b/tests/selenium.js @@ -1,20 +1,16 @@ (function(){ "use strict;"; - var WebDriver = require('sync-webdriver'), - Bacon = require('baconjs').Bacon, - express = require('express'), + var Bacon = require('baconjs').Bacon, + wd = require('wd'), http = require("http"), https = require("https"), url = require("url"), path = require("path"), base64_arraybuffer = require('base64-arraybuffer'), PNG = require('png-js'), - fs = require("fs"), - googleapis = require('googleapis'), - jwt = require('jwt-sign'); + fs = require("fs"); var port = 8080, - app = express(), colors = { red: "\x1b[1;31m", blue: "\x1b[1;36m", @@ -23,16 +19,6 @@ clear: "\x1b[0m" }; - var server = app.listen(port); - - app.use('/index.html', function(req, res){ - res.send(""); - }); - - app.use('/', express.static(__dirname + "/../")); - function mapStat(item) { return Bacon.combineTemplate({ stat: Bacon.fromNodeCallback(fs.stat, item), @@ -81,14 +67,6 @@ return (100 - (Math.round((diff/h2cPixels.length) * 10000) / 100)); } - function canvasToDataUrl(canvas) { - return canvas.toDataURL("image/png").substring(22); - } - - function closeServer() { - server.close(); - } - function findResult(testName, tests) { var item = null; return tests.some(function(testCase) { @@ -140,288 +118,108 @@ } } - function httpget(options) { - return Bacon.fromCallback(function(callback) { - https.get(options, function(res){ - var data = ''; - - res.on('data', function (chunk){ - data += chunk; - }); - - res.on('end',function(){ - callback(data); - }); - }); - }); - } - - function parseJSON(str) { - return JSON.parse(str); - } - - function writeResults() { - Object.keys(results).forEach(function(browser) { - var filename = "tests/results/" + browser + ".json"; - try { - var oldResults = JSON.parse(fs.readFileSync(filename)); - compareResults(oldResults, results[browser], browser); - } catch(e) {} - - var date = new Date(); - var result = JSON.stringify({ - browser: browser, - results: results[browser], - timestamp: date.toISOString() - }); - - if (process.env.MONGOLAB_APIKEY) { - var options = { - host: "api.mongolab.com", - port: 443, - path: "/api/1/databases/html2canvas/collections/webdriver-results?apiKey=" + process.env.MONGOLAB_APIKEY + '&q={"browser":"' + browser + '"}&fo=true&s={"timestamp":-1}' - }; - - httpget(options).map(parseJSON).onValue(function(data) { - compareResults(data.results, results[browser], browser); - - options.method = 'POST'; - options.path = "/api/1/databases/html2canvas/collections/webdriver-results?apiKey=" + process.env.MONGOLAB_APIKEY; - options.headers = { - 'Content-Type': 'application/json', - 'Content-Length': result.length - }; - - console.log("Sending results for", browser); - var request = https.request(options, function(res) { - console.log(colors.green, "Results sent for", browser); - }); - - request.write(result); - request.end(); - }); - } - - console.log(colors.violet, "Writing", browser + ".json"); - fs.writeFile(filename, result); - }); - } - - function webdriverOptions(browserName, version, platform) { - var options = {}; - if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) { - options = { - port: 4445, - hostname: "localhost", - name: process.env.TRAVIS_JOB_ID || "Manual run", - username: process.env.SAUCE_USERNAME, - password: process.env.SAUCE_ACCESS_KEY, - desiredCapabilities: { - browserName: browserName, - version: version, - platform: platform, - "tunnel-identifier": process.env.TRAVIS_JOB_NUMBER - } - }; - } - return options; - } - - function mapResults(result) { - if (!results[result.browser]) { - results[result.browser] = []; - } - - results[result.browser].push({ - test: result.testCase, - result: result.accuracy - }); - } - function formatResultName(navigator) { - return (navigator.browser + "-" + ((navigator.version) ? navigator.version : "release") + "-" + navigator.platform).replace(/ /g, "").toLowerCase(); + return (navigator.browserName + "-" + ((navigator.version) ? navigator.version : "release") + "-" + navigator.platform).replace(/ /g, "").toLowerCase(); } - function webdriverStream(navigator) { - var drive = Bacon.fromCallback(discover, "drive", "v2").toProperty(); - var auth = Bacon.fromCallback(createToken, "95492219822.apps.googleusercontent.com").toProperty(); - - return Bacon.fromCallback(function(callback) { - new WebDriver.Session(webdriverOptions(navigator.browser, navigator.version, navigator.platform), function() { - var browser = this; - - var resultStream = Bacon.fromArray(tests).flatMap(function(testCase) { - console.log(colors.green, "STARTING",formatResultName(navigator), testCase, colors.clear); - browser.url = "http://localhost:" + port + "/" + testCase + "?selenium"; - var canvas = browser.element(".html2canvas", 15000); - var dataUrl = Bacon.constant(browser.execute(canvasToDataUrl, canvas)); - var screenshot = Bacon.constant(browser.screenshot()); - var result = dataUrl.flatMap(getPixelArray).combine(screenshot.flatMap(getPixelArray), calculateDifference); - console.log(colors.green, "COMPLETE", formatResultName(navigator), testCase, colors.clear); - return Bacon.combineTemplate({ - browser: formatResultName(navigator), - testCase: testCase, - accuracy: result, - dataUrl: dataUrl, - screenshot: screenshot + function webdriverStream(test) { + var browser = wd.remote("ondemand.saucelabs.com", 80, process.env.SAUCE_USERNAME, process.env.SAUCE_ACCESS_KEY); + var browserStream = new Bacon.Bus(); + var resultStream = Bacon.fromNodeCallback(browser, "init", test.capabilities) + .flatMap(Bacon.fromNodeCallback(browser, "setImplicitWaitTimeout", 15000) + .flatMap(function() { + Bacon.later(0, formatResultName(test.capabilities)).onValue(browserStream.push); + return Bacon.fromArray(test.cases).zip(browserStream.take(test.cases.length)).flatMap(function(options) { + var testCase = options[0]; + var name = options[1]; + console.log(colors.green, "STARTING", name, testCase, colors.clear); + return Bacon.fromNodeCallback(browser, "get", "http://localhost:" + port + "/" + testCase + "?selenium") + .flatMap(Bacon.combineTemplate({ + dataUrl: Bacon.fromNodeCallback(browser, "elementByCssSelector", ".html2canvas").flatMap(function(canvas) { + return Bacon.fromNodeCallback(browser, "execute", "return arguments[0].toDataURL('image/png').substring(22)", [canvas]); + }), + screenshot: Bacon.fromNodeCallback(browser, "takeScreenshot") + })).flatMap(function(result) { + return Bacon.combineTemplate({ + browser: name, + testCase: testCase, + accuracy: Bacon.constant(result.dataUrl).flatMap(getPixelArray).combine(Bacon.constant(result.screenshot).flatMap(getPixelArray), calculateDifference), + dataUrl: result.dataUrl, + screenshot: result.screenshot + }); + }); }); - }); + })); - if (fs.existsSync('tests/certificate.pem')) { - Bacon.combineWith(permissionRequest, drive, auth, Bacon.combineWith(uploadRequest, drive, auth, resultStream.doAction(mapResults).flatMap(createImages)).flatMap(executeRequest)).flatMap(executeRequestOriginal).onValue(uploadImages); - } - - resultStream.onEnd(callback); - }); + resultStream.onError(function(error) { + console.log(colors.red, "ERROR", test.capabilities.browserName, error); + browserStream.push(formatResultName(test.capabilities)); }); - } - function permissionRequest(client, authClient, images) { - var body = { - value: 'me', - type: 'anyone', - role: 'reader' - }; - - return images.map(function(data) { - var request = client.drive.permissions.insert({fileId: data.id}).withAuthClient(authClient); - request.body = body; - request.fileData = data; - return request; + resultStream.onValue(function(result) { + console.log(colors.green, "COMPLETE", result.browser, result.testCase, result.accuracy, "%", colors.clear); + browserStream.push(result.browser); }); - } - function executeRequest(requests) { - return Bacon.combineAsArray(requests.map(function(request) { - return Bacon.fromCallback(function(callback) { - request.execute(function(err, result) { - if (!err) { - callback(result); - } else { - console.log("Google drive error", err); - } - }); - }); - })); - } + resultStream.onEnd(function() { + browser.quit(); + }); - function executeRequestOriginal(requests) { - return Bacon.combineAsArray(requests.map(function(request) { - return Bacon.fromCallback(function(callback) { - request.execute(function(err, result) { - if (!err) { - callback(request.fileData); - } else { - console.log("Google drive error", err); - } - }); - }); - })); + return resultStream.fold([], pushToArray); } function createImages(data) { - var dataurlFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-html2canvas.png"; - var screenshotFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-screencapture.png"; - return Bacon.combineTemplate({ - name: data.testCase, - dataurl: Bacon.fromNodeCallback(fs.writeFile, dataurlFileName, data.dataUrl, "base64").map(function() { - return dataurlFileName; - }), - screenshot: Bacon.fromNodeCallback(fs.writeFile, screenshotFileName, data.screenshot, "base64").map(function() { - return screenshotFileName; - }) - }); - } - - function uploadImages(results) { - results.forEach(function(result) { - console.log(result.webContentLink); + var dataurlFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-html2canvas.png"; + var screenshotFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-screencapture.png"; + return Bacon.combineTemplate({ + name: data.testCase, + dataurl: Bacon.fromNodeCallback(fs.writeFile, dataurlFileName, data.dataUrl, "base64").map(function() { + return dataurlFileName; + }), + screenshot: Bacon.fromNodeCallback(fs.writeFile, screenshotFileName, data.screenshot, "base64").map(function() { + return screenshotFileName; + }) }); } - function discover(api, version, callback) { - googleapis.discover(api, version).execute(function(err, client) { - if (!err) { - callback(client); - } - }); + function pushToArray(array, item) { + array.push(item); + return array; } - function createToken(account, callback) { - var payload = { - "iss": '95492219822@developer.gserviceaccount.com', - "scope": 'https://www.googleapis.com/auth/drive', - "aud":"https://accounts.google.com/o/oauth2/token", - "exp": ~~(new Date().getTime() / 1000) + (30 * 60), - "iat": ~~(new Date().getTime() / 1000 - 60) - }, - key = fs.readFileSync('tests/certificate.pem', 'utf8'), - transporterTokenRequest = { - method: 'POST', - uri: 'https://accounts.google.com/o/oauth2/token', - form: { - grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', - assertion: jwt.sign(payload, key) - }, - json: true - }, - oauth2Client = new googleapis.OAuth2Client(account, "", ""); - - oauth2Client.transporter.request(transporterTokenRequest, function(err, result) { - if (!err) { - oauth2Client.credentials = result; - callback(oauth2Client); - } - }); - } - - function uploadRequest(client, authClient, data) { - return [ - client.drive.files.insert({title: data.dataurl, mimeType: 'image/png', description: process.env.TRAVIS_JOB_ID}).withMedia('image/png', fs.readFileSync(data.dataurl)).withAuthClient(authClient), - client.drive.files.insert({title: data.screenshot, mimeType: 'image/png', description: process.env.TRAVIS_JOB_ID}).withMedia('image/png', fs.readFileSync(data.screenshot)).withAuthClient(authClient) - ]; - } - - function runWebDriver() { + function runWebDriver(cases) { var browsers = [ { - browser: "chrome", + browserName: "chrome", platform: "Windows 7" },{ - browser: "firefox", + browserName: "firefox", version: "15", platform: "Windows 7" },{ - browser: "internet explorer", + browserName: "internet explorer", version: "9", platform: "Windows 7" },{ - browser: "internet explorer", + browserName: "internet explorer", version: "10", platform: "Windows 8" },{ - browser: "safari", + browserName: "safari", version: "6", platform: "OS X 10.8" },{ - browser: "chrome", + browserName: "chrome", platform: "OS X 10.8" } ]; - var testRunnerStream = Bacon.sequentially(1000, browsers).flatMap(webdriverStream); - testRunnerStream.onEnd(writeResults); - testRunnerStream.onEnd(closeServer); + return Bacon.combineTemplate({ + capabilities: Bacon.sequentially(1000, browsers), + cases: cases + }).flatMap(webdriverStream); } - var tests = [], - results = {}, - testStream = getTests("tests/cases"); - - testStream.onValue(function(test) { - tests.push(test); - }); - - exports.tests = function() { - testStream.onEnd(runWebDriver); - }; -})(); \ No newline at end of file + exports.tests = function() { + return getTests("tests/cases").fold([], pushToArray).flatMap(runWebDriver).mapError(false); + }; +})();