chore: add blink-diff third party library (#10984)

This commit is contained in:
Pavel Feldman 2021-12-17 15:53:37 -08:00 committed by GitHub
parent b6aad54b9f
commit 8f98074fc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 2372 additions and 1 deletions

View file

@ -24,6 +24,8 @@ import pixelmatch from 'pixelmatch';
import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch';
import { TestInfoImpl, UpdateSnapshots } from '../types';
import { addSuffixToFilePath } from '../util';
import BlinkDiff from '../third_party/blink-diff';
import PNGImage from '../third_party/png-js';
// Note: we require the pngjs version of pixelmatch to avoid version mismatches.
const { PNG } = require(require.resolve('pngjs', { paths: [require.resolve('pixelmatch')] })) as typeof import('pngjs');
@ -66,7 +68,16 @@ function compareImages(actualBuffer: Buffer | string, expectedBuffer: Buffer, mi
};
}
const diff = new PNG({ width: expected.width, height: expected.height });
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { threshold: 0.2, ...options });
const thresholdOptions = { threshold: 0.2, ...options };
if (process.env.PW_USE_BLINK_DIFF && mimeType === 'image/png') {
const diff = new BlinkDiff({
imageA: new PNGImage(expected as any),
imageB: new PNGImage(actual as any),
});
const result = diff.runSync();
return result.code !== BlinkDiff.RESULT_IDENTICAL ? { diff: PNG.sync.write(diff._imageOutput.getImage()) } : null;
}
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, thresholdOptions);
return count > 0 ? { diff: PNG.sync.write(diff) } : null;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,466 @@
/**
* The MIT License (MIT)
* Copyright (c) 2014-2015 Yahoo! Inc.
* Modifications copyright (c) Microsoft Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// @ts-check
/**
* @typedef {number} int
* @typedef {number} float
* @typedef {{red: int, green: int, blue: int, alpha: int | undefined, opacity: float}} RGBAOColor
*/
const { PNG } = require('pngjs');
const fs = require('fs');
class PNGImage {
/**
* Creates an image by dimensions
*
* @static
* @method createImage
* @param {int} width
* @param {int} height
* @return {PNGImage}
*/
static createImage = function (width, height) {
var image = new PNG({
width: width,
height: height
});
return new PNGImage(image);
}
/**
* Copies an already existing image
*
* @static
* @method copyImage
* @param {PNGImage} image
* @return {PNGImage}
*/
static copyImage(image) {
var newImage = this.createImage(image.getWidth(), image.getHeight());
PNG.bitblt(image.getImage(), newImage.getImage(), 0, 0, image.getWidth(), image.getHeight(), 0, 0);
return newImage;
}
/**
* @param {Buffer} buffer
* @returns {PNGImage}
*/
static loadImageSync(buffer) {
return new PNGImage(PNG.sync.read(buffer));
}
/**
* @param {string} path
* @returns {PNGImage}
*/
static readImageSync(path) {
return this.loadImageSync(fs.readFileSync(path));
}
/** @param {PNG} image */
constructor(image) {
/** @type {PNG} */
this._image = image;
}
/**
* Gets the original png-js object
*
* @method getImage
* @return {PNG}
*/
getImage() {
return this._image;
}
/**
* Gets the width of the image
*
* @method getWidth
* @return {int}
*/
getWidth() {
return this._image.width;
}
/**
* Gets the height of the image
*
* @method getHeight
* @return {int}
*/
getHeight() {
return this._image.height;
}
/**
* Clips the current image by modifying it in-place
*
* @method clip
* @param {int} x Starting x-coordinate
* @param {int} y Starting y-coordinate
* @param {int} width Width of area relative to starting coordinate
* @param {int} height Height of area relative to starting coordinate
*/
clip(x, y, width, height) {
var image;
width = Math.min(width, this.getWidth() - x);
height = Math.min(height, this.getHeight() - y);
if ((width < 0) || (height < 0)) {
throw new Error('Width and height cannot be negative.');
}
image = new PNG({
width: width, height: height
});
this._image.bitblt(image, x, y, width, height, 0, 0);
this._image = image;
}
/**
* Fills an area with the specified color
*
* @method fillRect
* @param {int} x Starting x-coordinate
* @param {int} y Starting y-coordinate
* @param {int} width Width of area relative to starting coordinate
* @param {int} height Height of area relative to starting coordinate
* @param {RGBAOColor} color
*/
fillRect(x, y, width, height, color) {
var i,
iLen = x + width,
j,
jLen = y + height,
index;
for (i = x; i < iLen; i++) {
for (j = y; j < jLen; j++) {
index = this.getIndex(i, j);
this.setAtIndex(index, color);
}
}
}
/**
* Gets index of a specific coordinate
*
* @method getIndex
* @param {int} x X-coordinate of pixel
* @param {int} y Y-coordinate of pixel
* @return {int} Index of pixel
*/
getIndex(x, y) {
return (this.getWidth() * y) + x;
}
/**
* Writes the image to the filesystem
*
* @method writeImage
* @param {string} filename Path to file
* @param {(error: Error | undefined, image: PNGImage) => void} fn Callback
*/
writeImage(filename, fn) {
fn = fn || (() => {});
this._image.pack().pipe(fs.createWriteStream(filename)).once('close', () => {
this._image.removeListener('error', fn);
fn(undefined, this);
}).once('error', (err) => {
this._image.removeListener('close', fn);
fn(err, this);
});
}
/**
* @param {string} filename
*/
writeImageSync(filename) {
fs.writeFileSync(filename, PNG.sync.write(this._image));
}
/**
* Gets the color of a pixel at a specific index
*
* @method getPixel
* @param {int} idx Index of pixel
* @return {int} Color
*/
getAtIndex(idx) {
return this.getColorAtIndex(idx) | (this.getAlpha(idx) << 24);
}
/**
* Gets the color of a pixel at a specific coordinate
*
* @method getAt
* @param {int} x X-coordinate of pixel
* @param {int} y Y-coordinate of pixel
* @return {int} Color
*/
getAt(x, y) {
var idx = this.getIndex(x, y);
return this.getAtIndex(idx);
}
/**
* Gets the color of a pixel at a specific coordinate
* Alias for getAt
*
* @method getPixel
* @param {int} x X-coordinate of pixel
* @param {int} y Y-coordinate of pixel
* @return {int} Color
*/
getPixel(x, y) {
return this.getAt(x, y);
}
/**
* Sets the color of a pixel at a specific index
*
* @method setAtIndex
* @param {int} idx Index of pixel
* @param {RGBAOColor|int} color
*/
setAtIndex(idx, color) {
if (typeof color === 'object') {
if (color.red !== undefined) this.setRed(idx, color.red, color.opacity);
if (color.green !== undefined) this.setGreen(idx, color.green, color.opacity);
if (color.blue !== undefined) this.setBlue(idx, color.blue, color.opacity);
if (color.alpha !== undefined) this.setAlpha(idx, color.alpha, color.opacity);
} else {
this.setRed(idx, color & 0xff);
this.setGreen(idx, (color & 0xff00) >> 8);
this.setBlue(idx, (color & 0xff0000) >> 16);
this.setAlpha(idx, (color & 0xff000000) >> 24);
}
}
/**
* Sets the color of a pixel at a specific coordinate
*
* @method setAt
* @param {int} x X-coordinate for pixel
* @param {int} y Y-coordinate for pixel
* @param {RGBAOColor} color
*/
setAt(x, y, color) {
var idx = this.getIndex(x, y);
this.setAtIndex(idx, color);
}
/**
* Sets the color of a pixel at a specific coordinate
* Alias for setAt
*
* @method setPixel
* @param {int} x X-coordinate for pixel
* @param {int} y Y-coordinate for pixel
* @param {RGBAOColor} color
*/
setPixel(x, y, color) {
this.setAt(x, y, color);
}
/**
* Gets the color of a pixel at a specific index
*
* @method getColorAtIndex
* @param {int} idx Index of pixel
* @return {int} Color
*/
getColorAtIndex(idx) {
return this.getRed(idx) | (this.getGreen(idx) << 8) | (this.getBlue(idx) << 16);
}
/**
* Gets the color of a pixel at a specific coordinate
*
* @method getColor
* @param {int} x X-coordinate of pixel
* @param {int} y Y-coordinate of pixel
* @return {int} Color
*/
getColor(x, y) {
var idx = this.getIndex(x, y);
return this.getColorAtIndex(idx);
}
/**
* Calculates the final color value for opacity
*
* @method _calculateColorValue
* @param {int} originalValue
* @param {int} paintValue
* @param {number} [opacity]
* @return {int}
* @private
*/
_calculateColorValue(originalValue, paintValue, opacity) {
var originalPart, paintPart;
if (opacity === undefined) {
return paintValue;
} else {
originalPart = originalValue * (1 - opacity);
paintPart = (paintValue * opacity);
return Math.floor(originalPart + paintPart);
}
}
/**
* Get the red value of a pixel
*
* @method getRed
* @param {int} idx Index of pixel
* @return {int}
*/
getRed(idx) {
return this._getValue(idx, 0);
}
/**
* Set the red value of a pixel
*
* @method setRed
* @param {int} idx Index of pixel
* @param {int} value Value for pixel
* @param {number} [opacity] Opacity of value set
*/
setRed(idx, value, opacity) {
this._setValue(idx, 0, value, opacity);
}
/**
* Get the green value of a pixel
*
* @method getGreen
* @param {int} idx Index of pixel
* @return {int}
*/
getGreen(idx) {
return this._getValue(idx, 1);
}
/**
* Set the green value of a pixel
*
* @method setGreen
* @param {int} idx Index of pixel
* @param {int} value Value for pixel
* @param {number} [opacity] Opacity of value set
*/
setGreen(idx, value, opacity) {
this._setValue(idx, 1, value, opacity);
}
/**
* Get the blue value of a pixel
*
* @method getBlue
* @param {int} idx Index of pixel
* @return {int}
*/
getBlue(idx) {
return this._getValue(idx, 2);
}
/**
* Set the blue value of a pixel
*
* @method setBlue
* @param {int} idx Index of pixel
* @param {int} value Value for pixel
* @param {number} [opacity] Opacity of value set
*/
setBlue(idx, value, opacity) {
this._setValue(idx, 2, value, opacity);
}
/**
* Get the alpha value of a pixel
*
* @method getAlpha
* @param {int} idx Index of pixel
* @return {int}
*/
getAlpha(idx) {
return this._getValue(idx, 3);
}
/**
* Set the alpha value of a pixel
*
* @method setAlpha
* @param {int} idx Index of pixel
* @param {int} value Value for pixel
* @param {number} [opacity] Opacity of value set
*/
setAlpha(idx, value, opacity) {
this._setValue(idx, 3, value, opacity);
}
/**
* Sets the value of a pixel
*
* @method _getValue
* @param {int} offset Offset of a value
* @param {int} colorOffset Offset of a color
* @return {int}
* @private
*/
_getValue(offset, colorOffset) {
var localOffset = offset << 2;
return this._image.data[localOffset + colorOffset];
}
/**
* Sets the value of a pixel
*
* @method _setValue
* @param {int} offset Offset of a value
* @param {int} colorOffset Offset of a color
* @param {int} value Value for pixel
* @param {number} [opacity] Opacity of value set
* @private
*/
_setValue(offset, colorOffset, value, opacity) {
var previousValue = this._getValue(offset, colorOffset),
localOffset = offset << 2;
this._image.data[localOffset + colorOffset] = this._calculateColorValue(previousValue, value, opacity);
}
}
module.exports = PNGImage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

View file

@ -0,0 +1,846 @@
/**
* The MIT License (MIT)
* Copyright (c) 2014-2015 Yahoo! Inc.
* Modifications copyright (c) Microsoft Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// @ts-check
import { test as it, expect } from './playwright-test-fixtures';
import fs from 'fs';
import path from 'path';
import BlinkDiff from '../../packages/playwright-test/lib/third_party/blink-diff';
import PNGImage from '../../packages/playwright-test/lib/third_party/png-js';
/**
* @param {string} type
* @returns {PNGImage}
*/
function generateImage(type) {
let image;
switch (type) {
case 'small-1':
image = PNGImage.createImage(2, 2);
image.setAt(0, 0, { red: 10, green: 20, blue: 30, alpha: 40 });
image.setAt(0, 1, { red: 50, green: 60, blue: 70, alpha: 80 });
image.setAt(1, 0, { red: 90, green: 100, blue: 110, alpha: 120 });
image.setAt(1, 1, { red: 130, green: 140, blue: 150, alpha: 160 });
break;
case 'small-2':
image = PNGImage.createImage(2, 2);
image.setAt(0, 0, { red: 210, green: 220, blue: 230, alpha: 240 });
image.setAt(0, 1, { red: 10, green: 20, blue: 30, alpha: 40 });
image.setAt(1, 0, { red: 50, green: 60, blue: 70, alpha: 80 });
image.setAt(1, 1, { red: 15, green: 25, blue: 35, alpha: 45 });
break;
case 'small-3':
image = PNGImage.createImage(2, 2);
break;
case 'medium-1':
image = PNGImage.createImage(3, 3);
image.setAt(0, 0, { red: 130, green: 140, blue: 150, alpha: 160 });
image.setAt(0, 1, { red: 170, green: 180, blue: 190, alpha: 200 });
image.setAt(0, 2, { red: 210, green: 220, blue: 230, alpha: 240 });
image.setAt(1, 0, { red: 15, green: 25, blue: 35, alpha: 45 });
image.setAt(1, 1, { red: 55, green: 65, blue: 75, alpha: 85 });
image.setAt(1, 2, { red: 95, green: 105, blue: 115, alpha: 125 });
image.setAt(2, 0, { red: 10, green: 20, blue: 30, alpha: 40 });
image.setAt(2, 1, { red: 50, green: 60, blue: 70, alpha: 80 });
image.setAt(2, 2, { red: 90, green: 100, blue: 110, alpha: 120 });
break;
case 'medium-2':
image = PNGImage.createImage(3, 3);
image.setAt(0, 0, { red: 95, green: 15, blue: 165, alpha: 26 });
image.setAt(0, 1, { red: 15, green: 225, blue: 135, alpha: 144 });
image.setAt(0, 2, { red: 170, green: 80, blue: 210, alpha: 2 });
image.setAt(1, 0, { red: 50, green: 66, blue: 23, alpha: 188 });
image.setAt(1, 1, { red: 110, green: 120, blue: 63, alpha: 147 });
image.setAt(1, 2, { red: 30, green: 110, blue: 10, alpha: 61 });
image.setAt(2, 0, { red: 190, green: 130, blue: 180, alpha: 29 });
image.setAt(2, 1, { red: 10, green: 120, blue: 31, alpha: 143 });
image.setAt(2, 2, { red: 155, green: 165, blue: 15, alpha: 185 });
break;
case 'slim-1':
image = PNGImage.createImage(1, 3);
image.setAt(0, 0, { red: 15, green: 225, blue: 135, alpha: 144 });
image.setAt(0, 1, { red: 170, green: 80, blue: 210, alpha: 2 });
image.setAt(0, 2, { red: 50, green: 66, blue: 23, alpha: 188 });
break;
case 'slim-2':
image = PNGImage.createImage(3, 1);
image.setAt(0, 0, { red: 15, green: 225, blue: 135, alpha: 144 });
image.setAt(1, 0, { red: 170, green: 80, blue: 210, alpha: 2 });
image.setAt(2, 0, { red: 50, green: 66, blue: 23, alpha: 188 });
break;
}
return /** @type {PNGImage} */ (image);
}
/**
* @param {Buffer} buf1
* @param {Buffer} buf2
* @returns boolean
*/
function compareBuffer(buf1, buf2) {
if (buf1.length !== buf2.length)
return false;
for (let i = 0, len = buf1.length; i < len; i++) {
if (buf1[i] !== buf2[i])
return false;
}
return true;
}
it.describe('Blink-Diff', () => {
it.describe('Default values', () => {
/** @type {BlinkDiff} */
let instance;
it.beforeEach(() => {
instance = new BlinkDiff({
imageA: 'image-a' as any, imageAPath: 'path to image-a', imageB: 'image-b' as any, imageBPath: 'path to image-b',
composition: false
});
});
it('should have the right values for imageA', () => {
expect(instance._imageA).toBe('image-a');
});
it('should have the right values for imageAPath', () => {
expect(instance._imageAPath).toBe('path to image-a');
});
it('should have the right values for imageB', () => {
expect(instance._imageB).toBe('image-b');
});
it('should have the right values for imageBPath', () => {
expect(instance._imageBPath).toBe('path to image-b');
});
it('should not have a value for imageOutputPath', () => {
expect(instance._imageOutputPath).toBeUndefined();
});
it('should not have a value for thresholdType', () => {
expect(instance._thresholdType).toBe('pixel');
});
it('should not have a value for threshold', () => {
expect(instance._threshold).toBe(500);
});
it('should not have a value for delta', () => {
expect(instance._delta).toBe(20);
});
it('should not have a value for outputMaskRed', () => {
expect(instance._outputMaskRed).toBe(255);
});
it('should not have a value for outputMaskGreen', () => {
expect(instance._outputMaskGreen).toBe(0);
});
it('should not have a value for outputMaskBlue', () => {
expect(instance._outputMaskBlue).toBe(0);
});
it('should not have a value for outputMaskAlpha', () => {
expect(instance._outputMaskAlpha).toBe(255);
});
it('should not have a value for outputMaskOpacity', () => {
expect(instance._outputMaskOpacity).toBe(0.7);
});
it('should not have a value for outputBackgroundRed', () => {
expect(instance._outputBackgroundRed).toBe(0);
});
it('should not have a value for outputBackgroundGreen', () => {
expect(instance._outputBackgroundGreen).toBe(0);
});
it('should not have a value for outputBackgroundBlue', () => {
expect(instance._outputBackgroundBlue).toBe(0);
});
it('should not have a value for outputBackgroundAlpha', () => {
expect(instance._outputBackgroundAlpha).toBeUndefined();
});
it('should not have a value for outputBackgroundOpacity', () => {
expect(instance._outputBackgroundOpacity).toBe(0.6);
});
it('should not have a value for copyImageAToOutput', () => {
expect(instance._copyImageAToOutput).toBeTruthy();
});
it('should not have a value for copyImageBToOutput', () => {
expect(instance._copyImageBToOutput).toBeFalsy();
});
it('should not have a value for filter', () => {
expect(instance._filter).toEqual([]);
});
it('should not have a value for debug', () => {
expect(instance._debug).toBeFalsy();
});
it.describe('Special cases', () => {
/** @type {BlinkDiff} */
let instance;
it.beforeEach(() => {
instance = new BlinkDiff({
imageA: 'image-a' as any, imageB: 'image-b' as any
});
});
it('should have the images', () => {
expect(instance._imageA).toBe('image-a');
expect(instance._imageB).toBe('image-b');
});
});
});
it.describe('Methods', () => {
/** @type {BlinkDiff} */
let instance;
it.beforeEach(() => {
instance = new BlinkDiff({
imageA: 'image-a' as any, imageAPath: 'path to image-a', imageB: 'image-b' as any, imageBPath: 'path to image-b'
});
});
it.describe('hasPassed', () => {
it('should pass when identical', () => {
expect(instance.hasPassed(BlinkDiff.RESULT_IDENTICAL)).toBeTruthy();
});
it('should pass when similar', () => {
expect(instance.hasPassed(BlinkDiff.RESULT_SIMILAR)).toBeTruthy();
});
it('should not pass when unknown', () => {
expect(instance.hasPassed(BlinkDiff.RESULT_UNKNOWN)).toBeFalsy();
});
it('should not pass when different', () => {
expect(instance.hasPassed(BlinkDiff.RESULT_DIFFERENT)).toBeFalsy();
});
});
it.describe('_colorDelta', () => {
it('should calculate the delta', () => {
const color1 = {
c1: 23, c2: 87, c3: 89, c4: 234
}, color2 = {
c1: 84, c2: 92, c3: 50, c4: 21
};
expect(instance._colorDelta(color1, color2)).toBeGreaterThanOrEqual(225.02);
expect(instance._colorDelta(color1, color2)).toBeLessThanOrEqual(225.03);
});
});
it.describe('_loadImage', () => {
/** @type {PNGImage} */
let image;
it.beforeEach(() => {
image = generateImage('medium-2');
});
it.describe('from Image', () => {
it('should use already loaded image', () => {
const result = instance._loadImageSync('pathToFile', image);
expect(result).toBeInstanceOf(PNGImage);
expect(result).toBe(image);
});
});
it.describe('from Path', () => {
it('should load image when only path given', async () => {
const image = await instance._loadImageSync(path.join(__dirname, 'assets', 'test.png'));
const compare = compareBuffer(image.getImage().data, image.getImage().data);
expect(compare).toBeTruthy();
});
});
it.describe('from Buffer', () => {
/** @type {Buffer} */
let buffer;
it.beforeEach(() => {
buffer = fs.readFileSync(path.join(__dirname, 'assets', 'test.png'));
});
it('should load image from buffer if given', async () => {
const image = await instance._loadImageSync('pathToFile', buffer);
const compare = compareBuffer(image.getImage().data, image.getImage().data);
expect(compare).toBeTruthy();
});
});
});
it.describe('_copyImage', () => {
it('should copy the image', () => {
const image1 = generateImage('small-1');
const image2 = generateImage('small-2');
instance._copyImage(image1, image2);
expect(image1.getAt(0, 0)).toBe(image2.getAt(0, 0));
expect(image1.getAt(0, 1)).toBe(image2.getAt(0, 1));
expect(image1.getAt(1, 0)).toBe(image2.getAt(1, 0));
expect(image1.getAt(1, 1)).toBe(image2.getAt(1, 1));
});
});
it.describe('_correctDimensions', () => {
it.describe('Negative Values', () => {
it('should correct negative x values', () => {
const rect = { x: -10, y: 23, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(0);
expect(rect.y).toBe(23);
expect(rect.width).toBe(42);
expect(rect.height).toBe(57);
});
it('should correct negative y values', () => {
const rect = { x: 10, y: -23, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(10);
expect(rect.y).toBe(0);
expect(rect.width).toBe(42);
expect(rect.height).toBe(57);
});
it('should correct negative width values', () => {
const rect = { x: 10, y: 23, width: -42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(10);
expect(rect.y).toBe(23);
expect(rect.width).toBe(0);
expect(rect.height).toBe(57);
});
it('should correct negative height values', () => {
const rect = { x: 10, y: 23, width: 42, height: -57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(10);
expect(rect.y).toBe(23);
expect(rect.width).toBe(42);
expect(rect.height).toBe(0);
});
it('should correct all negative values', () => {
const rect = { x: -10, y: -23, width: -42, height: -57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(0);
expect(rect.y).toBe(0);
expect(rect.width).toBe(0);
expect(rect.height).toBe(0);
});
});
it.describe('Dimensions', () => {
it('should correct too big x values', () => {
const rect = { x: 1000, y: 23, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(299);
expect(rect.y).toBe(23);
expect(rect.width).toBe(1);
expect(rect.height).toBe(57);
});
it('should correct too big y values', () => {
const rect = { x: 10, y: 2300, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(10);
expect(rect.y).toBe(199);
expect(rect.width).toBe(42);
expect(rect.height).toBe(1);
});
it('should correct too big width values', () => {
const rect = { x: 11, y: 23, width: 4200, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(11);
expect(rect.y).toBe(23);
expect(rect.width).toBe(289);
expect(rect.height).toBe(57);
});
it('should correct too big height values', () => {
const rect = { x: 11, y: 23, width: 42, height: 5700 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(11);
expect(rect.y).toBe(23);
expect(rect.width).toBe(42);
expect(rect.height).toBe(177);
});
it('should correct too big width and height values', () => {
const rect = { x: 11, y: 23, width: 420, height: 570 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(11);
expect(rect.y).toBe(23);
expect(rect.width).toBe(289);
expect(rect.height).toBe(177);
});
});
it.describe('Border Dimensions', () => {
it('should correct too big x values', () => {
const rect = { x: 300, y: 23, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(299);
expect(rect.y).toBe(23);
expect(rect.width).toBe(1);
expect(rect.height).toBe(57);
});
it('should correct too big y values', () => {
const rect = { x: 10, y: 200, width: 42, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(10);
expect(rect.y).toBe(199);
expect(rect.width).toBe(42);
expect(rect.height).toBe(1);
});
it('should correct too big width values', () => {
const rect = { x: 11, y: 23, width: 289, height: 57 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(11);
expect(rect.y).toBe(23);
expect(rect.width).toBe(289);
expect(rect.height).toBe(57);
});
it('should correct too big height values', () => {
const rect = { x: 11, y: 23, width: 42, height: 177 };
instance._correctDimensions(300, 200, rect);
expect(rect.x).toBe(11);
expect(rect.y).toBe(23);
expect(rect.width).toBe(42);
expect(rect.height).toBe(177);
});
});
});
it.describe('_crop', () => {
/** @type {PNGImage} */
let croppedImage;
/** @type {PNGImage} */
let expectedImage;
it.beforeEach(() => {
croppedImage = generateImage('medium-1');
expectedImage = generateImage('medium-1');
});
it('should crop image', () => {
instance._crop('Medium-1', croppedImage, { x: 1, y: 2, width: 2, height: 1 });
expect(croppedImage.getWidth()).toBe(2);
expect(croppedImage.getHeight()).toBe(1);
expect(croppedImage.getAt(0, 0)).toBe(expectedImage.getAt(1, 2));
expect(croppedImage.getAt(1, 0)).toBe(expectedImage.getAt(2, 2));
});
});
it.describe('_clip', () => {
it('should clip the image small and medium', () => {
const image1 = generateImage('small-1'), image2 = generateImage('medium-2');
instance._clip(image1, image2);
expect(image1.getWidth()).toBe(image2.getWidth());
expect(image1.getHeight()).toBe(image2.getHeight());
});
it('should clip the image medium and small', () => {
const image1 = generateImage('medium-1'), image2 = generateImage('small-2');
instance._clip(image1, image2);
expect(image1.getWidth()).toBe(image2.getWidth());
expect(image1.getHeight()).toBe(image2.getHeight());
});
it('should clip the image slim-1 and medium', () => {
const image1 = generateImage('slim-1'), image2 = generateImage('medium-1');
instance._clip(image1, image2);
expect(image1.getWidth()).toBe(image2.getWidth());
expect(image1.getHeight()).toBe(image2.getHeight());
});
it('should clip the image slim-2 and medium', () => {
const image1 = generateImage('slim-2'), image2 = generateImage('medium-1');
instance._clip(image1, image2);
expect(image1.getWidth()).toBe(image2.getWidth());
expect(image1.getHeight()).toBe(image2.getHeight());
});
it('should clip the image small and small', () => {
const image1 = generateImage('small-2'), image2 = generateImage('small-1');
instance._clip(image1, image2);
expect(image1.getWidth()).toBe(image2.getWidth());
expect(image1.getHeight()).toBe(image2.getHeight());
});
});
it.describe('isAboveThreshold', () => {
it.describe('Pixel threshold', () => {
it.beforeEach(() => {
instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL;
instance._threshold = 50;
});
it('should be below threshold', () => {
expect(instance.isAboveThreshold(49)).toBeFalsy();
});
it('should be above threshold on border', () => {
expect(instance.isAboveThreshold(50)).toBeTruthy();
});
it('should be above threshold', () => {
expect(instance.isAboveThreshold(51)).toBeTruthy();
});
});
it.describe('Percent threshold', () => {
it.beforeEach(() => {
instance._thresholdType = BlinkDiff.THRESHOLD_PERCENT;
instance._threshold = 0.1;
});
it('should be below threshold', () => {
expect(instance.isAboveThreshold(9, 100)).toBeFalsy();
});
it('should be above threshold on border', () => {
expect(instance.isAboveThreshold(10, 100)).toBeTruthy();
});
it('should be above threshold', () => {
expect(instance.isAboveThreshold(11, 100)).toBeTruthy();
});
});
});
it.describe('Comparison', () => {
/** @type {PNGImage} */
let image1;
/** @type {PNGImage} */
let image2;
/** @type {PNGImage} */
let image3;
/** @type {PNGImage} */
let image4;
/** @type {{ red: number, green: number, blue: number, alpha: number }} */
let maskColor;
/** @type {{ red: number, green: number, blue: number, alpha: number }} */
let shiftColor;
/** @type {{ red: number, green: number, blue: number, alpha: number }} */
let backgroundMaskColor;
it.beforeEach(() => {
image1 = generateImage('small-1');
image2 = generateImage('small-2');
image3 = generateImage('small-3');
image4 = generateImage('small-1');
maskColor = {
red: 123, green: 124, blue: 125, alpha: 126
};
shiftColor = {
red: 200, green: 100, blue: 0, alpha: 113
};
backgroundMaskColor = {
red: 31, green: 33, blue: 35, alpha: 37
};
});
it.describe('_pixelCompare', () => {
it('should have no differences with a zero dimension', () => {
const deltaThreshold = 10, width = 0, height = 0, hShift = 0, vShift = 0;
const result = instance._pixelCompare(image1, image2, image3, deltaThreshold, width, height, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toBe(0);
});
it('should have all differences', () => {
const deltaThreshold = 10, width = 2, height = 2, hShift = 0, vShift = 0;
const result = instance._pixelCompare(image1, image2, image3, deltaThreshold, width, height, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toBe(4);
});
it('should have some differences', () => {
const deltaThreshold = 100, width = 2, height = 2, hShift = 0, vShift = 0;
const result = instance._pixelCompare(image1, image2, image3, deltaThreshold, width, height, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toBe(2);
});
});
it.describe('_compare', () => {
it.beforeEach(() => {
instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL;
instance._threshold = 3;
});
it('should be different', () => {
const deltaThreshold = 10, hShift = 0, vShift = 0;
const result = instance._compare(image1, image2, image3, deltaThreshold, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toEqual({
code: BlinkDiff.RESULT_DIFFERENT, differences: 4, dimension: 4, width: 2, height: 2
});
});
it('should be similar', () => {
const deltaThreshold = 100, hShift = 0, vShift = 0;
const result = instance._compare(image1, image2, image3, deltaThreshold, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toEqual({
code: BlinkDiff.RESULT_SIMILAR, differences: 2, dimension: 4, width: 2, height: 2
});
});
it('should be identical', () => {
const deltaThreshold = 10, hShift = 0, vShift = 0;
const result = instance._compare(image1, image4, image3, deltaThreshold, maskColor, shiftColor, backgroundMaskColor, hShift, vShift);
expect(result).toEqual({
code: BlinkDiff.RESULT_IDENTICAL, differences: 0, dimension: 4, width: 2, height: 2
});
});
});
});
it.describe('Run', () => {
it.beforeEach(() => {
instance._imageA = generateImage('small-1');
instance._imageB = generateImage('medium-1');
instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL;
instance._threshold = 3;
instance._composition = false;
});
it('should crop image-a', async () => {
instance._cropImageA = { width: 1, height: 2 };
console.log('A');
const result = instance.runSync();
expect(result.dimension).toBe(2);
});
it('should crop image-b', async () => {
instance._cropImageB = { width: 1, height: 1 };
const result = instance.runSync();
expect(result.dimension).toBe(1);
});
it('should clip image-b', async () => {
const result = instance.runSync();
expect(result.dimension).toBe(4);
});
it('should crop and clip images', async () => {
instance._cropImageA = { width: 1, height: 2 };
instance._cropImageB = { width: 1, height: 1 };
const result = instance.runSync();
expect(result.dimension).toBe(1);
});
it('should write output file', async ({}, testInfo) => {
instance._imageOutputPath = testInfo.outputPath('tmp.png');
instance.runSync();
if (!fs.existsSync(testInfo.outputPath('tmp.png')))
throw new Error('Could not write file.');
});
it('should compare image-a to image-b', async () => {
const result = instance.runSync();
expect(result.code).toBe(BlinkDiff.RESULT_DIFFERENT);
});
it('should be black', async () => {
instance._delta = 1000;
instance._copyImageAToOutput = false;
instance._copyImageBToOutput = false;
instance._outputBackgroundRed = 0;
instance._outputBackgroundGreen = 0;
instance._outputBackgroundBlue = 0;
instance._outputBackgroundAlpha = 0;
instance._outputBackgroundOpacity = undefined;
instance.runSync();
expect(instance._imageOutput.getAt(0, 0)).toBe(0);
});
it('should copy image-a to output by default', async () => {
instance._delta = 1000;
instance._outputBackgroundRed = undefined;
instance._outputBackgroundGreen = undefined;
instance._outputBackgroundBlue = undefined;
instance._outputBackgroundAlpha = undefined;
instance._outputBackgroundOpacity = undefined;
instance.runSync();
expect(instance._imageOutput.getAt(0, 0)).toBe(instance._imageA.getAt(0, 0));
});
it('should copy image-a to output', async () => {
instance._delta = 1000;
instance._copyImageAToOutput = true;
instance._copyImageBToOutput = false;
instance._outputBackgroundRed = undefined;
instance._outputBackgroundGreen = undefined;
instance._outputBackgroundBlue = undefined;
instance._outputBackgroundAlpha = undefined;
instance._outputBackgroundOpacity = undefined;
instance.runSync();
expect(instance._imageOutput.getAt(0, 0)).toBe(instance._imageA.getAt(0, 0));
});
it('should copy image-b to output', async () => {
instance._delta = 1000;
instance._copyImageAToOutput = false;
instance._copyImageBToOutput = true;
instance._outputBackgroundRed = undefined;
instance._outputBackgroundGreen = undefined;
instance._outputBackgroundBlue = undefined;
instance._outputBackgroundAlpha = undefined;
instance._outputBackgroundOpacity = undefined;
instance.runSync();
expect(instance._imageOutput.getAt(0, 0)).toBe(instance._imageB.getAt(0, 0));
});
});
it.describe('Color-Conversion', () => {
it('should convert RGB to XYZ', () => {
const color = /** @type {any} */ (instance._convertRgbToXyz({ c1: 92 / 255, c2: 255 / 255, c3: 162 / 255, c4: 1 }));
expect(color.c1).toBeCloseTo(0.6144431682352941, 0.0001);
expect(color.c2).toBeCloseTo(0.8834245847058824, 0.0001);
expect(color.c3).toBeCloseTo(0.6390158682352941, 0.0001);
expect(color.c4).toBeCloseTo(1, 0.0001);
});
it('should convert Xyz to CIELab', () => {
const color = /** @type {any} */ (instance._convertXyzToCieLab({
c1: 0.6144431682352941, c2: 0.8834245847058824, c3: 0.6390158682352941, c4: 1
}));
expect(color.c1).toBeCloseTo(95.30495102757038, 0.0001);
expect(color.c2).toBeCloseTo(-54.68933740774734, 0.0001);
expect(color.c3).toBeCloseTo(19.63870174748623, 0.0001);
expect(color.c4).toBeCloseTo(1, 0.0001);
});
});
});
});

View file

@ -421,6 +421,7 @@ test('should compare different PNG images', async ({ runInlineTest }, testInfo)
});
test('should respect threshold', async ({ runInlineTest }) => {
test.skip(!!process.env.PW_USE_BLINK_DIFF);
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({
@ -441,6 +442,7 @@ test('should respect threshold', async ({ runInlineTest }) => {
});
test('should respect project threshold', async ({ runInlineTest }) => {
test.skip(!!process.env.PW_USE_BLINK_DIFF);
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({