diff --git a/index.js b/index.js index 74925c7..d31d982 100644 --- a/index.js +++ b/index.js @@ -8,10 +8,8 @@ const areColorsSame = require('./lib/same-colors'); const AntialiasingComparator = require('./lib/antialiasing-comparator'); const IgnoreCaretComparator = require('./lib/ignore-caret-comparator'); const utils = require('./lib/utils'); -const readPair = utils.readPair; -const getDiffPixelsCoords = utils.getDiffPixelsCoords; - -const JND = 2.3; // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye +const {getDiffPixelsCoords} = utils; +const {JND} = require('./lib/constants'); const makeAntialiasingComparator = (comparator, png1, png2, opts) => { const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2, opts); @@ -120,36 +118,40 @@ const getToleranceFromOpts = (opts) => { const prepareOpts = (opts) => { opts.tolerance = getToleranceFromOpts(opts); - _.defaults(opts, { + return _.defaults(opts, { ignoreCaret: true, ignoreAntialiasing: true, antialiasingTolerance: 0 }); }; -const getMaxDiffBounds = (first, second) => ({ - left: 0, - top: 0, - right: Math.max(first.width, second.width) - 1, - bottom: Math.max(first.height, second.height) - 1 -}); +const getMaxDiffBounds = (first, second) => { + const {x: left, y: top} = first.getActualCoord(0, 0); + + return { + left, + top, + right: left + Math.max(first.width, second.width) - 1, + bottom: top + Math.max(first.height, second.height) - 1 + }; +}; -module.exports = exports = function looksSame(reference, image, opts, callback) { +module.exports = exports = function looksSame(image1, image2, opts, callback) { if (!callback) { callback = opts; opts = {}; } - prepareOpts(opts); + opts = prepareOpts(opts); + [image1, image2] = utils.formatImages(image1, image2); - readPair(reference, image, (error, pair) => { + utils.readPair(image1, image2, (error, pair) => { if (error) { return callback(error); } - const first = pair.first; - const second = pair.second; - const refImg = {size: {width: pair.first.width, height: pair.first.height}}; + const {first, second} = pair; + const refImg = {size: {width: first.width, height: first.height}}; const metaInfo = {refImg}; if (first.width !== second.width || first.height !== second.height) { @@ -167,21 +169,21 @@ module.exports = exports = function looksSame(reference, image, opts, callback) }); }; -exports.getDiffArea = function(reference, image, opts, callback) { +exports.getDiffArea = function(image1, image2, opts, callback) { if (!callback) { callback = opts; opts = {}; } - prepareOpts(opts); + opts = prepareOpts(opts); + [image1, image2] = utils.formatImages(image1, image2); - readPair(reference, image, (error, pair) => { + utils.readPair(image1, image2, (error, pair) => { if (error) { return callback(error); } - const first = pair.first; - const second = pair.second; + const {first, second} = pair; if (first.width !== second.width || first.height !== second.height) { return process.nextTick(() => callback(null, getMaxDiffBounds(first, second))); @@ -200,9 +202,11 @@ exports.getDiffArea = function(reference, image, opts, callback) { }; exports.createDiff = function saveDiff(opts, callback) { - prepareOpts(opts); + opts = prepareOpts(opts); + + const [image1, image2] = utils.formatImages(opts.reference, opts.current); - readPair(opts.reference, opts.current, (error, {first, second}) => { + utils.readPair(image1, image2, (error, {first, second}) => { if (error) { return callback(error); } diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..ef65f3b --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + JND: 2.3, // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye + REQUIRED_IMAGE_FIELDS: ['source', 'boundingBox'], + REQUIRED_BOUNDING_BOX_FIELDS: ['left', 'top', 'right', 'bottom'] +}; diff --git a/lib/diff-area.js b/lib/diff-area.js index 2aed36c..662c881 100644 --- a/lib/diff-area.js +++ b/lib/diff-area.js @@ -6,7 +6,7 @@ module.exports = class DiffArea { this._updated = false; } - update(x, y) { + update({x, y}) { const {left, top, right, bottom} = this._diffArea; this._diffArea = { diff --git a/lib/png.js b/lib/png.js deleted file mode 100644 index 8c0f266..0000000 --- a/lib/png.js +++ /dev/null @@ -1,129 +0,0 @@ -'use strict'; -const fs = require('fs'); -const PNG = require('pngjs').PNG; -const concat = require('concat-stream'); - -/** - * @class PNGImage - */ -class PNGImage { - /** - * @param {Object} png - * @constructor - */ - constructor(png) { - this._png = png; - } - - /** - * Returns color of pixel with given coordinates - * @param {Number} x coordinate - * @param {Number} y coordinate - * @returns {{R: (*|Object), G: (*|Object), B: (*|Object)}} - */ - getPixel(x, y) { - const idx = this._getIdx(x, y); - return { - R: this._png.data[idx], - G: this._png.data[idx + 1], - B: this._png.data[idx + 2] - }; - } - - /** - * Sets color data to pixel with given coordinates - * @param {Number} x coordinate - * @param {Number} y coordinate - * @param {Object} color - */ - setPixel(x, y, color) { - const idx = this._getIdx(x, y); - this._png.data[idx] = color.R; - this._png.data[idx + 1] = color.G; - this._png.data[idx + 2] = color.B; - this._png.data[idx + 3] = 255; - } - - /** - * Returns image width - * @returns {Number} - */ - get width() { - return this._png.width; - } - - /** - * Returns image height - * @returns {Number} - */ - get height() { - return this._png.height; - } - - /** - * Returns index of pixel for given coordinates - * @param {Number} x coordinate - * @param {Number} y coordinate - * @returns {Number} - * @private - */ - _getIdx(x, y) { - return (this._png.width * y + x) * 4; - } - - /** - * Saves image to file for given path - * @param {String} path - * @param {function} callback function - */ - save(path, callback) { - const writeStream = fs.createWriteStream(path); - this._png.pack().pipe(writeStream); - - writeStream.on('error', (error) => callback(error)); - writeStream.on('finish', () => callback(null)); - } - - createBuffer(callback) { - this._png.pack().pipe(concat(gotDiff)); - this._png.on('error', (error) => callback(error, null)); - - function gotDiff(data) { - callback(null, data); - } - } -} - -/** - * Returns png image file loaded from given file path - * @param {String} filePath - image file path - * @param {function} callback function - */ -exports.fromFile = (filePath, callback) => { - fs.readFile(filePath, (error, data) => { - error - ? callback(error, null) - : exports.fromBuffer(data, callback); - }); -}; - -/** - * Returns png image loaded from buffer - * @param {Buffer} buffer - image buffer - * @param {function} callback function - */ -exports.fromBuffer = (buffer, callback) => { - const png = new PNG(); - png.parse(buffer, (error) => { - error - ? callback(error, null) - : callback(null, new PNGImage(png)); - }); -}; - -/** - * Returns new empty png image of given size - * @param {Number} width - image width - * @param {Number} height - image height - */ -exports.empty = (width, height) => new PNGImage(new PNG({width, height})); diff --git a/lib/png/bounded-png.js b/lib/png/bounded-png.js new file mode 100644 index 0000000..9b49e85 --- /dev/null +++ b/lib/png/bounded-png.js @@ -0,0 +1,35 @@ +'use strict'; + +const PNGImage = require('./png'); + +module.exports = class BoundedPNGImage extends PNGImage { + constructor(png, boundingBox) { + super(png); + + this._boundingBox = boundingBox; + } + + getPixel(x, y) { + const {x: actX, y: actY} = this.getActualCoord(x, y); + + return super.getPixel(actX, actY); + } + + setPixel(x, y, color) { + const {x: actX, y: actY} = this.getActualCoord(x, y); + + super.setPixel(actX, actY, color); + } + + getActualCoord(x, y) { + return {x: x + this._boundingBox.left, y: y + this._boundingBox.top}; + } + + get width() { + return this._boundingBox.right - this._boundingBox.left + 1; + } + + get height() { + return this._boundingBox.bottom - this._boundingBox.top + 1; + } +}; diff --git a/lib/png/index.js b/lib/png/index.js new file mode 100644 index 0000000..6e73b31 --- /dev/null +++ b/lib/png/index.js @@ -0,0 +1,31 @@ +'use strict'; + +const fs = require('fs'); +const {PNG} = require('pngjs'); +const OriginalPNG = require('./original-png'); +const BoundedPNG = require('./bounded-png'); + +exports.create = (png, {boundingBox} = {}) => { + return boundingBox + ? BoundedPNG.create(png, boundingBox) + : OriginalPNG.create(png); +}; + +exports.fromFile = (filePath, opts = {}, callback) => { + fs.readFile(filePath, (error, data) => { + error + ? callback(error, null) + : exports.fromBuffer(data, opts, callback); + }); +}; + +exports.fromBuffer = (buffer, opts = {}, callback) => { + const png = new PNG(); + png.parse(buffer, (error) => { + error + ? callback(error, null) + : callback(null, exports.create(png, opts)); + }); +}; + +exports.empty = (width, height) => exports.create(new PNG({width, height})); diff --git a/lib/png/original-png.js b/lib/png/original-png.js new file mode 100644 index 0000000..47c8408 --- /dev/null +++ b/lib/png/original-png.js @@ -0,0 +1,17 @@ +'use strict'; + +const PNGImage = require('./png'); + +module.exports = class OriginalPNGImage extends PNGImage { + getActualCoord(x, y) { + return {x, y}; + } + + get width() { + return this._png.width; + } + + get height() { + return this._png.height; + } +}; diff --git a/lib/png/png.js b/lib/png/png.js new file mode 100644 index 0000000..b6f1fe7 --- /dev/null +++ b/lib/png/png.js @@ -0,0 +1,64 @@ +'use strict'; + +const fs = require('fs'); +const concat = require('concat-stream'); + +module.exports = class PNGImage { + static create(...args) { + return new this(...args); + } + + constructor(png) { + this._png = png; + } + + getPixel(x, y) { + const idx = this._getIdx(x, y); + return { + R: this._png.data[idx], + G: this._png.data[idx + 1], + B: this._png.data[idx + 2] + }; + } + + setPixel(x, y, color) { + const idx = this._getIdx(x, y); + this._png.data[idx] = color.R; + this._png.data[idx + 1] = color.G; + this._png.data[idx + 2] = color.B; + this._png.data[idx + 3] = 255; + } + + getActualCoord() { + throw new Error('Not implemented'); + } + + get width() { + throw new Error('Not implemented'); + } + + get height() { + throw new Error('Not implemented'); + } + + _getIdx(x, y) { + return (this._png.width * y + x) * 4; + } + + save(path, callback) { + const writeStream = fs.createWriteStream(path); + this._png.pack().pipe(writeStream); + + writeStream.on('error', (error) => callback(error)); + writeStream.on('finish', () => callback(null)); + } + + createBuffer(callback) { + this._png.pack().pipe(concat(gotDiff)); + this._png.on('error', (error) => callback(error, null)); + + function gotDiff(data) { + callback(null, data); + } + } +}; diff --git a/lib/utils.js b/lib/utils.js index 7a16f82..9729410 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,9 +1,11 @@ 'use strict'; +const _ = require('lodash'); +const png = require('./png'); const DiffArea = require('./diff-area'); -const png = require('../lib/png'); +const validators = require('./validators'); -const readPair = (first, second, callback) => { +exports.readPair = (first, second, callback) => { const src = {first, second}; const result = {first: null, second: null}; @@ -11,10 +13,10 @@ const readPair = (first, second, callback) => { let failed = false; ['first', 'second'].forEach((key) => { - const source = src[key]; + const {source, ...opts} = src[key]; const readFunc = Buffer.isBuffer(source) ? png.fromBuffer : png.fromFile; - readFunc(source, (error, png) => { + readFunc(source, opts, (error, png) => { if (failed) { return; } @@ -34,7 +36,7 @@ const readPair = (first, second, callback) => { }); }; -const getDiffPixelsCoords = (png1, png2, predicate, opts, callback) => { +exports.getDiffPixelsCoords = (png1, png2, predicate, opts, callback) => { if (!callback) { callback = opts; opts = {}; @@ -61,13 +63,14 @@ const getDiffPixelsCoords = (png1, png2, predicate, opts, callback) => { }); if (!result) { - diffArea.update(x, y); + diffArea.update(png1.getActualCoord(x, y)); if (stopOnFirstFail) { return callback(diffArea); } } } + y++; if (y < height) { @@ -81,7 +84,10 @@ const getDiffPixelsCoords = (png1, png2, predicate, opts, callback) => { processRow(0); }; -module.exports = { - readPair, - getDiffPixelsCoords +exports.formatImages = (img1, img2) => { + validators.validateImages(img1, img2); + + return [img1, img2].map((i) => { + return _.isObject(i) && !Buffer.isBuffer(i) ? i : {source: i, boundingBox: null}; + }); }; diff --git a/lib/validators.js b/lib/validators.js new file mode 100644 index 0000000..cb6602d --- /dev/null +++ b/lib/validators.js @@ -0,0 +1,34 @@ +'use strict'; + +const _ = require('lodash'); +const {REQUIRED_IMAGE_FIELDS, REQUIRED_BOUNDING_BOX_FIELDS} = require('./constants'); + +const validateRequiredFields = (value, fields) => { + [].concat(fields).forEach((field) => { + if (!_.hasIn(value, field)) { + throw new TypeError(`Field "${field}" does not exist in ${JSON.stringify(value)}`); + } + }); +}; + +const validateBoundingBoxCoords = ({boundingBox}) => { + if (boundingBox.left > boundingBox.right) { + throw new TypeError('"left" coordinate in "boundingBox" field cannot be greater than "right"'); + } + + if (boundingBox.top > boundingBox.bottom) { + throw new TypeError('"top" coordinate in "boundingBox" field cannot be greater than "bottom"'); + } +}; + +exports.validateImages = (img1, img2) => { + [img1, img2].forEach((i) => { + if (Buffer.isBuffer(i) || !_.isObject(i)) { + return; + } + + validateRequiredFields(i, REQUIRED_IMAGE_FIELDS); + validateRequiredFields(i.boundingBox, REQUIRED_BOUNDING_BOX_FIELDS); + validateBoundingBoxCoords(i); + }); +}; diff --git a/test/data/src/bounding-box-diff-1.png b/test/data/src/bounding-box-diff-1.png new file mode 100644 index 0000000..cb88be0 Binary files /dev/null and b/test/data/src/bounding-box-diff-1.png differ diff --git a/test/data/src/bounding-box-diff-2.png b/test/data/src/bounding-box-diff-2.png new file mode 100644 index 0000000..cfa463a Binary files /dev/null and b/test/data/src/bounding-box-diff-2.png differ diff --git a/test/data/src/bounding-box-ref-1.png b/test/data/src/bounding-box-ref-1.png new file mode 100644 index 0000000..77c023c Binary files /dev/null and b/test/data/src/bounding-box-ref-1.png differ diff --git a/test/data/src/bounding-box-ref-2.png b/test/data/src/bounding-box-ref-2.png new file mode 100644 index 0000000..2084917 Binary files /dev/null and b/test/data/src/bounding-box-ref-2.png differ diff --git a/test/diff-area.js b/test/diff-area.js index d77b150..ad9de8d 100644 --- a/test/diff-area.js +++ b/test/diff-area.js @@ -13,7 +13,7 @@ describe('DiffArea', () => { it('should update diff area', () => { const diffArea = new DiffArea(); - diffArea.update(99, 99); + diffArea.update({x: 99, y: 99}); expect(diffArea.area).to.deep.equal({left: 99, top: 99, right: 99, bottom: 99}); }); @@ -28,7 +28,7 @@ describe('DiffArea', () => { it('should return "false" if area is not empty', () => { const diffArea = new DiffArea(); - diffArea.update(99, 99); + diffArea.update({x: 99, y: 99}); expect(diffArea.isEmpty()).to.equal(false); }); diff --git a/test/test.js b/test/test.js index 895a7b7..a4d9ba6 100644 --- a/test/test.js +++ b/test/test.js @@ -7,8 +7,7 @@ const expect = require('chai').expect; const looksSame = require('..'); const utils = require('../lib/utils'); -const readPair = utils.readPair; -const getDiffPixelsCoords = utils.getDiffPixelsCoords; +const {readPair, formatImages, getDiffPixelsCoords} = utils; const areColorsSame = require('../lib/same-colors'); const imagePath = (name) => path.join(__dirname, 'data', name); @@ -28,6 +27,12 @@ const forFilesAndBuffers = (callback) => { }; describe('looksSame', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + it('should throw if both tolerance and strict options set', () => { expect(() => { looksSame(srcPath('ref.png'), srcPath('same.png'), { @@ -37,6 +42,26 @@ describe('looksSame', () => { }).to.throw(TypeError); }); + it('should format images', (done) => { + sandbox.spy(utils, 'formatImages'); + + looksSame(srcPath('ref.png'), srcPath('same.png'), () => { + assert.calledOnceWith(utils.formatImages, srcPath('ref.png'), srcPath('same.png')); + done(); + }); + }); + + it('should read formatted images', (done) => { + const [formattedImg1, formattedImg2] = [{source: srcPath('ref.png')}, {source: srcPath('same.png')}]; + sandbox.stub(utils, 'formatImages').returns([formattedImg1, formattedImg2]); + sandbox.spy(utils, 'readPair'); + + looksSame(srcPath('ref.png'), srcPath('same.png'), () => { + assert.calledOnceWith(utils.readPair, formattedImg1, formattedImg2); + done(); + }); + }); + forFilesAndBuffers((getImage) => { it('should return true for similar images', (done) => { looksSame(getImage('ref.png'), getImage('same.png'), (error, {equal}) => { @@ -147,6 +172,74 @@ describe('looksSame', () => { }); }); + describe('with comparing by areas', () => { + forFilesAndBuffers((getImage) => { + describe('if passed areas have different sizes', () => { + it('should return "false"', (done) => { + looksSame( + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 1, top: 1, right: 2, bottom: 1}}, + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 5, top: 5, right: 5, bottom: 6}}, + (error, {equal}) => { + assert.equal(error, null); + assert.equal(equal, false); + done(); + } + ); + }); + + it('should return diff bound for first image equal to a bigger area', (done) => { + looksSame( + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 1, top: 1, right: 2, bottom: 1}}, + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 5, top: 5, right: 5, bottom: 6}}, + (error, {diffBounds}) => { + assert.equal(error, null); + assert.deepEqual(diffBounds, {left: 1, top: 1, right: 2, bottom: 2}); + done(); + } + ); + }); + }); + + describe('if passed areas have the same sizes but located in various places', () => { + it('should return true if images are equal', (done) => { + looksSame( + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 1, top: 1, right: 4, bottom: 4}}, + {source: getImage('bounding-box-diff-2.png'), boundingBox: {left: 5, top: 5, right: 8, bottom: 8}}, + (error, {equal}) => { + assert.equal(error, null); + assert.equal(equal, true); + done(); + } + ); + }); + + it('should return false if images are different', (done) => { + looksSame( + {source: getImage('bounding-box-ref-1.png'), boundingBox: {left: 1, top: 1, right: 4, bottom: 4}}, + {source: getImage('bounding-box-ref-2.png'), boundingBox: {left: 5, top: 5, right: 8, bottom: 8}}, + (error, {equal}) => { + assert.equal(error, null); + assert.equal(equal, false); + done(); + } + ); + }); + + it('should return diff bound for first image if images are different', (done) => { + looksSame( + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 1, top: 1, right: 2, bottom: 1}}, + {source: getImage('bounding-box-diff-1.png'), boundingBox: {left: 5, top: 5, right: 5, bottom: 6}}, + (error, {diffBounds}) => { + assert.equal(error, null); + assert.deepEqual(diffBounds, {left: 1, top: 1, right: 2, bottom: 2}); + done(); + } + ); + }); + }); + }); + }); + describe('with ignoreCaret', () => { forFilesAndBuffers((getImage) => { it('should ignore caret by default', (done) => { @@ -269,6 +362,8 @@ describe('looksSame', () => { }); describe('createDiff', () => { + const sandbox = sinon.createSandbox(); + beforeEach(() => { this.tempName = temp.path({suffix: '.png'}); }); @@ -277,6 +372,8 @@ describe('createDiff', () => { if (fs.existsSync(this.tempName)) { fs.unlinkSync(this.tempName); } + + sandbox.restore(); }); it('should throw if both tolerance and strict options set', () => { @@ -292,6 +389,36 @@ describe('createDiff', () => { }).to.throw(TypeError); }); + it('should format images', (done) => { + sandbox.spy(utils, 'formatImages'); + + looksSame.createDiff({ + reference: srcPath('ref.png'), + current: srcPath('same.png'), + diff: this.tempName, + highlightColor: '#ff00ff' + }, () => { + assert.calledOnceWith(utils.formatImages, srcPath('ref.png'), srcPath('same.png')); + done(); + }); + }); + + it('should read formatted images', (done) => { + const [formattedImg1, formattedImg2] = [{source: srcPath('ref.png')}, {source: srcPath('same.png')}]; + sandbox.stub(utils, 'formatImages').returns([formattedImg1, formattedImg2]); + sandbox.spy(utils, 'readPair'); + + looksSame.createDiff({ + reference: srcPath('ref.png'), + current: srcPath('same.png'), + diff: this.tempName, + highlightColor: '#ff00ff' + }, () => { + assert.calledOnceWith(utils.readPair, formattedImg1, formattedImg2); + done(); + }); + }); + it('should copy a reference image if there is no difference', (done) => { const _this = this; looksSame.createDiff({ @@ -451,6 +578,27 @@ describe('createDiff', () => { }); }); + describe('with comparing by areas', () => { + it('should create diff image equal to reference', (done) => { + looksSame.createDiff({ + reference: {source: srcPath('bounding-box-ref-1.png'), boundingBox: {left: 1, top: 1, right: 4, bottom: 4}}, + current: {source: srcPath('bounding-box-ref-2.png'), boundingBox: {left: 5, top: 5, right: 8, bottom: 8}}, + diff: this.tempName, + highlightColor: '#FF00FF' + }, () => { + looksSame( + {source: srcPath('bounding-box-diff-1.png'), boundingBox: {left: 1, top: 1, right: 4, bottom: 4}}, + this.tempName, + (error, {equal}) => { + assert.equal(error, null); + assert.equal(equal, true); + done(); + } + ); + }); + }); + }); + describe('with antialiasing', () => { describe('if there is only diff in antialiased pixels', () => { it('should create diff image equal to reference if ignore antialiasing is not set', (done) => { @@ -640,6 +788,30 @@ describe('colors', () => { }); describe('getDiffArea', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => sandbox.restore()); + + it('should format images', (done) => { + sandbox.spy(utils, 'formatImages'); + + looksSame.getDiffArea(srcPath('ref.png'), srcPath('same.png'), () => { + assert.calledOnceWith(utils.formatImages, srcPath('ref.png'), srcPath('same.png')); + done(); + }); + }); + + it('should read formatted images', (done) => { + const [formattedImg1, formattedImg2] = [{source: srcPath('ref.png')}, {source: srcPath('same.png')}]; + sandbox.stub(utils, 'formatImages').returns([formattedImg1, formattedImg2]); + sandbox.spy(utils, 'readPair'); + + looksSame.getDiffArea(srcPath('ref.png'), srcPath('same.png'), () => { + assert.calledOnceWith(utils.readPair, formattedImg1, formattedImg2); + done(); + }); + }); + it('should return null for similar images', (done) => { looksSame.getDiffArea(srcPath('ref.png'), srcPath('same.png'), (error, result) => { expect(error).to.equal(null); @@ -686,7 +858,9 @@ describe('getDiffArea', () => { describe('getDiffPixelsCoords', () => { it('should return all diff area by default', (done) => { - readPair(srcPath('ref.png'), srcPath('different.png'), (error, pair) => { + const [img1, img2] = formatImages(srcPath('ref.png'), srcPath('different.png')); + + readPair(img1, img2, (error, pair) => { getDiffPixelsCoords(pair.first, pair.second, areColorsSame, (result) => { expect(result.area).to.deep.equal({left: 0, top: 0, right: 49, bottom: 39}); done(); @@ -695,7 +869,9 @@ describe('getDiffPixelsCoords', () => { }); it('should return first non-matching pixel if asked for', (done) => { - readPair(srcPath('ref.png'), srcPath('different.png'), (error, pair) => { + const [img1, img2] = formatImages(srcPath('ref.png'), srcPath('different.png')); + + readPair(img1, img2, (error, pair) => { getDiffPixelsCoords(pair.first, pair.second, areColorsSame, {stopOnFirstFail: true}, (result) => { expect(result.area).to.deep.equal({left: 49, top: 0, right: 49, bottom: 0}); done(); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..c04dccf --- /dev/null +++ b/test/utils.js @@ -0,0 +1,47 @@ +'use strict'; + +const {formatImages} = require('../lib/utils'); +const validators = require('../lib/validators'); + +describe('lib/utils', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + sandbox.stub(validators, 'validateImages'); + }); + + afterEach(() => sandbox.restore()); + + describe('formatImages', () => { + it('should validate images', () => { + formatImages('img1', 'img2'); + + assert.calledOnce(validators.validateImages); + assert.calledWith(validators.validateImages, 'img1', 'img2'); + }); + + it('should not format images passed as object', () => { + const [img1, img2] = [{source: 'img-path-1'}, {source: 'img-path-1'}]; + const [formattedImg1, formattedImg2] = formatImages(img1, img2); + + assert.deepEqual(formattedImg1, img1); + assert.deepEqual(formattedImg2, img2); + }); + + it('should format images passed as buffers', () => { + const [img1, img2] = [Buffer.from('img-1'), Buffer.from('img-2')]; + const [formattedImg1, formattedImg2] = formatImages(img1, img2); + + assert.deepEqual(formattedImg1, {source: img1, boundingBox: null}); + assert.deepEqual(formattedImg2, {source: img2, boundingBox: null}); + }); + + it('should format images passed as strings', () => { + const [img1, img2] = ['img-path-1', 'img-path-2']; + const [formattedImg1, formattedImg2] = formatImages(img1, img2); + + assert.deepEqual(formattedImg1, {source: img1, boundingBox: null}); + assert.deepEqual(formattedImg2, {source: img2, boundingBox: null}); + }); + }); +}); diff --git a/test/validators.js b/test/validators.js new file mode 100644 index 0000000..b99c100 --- /dev/null +++ b/test/validators.js @@ -0,0 +1,58 @@ +'use strict'; + +const _ = require('lodash'); +const {validateImages} = require('../lib/validators'); + +describe('lib/validators', () => { + describe('validateImages', () => { + it('should not throws if called with buffers', () => { + assert.doesNotThrow(() => validateImages(Buffer.from('one'), Buffer.from('two'))); + }); + + describe('should throws if', () => { + it('required field "source" does not exist', () => { + assert.throws(() => { + return validateImages({}, {}); + }, TypeError, 'Field "source" does not exist'); + }); + + it('required field "boundingBox" does not exist', () => { + assert.throws(() => { + return validateImages( + {source: 'image-path'}, + {source: 'image-path'}, + ); + }, TypeError, 'Field "boundingBox" does not exist'); + }); + + ['left', 'top', 'right', 'bottom'].forEach((field) => { + it(`required field "${field}" does not exist in "boundingBox"`, () => { + assert.throws(() => { + return validateImages( + {source: 'image-path', boundingBox: _.omit({left: 0, top: 0, right: 0, bottom: 0}, field)}, + {source: 'image-path', boundingBox: _.omit({left: 0, top: 0, right: 0, bottom: 0}, field)} + ); + }, TypeError, `Field "${field}" does not exist`); + }); + }); + + it('"left" coordinate in "boundingBox" field greater than "right"', () => { + assert.throws(() => { + return validateImages( + {source: 'image-path', boundingBox: {left: 1, top: 0, right: 0, bottom: 0}}, + {source: 'image-path', boundingBox: {left: 1, top: 0, right: 0, bottom: 0}} + ); + }, TypeError, '"left" coordinate in "boundingBox" field cannot be greater than "right"'); + }); + + it('"top" coordinate in "boundingBox" field greater than "bottom"', () => { + assert.throws(() => { + return validateImages( + {source: 'image-path', boundingBox: {left: 0, top: 1, right: 0, bottom: 0}}, + {source: 'image-path', boundingBox: {left: 0, top: 1, right: 0, bottom: 0}} + ); + }, TypeError, '"top" coordinate in "boundingBox" field cannot be greater than "bottom"'); + }); + }); + }); +});