Skip to content

Commit

Permalink
Merge pull request #105 from NiGhTTraX/pixel-differ-config
Browse files Browse the repository at this point in the history
Allow customizing PixelDiffer
  • Loading branch information
NiGhTTraX authored Jun 21, 2019
2 parents 563299c + 067c4ca commit 831004e
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 34 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ import { remote } from 'webdriverio';

### Config

- `createMissingBaselines?`: If set to true `Mugshot.check` will pass if a baseline is not found and it will create the baseline from the screenshot it takes.
- `createMissingBaselines`: If set to true `Mugshot.check` will pass if a baseline is not found and it will create the baseline from the screenshot it takes.
- `updateBaselines`: When set to true Mugshot will overwrite any existing baselines and will create missing ones (equivalent to setting `createMissingBaselines: true`).
- `pngDiffer`: Will be used to compare images and create diffs. By default it uses [PixelDiffer](./README.md#pixeldiffer).
- `fs`: Will be used to read baselines and write results. By default it uses [fs-extra](https://www.npmjs.com/package/fs-extra).
- `pngProcessor`: Will be used to process the screenshots e.g. crop them, paint over them etc. By default it uses [jimp](https://github.com/oliver-moran/jimp).


### `check(name, selector?, options?)`
Expand All @@ -86,6 +89,16 @@ A selector can be passed as the second argument and will tell Mugshot to only sc
You can ignore a single element on the page (for now, ignoring multiple elements is a planned feature) by passing a selector through the `ignore` option. The first element identified by that selector will be painted black before taking any screenshots.


## PixelDiffer

Default differ implementation that compares images pixel by pixel.

### Config

- `threshold`: A number between 0 and 1 representing the max difference in % between 2 pixels to be considered identical. 0 means the pixel need to be identical, 1 means two completely different images will be identical. 0.1 means black (#000) and 90% gray (0a0a0a) will be identical. Defaults to 0.
- `diffColor`: The color used to mark different pixels. Defaults to red.


## Reducing flakiness

A frequent source of flakiness in visual tests is dynamic data e.g. the current date or live API data. You can ignore elements that contain such data by by painting over them with a solid color square. See the [ignore option](./packages/mugshot/README.md#ignoring-elements) for more details.
Expand Down
4 changes: 2 additions & 2 deletions packages/mugshot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PNGProcessor from './interfaces/png-processor';
import PNGDiffer from './interfaces/png-differ';
import FileSystem from './interfaces/file-system';
import Screenshotter from './interfaces/screenshotter';
import pixelDiffer from './lib/pixel-differ';
import PixelDiffer from './lib/pixel-differ';

export default Mugshot;

Expand All @@ -14,6 +14,6 @@ export {
PNGProcessor,
FileSystem,
Screenshotter,
pixelDiffer,
PixelDiffer,
ElementNotFound
};
4 changes: 2 additions & 2 deletions packages/mugshot/src/lib/mugshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import FileSystem from '../interfaces/file-system';
import PNGProcessor from '../interfaces/png-processor';
import Screenshotter, { ScreenshotOptions } from '../interfaces/screenshotter';
import JimpProcessor from './jimp-processor';
import pixelDiffer from './pixel-differ';
import PixelDiffer from './pixel-differ';
import MugshotScreenshotter from './mugshot-screenshotter';

export type MugshotIdenticalResult = {
Expand Down Expand Up @@ -100,7 +100,7 @@ export default class Mugshot {
resultsPath: string,
{
fs = fsExtra,
pngDiffer = pixelDiffer,
pngDiffer = new PixelDiffer(),
pngProcessor = new JimpProcessor(),
screenshotter = new MugshotScreenshotter(browser, pngProcessor),
createMissingBaselines = !isCI,
Expand Down
47 changes: 39 additions & 8 deletions packages/mugshot/src/lib/pixel-differ.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import PNGDiffer from '../interfaces/png-differ';
import pixelmatch from '../vendor/pixelmatch';
import PNGDiffer, { DiffResult } from '../interfaces/png-differ';
import pixelmatch, { Color } from '../vendor/pixelmatch';
import CustomJimp from '../vendor/custom-jimp';

const pixelDiffer: PNGDiffer = {
compare: async (expected: Buffer, actual: Buffer) => {
interface PixelDifferOptions {
/**
* The color used to mark different pixels.
*/
diffColor?: Color;

/**
* A number between 0 and 1 representing the max difference in %
* between 2 pixels to be considered identical. 0 means the pixel
* need to be identical, 1 means two completely different images
* will be identical. 0.1 means black (#000) and 90% gray (0a0a0a)
* will be identical.
*/
threshold?: number;
}

export default class PixelDiffer implements PNGDiffer {
private readonly diffColor: Color;

private readonly threshold: number;

constructor({
diffColor = { r: 255, g: 0, b: 0 },
threshold = 0
}: PixelDifferOptions = {}) {
this.diffColor = diffColor;
this.threshold = threshold;
}

compare = async (expected: Buffer, actual: Buffer): Promise<DiffResult> => {
const expectedJimp = await CustomJimp.read(expected);
const actualJimp = await CustomJimp.read(actual);

Expand All @@ -27,7 +55,12 @@ const pixelDiffer: PNGDiffer = {
actualJimp.bitmap.data,
diffJimp.bitmap.data, // this will be modified
smallestWidth,
smallestHeight
smallestHeight,
// TODO: set threshold to 0
{
diffColor: this.diffColor,
threshold: this.threshold
}
);

const matches = numDiffPixels === 0;
Expand All @@ -51,6 +84,4 @@ const pixelDiffer: PNGDiffer = {
diff: await diffJimp.getBufferAsync(CustomJimp.MIME_PNG)
};
}
};

export default pixelDiffer;
}
25 changes: 19 additions & 6 deletions packages/mugshot/src/vendor/pixelmatch.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
/* eslint-disable */
export type Color = {
r: number;
g: number;
b: number;
}

export type PixelmatchOptions = {
threshold?: number;
includeAA?: boolean;
diffColor?: Color;
}

// TODO: pull in package after https://github.com/mapbox/pixelmatch/pull/46 is applied.
export default function pixelmatch(
img1: Buffer,
img2: Buffer,
output: Buffer,
width: number,
height: number,
options?: { threshold?: any; includeAA?: any; }
{
threshold = 0.1,
includeAA = false,
diffColor = { r: 255, g: 0, b: 0 }
}: PixelmatchOptions = {}
) {
if (img1.length !== img2.length) {
throw new Error('Image sizes do not match.');
}

if (!options) options = {};

const threshold = options.threshold === undefined ? 0.1 : options.threshold;
// TODO: turn this into a param along with diff and AA colors
const alpha = 0;

Expand All @@ -34,13 +47,13 @@ export default function pixelmatch(
// the color difference is above the threshold
if (delta > maxDelta) {
// check it's a real rendering difference or just anti-aliasing
if (!options.includeAA && (antialiased(img1, x, y, width, height, img2)
if (!includeAA && (antialiased(img1, x, y, width, height, img2)
|| antialiased(img2, x, y, width, height, img1))) {
// one of the pixels is anti-aliasing; draw as yellow and do not count as difference
if (output) drawPixel(output, pos, 255, 255, 0);
} else {
// found substantial difference not caused by anti-aliasing; draw it as red
if (output) drawPixel(output, pos, 255, 0, 0);
if (output) drawPixel(output, pos, diffColor.r, diffColor.g, diffColor.b);
diff++;
}
} else if (output) {
Expand Down
60 changes: 47 additions & 13 deletions packages/mugshot/tests/node/specs/lib/pixel-differ.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { expectIdenticalBuffers, describe, expect, it } from '../../../../../../tests/node/suite';
import pixelDiffer from '../../../../src/lib/pixel-differ';
import PixelDiffer from '../../../../src/lib/pixel-differ';
import {
black90Square50x50Buffer,
blackSquare100x100Buffer,
blackSquare100x50Buffer,
blackSquare50x100Buffer,
blackSquare50x50Buffer,
blackSquare50x50Buffer, blueSquare50x50Buffer,
diffBlackSquare100x100BlackSquare100x50Buffer,
diffBlackSquare100x100BlackSquare50x100Buffer,
diffBlackSquare100x100BlackSquare50x50Buffer,
redSquare100x100Buffer,
redSquare100x100Buffer, redSquare50x50Buffer,
whiteSquare100x100Buffer
} from '../../../../../../tests/node/fixtures';

describe('PixelDiffer', () => {
describe('same width and height', () => {
it('should compare identical buffers', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
blackSquare100x100Buffer
);
Expand All @@ -24,7 +25,7 @@ describe('PixelDiffer', () => {
});

it('should not create a diff for identical buffers', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
blackSquare100x100Buffer
);
Expand All @@ -34,7 +35,7 @@ describe('PixelDiffer', () => {
});

it('should compare different buffers', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
whiteSquare100x100Buffer
);
Expand All @@ -43,7 +44,7 @@ describe('PixelDiffer', () => {
});

it('should create a diff for different buffers', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
whiteSquare100x100Buffer
);
Expand All @@ -54,12 +55,21 @@ describe('PixelDiffer', () => {
redSquare100x100Buffer
);
});

it('should have 0 threshold by default', async () => {
const result = await new PixelDiffer().compare(
blackSquare50x50Buffer,
black90Square50x50Buffer
);

expect(result.matches).to.be.false;
});
});

// The target buffer is a subregion of the source buffer.
describe('different sizes and same content', () => {
it('with different width and height should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
blackSquare50x50Buffer
);
Expand All @@ -73,7 +83,7 @@ describe('PixelDiffer', () => {
});

it('with different width should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
blackSquare50x100Buffer
);
Expand All @@ -87,7 +97,7 @@ describe('PixelDiffer', () => {
});

it('with different height should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
blackSquare100x100Buffer,
blackSquare100x50Buffer
);
Expand All @@ -103,7 +113,7 @@ describe('PixelDiffer', () => {

describe('different sizes and different content', () => {
it('with different width and height should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
whiteSquare100x100Buffer,
blackSquare50x50Buffer
);
Expand All @@ -117,7 +127,7 @@ describe('PixelDiffer', () => {
});

it('with different width should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
whiteSquare100x100Buffer,
blackSquare50x100Buffer
);
Expand All @@ -131,7 +141,7 @@ describe('PixelDiffer', () => {
});

it('with different height should create a diff', async () => {
const result = await pixelDiffer.compare(
const result = await new PixelDiffer().compare(
whiteSquare100x100Buffer,
blackSquare100x50Buffer
);
Expand All @@ -144,4 +154,28 @@ describe('PixelDiffer', () => {
);
});
});

describe('config', () => {
it('should apply custom diff color', async () => {
const result = await new PixelDiffer({ diffColor: { r: 0, g: 0, b: 255 } }).compare(
blackSquare50x50Buffer,
redSquare50x50Buffer
);

await expectIdenticalBuffers(
// @ts-ignore because `.diff` is only present if we narrow by `.matches === false`
result.diff,
blueSquare50x50Buffer
);
});

it('should apply custom threshold', async () => {
const result = await new PixelDiffer({ threshold: 0.1 }).compare(
blackSquare50x50Buffer,
black90Square50x50Buffer
);

expect(result.matches).to.be.true;
});
});
});
Binary file modified tests/gui/screenshots/chrome/simple.diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/gui/screenshots/firefox/simple.diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions tests/gui/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
runnerDescribe,
runnerIt
} from '../mocha-runner';
import pixelDiffer from '../../packages/mugshot/src/lib/pixel-differ';
import PixelDiffer from '../../packages/mugshot/src/lib/pixel-differ';

export { expect };

Expand Down Expand Up @@ -125,7 +125,8 @@ export async function expectIdenticalScreenshots(
screenshot = await fs.readFile(screenshot);
}

expect((await pixelDiffer.compare(baseline, screenshot)).matches, message).to.be.true;
const differ = new PixelDiffer({ threshold: 0 });
expect((await differ.compare(baseline, screenshot)).matches, message).to.be.true;
}

export const screenshotsPath = path.join(__dirname, 'screenshots');
1 change: 1 addition & 0 deletions tests/node/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const diffBlackSquare100x100BlackSquare50x50Buffer = getBufferFixture('di
export const diffBlackSquare100x100BlackSquare100x50Buffer = getBufferFixture('diff-100x100-100x50');
export const diffBlackSquare100x100BlackSquare50x100Buffer = getBufferFixture('diff-100x100-50x100');
export const redSquare100x100Buffer = getBufferFixture('red-square-100x100');
export const black90Square50x50Buffer = getBufferFixture('black90-square-50x50');

// Generated from rgby.html and compressed using https://tinypng.com/.
export const rgbySquare100x100Buffer = getBufferFixture('rgby-square-100x100');
Expand Down
Binary file added tests/node/fixtures/black90-square-50x50.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 831004e

Please sign in to comment.