yahoo/kobold

View on GitHub
lib/kobold.js

Summary

Maintainability
F
3 days
Test Coverage
// Copyright 2014, Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.

var utils = require('preceptor-core').utils;
var Base = require('preceptor-core').Base;
var koboldCore = require('kobold-core');

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

var Promise = require('promise');
var log4js = require('log4js');

var defaultOptions = require('./defaultOptions');

/**
 * @class Kobold
 *
 * @property {Logger} _logger
 * @property {boolean} _verbose
 * @property {boolean} _highlightOnSuccess
 * @property {string} _build
 * @property {boolean} _failForOrphans
 * @property {boolean} _failOnAdditions
 * @property {object} _comparison
 * @property {StorageAdapter} _source
 * @property {StorageAdapter} _destination
 */
var Kobold = Base.extend(

    /**
     * Initializes the kobold test-runner
     *
     * @constructor
     * @param {object} [options]
     * @param {boolean} [options.verbose=false]
     * @param {boolean} [options.highlightOnSuccess=false] Create highlighting for successful tests?
     * @param {string} [options.build] Build identifier
     * @param {boolean} [options.failForOrphans=false] Fail test when screen is missing
     * @param {boolean} [options.failOnAdditions=false] Fail test when new screen was added
     * @param {object} [options.comparison] Image-diff options
     * @param {object} [options.source=options.storage] Source storage adapter configuration
     * @param {object} [options.destination=options.storage] Destination storage adapter configuration
     * @param {object} [options.storage] General storage adapter configuration
     */
    function (options) {
        this.__super();

        this._logger = log4js.getDefaultLogger();

        options = utils.deepExtend({}, [defaultOptions, options || {}]);

        this._verbose = options.verbose || false;
        if (!this._verbose) {
            log4js.setGlobalLogLevel('INFO');
        }

        this._highlightOnSuccess = options.highlightOnSuccess || false;

        this._build = options.build;

        this._failForOrphans = options.failForOrphans || false;
        this._failOnAdditions = options.failOnAdditions || false;

        this._comparison = utils.deepExtend({}, [options.comparison || {}, options.blinkDiff || {}]);

        this._source = koboldCore.buildStorageAdapter(this._build, options.source || options.storage);
        this._destination = koboldCore.buildStorageAdapter(this._build, options.destination || options.storage);
    },

    {
        /**
         * Gets the source
         *
         * @method getSource
         * @return {StorageAdapter}
         */
        getSource: function () {
            return this._source;
        },

        /**
         * Gets the destination
         *
         * @method getDestination
         * @return {StorageAdapter}
         */
        getDestination: function () {
            return this._destination;
        },


        /**
         * Determines the state of screens
         *
         * @method _determineScreensState
         * @param {string[]} screens
         * @return {{orphans:  <string[]>, additions:  <string[]>, comparisons:  <string[]> }}
         * @private
         */
        _determineScreensState: function (screens) {

            var buildScreens = screens[0],
                approvedScreens = screens[1],

                buildIndex,
                buildLength,
                approvedIndex,
                approvedLength,

                found, result;

            this._logger.debug("Found screens", screens);

            result = {
                orphans: [],
                additions: [],
                comparisons: []
            };

            // Find orphans and comparisons
            this._logger.debug("Find orphans and compare");
            for (approvedIndex = 0, approvedLength = approvedScreens.length; approvedIndex < approvedLength; approvedIndex++) {
                found = false;

                for (buildIndex = 0, buildLength = buildScreens.length; buildIndex < buildLength; buildIndex++) {
                    if (approvedScreens[approvedIndex] === buildScreens[buildIndex]) {
                        found = true;
                        break;
                    }
                }

                if (found) {
                    result.comparisons.push(approvedScreens[approvedIndex]);
                } else {
                    result.orphans.push(approvedScreens[approvedIndex]);
                }
            }

            // Find additions
            this._logger.debug("Find additions");
            for (buildIndex = 0, buildLength = buildScreens.length; buildIndex < buildLength; buildIndex++) {
                found = false;

                for (approvedIndex = 0, approvedLength = approvedScreens.length; approvedIndex < approvedLength; approvedIndex++) {
                    if (approvedScreens[approvedIndex] === buildScreens[buildIndex]) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    result.additions.push(buildScreens[buildIndex]);
                }
            }

            this._logger.debug("Found tests", result);
            return result;
        },

        /**
         * Determines all the tests
         *
         * @method _determineTests
         * @return {Promise} With { orphans: <string[]>, additions: <string[]>, comparisons: <string[]> }
         * @private
         */
        _determineTests: function () {

            this._logger.debug("Determine tests");

            return Promise.all([
                this.getSource().getBuildScreenNames(),
                this.getDestination().getCurrentApprovedScreenNames()
            ]).then(function (screens) {
                return this._determineScreensState(screens);
            }.bind(this), function (err) {
                this._logger.error("Error determining tests", err.stack);
            }.bind(this));
        },

        /**
         * Converts the filename to a user-friendly test-name
         *
         * @method _getTestName
         * @param {string} filename
         * @return {string}
         * @private
         */
        _getTestName: function (filename) {
            return filename.replace(/-/g, ' ').replace(/_/g, ' - ').replace(/\s/g, ' ');
        },


        /**
         * Runs all orphan tests
         *
         * @method _runOrphanTests
         * @param {string[]} orphans
         * @private
         */
        _runOrphanTests: function (orphans) {
            var i, len;

            this._logger.debug("Run orphans tests", orphans);
            for (i = 0, len = orphans.length; i < len; i++) {
                this._runOrphanTest(orphans[i]);
            }
        },

        /**
         * Runs an orphan test
         *
         * @method _runOrphanTest
         * @param {string} orphanName
         * @private
         */
        _runOrphanTest: function (orphanName) {

            this._logger.debug("Run orphans test", orphanName);
            this._describe(this._getTestName(orphanName), function () {

                if (this._failForOrphans) {
                    this._it('is orphaned', function () {
                        throw new Error("Approved screen is orphaned: " + orphanName);
                    });
                } else {
                    this._it('is orphaned');
                }

            }.bind(this));
        },


        /**
         * Runs all additions tests
         *
         * @method _runAdditionsTests
         * @param {string[]} additions
         * @private
         */
        _runAdditionsTests: function (additions) {
            var i, len;

            this._logger.debug("Run additions tests", additions);
            for (i = 0, len = additions.length; i < len; i++) {
                this._runAdditionsTest(additions[i]);
            }
        },

        /**
         * Runs an addition test
         *
         * @method _runAdditionsTest
         * @param {string} additionName
         * @private
         */
        _runAdditionsTest: function (additionName) {

            this._logger.debug("Run addition test", additionName);
            this._describe(this._getTestName(additionName), function () {

                if (this._failOnAdditions) {
                    this._it('is an addition', function () {
                        throw new Error("Screen is new: " + additionName);
                    });
                } else {
                    // Define as pending
                    this._it('is an addition');
                }

            }.bind(this));
        },


        /**
         * Runs all comparison tests
         *
         * @method _runComparisonTests
         * @param {string[]} comparisons
         * @private
         */
        _runComparisonTests: function (comparisons) {

            var i, len;

            this._logger.debug("Run comparison tests", comparisons);
            for (i = 0, len = comparisons.length; i < len; i++) {
                this._runComparisonTest(comparisons[i]);
            }
        },

        /**
         * Runs a comparison test
         *
         * @method _runComparisonTest
         * @param {string} comparisonName
         * @private
         */
        _runComparisonTest: function (comparisonName) {

            this._logger.debug("Run comparison tests", comparisonName);
            this._describe(this._getTestName(comparisonName), function () {

                this._it('should load the approved screen', function (done) {
                    this.getSource().getCurrentApprovedScreen(comparisonName).then(function (screen) {
                        this.approvedScreen = screen;
                        done();
                    }.bind(this), function (err) {
                        done(err);
                    });
                }.bind(this));

                this._it('should load the build screen', function (done) {
                    this.getDestination().getBuildScreen(comparisonName).then(function (screen) {
                        this.buildScreen = screen;
                        done();
                    }.bind(this), function (err) {
                        done(err);
                    });
                }.bind(this));

                this._it('should load the configuration of screen', function (done) {
                    this.getSource().getScreenConfig(comparisonName).then(function (config) {
                        this.screenConfig = config || {};
                        done();
                    }.bind(this), function (err) {
                        this.screenConfig = {};
                        done();
                    }.bind(this));
                }.bind(this));

                this._it('should be similar', function (done) {

                    var options = this._comparison,
                        blinkDiff;

                    options = utils.deepExtend({}, [options, this.screenConfig || {}]);

                    if (!this.approvedScreen) {
                        throw new Error('Approved screen is not loaded.');
                    }
                    if (!this.buildScreen) {
                        throw new Error('Build screen is not loaded.');
                    }

                    options.imageA = this.approvedScreen;
                    options.imageB = this.buildScreen;

                    blinkDiff = new BlinkDiff(options);
                    blinkDiff.run(function (err, result) {
                        if (err) {
                            done(err);
                        } else {

                            try {
                                this._processTestResults(comparisonName, blinkDiff, result.code).then(function () {
                                    done();
                                }, function (err) {
                                    done(err);
                                });
                            } catch (err) {
                                done(err);
                            }
                        }
                    }.bind(this));
                }.bind(this));
            }.bind(this));
        },


        /**
         * Run tests
         *
         * @method run
         * @return {Promise}
         */
        run: function () {

            var result = Promise.resolve();

            this._it('initialize tests', function () {

                this._logger.debug("Run tests");
                result = this._determineTests().then(function (tests) {

                    this._describe('Kobold', function () {

                        this._runOrphanTests(tests.orphans);
                        this._runAdditionsTests(tests.additions);
                        this._runComparisonTests(tests.comparisons);

                    }.bind(this));
                }.bind(this));
            }.bind(this));

            return result;
        },


        /**
         * Archiving test-results
         *
         * @method _archiveTest
         * @param {boolean} passed
         * @param {string} testName
         * @param {PNGImage} approvedImage
         * @param {PNGImage} buildImage
         * @param {PNGImage} highlightImage
         * @private
         */
        _archiveTest: function (passed, testName, approvedImage, buildImage, highlightImage) {

            var archiveList = [];

            this._logger.debug("Archive test", testName);
            if (approvedImage) {
                archiveList.push(this.getDestination().archiveApprovedScreen(testName, approvedImage));
            }
            if (buildImage) {
                archiveList.push(this.getDestination().archiveBuildScreen(testName, buildImage));
            }
            if (highlightImage && (!passed || (passed && this._highlightOnSuccess))) {
                archiveList.push(this.getDestination().archiveHighlightScreen(testName, highlightImage));
            }

            return Promise.all(archiveList);
        },

        /**
         * Processes the comparison test results
         *
         * @method _processTestResults
         * @param {string} comparisonName
         * @param {BlinkDiff} blinkDiff
         * @param {int} resultCode
         * @private
         */
        _processTestResults: function (comparisonName, blinkDiff, resultCode) {

            var approvedScreen = blinkDiff._imageA,
                buildScreen = blinkDiff._imageB,
                highlightScreen = blinkDiff._imageOutput,
                promise,
                passed;

            this._logger.debug("Process result", comparisonName, resultCode);

            passed = blinkDiff.hasPassed(resultCode);
            promise = this._archiveTest(passed, comparisonName, approvedScreen, buildScreen, highlightScreen);

            if (!passed) {
                promise = promise.then(function () {
                    throw new Error("Screens are different for " + comparisonName);
                });
            }

            return promise;
        },


        /**
         * Add 'describe' section to test-suite
         *
         * @method _describe
         * @param {string} title
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _describe: function (title, fn) {
            return describe(title, fn);
        },

        /**
         * Add 'it' section to the test-suite
         *
         * @method _it
         * @param {string} title
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _it: function (title, fn) {
            return it(title, fn);
        },

        /**
         * Add 'before' section to the test-suite
         *
         * @method _before
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _before: function (fn) {
            return before(fn);
        },

        /**
         * Add 'beforeEach' section to the test-suite
         *
         * @method _beforeEach
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _beforeEach: function (fn) {
            return beforeEach(fn);
        },

        /**
         * Add 'after' section to the test-suite
         *
         * @method _after
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _after: function (fn) {
            return after(fn);
        },

        /**
         * Add 'afterEach' section to the test-suite
         *
         * @method _afterEach
         * @param {function} [fn]
         * @return {object}
         * @private
         */
        _afterEach: function (fn) {
            return afterEach(fn);
        }
    }
);

module.exports = Kobold;