From 0682672242892b39f193e9f0968a33064751d526 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Fri, 18 Feb 2022 15:39:17 -0700 Subject: [PATCH] chore: move comparator logic to playwright-core (#12232) This will enable implementation of `toHaveScreenshot` on the server-side. Drive-by: drop blink-diff References #9938 --- packages/playwright-core/package.json | 3 + .../src/third_party/diff_match_patch.js | 0 .../src/utils}/comparators.ts | 10 - packages/playwright-test/package.json | 3 - .../src/matchers/toMatchSnapshot.ts | 2 +- .../src/third_party/blink-diff.js | 1046 ----------------- .../playwright-test/src/third_party/png-js.js | 466 -------- tests/playwright-test/blink-diff.spec.ts | 845 ------------- 8 files changed, 4 insertions(+), 2371 deletions(-) rename packages/{playwright-test => playwright-core}/src/third_party/diff_match_patch.js (100%) rename packages/{playwright-test/src/matchers => playwright-core/src/utils}/comparators.ts (90%) delete mode 100644 packages/playwright-test/src/third_party/blink-diff.js delete mode 100644 packages/playwright-test/src/third_party/png-js.js delete mode 100644 tests/playwright-test/blink-diff.spec.ts diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 4962e9ad57..655de43991 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -23,6 +23,7 @@ "./lib/grid/dockerGridFactory": "./lib/grid/dockerGridFactory.js", "./lib/outofprocess": "./lib/outofprocess.js", "./lib/utils/async": "./lib/utils/async.js", + "./lib/utils/comparators": "./lib/utils/comparators.js", "./lib/utils/httpServer": "./lib/utils/httpServer.js", "./lib/utils/multimap": "./lib/utils/multimap.js", "./lib/utils/processLauncher": "./lib/utils/processLauncher.js", @@ -38,11 +39,13 @@ }, "dependencies": { "commander": "8.3.0", + "colors": "1.4.0", "debug": "4.3.3", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.3", "mime": "3.0.0", + "pixelmatch": "5.2.1", "pngjs": "6.0.0", "progress": "2.0.3", "proper-lockfile": "4.1.2", diff --git a/packages/playwright-test/src/third_party/diff_match_patch.js b/packages/playwright-core/src/third_party/diff_match_patch.js similarity index 100% rename from packages/playwright-test/src/third_party/diff_match_patch.js rename to packages/playwright-core/src/third_party/diff_match_patch.js diff --git a/packages/playwright-test/src/matchers/comparators.ts b/packages/playwright-core/src/utils/comparators.ts similarity index 90% rename from packages/playwright-test/src/matchers/comparators.ts rename to packages/playwright-core/src/utils/comparators.ts index b234f7950e..144d83a953 100644 --- a/packages/playwright-test/src/matchers/comparators.ts +++ b/packages/playwright-core/src/utils/comparators.ts @@ -19,8 +19,6 @@ import colors from 'colors/safe'; import jpeg from 'jpeg-js'; import pixelmatch from 'pixelmatch'; import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch'; -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'); @@ -58,14 +56,6 @@ function compareImages(mimeType: string, actualBuffer: Buffer | string, expected } const diff = new PNG({ width: expected.width, height: expected.height }); 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); const pixelCount1 = options.pixelCount; diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index d45b84fd04..505b12cd79 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -51,16 +51,13 @@ "debug": "4.3.3", "expect": "27.2.5", "jest-matcher-utils": "27.2.5", - "jpeg-js": "0.4.3", "json5": "2.2.0", "mime": "3.0.0", "minimatch": "3.0.4", "ms": "2.1.3", "open": "8.4.0", "pirates": "4.0.4", - "pixelmatch": "5.2.1", "playwright-core": "1.20.0-next", - "pngjs": "6.0.0", "rimraf": "3.0.2", "source-map-support": "0.4.18", "stack-utils": "2.0.5", diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index edf576e817..fc24c41498 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -16,7 +16,7 @@ import type { Expect } from '../types'; import { currentTestInfo } from '../globals'; -import { mimeTypeToComparator, ComparatorResult, ImageComparatorOptions } from './comparators'; +import { mimeTypeToComparator, ComparatorResult, ImageComparatorOptions } from 'playwright-core/lib/utils/comparators'; import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString } from '../util'; import { UpdateSnapshots } from '../types'; import colors from 'colors/safe'; diff --git a/packages/playwright-test/src/third_party/blink-diff.js b/packages/playwright-test/src/third_party/blink-diff.js deleted file mode 100644 index 23d790703e..0000000000 --- a/packages/playwright-test/src/third_party/blink-diff.js +++ /dev/null @@ -1,1046 +0,0 @@ -/** - * 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 {{c1: number, c2: number, c3: number, c4: number}} Color - * @typedef {{red: int, green: int, blue: int, alpha: int | undefined, opacity: float}} RGBAOColor - * @typedef {{R: number | undefined, G: number | undefined, B: number | undefined}} Gamma - * @typedef {{width: number, height: number, x: number, y: number}} Rect - * @typedef {{width: number, height: number, dimention: number, differences: number[], code: number}} Result - * @typedef {{ - * imageA?: PNGImage|Buffer, - * imageAPath?: string, - * imageB?: PNGImage|Buffer, - * imageBPath?: string, - * imageOutputPath?: string, - * imageOutputLimit?: int, - * thresholdType?: string, - * threshold?: int, - * delta?: int, - * outputMaskRed?: int, - * outputMaskGreen?: int, - * outputMaskBlue?: int, - * outputMaskAlpha?: int, - * outputMaskOpacity?: float, - * outputShiftRed?: int, - * outputShiftGreen?: int, - * outputShiftBlue?: int, - * outputShiftAlpha?: int, - * outputShiftOpacity?: float, - * outputBackgroundRed?: int, - * outputBackgroundGreen?: int, - * outputBackgroundBlue?: int, - * outputBackgroundAlpha?: int, - * outputBackgroundOpacity?: float, - * blockOut?: Rect|Rect[], - * blockOutRed?: int, - * blockOutGreen?: int, - * blockOutBlue?: int, - * blockOutAlpha?: int, - * blockOutOpacity?: float, - * copyImageAToOutput?: boolean, - * copyImageBToOutput?: boolean, - * filter?: string[], - * debug?: boolean, - * composition?: boolean, - * composeLeftToRight?: boolean, - * composeTopToBottom?: boolean, - * hideShift?: boolean, - * hShift?: int, - * vShift?: int, - * cropImageA?: { x: number, y: number, width: number, height: number }, - * cropImageB?: { x: number, y: number, width: number, height: number }, - * perceptual?: boolean, - * gamma?: float, - * gammaR?: float, - * gammaG?: float, - * gammaB?: float, - * }} Options - */ - -const assert = require('assert'); -const PNGImage = require('./png-js'); -const { PNG } = require('pngjs'); - -/** - * @template G - * @param {any} value - * @param {G} defaultValue - * @returns {G} - */ -function load(value, defaultValue) { - return ((value == null) ? defaultValue : value); -} - -/** - * Blink-diff comparison class - */ -class BlinkDiff { - - /* - * @param {object} options - * @param {PNGImage|Buffer} options.imageA Image object of first image - * @param {string} options.imageAPath Path to first image - * @param {PNGImage|Buffer} options.imageB Image object of second image - * @param {string} options.imageBPath Path to second image - * @param {string} [options.imageOutputPath=undefined] Path to output image file - * @param {int} [options.imageOutputLimit=BlinkDiff.OUTPUT_ALL] Determines when an image output is created - * @param {string} [options.thresholdType=BlinkDiff.THRESHOLD_PIXEL] Defines the threshold of the comparison - * @param {int} [options.threshold=500] Threshold limit according to the comparison limit. - * @param {number} [options.delta=20] Distance between the color coordinates in the 4 dimensional color-space that will not trigger a difference. - * @param {int} [options.outputMaskRed=255] Value to set for red on difference pixel. 'Undefined' will not change the value. - * @param {int} [options.outputMaskGreen=0] Value to set for green on difference pixel. 'Undefined' will not change the value. - * @param {int} [options.outputMaskBlue=0] Value to set for blue on difference pixel. 'Undefined' will not change the value. - * @param {int} [options.outputMaskAlpha=255] Value to set for the alpha channel on difference pixel. 'Undefined' will not change the value. - * @param {float} [options.outputMaskOpacity=0.7] Strength of masking the pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. - * @param {int} [options.outputShiftRed=255] Value to set for red on shifted pixel. 'Undefined' will not change the value. - * @param {int} [options.outputShiftGreen=165] Value to set for green on shifted pixel. 'Undefined' will not change the value. - * @param {int} [options.outputShiftBlue=0] Value to set for blue on shifted pixel. 'Undefined' will not change the value. - * @param {int} [options.outputShiftAlpha=255] Value to set for the alpha channel on shifted pixel. 'Undefined' will not change the value. - * @param {float} [options.outputShiftOpacity=0.7] Strength of masking the shifted pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. - * @param {int} [options.outputBackgroundRed=0] Value to set for red as background. 'Undefined' will not change the value. - * @param {int} [options.outputBackgroundGreen=0] Value to set for green as background. 'Undefined' will not change the value. - * @param {int} [options.outputBackgroundBlue=0] Value to set for blue as background. 'Undefined' will not change the value. - * @param {int} [options.outputBackgroundAlpha=undefined] Value to set for the alpha channel as background. 'Undefined' will not change the value. - * @param {float} [options.outputBackgroundOpacity=0.6] Strength of masking the pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. - * @param {object|object[]} [options.blockOut] Object or list of objects with coordinates of blocked-out areas. - * @param {int} [options.blockOutRed=0] Value to set for red on blocked-out pixel. 'Undefined' will not change the value. - * @param {int} [options.blockOutGreen=0] Value to set for green on blocked-out pixel. 'Undefined' will not change the value. - * @param {int} [options.blockOutBlue=0] Value to set for blue on blocked-out pixel. 'Undefined' will not change the value. - * @param {int} [options.blockOutAlpha=255] Value to set for the alpha channel on blocked-out pixel. 'Undefined' will not change the value. - * @param {float} [options.blockOutOpacity=1.0] Strength of masking the blocked-out pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. - * @param {boolean} [options.copyImageAToOutput=true] Copies the first image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the first image. - * @param {boolean} [options.copyImageBToOutput=false] Copies the second image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the second image. - * @param {string[]} [options.filter=[]] Filters that will be applied before the comparison. Available filters are: blur, grayScale, lightness, luma, luminosity, sepia - * @param {boolean} [options.debug=false] When set, then the applied filters will be shown on the output image. - * @param {boolean} [options.composition=true] Should a composition be created to compare? - * @param {boolean} [options.composeLeftToRight=false] Create composition from left to right, otherwise let it decide on its own whats best - * @param {boolean} [options.composeTopToBottom=false] Create composition from top to bottom, otherwise let it decide on its own whats best - * @param {boolean} [options.hideShift=false] Hides shift highlighting by using the background color instead - * @param {int} [options.hShift=2] Horizontal shift for possible antialiasing - * @param {int} [options.vShift=2] Vertical shift for possible antialiasing - * @param {object} [options.cropImageA=null] Cropping for first image (default: no cropping) - * @param {int} [options.cropImageA.x=0] Coordinate for left corner of cropping region - * @param {int} [options.cropImageA.y=0] Coordinate for top corner of cropping region - * @param {int} [options.cropImageA.width] Width of cropping region (default: Width that is left) - * @param {int} [options.cropImageA.height] Height of cropping region (default: Height that is left) - * @param {object} [options.cropImageB=null] Cropping for second image (default: no cropping) - * @param {int} [options.cropImageB.x=0] Coordinate for left corner of cropping region - * @param {int} [options.cropImageB.y=0] Coordinate for top corner of cropping region - * @param {int} [options.cropImageB.width] Width of cropping region (default: Width that is left) - * @param {int} [options.cropImageB.height] Height of cropping region (default: Height that is left) - * @param {boolean} [options.perceptual=false] Turns perceptual comparison on - * @param {float} [options.gamma] Gamma correction for all colors - * @param {float} [options.gammaR] Gamma correction for red - * @param {float} [options.gammaG] Gamma correction for green - * @param {float} [options.gammaB] Gamma correction for blue - * - * @property {PNGImage} _imageA - * @property {PNGImage} _imageACompare - * @property {string} _imageAPath - * @property {PNGImage} _imageB - * @property {PNGImage} _imageBCompare - * @property {string} _imageBPath - * @property {PNGImage} _imageOutput - * @property {string} _imageOutputPath - * @property {int} _imageOutputLimit - * @property {string} _thresholdType - * @property {int} _threshold - * @property {number} _delta - * @property {int} _outputMaskRed - * @property {int} _outputMaskGreen - * @property {int} _outputMaskBlue - * @property {int} _outputMaskAlpha - * @property {float} _outputMaskOpacity - * @property {int} _outputShiftRed - * @property {int} _outputShiftGreen - * @property {int} _outputShiftBlue - * @property {int} _outputShiftAlpha - * @property {float} _outputShiftOpacity - * @property {int} _outputBackgroundRed - * @property {int} _outputBackgroundGreen - * @property {int} _outputBackgroundBlue - * @property {int} _outputBackgroundAlpha - * @property {float} _outputBackgroundOpacity - * @property {object[]} _blockOut - * @property {int} _blockOutRed - * @property {int} _blockOutGreen - * @property {int} _blockOutBlue - * @property {int} _blockOutAlpha - * @property {float} _blockOutOpacity - * @property {boolean} _copyImageAToOutput - * @property {boolean} _copyImageBToOutput - * @property {string[]} _filter - * @property {boolean} _debug - * @property {boolean} _composition - * @property {boolean} _composeLeftToRight - * @property {boolean} _composeTopToBottom - * @property {int} _hShift - * @property {int} _vShift - * @property {object} _cropImageA - * @property {int} _cropImageA.x - * @property {int} _cropImageA.y - * @property {int} _cropImageA.width - * @property {int} _cropImageA.height - * @property {object} _cropImageB - * @property {int} _cropImageB.x - * @property {int} _cropImageB.y - * @property {int} _cropImageB.width - * @property {int} _cropImageB.height - * @property {object} _refWhite - * @property {boolean} _perceptual - * @property {float} _gamma - * @property {float} _gammaR - * @property {float} _gammaG - * @property {float} _gammaB - */ - /** - * @param {Options} options - */ - constructor(options) { - - this._imageA = options.imageA; - this._imageAPath = options.imageAPath; - assert.ok(options.imageAPath || options.imageA, "Image A not given."); - - this._imageB = options.imageB; - this._imageBPath = options.imageBPath; - assert.ok(options.imageBPath || options.imageB, "Image B not given."); - - this._imageOutput = null; - this._imageOutputPath = options.imageOutputPath; - this._imageOutputLimit = load(options.imageOutputLimit, BlinkDiff.OUTPUT_ALL); - - // Pixel or Percent - this._thresholdType = load(options.thresholdType, BlinkDiff.THRESHOLD_PIXEL); - - // How many pixels different to ignore. - this._threshold = load(options.threshold, 500); - - this._delta = load(options.delta, 20); - - this._outputMaskRed = load(options.outputMaskRed, 255); - this._outputMaskGreen = load(options.outputMaskGreen, 0); - this._outputMaskBlue = load(options.outputMaskBlue, 0); - this._outputMaskAlpha = load(options.outputMaskAlpha, 255); - this._outputMaskOpacity = load(options.outputMaskOpacity, 0.7); - - this._outputBackgroundRed = load(options.outputBackgroundRed, 0); - this._outputBackgroundGreen = load(options.outputBackgroundGreen, 0); - this._outputBackgroundBlue = load(options.outputBackgroundBlue, 0); - this._outputBackgroundAlpha = options.outputBackgroundAlpha; - this._outputBackgroundOpacity = load(options.outputBackgroundOpacity, 0.6); - - if (options.hideShift) { - this._outputShiftRed = this._outputBackgroundRed; - this._outputShiftGreen = this._outputBackgroundGreen; - this._outputShiftBlue = this._outputBackgroundBlue; - this._outputShiftAlpha = this._outputBackgroundAlpha; - this._outputShiftOpacity = this._outputBackgroundOpacity; - - } else { - this._outputShiftRed = load(options.outputShiftRed, 200); - this._outputShiftGreen = load(options.outputShiftGreen, 100); - this._outputShiftBlue = load(options.outputShiftBlue, 0); - this._outputShiftAlpha = load(options.outputShiftAlpha, 255); - this._outputShiftOpacity = load(options.outputShiftOpacity, 0.7); - } - - const blockOut = /** @type {Rect[]|Rect}*/ (load(options.blockOut, [])); - /** @type {Rect[]}*/ - this._blockOut = Array.isArray(blockOut) ? blockOut : [blockOut]; - - this._blockOutRed = load(options.blockOutRed, 0); - this._blockOutGreen = load(options.blockOutGreen, 0); - this._blockOutBlue = load(options.blockOutBlue, 0); - this._blockOutAlpha = load(options.blockOutAlpha, 255); - this._blockOutOpacity = load(options.blockOutOpacity, 1.0); - - this._copyImageAToOutput = load(options.copyImageAToOutput, true); - this._copyImageBToOutput = load(options.copyImageBToOutput, false); - - this._filter = load(options.filter, []); - - this._debug = load(options.debug, false); - - this._composition = load(options.composition, true); - this._composeLeftToRight = load(options.composeLeftToRight, false); - this._composeTopToBottom = load(options.composeTopToBottom, false); - - this._hShift = load(options.hShift, 2); - this._vShift = load(options.vShift, 2); - - this._cropImageA = options.cropImageA; - this._cropImageB = options.cropImageB; - - // Prepare reference white - this._refWhite = this._convertRgbToXyz({c1: 1, c2: 1, c3: 1, c4: 1}); - - this._perceptual = load(options.perceptual, false); - - this._gamma = options.gamma; - this._gammaR = options.gammaR; - this._gammaG = options.gammaG; - this._gammaB = options.gammaB; - } - - /** - * Runs the comparison synchronously - * - * @method runSync - * @return {Result} Result of comparison { code, differences, dimension, width, height } - */ - runSync() { - - var i, len, rect, color; - - try { - this._imageA = this._loadImageSync(this._imageAPath, this._imageA); - this._imageB = this._loadImageSync(this._imageBPath, this._imageB); - - // Crop images if requested - if (this._cropImageA) { - this._correctDimensions(this._imageA.getWidth(), this._imageA.getHeight(), this._cropImageA); - this._crop("Image-A", this._imageA, this._cropImageA); - } - if (this._cropImageB) { - this._correctDimensions(this._imageB.getWidth(), this._imageB.getHeight(), this._cropImageB); - this._crop("Image-B", this._imageB, this._cropImageB); - } - - // Always clip - this._clip(this._imageA, this._imageB); - - this._imageOutput = PNGImage.createImage(this._imageA.getWidth(), this._imageA.getHeight()); - - // Make a copy when not in debug mode - if (this._debug) { - this._imageACompare = this._imageA; - this._imageBCompare = this._imageB; - } else { - this._imageACompare = PNGImage.copyImage(this._imageA); - this._imageBCompare = PNGImage.copyImage(this._imageB); - } - - // Block-out - color = { - red: this._blockOutRed, - green: this._blockOutGreen, - blue: this._blockOutBlue, - alpha: this._blockOutAlpha, - opacity: this._blockOutOpacity - }; - for (i = 0, len = this._blockOut.length; i < len; i++) { - rect = this._blockOut[i]; - - // Make sure the block-out parameters fit - this._correctDimensions(this._imageACompare.getWidth(), this._imageACompare.getHeight(), rect); - - this._imageACompare.fillRect(rect.x, rect.y, rect.width, rect.height, color); - this._imageBCompare.fillRect(rect.x, rect.y, rect.width, rect.height, color); - } - - // Copy image to composition - if (this._copyImageAToOutput) { - this._copyImage(this._debug ? this._imageACompare : this._imageA, this._imageOutput); - } else if (this._copyImageBToOutput) { - this._copyImage(this._debug ? this._imageBCompare : this._imageB, this._imageOutput); - } - - // Gamma correction - var gamma = undefined; - if (this._gamma || this._gammaR || this._gammaG || this._gammaB) { - gamma = { - R: this._gammaR || this._gamma, G: this._gammaG || this._gamma, B: this._gammaB || this._gamma - }; - } - - // Comparison - var result = this._compare(this._imageACompare, this._imageBCompare, this._imageOutput, this._delta, - { // Output-Mask color - red: this._outputMaskRed, - green: this._outputMaskGreen, - blue: this._outputMaskBlue, - alpha: this._outputMaskAlpha, - opacity: this._outputMaskOpacity - }, { // Output-Shift color - red: this._outputShiftRed, - green: this._outputShiftGreen, - blue: this._outputShiftBlue, - alpha: this._outputShiftAlpha, - opacity: this._outputShiftOpacity - }, { // Background color - red: this._outputBackgroundRed, - green: this._outputBackgroundGreen, - blue: this._outputBackgroundBlue, - alpha: this._outputBackgroundAlpha, - opacity: this._outputBackgroundOpacity - }, - this._hShift, this._vShift, - this._perceptual, - gamma - ); - - // Create composition if requested - if (this._debug) { - this._imageOutput = this._createComposition(this._imageACompare, this._imageBCompare, this._imageOutput); - } else { - this._imageOutput = this._createComposition(this._imageA, this._imageB, this._imageOutput); - } - - // Need to write to the filesystem? - if (this._imageOutputPath && this._withinOutputLimit(result.code, this._imageOutputLimit)) { - this._imageOutput.writeImageSync(this._imageOutputPath); - this.log("Wrote differences to " + this._imageOutputPath); - } - - return result; - - } catch (err) { - console.error(err.stack); - throw err; - } - } - - /** - * Determines if result is within the output limit - * - * @method _withinOutputLimit - * @param {int} resultCode - * @param {int} outputLimit - * @return {boolean} - */ - _withinOutputLimit(resultCode, outputLimit) { - return this._convertResultCodeToRelativeValue(resultCode) <= outputLimit; - } - - /** - * Converts the result-code to a relative value - * - * @method _convertResultCodeToRelativeValue - * @param {int} resultCode - * @return {int} - */ - _convertResultCodeToRelativeValue(resultCode) { - - /** @type {any} */ - var valueMap = { - 0: 0, 1: 10, 7: 20, 5: 30 - }; - - return valueMap[resultCode] !== undefined ? valueMap[resultCode] : 0; - } - - /** - * Creates a comparison image - * - * @method _createComposition - * @param {PNGImage} imageA - * @param {PNGImage} imageB - * @param {PNGImage} imageOutput - * @return {PNGImage} - */ - _createComposition(imageA, imageB, imageOutput) { - - var width, height, image = imageOutput; - - if (this._composition) { - width = Math.max(imageA.getWidth(), imageB.getWidth()); - height = Math.max(imageA.getHeight(), imageB.getHeight()); - - if (((width > height) && !this._composeLeftToRight) || this._composeTopToBottom) { - image = PNGImage.createImage(width, height * 3); - - PNG.bitblt(imageA.getImage(), image.getImage(), 0, 0, imageA.getWidth(), imageA.getHeight(), 0, 0); - PNG.bitblt(imageOutput.getImage(), image.getImage(), 0, 0, imageOutput.getWidth(), imageOutput.getHeight(), 0, height); - PNG.bitblt(imageB.getImage(), image.getImage(), 0, 0, imageB.getWidth(), imageB.getHeight(), 0, height * 2); - } else { - image = PNGImage.createImage(width * 3, height); - - PNG.bitblt(imageA.getImage(), image.getImage(), 0, 0, imageA.getWidth(), imageA.getHeight(), 0, 0); - PNG.bitblt(imageOutput.getImage(), image.getImage(), 0, 0, imageOutput.getWidth(), imageOutput.getHeight(), width, 0); - PNG.bitblt(imageB.getImage(), image.getImage(), 0, 0, imageB.getWidth(), imageB.getHeight(), width * 2, 0); - } - } - - return image; - } - - /** - * Loads the image or uses the already available image - * - * @method _loadImageSync - * @param {string|undefined} path - * @param {PNGImage|Buffer=} image - * @return {PNGImage} - */ - _loadImageSync(path, image) { - - if (image instanceof Buffer) { - return PNGImage.loadImageSync(image); - - } else if ((typeof path === 'string') && !image) { - return PNGImage.readImageSync(path); - - } else { - return /** @type {PNGImage} */ (image); - } - } - - /** - * Copies one image into another image - * - * @method _copyImage - * @param {PNGImage} imageSrc - * @param {PNGImage} imageDst - */ - _copyImage(imageSrc, imageDst) { - PNG.bitblt(imageSrc.getImage(), imageDst.getImage(), 0, 0, imageSrc.getWidth(), imageSrc.getHeight(), 0, 0); - } - - - /** - * Is the difference above the set threshold? - * - * @method isAboveThreshold - * @param {int} items - * @param {int} total - * @return {boolean} - */ - isAboveThreshold(items, total) { - - if ((this._thresholdType === BlinkDiff.THRESHOLD_PIXEL) && (this._threshold <= items)) { - return true; - } else if (this._threshold <= (items / total)) { - return true; - } - - return false; - } - - - /** - * Log method that can be overwritten to modify the logging behavior. - * - * @method log - * @param {string} text - */ - log(text) { - // Nothing here; Overwrite this to add some functionality - } - - - /** - * Has comparison passed? - * - * @method hasPassed - * @param {int} result Comparison result-code - * @return {boolean} - */ - hasPassed(result) { - return ((result !== BlinkDiff.RESULT_DIFFERENT) && (result !== BlinkDiff.RESULT_UNKNOWN)); - } - - - /** - * Clips the images to the lower resolution of both - * - * @method _clip - * @param {PNGImage} imageA Source image - * @param {PNGImage} imageB Destination image - */ - _clip(imageA, imageB) { - - var minWidth, minHeight; - - if ((imageA.getWidth() != imageB.getWidth()) || (imageA.getHeight() != imageB.getHeight())) { - - minWidth = imageA.getWidth(); - if (imageB.getWidth() < minWidth) { - minWidth = imageB.getWidth(); - } - - minHeight = imageA.getHeight(); - if (imageB.getHeight() < minHeight) { - minHeight = imageB.getHeight(); - } - - this.log("Clipping to " + minWidth + " x " + minHeight); - - imageA.clip(0, 0, minWidth, minHeight); - imageB.clip(0, 0, minWidth, minHeight); - } - } - - /** - * Crops the source image to the bounds of rect - * - * @method _crop - * @param {string} which Title of image to crop - * @param {PNGImage} image Source image - * @param {object} rect Values for rect - * @param {int} rect.x X value of rect - * @param {int} rect.y Y value of rect - * @param {int} rect.width Width value of rect - * @param {int} rect.height Height value of rect - */ - _crop(which, image, rect) { - - this.log("Cropping " + which + " from " + rect.x + "," + rect.y + " by " + rect.width + " x " + rect.height); - - image.clip(rect.x, rect.y, rect.width, rect.height); - } - - /** - * Correcting area dimensions if necessary - * - * Note: - * Priority is on the x/y coordinates, and not on the size since the size will then be removed anyways. - * - * @method _correctDimensions - * @param {int} width - * @param {int} height - * @param {object} rect Values for rect - * @param {int} rect.x X value of rect - * @param {int} rect.y Y value of rect - * @param {int} rect.width Width value of rect - * @param {int} rect.height Height value of rect - */ - _correctDimensions(width, height, rect) { - - // Set values if none given - rect.x = rect.x || 0; - rect.y = rect.y || 0; - rect.width = rect.width || width; - rect.height = rect.height || height; - - // Check negative values - rect.x = Math.max(0, rect.x); - rect.y = Math.max(0, rect.y); - rect.width = Math.max(0, rect.width); - rect.height = Math.max(0, rect.height); - - // Check dimensions - rect.x = Math.min(rect.x, width - 1); // -1 to make sure that there is an image - rect.y = Math.min(rect.y, height - 1); - rect.width = Math.min(rect.width, width - rect.x); - rect.height = Math.min(rect.height, height - rect.y); - } - - /** - * Calculates the distance of colors in the 4 dimensional color space - * - * @method _colorDelta - * @param {Color} color1 Values for color 1 - * @param {Color} color2 Values for color 2 - * @return {number} Distance - */ - _colorDelta(color1, color2) { - var c1, c2, c3, c4; - - c1 = Math.pow(color1.c1 - color2.c1, 2); - c2 = Math.pow(color1.c2 - color2.c2, 2); - c3 = Math.pow(color1.c3 - color2.c3, 2); - c4 = Math.pow(color1.c4 - color2.c4, 2); - - return Math.sqrt(c1 + c2 + c3 + c4); - } - - /** - * Gets the color of an image by the index - * - * @method _getColor - * @param {PNGImage} image Image - * @param {int} idx Index of pixel in image - * @param {boolean=} perceptual - * @param {Gamma=} gamma - * @return {Color} - */ - _getColor(image, idx, perceptual, gamma) { - var color = { - c1: image.getRed(idx), c2: image.getGreen(idx), c3: image.getBlue(idx), c4: image.getAlpha(idx) - }; - - if (perceptual || gamma) { - color = this._correctGamma(color, gamma); - color = this._convertRgbToXyz(color); - color = this._convertXyzToCieLab(color); - } - - return color; - } - - /** - * Correct gamma and return color in [0, 1] range - * - * @method _correctGamma - * @param {Color} color - * @param {Gamma=} gamma - * @return {Color} - */ - _correctGamma(color, gamma) { - - // Convert to range [0, 1] - var result = { - c1: color.c1 / 255, c2: color.c2 / 255, c3: color.c3 / 255, c4: color.c4 - }; - - if (gamma && (gamma.R !== undefined || gamma.G !== undefined || gamma.B !== undefined)) { - if (gamma.R !== undefined) { - result.c1 = Math.pow(result.c1, gamma.R); - } - if (gamma.G !== undefined) { - result.c2 = Math.pow(result.c2, gamma.G); - } - if (gamma.B !== undefined) { - result.c3 = Math.pow(result.c3, gamma.B); - } - } - - return result; - } - - /** - * Converts the color from RGB to XYZ - * - * @method _convertRgbToXyz - * @param {Color} color - * @return {Color} - */ - _convertRgbToXyz(color) { - var result = {}; - - result.c1 = color.c1 * 0.4887180 + color.c2 * 0.3106803 + color.c3 * 0.2006017; - result.c2 = color.c1 * 0.1762044 + color.c2 * 0.8129847 + color.c3 * 0.0108109; - result.c3 = color.c2 * 0.0102048 + color.c3 * 0.9897952; - result.c4 = color.c4; - - return result; - } - - /** - * Converts the color from XYZ to CieLab - * - * @method _convertXyzToCieLab - * @param {Color} color - * @return {Color} - */ - _convertXyzToCieLab(color) { - var result = {}, c1, c2, c3; - - /** - * @param {number} t - * @returns {number} - */ - function f (t) { - return (t > 0.00885645167904) ? Math.pow(t, 1 / 3) : 70.08333333333263 * t + 0.13793103448276; - } - - c1 = f(color.c1 / this._refWhite.c1); - c2 = f(color.c2 / this._refWhite.c2); - c3 = f(color.c3 / this._refWhite.c3); - - result.c1 = (116 * c2) - 16; - result.c2 = 500 * (c1 - c2); - result.c3 = 200 * (c2 - c3); - result.c4 = color.c4; - - return result; - } - - /** - * Calculates the lower limit - * - * @method _calculateLowerLimit - * @param {int} value - * @param {int} min - * @param {int} shift - * @return {int} - */ - _calculateLowerLimit(value, min, shift) { - return (value - shift) < min ? -(shift + (value - shift)) : -shift; - } - - /** - * Calculates the upper limit - * - * @method _calculateUpperLimit - * @param {int} value - * @param {int} max - * @param {int} shift - * @return {int} - */ - _calculateUpperLimit(value, max, shift) { - return (value + shift) > max ? (max - value) : shift; - } - - /** - * Checks if any pixel in the shift surrounding has a comparable color - * - * @method _shiftCompare - * @param {int} x - * @param {int} y - * @param {Color} color - * @param {number} deltaThreshold - * @param {PNGImage} imageA - * @param {PNGImage} imageB - * @param {int} width - * @param {int} height - * @param {int} hShift - * @param {int} vShift - * @param {boolean=} perceptual - * @param {Gamma=} gamma - * @return {boolean} Is pixel within delta found in surrounding? - */ - _shiftCompare(x, y, color, deltaThreshold, imageA, imageB, width, height, hShift, vShift, perceptual, gamma) { - - var i, xOffset, xLow, xHigh, yOffset, yLow, yHigh, delta, localDeltaThreshold; - - if ((hShift > 0) || (vShift > 0)) { - - xLow = this._calculateLowerLimit(x, 0, hShift); - xHigh = this._calculateUpperLimit(x, width - 1, hShift); - - yLow = this._calculateLowerLimit(y, 0, vShift); - yHigh = this._calculateUpperLimit(y, height - 1, vShift); - - for (xOffset = xLow; xOffset <= xHigh; xOffset++) { - for (yOffset = yLow; yOffset <= yHigh; yOffset++) { - - if ((xOffset != 0) || (yOffset != 0)) { - - i = imageB.getIndex(x + xOffset, y + yOffset); - - var color1 = this._getColor(imageA, i, perceptual, gamma); - localDeltaThreshold = this._colorDelta(color, color1); - - var color2 = this._getColor(imageB, i, perceptual, gamma); - delta = this._colorDelta(color, color2); - - if ((Math.abs(delta - localDeltaThreshold) < deltaThreshold) && (localDeltaThreshold > deltaThreshold)) { - return true; - } - } - } - } - } - - return false; - } - - /** - * Does a quick comparison between the supplied images - * - * @method _pixelCompare - * @param {PNGImage} imageA - * @param {PNGImage} imageB - * @param {PNGImage} imageOutput - * @param {number} deltaThreshold - * @param {int} width Width of image - * @param {int} height Height of image - * @param {RGBAOColor} outputMaskColor - * @param {RGBAOColor} outputShiftColor - * @param {RGBAOColor} backgroundColor - * @param {int=} hShift Horizontal shift - * @param {int=} vShift Vertical shift - * @param {boolean=} perceptual - * @param {Gamma=} gamma - * @return {int} Number of pixel differences - */ - _pixelCompare(imageA, imageB, imageOutput, deltaThreshold, width, height, outputMaskColor, outputShiftColor, backgroundColor, hShift, vShift, perceptual, gamma) { - var difference = 0, i, x, y, delta; - - for (x = 0; x < width; x++) { - for (y = 0; y < height; y++) { - i = imageA.getIndex(x, y); - - var color1 = this._getColor(imageA, i, perceptual, gamma); - var color2 = this._getColor(imageB, i, perceptual, gamma); - - delta = this._colorDelta(color1, color2); - - if (delta > deltaThreshold) { - - if (this._shiftCompare(x, y, color1, deltaThreshold, imageA, imageB, width, height, hShift || 0, vShift || 0, perceptual, gamma) && this._shiftCompare(x, y, color2, deltaThreshold, imageB, imageA, width, height, hShift || 0, vShift || 0, perceptual, gamma)) { - imageOutput.setAtIndex(i, outputShiftColor); - } else { - difference++; - imageOutput.setAtIndex(i, outputMaskColor); - } - - } else { - imageOutput.setAtIndex(i, backgroundColor); - } - } - } - - return difference; - } - - /** - * Compares the two images supplied - * - * @method _compare - * @param {PNGImage} imageA - * @param {PNGImage} imageB - * @param {PNGImage} imageOutput - * @param {number} deltaThreshold - * @param {RGBAOColor} outputMaskColor - * @param {RGBAOColor} outputShiftColor - * @param {RGBAOColor} backgroundColor - * @param {int=} hShift - * @param {int=} vShift - * @param {boolean=} perceptual - * @param {Gamma=} gamma - * @return {Result} - */ - _compare(imageA, imageB, imageOutput, deltaThreshold, outputMaskColor, outputShiftColor, backgroundColor, hShift, vShift, perceptual, gamma) { - - /** @type {any} */ - var result = { - code: BlinkDiff.RESULT_UNKNOWN, - differences: undefined, - dimension: undefined, - width: undefined, - height: undefined - }; - - // Get some data needed for comparison - result.width = imageA.getWidth(); - result.height = imageA.getHeight(); - result.dimension = result.width * result.height; - - // Check if identical - result.differences = this._pixelCompare(imageA, imageB, imageOutput, deltaThreshold, result.width, result.height, outputMaskColor, outputShiftColor, backgroundColor, hShift, vShift, perceptual, gamma); - - // Result - if (result.differences == 0) { - this.log("Images are identical or near identical"); - result.code = BlinkDiff.RESULT_IDENTICAL; - return result; - - } else if (this.isAboveThreshold(result.differences, result.dimension)) { - this.log("Images are visibly different"); - this.log(result.differences + " pixels are different"); - result.code = BlinkDiff.RESULT_DIFFERENT; - return result; - - } else { - this.log("Images are similar"); - this.log(result.differences + " pixels are different"); - result.code = BlinkDiff.RESULT_SIMILAR; - return result; - } - } -}; - - -/** - * Threshold-type for pixel - * - * @static - * @property THRESHOLD_PIXEL - * @type {string} - */ -BlinkDiff.THRESHOLD_PIXEL = 'pixel'; - -/** - * Threshold-type for percent of all pixels - * - * @static - * @property THRESHOLD_PERCENT - * @type {string} - */ -BlinkDiff.THRESHOLD_PERCENT = 'percent'; - - -/** - * Unknown result of the comparison - * - * @static - * @property RESULT_UNKNOWN - * @type {int} - */ -BlinkDiff.RESULT_UNKNOWN = 0; - -/** - * The images are too different - * - * @static - * @property RESULT_DIFFERENT - * @type {int} - */ -BlinkDiff.RESULT_DIFFERENT = 1; - -/** - * The images are very similar, but still below the threshold - * - * @static - * @property RESULT_SIMILAR - * @type {int} - */ -BlinkDiff.RESULT_SIMILAR = 7; - -/** - * The images are identical (or near identical) - * - * @static - * @property RESULT_IDENTICAL - * @type {int} - */ -BlinkDiff.RESULT_IDENTICAL = 5; - - -/** - * Create output when images are different - * - * @static - * @property OUTPUT_DIFFERENT - * @type {int} - */ -BlinkDiff.OUTPUT_DIFFERENT = 10; - -/** - * Create output when images are similar or different - * - * @static - * @property OUTPUT_SIMILAR - * @type {int} - */ -BlinkDiff.OUTPUT_SIMILAR = 20; - -/** - * Force output of all comparisons - * - * @static - * @property OUTPUT_ALL - * @type {int} - */ -BlinkDiff.OUTPUT_ALL = 100; - -module.exports = BlinkDiff; diff --git a/packages/playwright-test/src/third_party/png-js.js b/packages/playwright-test/src/third_party/png-js.js deleted file mode 100644 index e959bbff30..0000000000 --- a/packages/playwright-test/src/third_party/png-js.js +++ /dev/null @@ -1,466 +0,0 @@ -/** - * 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; diff --git a/tests/playwright-test/blink-diff.spec.ts b/tests/playwright-test/blink-diff.spec.ts deleted file mode 100644 index 170caa0acb..0000000000 --- a/tests/playwright-test/blink-diff.spec.ts +++ /dev/null @@ -1,845 +0,0 @@ -/** - * 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 }; - 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); - }); - }); - }); -});