diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..17740dc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,27 @@ +{ + // "parser": "babel-eslint", + "root": true, + "extends": "eslint:recommended", + "rules": { + "strict": 0, + "quotes": [0, "single"], + "comma-dangle": 0, + "curly": [1, "multi-line"], + "no-console": 0, + "no-use-before-define": [1, "nofunc"], + "no-unused-vars": [1, { + "vars": "local", + "varsIgnorePattern": "^_" + }], + "no-underscore-dangle": 0, + "new-cap": 0, + "consistent-return": 0, + "camelcase": 0, + "dot-notation": 0, + "semi": [1, "always"], + }, + env: { + // "es6": true, + "node": true + } +} diff --git a/.gitignore b/.gitignore index f67609d..e3648b8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ pids *.pid *.seed +# Temp files +.tmp* + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/.travis.yml b/.travis.yml index f0f5793..15d05aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,24 +5,15 @@ language: node_js node_js: - '0.10' + - '0.12' + - '4' + - '5' before_install: - sudo apt-get update - sudo apt-get install imagemagick graphicsmagick libcairo2-dev -before_script: - - "npm install -g bower http-server" - - "cd test/site && bower install && cd ../../" - - "curl -O http://selenium-release.storage.googleapis.com/2.43/selenium-server-standalone-2.43.1.jar" - - "java -jar selenium-server-standalone-2.43.1.jar -host 127.0.0.1 -port 4444 2>/dev/null 1>/dev/null &" - - "http-server -p 8080 &" - - "sleep 10" - - "if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then ./node_modules/.bin/istanbul i lib -o lib-cov && cp lib/getPageInfo.js lib-cov && cp lib/makeScreenshot.js lib-cov && cp lib/documentScreenshot.js lib-cov && cp lib/viewportScreenshot.js lib-cov && cp lib/startSession.js lib-cov && cp lib/setScreenWidth.js lib-cov; fi" - -script: "npm run-script travis" - -after_script: - - "if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then cat lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi" +script: "npm test" env: matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f610f7..549f546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Release History +## 2.0.0beta-rc1 (2015-10-28) +* merged dev branch to make WebdriverCSS compatible with v3 + ## v1.1.10 (2015-12-19) * Adjust scrolling to better support sticky headers (#131) * Add option to tweak node-resemble-js image comparison (#121) diff --git a/README.md b/README.md index a7a0240..d055a3b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -WebdriverCSS [![Version](http://img.shields.io/badge/version-v1.1.10-brightgreen.svg)](https://www.npmjs.org/package/webdrivercss) [![Build Status](https://travis-ci.org/webdriverio/webdrivercss.png?branch=master)](https://travis-ci.org/webdriverio/webdrivercss) [![Coverage Status](https://coveralls.io/repos/webdriverio/webdrivercss/badge.png?branch=master)](https://coveralls.io/r/webdriverio/webdrivercss?branch=master) [![Join the chat at https://gitter.im/webdriverio/webdrivercss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/webdriverio/webdrivercss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +WebdriverCSS [![Version](http://img.shields.io/badge/version-v2.0.0beta-rc1-brightgreen.svg)](https://www.npmjs.org/package/webdrivercss) [![Build Status](https://travis-ci.org/webdriverio/webdrivercss.png?branch=master)](https://travis-ci.org/webdriverio/webdrivercss) [![Coverage Status](https://coveralls.io/repos/webdriverio/webdrivercss/badge.png?branch=master)](https://coveralls.io/r/webdriverio/webdrivercss?branch=master) [![Join the chat at https://gitter.im/webdriverio/webdrivercss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/webdriverio/webdrivercss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ============ --- diff --git a/lib/asyncCallback.js b/lib/asyncCallback.js index 2fb5d38..091830f 100644 --- a/lib/asyncCallback.js +++ b/lib/asyncCallback.js @@ -4,53 +4,56 @@ * run workflow again or execute callback function */ -var workflow = require('./workflow.js'), - endSession = require('./endSession.js'); +var endSession = require('./endSession.js'); +var Promise = require('bluebird'); module.exports = function(err) { - - var that = this; + var workflow = require('./workflow'); + var ctx = this; /** * if error occured don't do another shot (if multiple screen width are set) */ /*istanbul ignore next*/ - if(err) { - return this.cb(err); - } - - /** - * on multiple screenWidth or multiple page elements - * repeat workflow - */ - if(this.screenWidth && this.screenWidth.length) { + if (err) return Promise.reject(err); + return Promise.try(function() { /** - * if multiple screen widths are given - * start workflow all over again with same parameter + * on multiple screenWidth or multiple page elements + * repeat workflow */ - this.queuedShots[0].screenWidth = this.screenWidth; - return workflow.call(this.self, this.pagename, this.queuedShots, this.cb); + if(ctx.screenWidth && ctx.screenWidth.length) { + + /** + * if multiple screen widths are given + * start workflow all over again with same parameter + */ + ctx.queuedShots[0].screenWidth = ctx.screenWidth; + return workflow.call(ctx.self, ctx.pagename, ctx.queuedShots); + + } else if (ctx.queuedShots.length > 1) { - } else if (this.queuedShots.length > 1) { + /** + * if multiple page modules are given + */ + return endSession.call(ctx) + .then(function() { + ctx.queuedShots.shift(); + return workflow.call(ctx.self, ctx.pagename, ctx.queuedShots, ctx.cb); + }); + + } /** - * if multiple page modules are given + * finish command */ - return endSession.call(this, function() { - that.queuedShots.shift(); - return workflow.call(that.self, that.pagename, that.queuedShots, that.cb); + return endSession.call(ctx) + .then(function() { + ctx.self.takeScreenshot = undefined; + return ctx.self.resultObject; + }) + .finally(function() { + ctx.self.resultObject = {}; }); - - } - - /** - * finish command - */ - return endSession.call(this, function(err) { - that.self.takeScreenshot = undefined; - that.cb(err, that.self.resultObject); - that.self.resultObject = {}; }); - }; diff --git a/lib/compareImages.js b/lib/compareImages.js index 15133a9..7e34447 100644 --- a/lib/compareImages.js +++ b/lib/compareImages.js @@ -5,38 +5,42 @@ */ var resemble = require('node-resemble-js'); +var Promise = require('bluebird'); module.exports = function() { - /** - * need to find done function because gm doesn't have node like callbacks (err,res) - */ - var done = arguments[arguments.length - 1]; - - /** - * if there is no need for image comparison or no images gets saved on fs, just continue - */ - if(!this.isComparable || !this.self.saveImages) { - return done(); - } - - /** - * compare images - */ - var diff = resemble(this.baselinePath).compareTo(this.regressionPath); - - /** - * map 'ignore' configuration to resemble options - */ - var ignore = this.currentArgs.ignore || ""; - if (ignore.indexOf("color") === 0) { - diff.ignoreColors(); - } else if (ignore.indexOf("antialias") === 0) { - diff.ignoreAntialiasing(); - } - - /** - * execute the comparison - */ - diff.onComplete(done.bind(null,null)); + var ctx = this; + + return Promise.try(function() { + /** + * if there is no need for image comparison or no images gets saved on fs, just continue + */ + if(!ctx.isComparable || !ctx.self.saveImages) { + return; + } + + /** + * compare images + */ + var diff = resemble(ctx.baselinePath).compareTo(ctx.regressionPath); + + /** + * map 'ignore' configuration to resemble options + */ + var ignore = ctx.currentArgs.ignore || ""; + if (ignore.indexOf("color") === 0) { + diff.ignoreColors(); + } else if (ignore.indexOf("antialias") === 0) { + diff.ignoreAntialiasing(); + } + + /** + * execute the comparison + */ + return new Promise(function(resolve, reject) { + diff.onComplete(function(res) { // this callback has bad arity + resolve(res); + }); + }); + }); }; diff --git a/lib/cropImage.js b/lib/cropImage.js index 8a7fc34..e2e3d3d 100644 --- a/lib/cropImage.js +++ b/lib/cropImage.js @@ -4,99 +4,94 @@ * crop image according to user arguments and its position on screen and save it */ -var gm = require('gm'), - async = require('async'), - request = require('request'), - exclude = require('./exclude.js'); +var Promise = require('bluebird'); +var gm = require('gm'); +Promise.promisifyAll(gm.prototype); +var request = Promise.promisify(require('request'), {multiArg: true}); +var exclude = require('./exclude.js'); -module.exports = function(res, done) { +module.exports = function(res) { - var that = this, - excludeRect = res.excludeRect, - shot = gm(this.screenshot).quality(100), - cropDim; + var ctx = this; + var excludeRect = res.excludeRect; + var shot = gm(ctx.screenshot).quality(100); + var cropDim; - var x = parseInt(this.currentArgs.x, 10); - var y = parseInt(this.currentArgs.y, 10); - var width = parseInt(this.currentArgs.width, 10); - var height = parseInt(this.currentArgs.height, 10); + return Promise.try(function() { - if (!isNaN(x) && !isNaN(y) && !isNaN(width) && !isNaN(height)) { + var x = parseInt(ctx.currentArgs.x, 10); + var y = parseInt(ctx.currentArgs.y, 10); + var width = parseInt(ctx.currentArgs.width, 10); + var height = parseInt(ctx.currentArgs.height, 10); - /** - * crop image with given arguments - */ - cropDim = { - x: x - res.scrollPos.x, - y: y - res.scrollPos.y, - width: width, - height: height - }; - - exclude(shot, excludeRect); - shot.crop(cropDim.width, cropDim.height, cropDim.x, cropDim.y); + if (!isNaN(x) && !isNaN(y) && !isNaN(width) && !isNaN(height)) { - } else if (res && res.elemBounding) { + /** + * crop image with given arguments + */ + cropDim = { + x: x - res.scrollPos.x, + y: y - res.scrollPos.y, + width: width, + height: height + }; - /** - * or use boundary of specific CSS element - */ - cropDim = { - x: res.elemBounding.left + (res.elemBounding.width / 2), - y: res.elemBounding.top + (res.elemBounding.height / 2), - width: isNaN(width) ? res.elemBounding.width : width, - height: isNaN(height) ? res.elemBounding.height : height - }; + exclude(shot, excludeRect); + shot.crop(cropDim.width, cropDim.height, cropDim.x, cropDim.y); - exclude(shot, excludeRect); - shot.crop(cropDim.width, cropDim.height, cropDim.x - (cropDim.width / 2), cropDim.y - (cropDim.height / 2)); + } else if (res && res.elemBounding) { - } else { - exclude(shot, excludeRect); - } + /** + * or use boundary of specific CSS element + */ + cropDim = { + x: res.elemBounding.left + (res.elemBounding.width / 2), + y: res.elemBounding.top + (res.elemBounding.height / 2), + width: isNaN(width) ? res.elemBounding.width : width, + height: isNaN(height) ? res.elemBounding.height : height + }; - async.waterfall([ - /** - * save image to fs - */ - function(cb) { - if(!that.self.saveImages) { - return cb(); - } + exclude(shot, excludeRect); + shot.crop(cropDim.width, cropDim.height, cropDim.x - (cropDim.width / 2), cropDim.y - (cropDim.height / 2)); - return shot.write(that.filename || that.baselinePath, cb); - }, - /** - * generate image buffer - */ - function() { - var cb = arguments[arguments.length - 1]; - return shot.toBuffer('PNG', cb); - }, + } else { + exclude(shot, excludeRect); + } + }) + .then(function() { + return Promise.try(function() { + if (!ctx.self.saveImages) return; + /** + * save image to fs + */ + return shot.writeAsync(ctx.filename || ctx.baselinePath); + }) + .then(function() { + /** + * generate image buffer + */ + return shot.toBufferAsync('PNG'); + }) /** * upload image to applitools */ - function(buffer) { - var cb = arguments[arguments.length - 1]; - if (!that.self.usesApplitools) { - return cb(); - } - request({ - qs: {apiKey: that.applitools.apiKey}, - url: that.self.host + '/api/sessions/running/' + that.self.sessionId, + .then(function(buffer) { + if (!ctx.self.usesApplitools) return; + return request({ + qs: {apiKey: ctx.applitools.apiKey}, + url: ctx.self.host + '/api/sessions/running/' + ctx.self.sessionId, method: 'POST', - headers: that.self.headers, - timeout: that.self.reqTimeout, + headers: ctx.self.headers, + timeout: ctx.self.reqTimeout, json: { 'appOutput': { 'title': res.title, 'screenshot64': new Buffer(buffer).toString('base64') }, - 'tag': that.currentArgs.tag || '', - 'ignoreMismatch': that.currentArgs.ignoreMismatch || false + 'tag': ctx.currentArgs.tag || '', + 'ignoreMismatch': ctx.currentArgs.ignoreMismatch || false } - }, cb); - } - ], done); - + }); + }); + }); }; diff --git a/lib/documentScreenshot.js b/lib/documentScreenshot.js index 07ef210..dc337e3 100644 --- a/lib/documentScreenshot.js +++ b/lib/documentScreenshot.js @@ -24,228 +24,209 @@ /* global document,window */ -var async = require('async'), - fs = require('fs'), - gm = require('gm'), - rimraf = require('rimraf'), - generateUUID = require('./generateUUID.js'), - path = require('path'); +var Promise = require('bluebird'); +var fs = Promise.promisifyAll(require('fs')); +var gm = require('gm'); +Promise.promisifyAll(gm.prototype); +var rimraf = Promise.promisify(require('rimraf')); +var generateUUID = require('./generateUUID.js'); +var path = require('path'); module.exports = function documentScreenshot(fileName) { var ErrorHandler = this.instance.ErrorHandler; - /*! - * make sure that callback contains chainit callback - */ - var callback = arguments[arguments.length - 1]; - /*! * parameter check */ if (typeof fileName !== 'string') { - return callback(new ErrorHandler.CommandError('number or type of arguments don\'t agree with saveScreenshot command')); + return Promise.reject( + new ErrorHandler.CommandError('first parameter to documentScreenshot() must be a string fileName')); } - var self = this.instance, - response = { - execute: [], - screenshot: [] - }, - tmpDir = null, - cropImages = [], - currentXPos = 0, - currentYPos = 0, - screenshot = null, - scrollFn = function(w, h) { - /** - * IE8 or older - */ - if(document.all && !document.addEventListener) { - /** - * this still might not work - * seems that IE8 scroll back to 0,0 before taking screenshots - */ - document.body.style.marginTop = '-' + h + 'px'; - document.body.style.marginLeft = '-' + w + 'px'; - return; - } + var self = this.instance; + var response = { + execute: [], + screenshot: [] + }; + var tmpDir = null; + var cropImages = []; + var currentXPos = 0; + var currentYPos = 0; + var screenshot = null; - document.documentElement.style.webkitTransform = 'translate(-' + w + 'px, -' + h + 'px)'; - document.documentElement.style.mozTransform = 'translate(-' + w + 'px, -' + h + 'px)'; - document.documentElement.style.msTransform = 'translate(-' + w + 'px, -' + h + 'px)'; - document.documentElement.style.oTransform = 'translate(-' + w + 'px, -' + h + 'px)'; - document.documentElement.style.transform = 'translate(-' + w + 'px, -' + h + 'px)'; - }; + /*! + * create tmp directory to cache viewport shots + */ + return Promise.try(function createTmp() { + var uuid = generateUUID(); + tmpDir = path.join(__dirname, '..', '.tmp-' + uuid); + + return fs.existsAsync(tmpDir) + .then(function(exists) { + if (!exists) return fs.mkdirAsync(tmpDir, '0755'); + }); + }) + /*! + * prepare page scan + */ + .then(function pageScan() { + return self.execute(getViewportSize); + }) + /*! + * take viewport shots and cache them into tmp dir + */ + .then(function takeShots(res) { + response.execute.push(res); + response.screenshot = []; + + function takeScreenshot() { + + // Take screenshot + return self.screenshot.call(self) + // Cache image into tmp dir + .then(function cacheImage(res) { + var file = tmpDir + '/' + currentXPos + '-' + currentYPos + '.png'; + var image = gm(new Buffer(res.value, 'base64')); + + if (response.execute[0].value.devicePixelRatio > 1) { + var percent = 100 / response.execute[0].value.devicePixelRatio; + image.resize(percent, percent, "%"); + } - async.waterfall([ + image.crop(response.execute[0].value.screenWidth, response.execute[0].value.screenHeight, 0, 0); + response.screenshot.push(res); - /*! - * create tmp directory to cache viewport shots - */ - function(cb) { - var uuid = generateUUID(); - tmpDir = path.join(__dirname, '..', '.tmp-' + uuid); + if (!cropImages[currentXPos]) { + cropImages[currentXPos] = []; + } + + cropImages[currentXPos][currentYPos] = file; - fs.exists(tmpDir, function(exists) { - return exists ? cb() : fs.mkdir(tmpDir, '0755', cb); + currentYPos++; + if (currentYPos > Math.floor(response.execute[0].value.documentHeight / response.execute[0].value.screenHeight)) { + currentYPos = 0; + currentXPos++; + } + return image.writeAsync(file); + }) + // scroll to next area + .then(function scrollToNext() { + return self.execute(scrollFn, + currentXPos * response.execute[0].value.screenWidth, + currentYPos * response.execute[0].value.screenHeight + ) + .then(function (res) { + response.execute.push(res); + }) + // FIXME do we need this? + .pause(100); }); - }, + } - /*! - * prepare page scan - */ - function() { - var cb = arguments[arguments.length - 1]; - self.execute(function() { - /** - * remove scrollbars - */ - // reset height in case we're changing viewports - document.documentElement.style.height = 'auto'; - document.documentElement.style.height = document.documentElement.scrollHeight + 'px'; - document.documentElement.style.overflow = 'hidden'; - - /** - * scroll back to start scanning - */ - window.scrollTo(0, 0); - - /** - * get viewport width/height and total width/height - */ - return { - screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), - screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0), - documentWidth: document.documentElement.scrollWidth, - documentHeight: document.documentElement.scrollHeight, - devicePixelRatio: window.devicePixelRatio - }; - }, cb); - }, + // async while expression + function loop() { + return takeScreenshot() + .then(function checkIfDone() { + if (currentXPos < (response.execute[0].value.documentWidth / response.execute[0].value.screenWidth)) { + return loop(); + } + }); + } /*! - * take viewport shots and cache them into tmp dir + * run scan */ - function(res, cb) { - response.execute.push(res); - - /*! - * run scan - */ - async.whilst( - - /*! - * while expression - */ - function() { - return (currentXPos < (response.execute[0].value.documentWidth / response.execute[0].value.screenWidth)); - }, - - /*! - * loop function - */ - function(finishedScreenshot) { - response.screenshot = []; - - async.waterfall([ - - /*! - * take screenshot of viewport - */ - self.screenshot.bind(self), - - /*! - * cache image into tmp dir - */ - function(res, cb) { - var file = tmpDir + '/' + currentXPos + '-' + currentYPos + '.png'; - var image = gm(new Buffer(res.value, 'base64')); - - if (response.execute[0].value.devicePixelRatio > 1) { - var percent = 100 / response.execute[0].value.devicePixelRatio; - image.resize(percent, percent, "%"); - } - - image.crop(response.execute[0].value.screenWidth, response.execute[0].value.screenHeight, 0, 0); - image.write(file, cb); - response.screenshot.push(res); - - if (!cropImages[currentXPos]) { - cropImages[currentXPos] = []; - } - - cropImages[currentXPos][currentYPos] = file; - - currentYPos++; - if (currentYPos > Math.floor(response.execute[0].value.documentHeight / response.execute[0].value.screenHeight)) { - currentYPos = 0; - currentXPos++; - } - }, - - /*! - * scroll to next area - */ - function() { - self.execute(scrollFn, - currentXPos * response.execute[0].value.screenWidth, - currentYPos * response.execute[0].value.screenHeight, - function(val, res) { - response.execute.push(res); - } - ).pause(100).call(arguments[arguments.length - 1]); - } - - ], finishedScreenshot); - }, - cb - ); - }, + return loop(); + }) + // FIXME this seems like an optimization opportunity, we're thrashing disk; + // could also append in the loop above + .then(function concat() { + var subImg = 0; + return Promise.mapSeries(cropImages, function(item) { + var col = gm(item.shift()); + col.append.apply(col, item); + + if (!screenshot) { + screenshot = col; + return col.writeAsync(fileName); + } else { + return col.writeAsync(tmpDir + '/' + (++subImg) + '.png') + .then(function() { + return gm(fileName).append(tmpDir + '/' + subImg + '.png', true).writeAsync(fileName); + }); + } + }); + }) + /*! + * crop screenshot regarding page size + */ + .then(function cropScreenshot() { + return gm(fileName) + .crop(response.execute[0].value.documentWidth, response.execute[0].value.documentHeight, 0, 0) + .writeAsync(fileName); + }) + /*! + * remove tmp dir + */ + .then(function() { + return rimraf(tmpDir); + }) + /*! + * scroll back to start position + */ + .then(function() { + return self.execute(scrollFn, 0, 0); + }) + // Return response object. + .return(response); +}; - /*! - * concats all shots +/*eslint-disable*/ +function scrollFn(w, h) { + /** + * IE8 or older + */ + if(document.all && !document.addEventListener) { + /** + * this still might not work + * seems that IE8 scroll back to 0,0 before taking screenshots */ - function(cb) { - var subImg = 0; - - async.eachSeries(cropImages, function(x, cb) { - var col = gm(x.shift()); - col.append.apply(col, x); - - if (!screenshot) { - screenshot = col; - col.write(fileName, cb); - } else { - col.write(tmpDir + '/' + (++subImg) + '.png', function() { - gm(fileName).append(tmpDir + '/' + subImg + '.png', true).write(fileName, cb); - }); - } - }, cb); - }, + document.body.style.marginTop = '-' + h + 'px'; + document.body.style.marginLeft = '-' + w + 'px'; + return; + } - /*! - * crop screenshot regarding page size - */ - function() { - gm(fileName).crop(response.execute[0].value.documentWidth, response.execute[0].value.documentHeight, 0, 0).write(fileName, arguments[arguments.length - 1]); - }, + document.documentElement.style.webkitTransform = 'translate(-' + w + 'px, -' + h + 'px)'; + document.documentElement.style.mozTransform = 'translate(-' + w + 'px, -' + h + 'px)'; + document.documentElement.style.msTransform = 'translate(-' + w + 'px, -' + h + 'px)'; + document.documentElement.style.oTransform = 'translate(-' + w + 'px, -' + h + 'px)'; + document.documentElement.style.transform = 'translate(-' + w + 'px, -' + h + 'px)'; +}; - /*! - * remove tmp dir - */ - function() { - rimraf(tmpDir, arguments[arguments.length - 1]); - }, +function getViewportSize() { + /** + * remove scrollbars + */ + // reset height in case we're changing viewports + document.documentElement.style.height = 'auto'; + document.documentElement.style.height = document.documentElement.scrollHeight + 'px'; + document.documentElement.style.overflow = 'hidden'; - /*! - * scroll back to start position - */ - function(cb) { - self.execute(scrollFn, 0, 0, cb); - } - ], function(err) { - callback(err, null, response); - }); + /** + * scroll back to start scanning + */ + window.scrollTo(0, 0); -}; + /** + * get viewport width/height and total width/height + */ + return { + screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0), + documentWidth: document.documentElement.scrollWidth, + documentHeight: document.documentElement.scrollHeight, + devicePixelRatio: window.devicePixelRatio + }; +} +/*eslint-enable*/ diff --git a/lib/endSession.js b/lib/endSession.js index e50f617..9f55f00 100644 --- a/lib/endSession.js +++ b/lib/endSession.js @@ -1,68 +1,51 @@ 'use strict'; -var async = require('async'), - merge = require('deepmerge'), - request = require('request'); +var merge = require('deepmerge'); +var Promise = require('bluebird'); +var request = Promise.promisify(require('request'), {multiArgs: true}); -module.exports = function(done) { +module.exports = function() { - var that = this; + var ctx = this; - async.waterfall([ + return Promise.try(function setOriginalResolution() { /** * if screenwidth was set, get back to old resolution */ - function(cb) { - if (!that.self.defaultScreenDimension) { - return cb(); - } - - that.instance.windowHandleSize({ - width: that.self.defaultScreenDimension.width, - height: that.self.defaultScreenDimension.height - }, cb); - }, - /** - * end session when using applitools - */ - function() { - var cb = arguments[arguments.length - 1]; - - if(!that.self.usesApplitools) { - return cb(); - } - - // Whether or not we should automatically save this test as baseline. - var updateBaseline = (that.self.isNew && that.applitools.saveNewTests) || - (!that.self.isNew && that.applitools.saveFailedTests); - - return request({ - qs: {apiKey: that.applitools.apiKey, updateBaseline: updateBaseline}, - url: that.self.host + '/api/sessions/running/' + that.self.sessionId, - method: 'DELETE', - headers: that.self.headers, - timeout: that.self.reqTimeout - }, cb); - }, - /** - * clear session, store result - */ - function(res, body) { - var cb = arguments[arguments.length - 1]; - - if(body) { - that.self.resultObject[that.currentArgs.name] = merge({ - id: that.self.sessionId, - url: that.self.url - }, JSON.parse(body)); - that.self.url = undefined; - that.self.sessionId = undefined; - that.self.isNew = undefined; - } - return cb(); - } - - ], function(err) { - return done(err); + if (!ctx.self.defaultScreenDimension) return; + + return ctx.instance.windowHandleSize({ + width: ctx.self.defaultScreenDimension.width, + height: ctx.self.defaultScreenDimension.height + }); + }) + /** + * If we have an applitools session open, close it + */ + .then(function closeApplitools() { + if (!ctx.self.usesApplitools) return; + + // Whether or not we should automatically save this test as baseline. + var updateBaseline = (ctx.self.isNew && ctx.applitools.saveNewTests) || + (!ctx.self.isNew && ctx.applitools.saveFailedTests); + + return request({ + qs: {apiKey: ctx.applitools.apiKey, updateBaseline: updateBaseline}, + url: ctx.self.host + '/api/sessions/running/' + ctx.self.sessionId, + method: 'DELETE', + headers: ctx.self.headers, + timeout: ctx.self.reqTimeout + }) + .spread(function clearAndStore(res, body) { + if (!body) return; + + ctx.self.resultObject[ctx.currentArgs.name] = merge({ + id: ctx.self.sessionId, + url: ctx.self.url + }, JSON.parse(body)); + ctx.self.url = undefined; + ctx.self.sessionId = undefined; + ctx.self.isNew = undefined; + }); }); }; diff --git a/lib/getPageInfo.js b/lib/getPageInfo.js index 2b266c4..a89aac5 100644 --- a/lib/getPageInfo.js +++ b/lib/getPageInfo.js @@ -5,8 +5,8 @@ * IMPORTANT: all of this code gets executed on browser side, so you won't have * access to node specific interfaces at all */ -var async = require('async'), - merge = require('deepmerge'); +var Promise = require('bluebird'); +var merge = require('deepmerge'); /** * little helper function to check against argument values @@ -17,165 +17,160 @@ function isNumber(variable) { return typeof variable === 'number'; } -module.exports = function(done) { - var that = this, - response = { - excludeRect: [], - scrollPos: {x: 0, y:0}, - }, - excludeRect = [], - element = that.currentArgs.elem; +module.exports = function() { + var that = this; + var response = { + excludeRect: [], + scrollPos: {x: 0, y:0}, + }; + var excludeRect = []; + var element = that.currentArgs.elem; + + return Promise.resolve(that.instance.execute(getDocumentMeta)) + .then(function getElementInformation(res) { + response = merge(response, res.value); + + if(!element) { + return {}; + } - async.waterfall([ /** - * get page information + * needs to get defined that verbose to make it working in IE driver */ - function(cb) { - that.instance.execute(function() { - /** - * get current scroll position - * @return {Object} x and y coordinates of current scroll position - */ - var getScrollPosition = function() { - var x = 0, - y = 0; + return that.instance.selectorExecute(element, getElemBoundingRect); + }) + /** + * get information about exclude elements + */ + .then(function getExcludeInfo(res) { + response = merge(response, res); - if (typeof window.pageYOffset === 'number') { - - /* Netscape compliant */ - y = window.pageYOffset; - x = window.pageXOffset; + /** + * concatenate exclude elements to one dimensional array + * excludeElements = elements queried by specific selector strategy (typeof string) + * excludeCoords = x & y coords to exclude custom areas + */ + var excludeElements = []; - } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { + if (!that.currentArgs.exclude) { + return []; + } else if (!(that.currentArgs.exclude instanceof Array)) { + that.currentArgs.exclude = [that.currentArgs.exclude]; + } - /* DOM compliant */ - y = document.body.scrollTop; - x = document.body.scrollLeft; + that.currentArgs.exclude.forEach(function(excludeElement) { + if (typeof excludeElement === 'string') { + excludeElements.push(excludeElement); + } else { + /** + * excludeCoords are a set of x,y rectangle + * then just check if the first 4 coords are numbers (minumum to span a rectangle) + */ + if (isNumber(excludeElement.x0) && isNumber(excludeElement.x1) && + isNumber(excludeElement.y0) && isNumber(excludeElement.y1)) { + response.excludeRect.push(excludeElement); + } + } + }); - } else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) { + // Bail if no excludes + if(excludeElements.length === 0) { + return []; + } - /* IE6 standards compliant mode */ - y = document.documentElement.scrollTop; - x = document.documentElement.scrollLeft; + return that.instance.selectorExecute(excludeElements, getExcludeRects); + }) + .then(function(excludeElements) { + if(excludeElements && excludeElements.length) { + response.excludeRect = excludeRect.concat(excludeElements); + } + return response; + }); +}; - } +/*eslint-disable*/ +function getDocumentMeta() { + /** + * get current scroll position + * @return {Object} x and y coordinates of current scroll position + */ + var getScrollPosition = function() { + var x = 0, + y = 0; - return { - x: x, - y: y - }; - }; + if (typeof window.pageYOffset === 'number') { - return { - title: document.title, - scrollPos: getScrollPosition(), - screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), - screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - }; + /* Netscape compliant */ + y = window.pageYOffset; + x = window.pageXOffset; - }, cb); - }, + } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { - /** - * get element information - */ - function(res, cb) { - response = merge(response, res.value); + /* DOM compliant */ + y = document.body.scrollTop; + x = document.body.scrollLeft; - if(!element) { - return cb(null, {}, {}); - } + } else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) { - /** - * needs to get defined that verbose to make it working in IE driver - */ - that.instance.selectorExecute(element, function(elem) { - var boundingRect = elem[0].getBoundingClientRect(); - return { - elemBounding: { - width: boundingRect.width ? boundingRect.width : boundingRect.right - boundingRect.left, - height: boundingRect.height ? boundingRect.height : boundingRect.bottom - boundingRect.top, - top: boundingRect.top, - right: boundingRect.right, - bottom: boundingRect.bottom, - left: boundingRect.left - } - }; - }, cb); - }, + /* IE6 standards compliant mode */ + y = document.documentElement.scrollTop; + x = document.documentElement.scrollLeft; - /** - * get information about exclude elements - */ - function(res, responses, done) { - response = merge(response, res); - - /** - * concatenate exclude elements to one dimensional array - * excludeElements = elements queried by specific selector strategy (typeof string) - * excludeCoords = x & y coords to exclude custom areas - */ - var excludeElements = []; - - if (!that.currentArgs.exclude) { - return done(null, []); - } else if (!(that.currentArgs.exclude instanceof Array)) { - that.currentArgs.exclude = [that.currentArgs.exclude]; - } - - that.currentArgs.exclude.forEach(function(excludeElement) { - if (typeof excludeElement === 'string') { - excludeElements.push(excludeElement); - } else { - /** - * excludeCoords are a set of x,y rectangle - * then just check if the first 4 coords are numbers (minumum to span a rectangle) - */ - if (isNumber(excludeElement.x0) && isNumber(excludeElement.x1) && isNumber(excludeElement.y0) && isNumber(excludeElement.y1)) { - response.excludeRect.push(excludeElement); - } - } - }); - - if(excludeElements.length === 0) { - return done(null, []); - } + } - that.instance.selectorExecute(excludeElements, function() { + return { + x: x, + y: y + }; + }; + + return { + title: document.title, + scrollPos: getScrollPosition(), + screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) + }; +} - /** - * excludeElements are elements queried by specific selenium strategy - */ - var excludeElements = Array.prototype.slice.call(arguments), - excludeRect = []; +function getExcludeRects() { - excludeElements.forEach(function(elements) { + /** + * excludeElements are elements queried by specific selenium strategy + */ + var excludeElements = Array.prototype.slice.call(arguments), + excludeRect = []; - if(!elements) { - return; - } + excludeElements.forEach(function(elements) { - elements.forEach(function(elem) { - var elemRect = elem.getBoundingClientRect(); - excludeRect.push({ - x0: elemRect.left, - y0: elemRect.top, - x1: elemRect.right, - y1: elemRect.bottom - }); - }); - }); + if(!elements) { + return; + } - return excludeRect; + elements.forEach(function(elem) { + var elemRect = elem.getBoundingClientRect(); + excludeRect.push({ + x0: elemRect.left, + y0: elemRect.top, + x1: elemRect.right, + y1: elemRect.bottom + }); + }); + }); - }, done); - } - ], function(err, excludeElements) { + return excludeRect; +} - if(excludeElements && excludeElements.length) { - response.excludeRect = excludeRect.concat(excludeElements); +function getElemBoundingRect(elem) { + var boundingRect = elem[0].getBoundingClientRect(); + return { + elemBounding: { + width: boundingRect.width ? boundingRect.width : boundingRect.right - boundingRect.left, + height: boundingRect.height ? boundingRect.height : boundingRect.bottom - boundingRect.top, + top: boundingRect.top, + right: boundingRect.right, + bottom: boundingRect.bottom, + left: boundingRect.left } - - done(err, response); - }); -}; + }; +} +/*eslint-enable*/ diff --git a/lib/makeScreenshot.js b/lib/makeScreenshot.js index 5c2cd99..a0b4f00 100644 --- a/lib/makeScreenshot.js +++ b/lib/makeScreenshot.js @@ -1,45 +1,51 @@ 'use strict'; +var Promise = require('bluebird'); + /** * make screenshot via [GET] /session/:sessionId/screenshot */ var modifyElements = function(elements, style, value) { if(elements.length === 0) { - return; + return Promise.resolve(); } - this.instance.selectorExecute(elements, function() { - var args = Array.prototype.slice.call(arguments).filter(function(n){ return !!n; }), - style = args[args.length - 2], - value = args[args.length - 1]; + return this.instance.selectorExecute(elements, applyStyle, style, value); +}; + +function applyStyle() { + var args = Array.prototype.slice.call(arguments).filter(function(n){ return !!n; }), + style = args[args.length - 2], + value = args[args.length - 1]; - args.splice(-2); - for(var i = 0; i < args.length; ++i) { - for(var j = 0; j < args[i].length; ++j) { - args[i][j].style[style] = value; - } + args.splice(-2); + for(var i = 0; i < args.length; ++i) { + for(var j = 0; j < args[i].length; ++j) { + args[i][j].style[style] = value; } + } - }, style, value); -}; +} -module.exports = function(done) { +module.exports = function() { + var ctx = this; /** * take actual screenshot in given screensize just once */ - if(this.self.takeScreenshot === false) { - return done(); + if(ctx.self.takeScreenshot === false) { + return Promise.resolve(); } - this.self.takeScreenshot = false; + ctx.self.takeScreenshot = false; /** * gather all elements to hide */ var hiddenElements = [], removeElements = []; - this.queuedShots.forEach(function(args) { + + ctx.queuedShots.forEach(function(args) { if(typeof args.hide === 'string') { hiddenElements.push(args.hide); } @@ -57,18 +63,23 @@ module.exports = function(done) { /** * hide / remove elements */ - modifyElements.call(this, hiddenElements, 'visibility', 'hidden'); - modifyElements.call(this, removeElements, 'display', 'none'); - - /** - * take 100ms pause to give browser time for rendering - */ - this.instance.pause(100).saveDocumentScreenshot(this.screenshot, done); - - /** - * make hidden elements visible again - */ - modifyElements.call(this, hiddenElements, 'visibility', ''); - modifyElements.call(this, removeElements, 'display', ''); - + return Promise.all([ + modifyElements.call(ctx, hiddenElements, 'visibility', 'hidden'), + modifyElements.call(ctx, removeElements, 'display', 'none') + ]) + .then(function () { + /** + * take 100ms pause to give browser time for rendering + */ + return ctx.instance.pause(100).saveDocumentScreenshot(ctx.screenshot); + }) + .then(function () { + /** + * make hidden elements visible again + */ + return Promise.all([ + modifyElements.call(ctx, hiddenElements, 'visibility', ''), + modifyElements.call(ctx, removeElements, 'display', '') + ]); + }); }; diff --git a/lib/renameFiles.js b/lib/renameFiles.js index d3feb48..84f5276 100644 --- a/lib/renameFiles.js +++ b/lib/renameFiles.js @@ -1,31 +1,32 @@ 'use strict'; -var glob = require('glob'), - fs = require('fs'); +var Promise = require('bluebird'); +var glob = Promise.promisify(require('glob')); +var fs = require('fs'); +Promise.promisifyAll(fs); module.exports = function() { - var done = arguments[arguments.length - 1]; + var ctx = this; - glob('{' + this.regressionPath + ',' + this.baselinePath + '}', {}, function(err,files) { + return glob('{' + ctx.regressionPath + ',' + ctx.baselinePath + '}', {}) + .then(function(files) { /** * if no files were found continue */ if(files.length === 0) { - return done(); + return; } - this.isComparable = true; - this.filename = this.regressionPath; + ctx.isComparable = true; + ctx.filename = ctx.regressionPath; /** * rename existing files */ - if(files.length === 2 && this.updateBaseline && !this.self.usesApplitools) { - return fs.rename(this.regressionPath, this.baselinePath, done); - } else { - return done(); + if(files.length === 2 && ctx.updateBaseline && !ctx.self.usesApplitools) { + return fs.renameAsync(ctx.regressionPath, ctx.baselinePath); } - }.bind(this)); + }); }; diff --git a/lib/saveImageDiff.js b/lib/saveImageDiff.js index f94ce97..9316b87 100644 --- a/lib/saveImageDiff.js +++ b/lib/saveImageDiff.js @@ -1,105 +1,99 @@ 'use strict'; -var fs = require('fs'), - async = require('async'), - logWarning = require('./logWarning.js'); +var Promise = require('bluebird'); +var fs = require('fs'); +Promise.promisifyAll(fs); +var logWarning = require('./logWarning.js'); +var streamToPromise = require('./util/streamToPromise'); + +module.exports = function(imageDiff) { + + var ctx = this; + + return Promise.try(function() { + if(!imageDiff) { + ctx.self.resultObject[ctx.currentArgs.name].push({ + baselinePath: ctx.baselinePath, + message: 'first image of module "' + ctx.currentArgs.name + '" from page "' + ctx.pagename + '" successfully taken', + misMatchPercentage: 0, + isExactSameImage: true, + isSameDimensions: true, + isWithinMisMatchTolerance: true, + properties: ctx.currentArgs + }); -module.exports = function(imageDiff,done) { + return; + } - var that = this, - misMatchTolerance = parseFloat(imageDiff.misMatchPercentage,10); + /** + * if set misMatchTolerance is smaller then compared misMatchTolerance + * make image diff + */ + var misMatchTolerance = parseFloat(imageDiff.misMatchPercentage,10); + if(ctx.misMatchTolerance < misMatchTolerance) { - if(typeof imageDiff === 'function') { - this.self.resultObject[this.currentArgs.name].push({ - baselinePath: this.baselinePath, - message: 'first image of module "' + this.currentArgs.name + '" from page "' + this.pagename + '" successfully taken', - misMatchPercentage: 0, - isExactSameImage: true, - isSameDimensions: true, - isWithinMisMatchTolerance: true, - properties: this.currentArgs - }); + /*istanbul ignore next*/ + if(!imageDiff.isSameDimensions) { + logWarning.call(ctx.instance, 'DimensionWarning'); + } - return imageDiff(); - } + ctx.self.resultObject[ctx.currentArgs.name].push({ + baselinePath: ctx.baselinePath, + regressionPath: ctx.regressionPath, + diffPath: ctx.diffPath, + message: 'mismatch tolerance exceeded (+' + (misMatchTolerance - ctx.misMatchTolerance) + '), image-diff created', + misMatchPercentage: misMatchTolerance, + isExactSameImage: false, + isSameDimensions: imageDiff.isSameDimensions, + isWithinMisMatchTolerance: false, + properties: ctx.currentArgs + }); - /** - * if set misMatchTolerance is smaller then compared misMatchTolerance - * make image diff - */ - if(this.misMatchTolerance < misMatchTolerance) { - /*istanbul ignore next*/ - if(!imageDiff.isSameDimensions) { - logWarning.call(this.instance, 'DimensionWarning'); + var stream = imageDiff.getDiffImage(); + imageDiff.getDiffImage().pack().pipe(fs.createWriteStream(ctx.diffPath)); + return streamToPromise(stream); } - this.self.resultObject[this.currentArgs.name].push({ - baselinePath: this.baselinePath, - regressionPath: this.regressionPath, - diffPath: this.diffPath, - message: 'mismatch tolerance exceeded (+' + (misMatchTolerance - this.misMatchTolerance) + '), image-diff created', - misMatchPercentage: misMatchTolerance, - isExactSameImage: false, - isSameDimensions: imageDiff.isSameDimensions, - isWithinMisMatchTolerance: false, - properties: this.currentArgs - }); - - imageDiff.getDiffImage().pack() - .on('end', done.bind(null, null, this.resultObject)) - .pipe(fs.createWriteStream(this.diffPath)); - - } else { - - /** - * otherwise delete diff - */ - - async.waterfall([ + /** + * otherwise delete diff + */ + else { /** * check if diff shot exists */ - function(done) { - fs.exists(that.diffPath,done.bind(null,null)); - }, - /** - * remove diff if yes - */ - function(exists,done) { - if(exists) { - fs.unlink(that.diffPath,done); - } else { - done(); - } - }, + return fs.unlinkAsync(ctx.diffPath) + .catch(function(ignoredErr) { + return; // ignore + }) /** * Save a new baseline image, if one doesn't already exist. * * If one does exist, we delete the temporary regression. */ - function(done) { - fs.exists(that.baselinePath, function(exists) { - return !!exists ? fs.unlink(that.regressionPath, done) : fs.rename(that.regressionPath, that.baselinePath, done); + .then(function saveNewBaseline() { + return fs.statAsync(ctx.baselinePath) + .then(function(exists) { + return fs.unlinkAsync(ctx.regressionPath); + }) + .catch(function(e) { + return fs.renameAsync(ctx.regressionPath, ctx.baselinePath); + }); + }) + .then(function setResults() { + /** + * return result object to WebdriverIO instance + */ + ctx.self.resultObject[ctx.currentArgs.name].push({ + baselinePath: ctx.baselinePath, + message: 'mismatch tolerance not exceeded (~' + misMatchTolerance + '), baseline didn\'t change', + misMatchPercentage: misMatchTolerance, + isExactSameImage: misMatchTolerance === 0, + isSameDimensions: imageDiff.isSameDimensions, + isWithinMisMatchTolerance: true }); - } - ], function(err) { - - /** - * return result object to WebdriverIO instance - */ - that.self.resultObject[that.currentArgs.name].push({ - baselinePath: that.baselinePath, - message: 'mismatch tolerance not exceeded (~' + misMatchTolerance + '), baseline didn\'t change', - misMatchPercentage: misMatchTolerance, - isExactSameImage: misMatchTolerance === 0, - isSameDimensions: imageDiff.isSameDimensions, - isWithinMisMatchTolerance: true }); + } + }) - done(err); - - }); - - } }; diff --git a/lib/setScreenWidth.js b/lib/setScreenWidth.js index 3f53e21..89ed844 100644 --- a/lib/setScreenWidth.js +++ b/lib/setScreenWidth.js @@ -4,80 +4,66 @@ * if multiple screen width are given resize browser dimension */ -var async = require('async'), - takenScreenSizes = {}; +var takenScreenSizes = {}; +var Promise = require('bluebird'); -module.exports = function(done) { +module.exports = function() { var that = this; this.newScreenSize = {}; - async.waterfall([ + return Promise.try(function getResolution() { /** * get current browser resolution to change back to it * after all shots were taken (only if a screenWidth is set) */ - function(cb) { - if(!that.self.defaultScreenDimension && that.screenWidth && that.screenWidth.length) { - that.instance.windowHandleSize(function(err,res) { - that.self.defaultScreenDimension = res.value; - cb(); - }); - } else { - cb(); - } - }, - function(cb) { - - if(!that.screenWidth || that.screenWidth.length === 0) { - - /** - * if no screenWidth option was set just continue - */ - return cb(); - - } - - that.newScreenSize.width = parseInt(that.screenWidth.shift(), 10); - that.newScreenSize.height = parseInt(that.self.defaultScreenDimension.height, 10); - - that.self.takeScreenshot = false; - if(!takenScreenSizes[that.pagename] || takenScreenSizes[that.pagename].indexOf(that.newScreenSize.width) < 0) { - /** - * set flag to retake screenshot - */ - that.self.takeScreenshot = true; + if(!that.self.defaultScreenDimension && that.screenWidth && that.screenWidth.length) { + return that.instance.windowHandleSize() + .then(function(res) { + that.self.defaultScreenDimension = res.value; + }); + } + }) + .then(function() { + if(!that.screenWidth || that.screenWidth.length === 0) { + /** + * if no screenWidth option was set just continue + */ + return; + } - /** - * cache already taken screenshot / screenWidth combinations - */ - if(!takenScreenSizes[that.pagename]) { - takenScreenSizes[that.pagename] = [that.newScreenSize.width]; - } else { - takenScreenSizes[that.pagename].push(that.newScreenSize.width); - } - } + that.newScreenSize.width = parseInt(that.screenWidth.shift(), 10); + that.newScreenSize.height = parseInt(that.self.defaultScreenDimension.height, 10); + that.self.takeScreenshot = false; + if(!takenScreenSizes[that.pagename] || takenScreenSizes[that.pagename].indexOf(that.newScreenSize.width) < 0) { /** - * resize browser resolution + * set flag to retake screenshot */ - that.instance.call(function() { + that.self.takeScreenshot = true; - /** - * if shot will be taken in a specific screenWidth, rename file and append screen width - * value in filename - */ - that.baselinePath = that.baselinePath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); - that.regressionPath = that.regressionPath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); - that.diffPath = that.diffPath.replace(/\.(baseline|regression|diff)\.png/, '.' + that.newScreenSize.width + 'px.$1.png'); - that.screenshot = that.screenshot.replace(/\.png/, '.' + that.newScreenSize.width + 'px.png'); - that.filename = that.baselinePath; + /** + * cache already taken screenshot / screenWidth combinations + */ + if(!takenScreenSizes[that.pagename]) { + takenScreenSizes[that.pagename] = [that.newScreenSize.width]; + } else { + takenScreenSizes[that.pagename].push(that.newScreenSize.width); + } + } - that.instance.setViewportSize({width: that.newScreenSize.width, height: that.newScreenSize.height}) - .pause(100) - .call(cb); + /** + * resize browser resolution. + * if shot will be taken in a specific screenWidth, rename file and append screen width + * value in filename + */ + that.baselinePath = that.baselinePath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); + that.regressionPath = that.regressionPath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); + that.diffPath = that.diffPath.replace(/\.(baseline|regression|diff)\.png/, '.' + that.newScreenSize.width + 'px.$1.png'); + that.screenshot = that.screenshot.replace(/\.png/, '.' + that.newScreenSize.width + 'px.png'); + that.filename = that.baselinePath; - }); - } - ], done); + return that.instance.setViewportSize({width: that.newScreenSize.width, height: that.newScreenSize.height}) + .pause(100); + }); }; diff --git a/lib/startSession.js b/lib/startSession.js index 0e5fd27..6f497a3 100644 --- a/lib/startSession.js +++ b/lib/startSession.js @@ -1,85 +1,79 @@ 'use strict'; -var pkg = require('../package.json'), - request = require('request'), - async = require('async'), - WebdriverIO = require('webdriverio'); +var pkg = require('../package.json'); +var Promise = require('bluebird'); +var request = Promise.promisify(require('request')); +Promise.promisifyAll(request); +var WebdriverIO = require('webdriverio'); module.exports = function() { - var that = this, - done = arguments[arguments.length - 1]; - - /** - * skip when not using applitools - */ - if(!this.self.usesApplitools || this.self.sessionId) { - return done(); - } - - async.waterfall([ + var ctx = this; + return Promise.try(function() { /** - * get meta information of current session + * skip when not using applitools */ - function(cb) { - that.instance.execute(function() { - return { - useragent: navigator.userAgent, - screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), - documentHeight: document.documentElement.scrollHeight - }; - }, function(err, res) { - that.useragent = res.value.useragent; - that.displaySize = { - width: that.screenWidth && that.screenWidth.length ? that.screenWidth[0] : res.value.screenWidth, - height: res.value.documentHeight - }; - - return cb(); - }); - }, + if(!ctx.self.usesApplitools || ctx.self.sessionId) { + return; + } - /** - * initialise applitools session - */ - function(cb) { - request({ - url: that.self.host + '/api/sessions/running', - qs: {apiKey: that.applitools.apiKey}, + return Promise.resolve(ctx.instance.execute(getNavigator)) + .then(function(res) { + ctx.useragent = res.value.useragent; + ctx.displaySize = { + width: ctx.screenWidth && ctx.screenWidth.length ? ctx.screenWidth[0] : res.value.screenWidth, + height: res.value.documentHeight + }; + }) + .then(function() { + return request({ + url: ctx.self.host + '/api/sessions/running', + qs: {apiKey: ctx.applitools.apiKey}, method: 'POST', json: { 'startInfo': { - 'appIdOrName': that.applitools.appName, - 'scenarioIdOrName': that.currentArgs.name, + 'appIdOrName': ctx.applitools.appName, + 'scenarioIdOrName': ctx.currentArgs.name, 'batchInfo': { - 'id': that.applitools.batchId, - 'name': that.pagename, + 'id': ctx.applitools.batchId, + 'name': ctx.pagename, 'startedAt': new Date().toISOString() }, 'environment': { - 'displaySize': that.displaySize, - 'inferred': 'useragent:' + that.useragent + 'displaySize': ctx.displaySize, + 'inferred': 'useragent:' + ctx.useragent }, 'matchLevel': 'Strict', 'agentId': pkg.name + '/' + pkg.version } }, - headers: that.self.headers, - timeout: that.self.reqTimeout - }, cb); - - } - ], function(err, res, body) { - - if (err || res.statusCode !== 200 && res.statusCode !== 201) { - return done(new WebdriverIO.ErrorHandler.CommandError('Couldn\'t start applitools session')); - } - - that.self.sessionId = body.id; - that.self.url = body.url; - that.self.isNew = res.statusCode === 201; - return done(); + headers: ctx.self.headers, + timeout: ctx.self.reqTimeout + }); + }) + .then(function(res) { + if (res.statusCode !== 200 && res.statusCode !== 201) { + throw new Error(); + } + var body = res.body; + ctx.self.sessionId = body.id; + ctx.self.url = body.url; + ctx.self.isNew = res.statusCode === 201; + }) + .catch(function(err) { + throw new WebdriverIO.ErrorHandler.CommandError('Couldn\'t start applitools session: ' + err.stack); + }); }); }; + +/*eslint-disable*/ +function getNavigator() { + return { + useragent: navigator.userAgent, + screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + documentHeight: document.documentElement.scrollHeight + }; +} +/*eslint-enable*/ diff --git a/lib/syncImages.js b/lib/syncImages.js index e878ce3..bab9ae4 100644 --- a/lib/syncImages.js +++ b/lib/syncImages.js @@ -1,11 +1,17 @@ 'use strict'; -var fs = require('fs-extra'), - tar = require('tar'), - zlib = require('zlib'), - targz = require('tar.gz'), - rimraf = require('rimraf'), - request = require('request'); +var tar = require('tar'); +var zlib = require('zlib'); +var Promise = require('bluebird'); +var targz = require('tar.gz'); +Promise.promisifyAll(targz.prototype); +var fs = require('fs-extra'); +Promise.promisifyAll(fs); +var request = require('request'); +Promise.promisifyAll(request); +var rimraf = Promise.promisify(require('rimraf')); +var streamToPromise = require('./util/streamToPromise'); +var assign = require('object-assign'); /** * sync down @@ -13,52 +19,54 @@ var fs = require('fs-extra'), * * @param {Function} done callback to be called after sync finishes */ -var syncDown = function(done) { - - var args = { - url: this.api + (this.api.substr(-1) !== '/' ? '/' : '') + this.screenshotRoot + '.tar.gz', - headers: { 'accept-encoding': 'gzip,deflate' }, - }; - - if(typeof this.user === 'string' && typeof this.key === 'string') { - args.auth = { - user: this.user, - pass: this.key - }; - } - - var r = request.get(args), - self = this; - - r.on('error', done); - r.on('response', function(resp) { - - /*! - * no error if repository doesn't exists - */ - /*istanbul ignore if*/ - if(resp.statusCode === 404) { - return done(); - } - - /*istanbul ignore next*/ - if(resp.statusCode !== 200 || resp.headers['content-type'] !== 'application/octet-stream') { - return done(new Error('unexpected statusCode (' + resp.statusCode + ' != 200) or content-type (' + resp.headers['content-type'] + ' != application/octet-stream)')); - } - - /** - * check if repository directory already exists and - * clear it if yes - */ - if(fs.existsSync(self.screenshotRoot)) { - rimraf.sync(self.screenshotRoot); - fs.mkdirsSync(self.screenshotRoot, '0755', true); - } - - resp.pipe(zlib.Gunzip()).pipe(tar.Extract({ path: '.' })).on('end', done); - - }); -}; +function syncDown(ctx) { + + return Promise.fromCallback(function(done) { + var args = { + url: ctx.api + (ctx.api.substr(-1) !== '/' ? '/' : '') + ctx.screenshotRoot + '.tar.gz', + headers: { 'accept-encoding': 'gzip,deflate' }, + }; + + if(typeof ctx.user === 'string' && typeof ctx.key === 'string') { + args.auth = { + user: ctx.user, + pass: ctx.key + }; + } + + var r = request.get(args); + + r.on('error', done); + r.on('response', function(resp) { + + /*! + * no error if repository doesn't exists + */ + /*istanbul ignore if*/ + if(resp.statusCode === 404) { + return done(); + } + + /*istanbul ignore next*/ + if(resp.statusCode !== 200 || resp.headers['content-type'] !== 'application/octet-stream') { + return done(new Error('unexpected statusCode (' + resp.statusCode + ' != 200) or content-type (' + + resp.headers['content-type'] + ' != application/octet-stream)')); + } + + /** + * check if repository directory already exists and + * clear it if yes + */ + if(fs.existsSync(ctx.screenshotRoot)) { + rimraf.sync(ctx.screenshotRoot); + fs.mkdirsSync(ctx.screenshotRoot, '0755', true); + } + + resp.pipe(zlib.Gunzip()).pipe(tar.Extract({ path: '.' })).on('end', done); + + }); + }); + } /** * sync up @@ -66,35 +74,27 @@ var syncDown = function(done) { * * @param {Function} done callback to be called after tarball was uploaded */ -var syncUp = function(done) { - var screenshotRoot = this.screenshotRoot, - args = { url: this.api }, +function syncUp(ctx) { + var screenshotRoot = ctx.screenshotRoot, + args = { url: ctx.api }, tarballPath = screenshotRoot + '.tar.gz'; - if(typeof this.user === 'string' && typeof this.key === 'string') { + + if(typeof ctx.user === 'string' && typeof ctx.key === 'string') { args.auth = { - user: this.user, - pass: this.key + user: ctx.user, + pass: ctx.key }; } - new targz().compress(screenshotRoot, tarballPath, function(err){ - - /*istanbul ignore if*/ - if(err) { - return done(new Error(err)); - } - - var r = request.post(args, function () { - rimraf.sync(tarballPath); - done(); + return new targz().compressAsync(screenshotRoot, tarballPath) + .then(function() { + return request.postAsync(assign({}, args, {formData: {gz: fs.createReadStream(tarballPath)}})) + .then(function() { + return rimraf(tarballPath); }); - - var form = r.form(); - form.append('gz', fs.createReadStream(tarballPath)); - }); -}; +} /** * sync command @@ -103,24 +103,19 @@ var syncUp = function(done) { * @param {Function} done callback to be called after syncing * @return {Object} WebdriverCSS instance */ -module.exports = function(done) { +module.exports = function() { - if(!this.api) { - return done(new Error('No sync options specified! Please provide an api path and user/key (optional).')); - } + var ctx = this; - var sync = this.needToSync ? syncUp : syncDown; - this.needToSync = false; - - sync.call(this, function(err, httpResponse, body) { - - /*istanbul ignore next*/ - if(err || (httpResponse && httpResponse.statusCode !== 200)) { - return done(new Error(err || body)); + return Promise.try(function() { + if(!ctx.api) { + throw new Error('No sync options specified! Please provide an api path and user/key (optional).'); } - done(); + var sync = ctx.needToSync ? syncUp : syncDown; + ctx.needToSync = false; + return sync(ctx); }); - return this; + }; diff --git a/lib/util/streamToPromise.js b/lib/util/streamToPromise.js new file mode 100644 index 0000000..0492484 --- /dev/null +++ b/lib/util/streamToPromise.js @@ -0,0 +1,8 @@ +var Promise = require('bluebird'); + +module.exports = function streamToPromise(stream) { + return new Promise(function(resolve, reject) { + stream.on("end", resolve); + stream.on("error", reject); + }); +}; diff --git a/lib/viewportScreenshot.js b/lib/viewportScreenshot.js index 64ee704..5d6049a 100644 --- a/lib/viewportScreenshot.js +++ b/lib/viewportScreenshot.js @@ -6,97 +6,79 @@ * @param {String} filename path of file to be saved */ -var async = require('async'), - gm = require('gm'); +var Promise = require('bluebird'); +var gm = require('gm'); +Promise.promisifyAll(gm.prototype); module.exports = function viewportScreenshot(fileName) { var ErrorHandler = this.instance.ErrorHandler; - /*! - * make sure that callback contains chainit callback - */ - var callback = arguments[arguments.length - 1]; - /*! * parameter check */ if (typeof fileName !== 'string') { - return callback(new ErrorHandler.CommandError('number or type of arguments don\'t agree with saveScreenshot command')); + return Promise.reject( + new ErrorHandler.CommandError('number or type of arguments don\'t agree with saveScreenshot command')); } - var self = this.instance, - response = { - execute: [], - screenshot: [] - }; - - var getScrollingPosition = function() { - var position = [0, 0]; - if (typeof window.pageYOffset !== 'undefined') { - position = [ - window.pageXOffset, - window.pageYOffset - ]; - } else if (typeof document.documentElement.scrollTop !== 'undefined' && document.documentElement.scrollTop > 0) { - position = [ - document.documentElement.scrollLeft, - document.documentElement.scrollTop - ]; - } else if (typeof document.body.scrollTop !== 'undefined') { - position = [ - document.body.scrollLeft, - document.body.scrollTop - ]; - } - return position; + var self = this.instance; + var response = { + execute: [], + screenshot: [] }; - async.waterfall([ - - /*! - * get scroll position - */ - function(cb) { - self.execute(getScrollingPosition, null, cb); - }, - - /*! - * get viewport width/height and total width/height - */ - function(res, cb) { - response.execute.push(res); - - self.execute(function() { - return { - screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), - screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - }; - }, cb); - }, - function(res, cb) { - response.execute.push(res); - self.screenshot(cb); - }, - function(res, cb) { - response.screenshot = res; - - gm(new Buffer(res.value, 'base64')).crop( - // width - response.execute[1].value.screenWidth, - // height - response.execute[1].value.screenHeight, - // top - response.execute[0].value[0], - // left - response.execute[0].value[1] - ).write(fileName, cb); - - } - ], function(err) { - - callback(err, null, response); - + self.execute(getScrollingPosition, null) + .then(function getWidthHeight() { + return self.execute(getScreenDimensions); + }) + .then(function(res) { + response.execute.push(res); + return self.screenshot(); + }) + .then(function(res) { + response.screenshot = res; + return gm(new Buffer(res.value, 'base64')) + .crop( + // width + response.execute[1].value.screenWidth, + // height + response.execute[1].value.screenHeight, + // top + response.execute[0].value[0], + // left + response.execute[0].value[1] + ) + .writeAsync(fileName); }); +}; +/*eslint-disable*/ +function getScreenDimensions() { + return { + screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), + screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) + }; +} + +function getScrollingPosition() { + var position = [0, 0]; + if (typeof window.pageYOffset !== 'undefined') { + position = [ + window.pageXOffset, + window.pageYOffset + ]; + } else if (typeof document.documentElement.scrollTop !== 'undefined' && document.documentElement.scrollTop > 0) { + position = [ + document.documentElement.scrollLeft, + document.documentElement.scrollTop + ]; + } else if (typeof document.body.scrollTop !== 'undefined') { + position = [ + document.body.scrollLeft, + document.body.scrollTop + ]; + } + return position; }; +/*eslint-enable*/ diff --git a/lib/webdrivercss.js b/lib/webdrivercss.js index 9f71025..e1ddd72 100644 --- a/lib/webdrivercss.js +++ b/lib/webdrivercss.js @@ -4,12 +4,12 @@ * WebdriverCSS */ -var fs = require('fs-extra'), - workflow = require('./workflow.js'), - viewportScreenshot = require('./viewportScreenshot.js'), - documentScreenshot = require('./documentScreenshot.js'), - generateUUID = require('./generateUUID.js'), - syncImages = require('./syncImages'); +var fs = require('fs-extra'); +var workflow = require('./workflow.js'); +var viewportScreenshot = require('./viewportScreenshot.js'); +var documentScreenshot = require('./documentScreenshot.js'); +var generateUUID = require('./generateUUID.js'); +var syncImages = require('./syncImages'); /** * initialise plugin @@ -69,10 +69,10 @@ var WebdriverCSS = function(webdriverInstance, options) { /** * add WebdriverCSS command to WebdriverIO instance */ - this.instance.addCommand('saveViewportScreenshot', viewportScreenshot.bind(this)); - this.instance.addCommand('saveDocumentScreenshot', documentScreenshot.bind(this)); - this.instance.addCommand('webdrivercss', workflow.bind(this)); - this.instance.addCommand('sync', syncImages.bind(this)); + this.instance.addCommand('saveViewportScreenshot', viewportScreenshot.bind(this), true); + this.instance.addCommand('saveDocumentScreenshot', documentScreenshot.bind(this), true); + this.instance.addCommand('webdrivercss', workflow.bind(this), true); + this.instance.addCommand('sync', syncImages.bind(this), true); return this; }; diff --git a/lib/workflow.js b/lib/workflow.js index 6481ebb..7a3c964 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -4,15 +4,18 @@ * run regression test */ -var async = require('async'); - -module.exports = function(pagename, args) { - - /*! - * make sure that callback contains chainit callback - */ - var cb = arguments[arguments.length - 1]; - +var startSession = require('./startSession.js'); +var setScreenWidth = require('./setScreenWidth.js'); +var makeScreenshot = require('./makeScreenshot.js'); +var renameFiles = require('./renameFiles.js'); +var getPageInfo = require('./getPageInfo.js'); +var cropImage = require('./cropImage.js'); +var compareImages = require('./compareImages.js'); +var saveImageDiff = require('./saveImageDiff.js'); +var asyncCallback = require('./asyncCallback.js'); + + +module.exports = function workflow(pagename, args) { this.needToSync = true; /*istanbul ignore next*/ @@ -69,8 +72,7 @@ module.exports = function(pagename, args) { newScreenSize: 0, pageInfo: null, updateBaseline: (typeof currentArgs.updateBaseline === 'boolean') ? currentArgs.updateBaseline : this.updateBaseline, - screenWidth: currentArgs.screenWidth || [].concat(this.screenWidth), // create a copy of the origin default screenWidth - cb: cb + screenWidth: currentArgs.screenWidth || [].concat(this.screenWidth) // create a copy of the origin default screenWidth }; /** @@ -80,52 +82,13 @@ module.exports = function(pagename, args) { this.resultObject[currentArgs.name] = []; } - async.waterfall([ - - /** - * initialize session - */ - require('./startSession.js').bind(context), - - /** - * if multiple screen width are given resize browser dimension - */ - require('./setScreenWidth.js').bind(context), - - /** - * make screenshot via [GET] /session/:sessionId/screenshot - */ - require('./makeScreenshot.js').bind(context), - - /** - * check if files with id already exists - */ - require('./renameFiles.js').bind(context), - - /** - * get page informations - */ - require('./getPageInfo.js').bind(context), - - /** - * crop image according to user arguments and its position on screen and save it - */ - require('./cropImage.js').bind(context), - - /** - * compare images - */ - require('./compareImages.js').bind(context), - - /** - * save image diff - */ - require('./saveImageDiff.js').bind(context) - ], - /** - * run workflow again or execute callback function - */ - require('./asyncCallback.js').bind(context) - - ); + return startSession.call(context) + .then(setScreenWidth.bind(context)) + .then(makeScreenshot.bind(context)) + .then(renameFiles.bind(context)) + .then(getPageInfo.bind(context)) + .then(cropImage.bind(context)) + .then(compareImages.bind(context)) + .then(saveImageDiff.bind(context)) + .then(asyncCallback.bind(context)); }; diff --git a/package.json b/package.json index 3c248cf..67ea198 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "webdrivercss", - "version": "1.1.10", + "version": "2.0.0beta-rc1", "description": "Regression testing tool for WebdriverJS", "author": "Christian Bromann ", "license": "MIT", "main": "index.js", "scripts": { - "test": "./node_modules/.bin/mocha", - "travis": "./node_modules/.bin/mocha -R $MOCHA_REPORTERS", + "test": "bash ./test/test-run.sh", + "test-cov": "WEBDRIVERCSS_COVERAGE=1 bash ./test/test-run.sh", "prepublish": "npm prune" }, "repository": { @@ -15,25 +15,29 @@ "url": "git://github.com/webdriverio/webdrivercss.git" }, "dependencies": { - "async": "^0.9.0", + "bluebird": "^3.1.1", "deepmerge": "^0.2.7", + "fs-extra": "^0.18.2", "glob": "^5.0.5", "gm": "^1.17.0", "node-resemble-js": "0.0.4", + "object-assign": "^4.0.1", "request": "^2.55.0", "rimraf": "^2.3.2", "tar": "^2.1.0", - "fs-extra": "^0.18.2", "tar.gz": "^1.0.1" }, "devDependencies": { + "async": "^0.9.0", + "bower": "^1.7.2", "chai": "^2.3.0", "coveralls": "~2.11.2", + "http-server": "^0.8.5", "istanbul": "^0.3.13", "mocha": "^2.2.4", "mocha-istanbul": "^0.2.0", "nock": "^1.7.1", - "webdriverio": "^2.4.5" + "webdriverio": "^3.2.1" }, "keywords": [ "webdriverjs", diff --git a/test/spec/imageCapturing.js b/test/spec/imageCapturing.js index 54513bf..1815d64 100644 --- a/test/spec/imageCapturing.js +++ b/test/spec/imageCapturing.js @@ -24,7 +24,8 @@ describe('WebdriverCSS captures desired parts of a website as screenshot with sp .webdrivercss('testWithoutParameter', { name: 'withoutParams'}) .execute(function(){ return document.body.clientHeight; - }, function(err,res) { + }) + .then(function(res) { documentHeight = res.value; }) .call(done); diff --git a/test/test-run.sh b/test/test-run.sh new file mode 100644 index 0000000..91a959c --- /dev/null +++ b/test/test-run.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +finish() { + pkill -P $$ # kills all processes that have this pid - $$ - as the parent + exit $STATUS +} + +trap finish EXIT + +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +PROJECT_DIR=$DIR/.. +NODE_BIN=$PROJECT_DIR/node_modules/.bin +MOCHA_REPORTERS="spec" + +# Runs selenium-server on travis +if [[ $TRAVIS == 'true' ]]; then + curl -O http://selenium-release.storage.googleapis.com/2.43/selenium-server-standalone-2.43.1.jar + java -jar selenium-server-standalone-2.43.1.jar -host 127.0.0.1 -port 4444 2>/dev/null 1>/dev/null & + # Wait for it + while ! echo exit | nc localhost 4444; do sleep 1; done +else + nc -zv 127.0.0.1 4444 > /dev/null + if [[ $? -eq 1 ]]; then + echo "Start selenium server before testing (selenium-standalone start)!" + exit 1 + fi +fi + +# Setup static site +cd $PROJECT_DIR/test/site && $NODE_BIN/bower install --config.interactive=false && cd $PROJECT_DIR +$NODE_BIN/http-server -p 8080 & + +# Wait for http-server +while ! echo exit | nc localhost 8080; do sleep 1; done + +# Setup coverage +if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then + MOCHA_REPORTERS="mocha-istanbul" + $NODE_BIN/istanbul i lib -o lib-cov && \ + cp $PROJECT_DIR/lib/getPageInfo.js lib-cov && \ + cp $PROJECT_DIR/lib/makeScreenshot.js lib-cov && \ + cp $PROJECT_DIR/lib/documentScreenshot.js lib-cov && \ + cp $PROJECT_DIR/lib/viewportScreenshot.js lib-cov && \ + cp $PROJECT_DIR/lib/startSession.js lib-cov && \ + cp $PROJECT_DIR/lib/setScreenWidth.js lib-cov +fi + +# Run tests +BLUEBIRD_LONG_STACK_TRACES=1 $NODE_BIN/_mocha -R $MOCHA_REPORTERS +STATUS=$? + +# Echo coverage information +if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then + ./node_modules/coveralls/bin/coveralls.js < lcov.info +fi + +exit $STATUS