Source: eagle.js

/**
 * 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;