Skip to content

Commit

Permalink
feat(diff-snapshot): handle image sizes mismatch (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
sbekrin authored and anescobar1991 committed Mar 13, 2018
1 parent 9a10b9a commit fef4dd2
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 28 deletions.
3 changes: 0 additions & 3 deletions __tests__/__snapshots__/integration.spec.js.snap

This file was deleted.

41 changes: 26 additions & 15 deletions __tests__/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ const uniqueId = require('lodash/uniqueId');
const isPng = require('is-png');

describe('toMatchImageSnapshot', () => {
const imagePath = path.resolve(__dirname, './stubs', 'TestImage.png');
const imageData = fs.readFileSync(imagePath);
const fromStubs = file => path.resolve(__dirname, './stubs', file);
const imageData = fs.readFileSync(fromStubs('TestImage.png'));
const diffOutputDir = (snapshotsDir = '__image_snapshots__') => path.join(snapshotsDir, '/__diff_output__/');
const customSnapshotsDir = path.resolve(__dirname, '__custom_snapshots_dir__');
const cleanupRequiredIndicator = 'cleanup-required-';

const getIdentifierIndicatingCleanupIsRequired = () => uniqueId(cleanupRequiredIndicator);
const getSnapshotFilename = identifier => `${identifier}-snap.png`;
const diffExists = identifier => fs.existsSync(path.join(__dirname, diffOutputDir(), `${identifier}-diff.png`));

beforeAll(() => {
const { toMatchImageSnapshot } = require('../src'); // eslint-disable-line global-require
Expand Down Expand Up @@ -79,18 +79,14 @@ describe('toMatchImageSnapshot', () => {
() => expect(imageData).toMatchImageSnapshot({ customSnapshotIdentifier })
).not.toThrowError();

expect(
fs.existsSync(path.join(__dirname, diffOutputDir(), `${customSnapshotIdentifier}-diff.png`))
).toBe(false);
expect(diffExists(customSnapshotIdentifier)).toBe(false);
});
});

describe('failures', () => {
const failImagePath = path.resolve(__dirname, './stubs', 'TestImageFailure.png');
const failImageData = fs.readFileSync(failImagePath);

const oversizeImagePath = path.resolve(__dirname, './stubs', 'TestImageFailureOversize.png');
const oversizeImageData = fs.readFileSync(oversizeImagePath);
const failImageData = fs.readFileSync(fromStubs('TestImageFailure.png'));
const oversizeImageData = fs.readFileSync(fromStubs('TestImageFailureOversize.png'));
const biggerImageData = fs.readFileSync(fromStubs('TestImage150x150.png'));

it('fails for a different snapshot', () => {
const expectedError = /^Expected image to match or be a close match to snapshot but was 86\.55000000000001% different from snapshot \(8655 differing pixels\)\./;
Expand All @@ -107,7 +103,7 @@ describe('toMatchImageSnapshot', () => {
).toThrowError(expectedError);
});

it('fails gracefully with a differently sized image', () => {
it('fails with a differently sized images and outputs diff', () => {
const customSnapshotIdentifier = getIdentifierIndicatingCleanupIsRequired();

// First we need to write a new snapshot image
Expand All @@ -118,7 +114,23 @@ describe('toMatchImageSnapshot', () => {
// Test against an image much larger than the snapshot.
expect(
() => expect(oversizeImageData).toMatchImageSnapshot({ customSnapshotIdentifier })
).toThrowErrorMatchingSnapshot();
).toThrowError(/Expected image to match or be a close match to snapshot but was 83\.85395537525355% different from snapshot/);

expect(diffExists(customSnapshotIdentifier)).toBe(true);
});

it('fails with images without diff pixels after being resized', () => {
const customSnapshotIdentifier = getIdentifierIndicatingCleanupIsRequired();

expect(
() => expect(imageData).toMatchImageSnapshot({ customSnapshotIdentifier })
).not.toThrowError();

expect(
() => expect(biggerImageData).toMatchImageSnapshot({ customSnapshotIdentifier })
).toThrowError(/Expected image to match or be a close match to snapshot but was 54\.662222222222226% different from snapshot/);

expect(diffExists(customSnapshotIdentifier)).toBe(true);
});

it('writes a result image for failing tests', () => {
Expand All @@ -143,7 +155,6 @@ describe('toMatchImageSnapshot', () => {

it('removes result image from previous test runs for the same snapshot', () => {
const customSnapshotIdentifier = getIdentifierIndicatingCleanupIsRequired();
const pathToResultImage = path.join(__dirname, diffOutputDir(), `${customSnapshotIdentifier}-diff.png`);
// First we need to write a new snapshot image
expect(
() => expect(imageData).toMatchImageSnapshot({ customSnapshotIdentifier })
Expand All @@ -159,7 +170,7 @@ describe('toMatchImageSnapshot', () => {
() => expect(imageData).toMatchImageSnapshot({ customSnapshotIdentifier })
).not.toThrowError();

expect(fs.existsSync(pathToResultImage)).toBe(false);
expect(diffExists(customSnapshotIdentifier)).toBe(false);
});
});
});
Binary file added __tests__/stubs/TestImage150x150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 69 additions & 9 deletions src/diff-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,61 @@ const pixelmatch = require('pixelmatch');
const mkdirp = require('mkdirp');
const { PNG } = require('pngjs');

/**
* Helper function to create reusable image resizer
*/
const createImageResizer = (width, height) => (source) => {
const resized = new PNG({ width, height, fill: true });
PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
return resized;
};

/**
* Fills diff area with black transparent color for meaningful diff
*/
/* eslint-disable no-plusplus, no-param-reassign, no-bitwise */
const fillSizeDifference = (width, height) => (image) => {
const inArea = (x, y) => y > height || x > width;
for (let y = 0; y < image.height; y++) {
for (let x = 0; x < image.width; x++) {
if (inArea(x, y)) {
const idx = ((image.width * y) + x) << 2;
image.data[idx] = 0;
image.data[idx + 1] = 0;
image.data[idx + 2] = 0;
image.data[idx + 3] = 64;
}
}
}
return image;
};
/* eslint-enabled */

/**
* Aligns images sizes to biggest common value
* and fills new pixels with transparent pixels
*/
const alignImagesToSameSize = (firstImage, secondImage) => {
// Keep original sizes to fill extended area later
const firstImageWidth = firstImage.width;
const firstImageHeight = firstImage.height;
const secondImageWidth = secondImage.width;
const secondImageHeight = secondImage.height;
// Calculate biggest common values
const resizeToSameSize = createImageResizer(
Math.max(firstImageWidth, secondImageWidth),
Math.max(firstImageHeight, secondImageHeight)
);
// Resize both images
const resizedFirst = resizeToSameSize(firstImage);
const resizedSecond = resizeToSameSize(secondImage);
// Fill resized area with black transparent pixels
return [
fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond),
];
};

function diffImageToSnapshot(options) {
const {
receivedImageBuffer,
Expand All @@ -45,14 +100,16 @@ function diffImageToSnapshot(options) {

const diffConfig = Object.assign({}, defaultDiffConfig, customDiffConfig);

const receivedImage = PNG.sync.read(receivedImageBuffer);
const baselineImage = PNG.sync.read(fs.readFileSync(baselineSnapshotPath));

if (
receivedImage.height !== baselineImage.height || receivedImage.width !== baselineImage.width
) {
throw new Error('toMatchImageSnapshot(): Received image size must match baseline snapshot size in order to make comparison.');
}
const rawReceivedImage = PNG.sync.read(receivedImageBuffer);
const rawBaselineImage = PNG.sync.read(fs.readFileSync(baselineSnapshotPath));
const hasSizeMismatch = (
rawReceivedImage.height !== rawBaselineImage.height ||
rawReceivedImage.width !== rawBaselineImage.width
);
// Align images in size if different
const [receivedImage, baselineImage] = hasSizeMismatch
? alignImagesToSameSize(rawReceivedImage, rawBaselineImage)
: [rawReceivedImage, rawBaselineImage];
const imageWidth = receivedImage.width;
const imageHeight = receivedImage.height;
const diffImage = new PNG({ width: imageWidth, height: imageHeight });
Expand All @@ -70,7 +127,10 @@ function diffImageToSnapshot(options) {
const diffRatio = diffPixelCount / totalPixels;

let pass = false;
if (failureThresholdType === 'pixel') {
if (hasSizeMismatch) {
// Always fail test on image size mismatch
pass = false;
} else if (failureThresholdType === 'pixel') {
pass = diffPixelCount <= failureThreshold;
} else if (failureThresholdType === 'percent') {
pass = diffRatio <= failureThreshold;
Expand Down
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function configureToMatchImageSnapshot({

if (!pass) {
const differencePercentage = result.diffRatio * 100;

message = () => `Expected image to match or be a close match to snapshot but was ${differencePercentage}% different from snapshot (${result.diffPixelCount} differing pixels).\n`
+ `${chalk.bold.red('See diff for details:')} ${chalk.red(result.diffOutputPath)}`;
}
Expand Down

0 comments on commit fef4dd2

Please # to comment.