/** * Created by Julien Giffard on 09/09/14. * @module Eagle */ var fs = require('fs'), gm = require('gm'), _ = require('lodash'), resemble = require('resemble'), Command = require('leadfoot/Command'), pollUntil = require('leadfoot/helpers/pollUntil'), Q = require('q'), assert = require('chai').assert, fileSystem = require ('./util/fileSystem'); /** * Eagle Constructor * @constructor */ function Eagle() { Command.apply(this, arguments); } Eagle.prototype = Object.create(Command.prototype); Eagle.prototype.constructor = Eagle; Eagle.prototype.config = { timeout: 10000, url: null, screenshotServer: null, dimensions: { x:1024, y:768 } }; /** * Crops an image to specified element size and location * @private * @param {string} imagePath - path of the screenshot * @param {Object} elementSize - width and height used to crop the screenshot * @param {Object} elementLocation - x and y coordinates used to crop the screenshot * @returns {Q.promise} */ function cropScreenShot (/*String*/ imagePath, /*Object*/ elementSize, /*Object*/ elementLocation) { var dfd = Q.defer(), im = gm.subClass({ imageMagick: true }); im(imagePath) .crop(elementSize.width,elementSize.height,elementLocation.x,elementLocation.y) .write(imagePath, function (err) { dfd.resolve(true); if(err) { throw err; } }); return dfd.promise; } /** * Compares 2 images and, if there is a difference, * generates a third image with highlighted differences. * @private * @param {string} firstImgPath - path to the first image * @param {string} secondImgPath - path to the second image * @param {string} resultImgPath - path to the result image * @param {string} [imageServerUrl] - url of the server where images are available (if set, a direct link is provided in reports) * @returns {Q.promise} */ function compareScreenShots (/*string*/ firstImgPath, /*string*/ secondImgPath, /*string*/ resultImgPath,/*string?*/imageServerUrl){ var dfd = Q.defer(); if(fileSystem.doesFileExists(resultImgPath)){ fs.unlink(resultImgPath, function(err){ if(err) { console.log(err); } }); } if(firstImgPath !== secondImgPath) { resemble.resemble(firstImgPath).compareTo(secondImgPath).onComplete(function(data){ if(data.misMatchPercentage > 0) { var base64Data = data.getImageDataUrl().replace(/^data:image\/png;base64,/,""); fs.writeFile(resultImgPath, base64Data, 'base64', function(err){ if(err) { console.log(err); } }); } dfd.resolve((data.misMatchPercentage == 0)); }); } else { dfd.resolve(true); } return dfd.promise.then(function(areSame){ var msg = 'Images should be the same.\n'; if(imageServerUrl !== null){ msg += 'See : ' + 'http://' + imageServerUrl + resultImgPath + '\n'; } assert.equal(areSame, true, msg); }); } /** * Initializes configuration. * @param {Object} configuration - the Eagle configuration * @returns {Eagle.constructor} * @example Configuration example * Eagle.initConfig({ * timeout: 10000, * url: 'http://mywebsite.com/', * screenshotServer: 'http://localhost:1234/, * dimensions: { * x:1024, * y:768 * }); */ Eagle.prototype.initConfig = function(configuration){ this.config = _.merge(this.config, configuration); return new this.constructor(this, function () { return this.parent; }); }; /** * Initializes remote browser. * @returns {Eagle.constructor} */ Eagle.prototype.setup = function(){ if(!this.config.url){ throw new Error('Url is mandatory.'); } return new this.constructor(this, function () { return this.parent .setFindTimeout(this.parent.config.timeout) .setPageLoadTimeout(this.parent.config.timeout) .setExecuteAsyncTimeout(this.parent.config.timeout) .setWindowSize(this.parent.config.dimensions.x, this.parent.config.dimensions.y) .get(this.parent.config.url); }); }; /** * Takes and saves a screenshot fitted to the DOM element dimensions, * then asserts the 2 images (actual image and ref image). * If the REF image doesn't exists, REF image is created and assertion is true. * Else, images are compared and in case of differences, a third image is generated. * @summary Captures a screenshot of specified element and compares it to a reference image of the element. * @param {string} selector - CSS selector of specified element * @param {string} imageLocation - path of the screenshot directory * @param {string} imageName - name of the screenshot * @returns {Eagle.constructor} */ Eagle.prototype.captureElementByCssSelector = function(/*String*/ selector, /*String*/ imageLocation, /*String*/ imageName) { return new this.constructor(this, function () { var elementLocation = {}, elementSize = {}, testImage = imageLocation + '/' + imageName + '-LAST.png', refImage = imageLocation + '/' + imageName + '-REF.png', diffImage = imageLocation + '/' + imageName + '-DIFF.png', imagePath = (fileSystem.doesFileExists(refImage)) ? testImage : refImage; fileSystem.mkdirIfNotExists(imageLocation); return this.parent .findByCssSelector(selector) .getPosition() .then(function(location){ elementLocation = location; return this.parent; }) .end() .findByCssSelector(selector) .getSize() .then(function(size){ elementSize = size; return this.parent; }) .end() .takeScreenshot() .then(function(screenshot){ fs.writeFile(imagePath, screenshot, 'base64', function(err){ if(err) { throw err; } }); return this.parent; }) .then(function(){ return this.parent .execute('return window.scrollX;') .then(function(scrollX){ elementLocation.x -= scrollX; }) .execute('return window.scrollY;') .then(function(scrollY){ elementLocation.y -= scrollY; }); }) .then(function() { return cropScreenShot(imagePath,elementSize,elementLocation); }) .then(function() { return compareScreenShots(imagePath,refImage,diffImage, this.parent.config.screenshotServer); }) .catch(function (err){ console.log(err); }); }); }; /** * Takes and saves a screenshot of the current 'visible' page. * Unvisible elements won't be captured (like end of a scrollable page). * @param {string} imageLocation - path of the screenshot directory * @param {string} imageName - name of the screenshot * @returns {Eagle.constructor} */ Eagle.prototype.captureFullScreenShot = function(/*String*/ imageLocation, /*String*/ imageName) { return new this.constructor(this, function () { return this.parent .takeScreenshot() .then(function(screenshot){ fs.writeFile(imageLocation+'/'+imageName+'.png', screenshot, 'base64', function(err){ if(err) { throw err; } }); }); }); }; /** * Blurs the DOM active element. * The command is sent to the remote browser using the execute command. * @returns {Eagle.constructor} */ Eagle.prototype.blurActiveElement = function () { return new this.constructor(this, function () { return this.parent .then(pollUntil("document.activeElement.blur(); return document.activeElement === document.querySelector('body') ? true : null;",[], this.parent.config.timeout)) ; }); }; /** * Adds a spellcheck attribute to the HTML body tag and sets it to false. * The spellcheck will be disabled only for the current page. * @summary Disables spellcheck in the current page. * @returns {Eagle.constructor} */ Eagle.prototype.disableSpellCheck = function () { return new this.constructor(this, function () { var disableCommand = 'document.querySelector("body").setAttribute("spellcheck","false");'; return this.parent.execute(disableCommand); }); }; /** * Sets a cookie in current web page * @param {string} name - name of the cookie * @param {string} value - value of the cookie * @returns {Eagle.constructor} */ Eagle.prototype.setCookie = function (/*string*/name, /*string*/ value){ return new this.constructor(this, function () { var setCookieCmd = 'document.cookie = "' + name + '=' + value + '";'; return this.parent.execute(setCookieCmd); }); }; module.exports = Eagle;