preceptorjs/taxi

View on GitHub
lib/comparison/blinkdiff.js

Summary

Maintainability
F
3 days
Test Coverage
'use strict';

var logMethods = require('../log');
var type = require('../type');
var when = require('../when');

var PNGImage = require('pngjs-image');
var fs = require('fs');
var utils = require('preceptor-core').utils;
var path = require('path');
var mkdirp = require('mkdirp');

var BlinkDiff = require('blink-diff');

module.exports = BlinkDiffComparison;

/**
 * Blink-Diff comparison object
 *
 * @constructor
 * @class BlinkDiffComparison
 * @module WebDriver
 * @submodule Helpers
 * @param {Driver} driver
 */
function BlinkDiffComparison (driver) {
    this._driver = driver;
    this._failed = false;
}


/////////////////////
// Private Methods //
/////////////////////

/**
 * Logs a method call by an event
 *
 * @param {object} event
 * @method _logMethodCall
 * @private
 */
BlinkDiffComparison.prototype._logMethodCall = function (event) {
    event.target = 'BlinkDiffComparison';
    this._driver._logMethodCall(event);
};


/**
 * Performs a context dependent JSON request for the current session.
 * The result is parsed for errors.
 *
 * @method _requestJSON
 * @private
 * @param {String} method
 * @param {String} path
 * @param {*} [body]
 * @return {*}
 */
BlinkDiffComparison.prototype._requestJSON = function (method, path, body) {
    return this._driver._requestJSON(method, path, body);
};


////////////////////
// Public Methods //
////////////////////

/**
 * Gets the driver object.
 * Direct-access. No need to wait.
 *
 * @method getDriver
 * @return {Driver}
 */
BlinkDiffComparison.prototype.getDriver = function () {
    return this._driver;
};


/**
 * Gets the default comparison options
 *
 * @method getOptions
 * @return {object}
 */
BlinkDiffComparison.prototype.getOptions = function () {
    return this.getDriver().getValue('blinkDiff.options') || {};
};

/**
 * Should an output be created when comparison is successful?
 *
 * @method shouldOutputOnSuccess
 * @return {boolean}
 */
BlinkDiffComparison.prototype.shouldOutputOnSuccess = function () {

    var option = this.getDriver().getValue('blinkDiff.outputOnSuccess');

    if (option === undefined) {
        option = true;
    }

    return option;
};

/**
 * Should an exception be thrown when there are differences?
 *
 * @method shouldFailOnDifference
 * @return {boolean}
 */
BlinkDiffComparison.prototype.shouldFailOnDifference = function () {

    var option = this.getDriver().getValue('blinkDiff.failOnDifference');

    if (option === undefined) {
        option = true;
    }

    return option;
};

/**
 * Should screenshot be auto-approved?
 *
 * @method shouldAutoApprove
 * @return {boolean}
 */
BlinkDiffComparison.prototype.shouldAutoApprove = function () {
    return !!this.getDriver().getValue('blinkDiff.autoApprove');
};


/**
 * Batch failures until the end?
 *
 * @method shouldBatchFailures
 * @return {boolean}
 */
BlinkDiffComparison.prototype.shouldBatchFailures = function () {
    return !!this.getDriver().getValue('blinkDiff.batchFailures');
};

/**
 * Should comparison fail on additions?
 *
 * @method shouldFailOnAdditions
 * @return {boolean}
 */
BlinkDiffComparison.prototype.shouldFailOnAdditions = function () {
    return !!this.getDriver().getValue('blinkDiff.failOnAdditions');
};


/**
 * Gets the path where approved screenshots are found
 *
 * @method getApprovedPath
 * @return {string}
 */
BlinkDiffComparison.prototype.getApprovedPath = function () {
    return this.getDriver().getValue('blinkDiff.approvedPath') || process.cwd();
};

/**
 * Gets the path where current screenshots will be written to
 *
 * @method getBuildPath
 * @return {string}
 */
BlinkDiffComparison.prototype.getBuildPath = function () {
    return this.getDriver().getValue('blinkDiff.buildPath') || this.getApprovedPath();
};

/**
 * Gets the path where differences will be written to
 *
 * @method getDiffPath
 * @return {string}
 */
BlinkDiffComparison.prototype.getDiffPath = function () {
    return this.getDriver().getValue('blinkDiff.diffPath') || this.getBuildPath();
};

/**
 * Gets the name of the browser folder, categorizing screenshots from the same browser + platform
 *
 * @method _getFolderName
 * @return {string}
 * @private
 */
BlinkDiffComparison.prototype._getFolderName = function () {
    var browserId;

    browserId = this.getDriver().browserId().trim().replace(/\s\s+/g, ' ');
    browserId = utils.fileNameSafe(browserId);

    return browserId;
};

/**
 * Gets the name of the file-base, converting unique identifiers to filename safe strings
 *
 * @method _getBaseName
 * @param {string} title Unique title of image
 * @param {string} id Unique identifier for multiple images with the same title
 * @return {string}
 * @private
 */
BlinkDiffComparison.prototype._getBaseName = function (title, id) {
    return utils.fileNameSafe(title.trim().replace(/\s\s+/g, ' ')) + '_' + id;
};


/**
 * Gets the approved path for the current image
 *
 * @method _getApprovedImagePath
 * @param {string} title Unique title of image
 * @param {string} [id=1] Unique identifier for multiple images with the same title
 * @return {string}
 * @private
 */
BlinkDiffComparison.prototype._getApprovedImagePath = function (title, id) {
    var baseName = this._getBaseName(title, id || 1),
        folderName = this._getFolderName(),
        suffix = this._needsApprovedSuffix() ? '_approved' : '';

    return path.join(this.getApprovedPath(), folderName, baseName + suffix + '.png');
};

/**
 * Gets the build path for the current image
 *
 * @method _getBuildImagePath
 * @param {string} title Unique title of image
 * @param {string} [id=1] Unique identifier for multiple images with the same title
 * @return {string}
 * @private
 */
BlinkDiffComparison.prototype._getBuildImagePath = function (title, id) {
    var baseName = this._getBaseName(title, id || 1),
        folderName = this._getFolderName(),
        suffix = this._needsBuildSuffix() ? '_build' : '';

    return path.join(this.getBuildPath(), folderName, baseName + suffix + '.png');
};

/**
 * Gets the difference path for the current image
 *
 * @method _getDiffImagePath
 * @param {string} title Unique title of image
 * @param {string} [id=1] Unique identifier for multiple images with the same title
 * @return {string}
 * @private
 */
BlinkDiffComparison.prototype._getDiffImagePath = function (title, id) {
    var baseName = this._getBaseName(title, id || 1),
        folderName = this._getFolderName(),
        suffix = this._needsDiffSuffix() ? '_diff' : '';

    return path.join(this.getDiffPath(), folderName, baseName + suffix + '.png');
};

/**
 * Checks if a filename suffix is required for the approved folder
 *
 * Note:
 * This is only true if multiple file-types end up in the same folder
 *
 * @method _needsApprovedSuffix
 * @return {boolean}
 * @private
 */
BlinkDiffComparison.prototype._needsApprovedSuffix = function () {
    var approvedPath = this.getApprovedPath(),
        buildPath = this.getBuildPath(),
        diffPath = this.getDiffPath();

    return ((approvedPath == buildPath) || (approvedPath == diffPath));
};

/**
 * Checks if a filename suffix is required for the build folder
 *
 * Note:
 * This is only true if multiple file-types end up in the same folder
 *
 * @method _needsBuildSuffix
 * @return {boolean}
 * @private
 */
BlinkDiffComparison.prototype._needsBuildSuffix = function () {
    var approvedPath = this.getApprovedPath(),
        buildPath = this.getBuildPath(),
        diffPath = this.getDiffPath();

    return ((buildPath == approvedPath) || (buildPath == diffPath));
};

/**
 * Checks if a filename suffix is required for the diff folder
 *
 * Note:
 * This is only true if multiple file-types end up in the same folder
 *
 * @method _needsDiffSuffix
 * @return {boolean}
 * @private
 */
BlinkDiffComparison.prototype._needsDiffSuffix = function () {
    var approvedPath = this.getApprovedPath(),
        buildPath = this.getBuildPath(),
        diffPath = this.getDiffPath();

    return ((diffPath == buildPath) || (diffPath == approvedPath));
};


/**
 * Saves a blob to the filesystem
 *
 * @method _saveBlob
 * @param {string} filePath
 * @param {Buffer} blob
 * @private
 */
BlinkDiffComparison.prototype._saveBlob = function (filePath, blob) {

    var dirPath = path.dirname(filePath);

    mkdirp.sync(dirPath);
    fs.writeFileSync(filePath, blob);
};


/**
 * Prepares all of the folders, creating them if needed
 *
 * @method _prepareFolders
 * @private
 */
BlinkDiffComparison.prototype._prepareFolders = function () {

    [this.getApprovedPath(), this.getBuildPath(), this.getDiffPath()].forEach(function (path) {

        if (!fs.existsSync(path)) {
            mkdirp.sync(path);
        }
    });
};


/**
 * Compares a specific screenshot
 *
 * @method compare
 * @param {string} title Unique title of image
 * @param {Buffer} imageBlob Image buffer of current screenshot
 * @param {object} [options] Comparison options
 */
BlinkDiffComparison.prototype.compare = function (title, imageBlob, options) {

    var instance,
        diffPath, buildPath, approvedPath,
        result, passed, localOptions;

    this._prepareFolders();

    approvedPath = this._getApprovedImagePath(title, options.id);
    buildPath = this._getBuildImagePath(title, options.id);
    diffPath = this._getDiffImagePath(title, options.id);

    // Make sure that the "id" is removed so that it doesn't clash with BlinkDiff
    delete options.id;

    // Apply default options, overwritten by given options
    localOptions = utils.deepExtend({}, [this.getOptions(), options]);

    this._saveBlob(buildPath, imageBlob);

    if (!fs.existsSync(approvedPath)) {

        if (this.shouldAutoApprove()) {
            this._saveBlob(approvedPath, imageBlob);
        }

        if (this.shouldFailOnAdditions()) {
            this._failed = true;

            if (!this.shouldBatchFailures()) {
                throw new Error("Screenshot is an addition: " + title);
            }
        }

        return this.getDriver().utils().resolve(null);

    } else {

        localOptions.imageA = PNGImage.readImageSync(approvedPath);
        localOptions.imageB = PNGImage.loadImageSync(imageBlob);

        instance = new BlinkDiff(localOptions);
        result = instance.runSync();

        passed = instance.hasPassed(result.code);
        if (!passed || (passed && this.shouldOutputOnSuccess())) {
            this._saveBlob(diffPath, instance._imageOutput.toBlobSync());
        }

        if (!passed && this.shouldFailOnDifference()) {
            this._failed = true;

            if (!this.shouldBatchFailures()) {
                throw new Error("Screenshots are different for " + title);
            }
        }

        return this.getDriver().utils().resolve(passed);
    }
};


/**
 * Setup of comparison
 *
 * @method setup
 */
BlinkDiffComparison.prototype.setup = function () {
    // Do nothing
};

/**
 * Tear-down of comparison
 *
 * @method tearDown
 */
BlinkDiffComparison.prototype.tearDown = function () {
    if (this._failed && this.shouldBatchFailures()) {
        throw new Error('Differences encountered in one or more comparisons.');
    }
};

logMethods(BlinkDiffComparison.prototype);