diff --git a/.changeset/all-houses-care.md b/.changeset/all-houses-care.md new file mode 100644 index 000000000..b80cb62e0 --- /dev/null +++ b/.changeset/all-houses-care.md @@ -0,0 +1,38 @@ +--- +"@wdio/image-comparison-core": major +"@wdio/visual-service": major +--- + +### 💥 Breaking change: new image comparison engine + +We replaced the engine that powers every visual comparison. This is a breaking change, so please read the migration note below before upgrading. + +**The problem** + +Visual tests were flaky. Tests failed on differences that are impossible to see by eye, like sub-pixel font rendering, 1px anti-aliasing on edges and small shadow shifts between runs. The old engine (resemble.js) compared raw RGB values, which does not match how human vision works, and on larger screenshots it quietly skipped about a third of the pixels. So you got failures that were not real, and in some cases real changes that could slip through. + +On top of that, all the image handling (decode, crop, composite, rotate, resize) ran through [`jimp`](https://github.com/jimp-dev/jimp), a large dependency that we only used a small slice of and that is no longer actively maintained. + +**The solution** + +Two things changed under the hood: + +1. The comparison engine is now [pixelmatch](https://github.com/mapbox/pixelmatch). It compares images the way the eye perceives them (in the YIQ colour space) and detects anti-aliasing by checking both images at once. Invisible rendering noise now passes, and real regressions still fail. +2. `jimp` has been removed completely. PNG decode and encode now go through the small [`fast-png`](https://github.com/image-js/fast-png) library, and the handful of image operations we still need (crop, composite, canvas, opacity, rotate, resize) live in a tiny internal helper. The bundled resemble file is gone too. The net effect is a much lighter dependency footprint with no loss in functionality. + +**What you need to do** + +- Your public API does not change. `checkScreen`, `checkElement`, `checkFullPageScreen` and the matchers all work exactly as before, and the same `ignore` options are supported. +- Because the new engine measures differences differently, mismatch percentages will not match the old numbers exactly. You should re-run your suite once and re-accept your baselines so they are generated with the new engine. After that your tests should be noticeably more stable. + +**Also fixed in this release** + +- **Top-row artifact on full page screenshots:** Jimp's `contain()` centred the image, shifting content by 1px and creating a false diff across the top row. Replaced with buffer-level padding that anchors content at (0,0). +- **Ignored region 1px under-coverage:** The device-pixel size of an ignored region used `Math.floor`, which could drop a pixel when `cssSize * DPR` had a fractional part. Width and height now use `Math.ceil` so the full element is always covered. Position still uses `Math.floor`. +- **Comparison sensitivity matches what you were used to:** Switching engines meant retuning how strict a comparison is. The pixelmatch threshold is now aligned with the old resemble tolerances, so a difference that used to fail still fails and one that used to pass still passes. The diff highlight also uses a single consistent colour instead of varying per run. +- **Different image sizes no longer crash the comparison:** When a baseline and the actual screenshot had slightly different dimensions, the old flow threw an error and you lost the result. Both images are now normalised to the same size before they are compared, so a size change is reported as a visual difference you can review instead of a hard failure. +- **More reliable ignore regions with WebDriver BiDi:** With BiDi the calculated element bounds can be off by a pixel or two, which sometimes left part of an ignored element just outside the ignored area and caused a false diff. The BiDi emulated flow now uses a larger `ignoreRegionPadding` so the whole element stays covered. + +### Committers: 1 + +- Wim Selles ([@wswebcreation](https://github.com/wswebcreation)) diff --git a/.github/workflows/scheduled-tests.yml b/.github/workflows/scheduled-tests.yml index 680eb9cc7..491d348ce 100644 --- a/.github/workflows/scheduled-tests.yml +++ b/.github/workflows/scheduled-tests.yml @@ -4,12 +4,12 @@ on: workflow_dispatch: inputs: branch: - description: 'Branch to run tests against' + description: "Branch to run tests against" type: string required: true - default: 'main' + default: "main" schedule: - - cron: '0 */2 * * *' # Every 2 hours; disable via GitHub UI or let SCHEDULED_TESTS_UNTIL expire + - cron: "0 */2 * * *" # Every 2 hours; disable via GitHub UI or let SCHEDULED_TESTS_UNTIL expire permissions: contents: read @@ -87,7 +87,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.desktop --maxConcurrency=4 + run: pnpm test.lambdatest.desktop --maxConcurrency=3 - name: 📤 Upload artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 @@ -203,7 +203,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.sims.web --maxConcurrency=6 + run: pnpm test.lambdatest.sims.web --maxConcurrency=4 - name: 📤 Upload artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad5f4cf84..786b5fc01 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -154,7 +154,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.desktop --maxConcurrency=4 + run: pnpm test.lambdatest.desktop --maxConcurrency=3 - name: 📤 Upload artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 diff --git a/.gitignore b/.gitignore index 7cc05ab7a..2faf936e4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ build # folders .tmp +.pixelmatch-tmp/ /__snapshots__/ .idea/ localBaseline/ diff --git a/packages/image-comparison-core/package.json b/packages/image-comparison-core/package.json index a8021f944..cb19778bf 100644 --- a/packages/image-comparison-core/package.json +++ b/packages/image-comparison-core/package.json @@ -34,7 +34,8 @@ "watch:tsc": "pnpm run build:tsc -w" }, "dependencies": { - "jimp": "^1.6.1", + "fast-png": "^8.0.0", + "pixelmatch": "^7.2.0", "@wdio/logger": "^9.18.0", "@wdio/types": "^9.27.0" }, diff --git a/packages/image-comparison-core/src/helpers/options.ts b/packages/image-comparison-core/src/helpers/options.ts index bbd5209d7..83cf20f6f 100644 --- a/packages/image-comparison-core/src/helpers/options.ts +++ b/packages/image-comparison-core/src/helpers/options.ts @@ -11,7 +11,7 @@ import type { MethodImageCompareCompareOptions, ScreenMethodImageCompareCompareO import type { BeforeScreenshotOptions, BeforeScreenshotResult } from './beforeScreenshot.interfaces.js' import type { AfterScreenshotOptions } from './afterScreenshot.interfaces.js' import type { InstanceData } from '../methods/instanceData.interfaces.js' -import type { ComparisonIgnoreOption } from '../resemble/compare.interfaces.js' +import type { ComparisonIgnoreOption } from '../pixelmatch/compare.interfaces.js' import { logAllDeprecatedCompareOptions, isStorybook, @@ -58,12 +58,6 @@ export function defaultOptions(options: ClassOptions): DefaultOptions { waitForFontsLoaded: options.waitForFontsLoaded ?? true, alwaysSaveActualImage: options.alwaysSaveActualImage ?? true, - /** - * Compare options (merged sequentially): - * 1. Default options (fallback) - * 2. Root compareOptions (deprecated but supported) - * 3. User-provided compareOptions - */ compareOptions: { ...DEFAULT_COMPARE_OPTIONS, ...logAllDeprecatedCompareOptions(options), @@ -270,13 +264,10 @@ export function buildAfterScreenshotOptions({ return afterOptions } -/** - * Prepare ignore options for resemble.js comparison - */ export function prepareIgnoreOptions(imageCompareOptions: MethodImageCompareCompareOptions): ComparisonIgnoreOption[] { - const resembleIgnoreDefaults: ComparisonIgnoreOption[] = ['alpha', 'antialiasing', 'colors', 'less', 'nothing'] + const ignoreDefaults: ComparisonIgnoreOption[] = ['alpha', 'antialiasing', 'colors', 'less', 'nothing'] - return resembleIgnoreDefaults.filter((option) => + return ignoreDefaults.filter((option) => Object.keys(imageCompareOptions).find( (key: keyof typeof imageCompareOptions) => key.toLowerCase().includes(option) && imageCompareOptions[key], ), diff --git a/packages/image-comparison-core/src/methods/__snapshots__/images.test.ts.snap b/packages/image-comparison-core/src/methods/__snapshots__/images.test.ts.snap index e593ee0ef..7de262198 100644 --- a/packages/image-comparison-core/src/methods/__snapshots__/images.test.ts.snap +++ b/packages/image-comparison-core/src/methods/__snapshots__/images.test.ts.snap @@ -46,189 +46,21 @@ exports[`checkIfImageExists > should return false when file does not exist 1`] = exports[`checkIfImageExists > should return true when file exists 1`] = `true`; -exports[`cropAndConvertToDataURL > should crop image and add iOS bezel corners when isIOS is true 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should crop image and add iOS bezel corners when isIOS is true 2`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`cropAndConvertToDataURL > should crop image and add iOS bezel corners when isIOS is true 3`] = `"croppedImageData"`; - -exports[`cropAndConvertToDataURL > should crop image and return base64 data without iOS bezel corners 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should crop image and return base64 data without iOS bezel corners 2`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`cropAndConvertToDataURL > should crop image and return base64 data without iOS bezel corners 3`] = `"croppedImageData"`; - -exports[`cropAndConvertToDataURL > should handle Android device (isIOS false) without bezel corners 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle Android device (isIOS false) without bezel corners 2`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle Android device (isIOS false) without bezel corners 3`] = `"croppedImageData"`; - -exports[`cropAndConvertToDataURL > should handle different base64 input data 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle different base64 input data 2`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle different base64 input data 3`] = `"croppedImageData"`; - -exports[`cropAndConvertToDataURL > should handle different device pixel ratios 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle different device pixel ratios 2`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle different device pixel ratios 3`] = `"croppedImageData"`; - -exports[`cropAndConvertToDataURL > should handle landscape orientation with iOS bezel corners 1`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`cropAndConvertToDataURL > should handle landscape orientation with iOS bezel corners 2`] = ` -[ - [ - "image/png", - ], -] -`; +exports[`cropAndConvertToDataURL > should crop image and add iOS bezel corners when isIOS is true 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle landscape orientation with iOS bezel corners 3`] = `"croppedImageData"`; +exports[`cropAndConvertToDataURL > should crop image and return base64 data without iOS bezel corners 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle large crop dimensions 1`] = ` -[ - [ - { - "h": 2000, - "w": 3000, - "x": 1000, - "y": 500, - }, - ], -] -`; +exports[`cropAndConvertToDataURL > should handle Android device (isIOS false) without bezel corners 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle large crop dimensions 2`] = ` -[ - [ - "image/png", - ], -] -`; +exports[`cropAndConvertToDataURL > should handle different base64 input data 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle large crop dimensions 3`] = `"croppedImageData"`; +exports[`cropAndConvertToDataURL > should handle different device pixel ratios 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle zero dimensions 1`] = ` -[ - [ - { - "h": 0, - "w": 0, - "x": 0, - "y": 0, - }, - ], -] -`; +exports[`cropAndConvertToDataURL > should handle landscape orientation with iOS bezel corners 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle zero dimensions 2`] = ` -[ - [ - "image/png", - ], -] -`; +exports[`cropAndConvertToDataURL > should handle large crop dimensions 1`] = `"croppedImageData"`; -exports[`cropAndConvertToDataURL > should handle zero dimensions 3`] = `"croppedImageData"`; +exports[`cropAndConvertToDataURL > should handle zero dimensions 1`] = `"croppedImageData"`; exports[`getAdjustedAxis > should clamp end position to maxDimension when it exceeds maxDimension 1`] = ` [ @@ -307,7 +139,7 @@ exports[`getAdjustedAxis > should return adjusted coordinates within bounds 1`] ] `; -exports[`getRotatedImageIfNeeded > should call rotateBase64Image when landscape and height > width 1`] = `"differentRotatedData"`; +exports[`getRotatedImageIfNeeded > should call rotateBase64Image when landscape and height > width 1`] = `"croppedImageData"`; exports[`getRotatedImageIfNeeded > should call rotateBase64Image when landscape and height > width 2`] = ` [ @@ -561,28 +393,7 @@ exports[`makeCroppedBase64Image > should create cropped base64 image with defaul ] `; -exports[`makeCroppedBase64Image > should create cropped base64 image with default settings 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should create cropped base64 image with default settings 3`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`makeCroppedBase64Image > should create cropped base64 image with default settings 4`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should create cropped base64 image with default settings 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle custom resize dimensions 1`] = ` [ @@ -595,20 +406,7 @@ exports[`makeCroppedBase64Image > should handle custom resize dimensions 1`] = ` ] `; -exports[`makeCroppedBase64Image > should handle custom resize dimensions 2`] = ` -[ - [ - { - "h": 125, - "w": 225, - "x": 45, - "y": 15, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle custom resize dimensions 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle custom resize dimensions 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle different device pixel ratios 1`] = ` [ @@ -621,20 +419,7 @@ exports[`makeCroppedBase64Image > should handle different device pixel ratios 1` ] `; -exports[`makeCroppedBase64Image > should handle different device pixel ratios 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle different device pixel ratios 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle different device pixel ratios 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle different rectangle dimensions 1`] = ` [ @@ -647,20 +432,7 @@ exports[`makeCroppedBase64Image > should handle different rectangle dimensions 1 ] `; -exports[`makeCroppedBase64Image > should handle different rectangle dimensions 2`] = ` -[ - [ - { - "h": 300, - "w": 400, - "x": 100, - "y": 75, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle different rectangle dimensions 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle different rectangle dimensions 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle different screenshot sizes 1`] = ` [ @@ -673,20 +445,7 @@ exports[`makeCroppedBase64Image > should handle different screenshot sizes 1`] = ] `; -exports[`makeCroppedBase64Image > should handle different screenshot sizes 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle different screenshot sizes 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle different screenshot sizes 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle edge case with padding that exceeds image bounds 1`] = ` [ @@ -699,20 +458,7 @@ exports[`makeCroppedBase64Image > should handle edge case with padding that exce ] `; -exports[`makeCroppedBase64Image > should handle edge case with padding that exceeds image bounds 2`] = ` -[ - [ - { - "h": 150, - "w": 100, - "x": 900, - "y": 1850, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle edge case with padding that exceeds image bounds 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle edge case with padding that exceeds image bounds 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle iOS devices with bezel corners 1`] = ` [ @@ -725,20 +471,7 @@ exports[`makeCroppedBase64Image > should handle iOS devices with bezel corners 1 ] `; -exports[`makeCroppedBase64Image > should handle iOS devices with bezel corners 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle iOS devices with bezel corners 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle iOS devices with bezel corners 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle landscape orientation with rotation 1`] = ` [ @@ -751,20 +484,7 @@ exports[`makeCroppedBase64Image > should handle landscape orientation with rotat ] `; -exports[`makeCroppedBase64Image > should handle landscape orientation with rotation 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle landscape orientation with rotation 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle landscape orientation with rotation 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle web driver element screenshots 1`] = ` [ @@ -777,20 +497,7 @@ exports[`makeCroppedBase64Image > should handle web driver element screenshots 1 ] `; -exports[`makeCroppedBase64Image > should handle web driver element screenshots 2`] = ` -[ - [ - { - "h": 100, - "w": 200, - "x": 50, - "y": 25, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle web driver element screenshots 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle web driver element screenshots 2`] = `"croppedImageData"`; exports[`makeCroppedBase64Image > should handle zero rectangle dimensions 1`] = ` [ @@ -803,20 +510,7 @@ exports[`makeCroppedBase64Image > should handle zero rectangle dimensions 1`] = ] `; -exports[`makeCroppedBase64Image > should handle zero rectangle dimensions 2`] = ` -[ - [ - { - "h": 0, - "w": 0, - "x": 0, - "y": 0, - }, - ], -] -`; - -exports[`makeCroppedBase64Image > should handle zero rectangle dimensions 3`] = `"finalCroppedImageData"`; +exports[`makeCroppedBase64Image > should handle zero rectangle dimensions 2`] = `"croppedImageData"`; exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 1`] = ` [ @@ -835,1645 +529,137 @@ exports[`makeFullPageBase64Image > should create full page base64 image with mul ] `; -exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 2`] = ` +exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 2`] = `"croppedImageData"`; + +exports[`makeFullPageBase64Image > should handle canvas Y positions correctly 1`] = `"croppedImageData"`; + +exports[`makeFullPageBase64Image > should handle different device pixel ratios 1`] = ` [ [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, + "screenshot1-data", + 3, ], [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, + "screenshot2-data", + 3, ], [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, + "screenshot3-data", + 3, ], ] `; -exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 3`] = ` +exports[`makeFullPageBase64Image > should handle different device pixel ratios 2`] = `"croppedImageData"`; + +exports[`makeFullPageBase64Image > should handle different screenshot data for each iteration 1`] = ` [ [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 800, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 1600, - ], -] -`; - -exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 4`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`makeFullPageBase64Image > should create full page base64 image with multiple screenshots 5`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle canvas Y positions correctly 1`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 800, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 1600, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle canvas Y positions correctly 2`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle different device pixel ratios 1`] = ` -[ - [ - "screenshot1-data", - 3, - ], - [ - "screenshot2-data", - 3, - ], - [ - "screenshot3-data", - 3, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle different device pixel ratios 2`] = ` -[ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle different device pixel ratios 3`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 800, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 1600, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle different device pixel ratios 4`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle different screenshot data for each iteration 1`] = ` -[ - [ - "screenshot1-data", - 2, - ], - [ - "screenshot2-data", - 2, - ], - [ - "screenshot3-data", - 2, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle different screenshot data for each iteration 2`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle empty screenshots array 1`] = `[]`; - -exports[`makeFullPageBase64Image > should handle empty screenshots array 2`] = `[]`; - -exports[`makeFullPageBase64Image > should handle empty screenshots array 3`] = `[]`; - -exports[`makeFullPageBase64Image > should handle empty screenshots array 4`] = ` -[ - [ - "image/png", - ], -] -`; - -exports[`makeFullPageBase64Image > should handle empty screenshots array 5`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle landscape mode with rotation 1`] = ` -[ - [ - "screenshot1-data", - 2, - ], - [ - "screenshot2-data", - 2, - ], - [ - "screenshot3-data", - 2, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode with rotation 2`] = ` -[ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode with rotation 3`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy] { - "calls": [ - [ - "image/png", - ], - [ - "image/png", - ], - [ - "image/png", - ], - ], - "results": [ - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - ], - }, - "opacity": [MockFunction spy], - "rotate": [MockFunction spy] { - "calls": [ - [ - 90, - ], - [ - 90, - ], - [ - 90, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - }, - 0, - 0, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy] { - "calls": [ - [ - "image/png", - ], - [ - "image/png", - ], - [ - "image/png", - ], - ], - "results": [ - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - ], - }, - "opacity": [MockFunction spy], - "rotate": [MockFunction spy] { - "calls": [ - [ - 90, - ], - [ - 90, - ], - [ - 90, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - }, - 0, - 800, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy] { - "calls": [ - [ - "image/png", - ], - [ - "image/png", - ], - [ - "image/png", - ], - ], - "results": [ - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - { - "type": "return", - "value": Promise {}, - }, - ], - }, - "opacity": [MockFunction spy], - "rotate": [MockFunction spy] { - "calls": [ - [ - 90, - ], - [ - 90, - ], - [ - 90, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - }, - 0, - 1600, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode with rotation 4`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 1`] = ` -[ - [ - "screenshot1-data", - 2, - ], - [ - "screenshot2-data", - 2, - ], - [ - "screenshot3-data", - 2, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 2`] = ` -[ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 3`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 800, + "screenshot1-data", + 2, ], [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 400, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 1600, + "screenshot2-data", + 2, ], -] -`; - -exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 4`] = `"fullPageImageData"`; - -exports[`makeFullPageBase64Image > should handle large canvas dimensions 1`] = ` -[ [ - "large-screenshot-data", + "screenshot3-data", 2, ], ] `; -exports[`makeFullPageBase64Image > should handle large canvas dimensions 2`] = ` -[ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; +exports[`makeFullPageBase64Image > should handle different screenshot data for each iteration 2`] = `"croppedImageData"`; -exports[`makeFullPageBase64Image > should handle large canvas dimensions 3`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], -] -`; +exports[`makeFullPageBase64Image > should handle empty screenshots array 1`] = `[]`; -exports[`makeFullPageBase64Image > should handle large canvas dimensions 4`] = `"fullPageImageData"`; +exports[`makeFullPageBase64Image > should handle empty screenshots array 2`] = `"croppedImageData"`; -exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 1`] = ` +exports[`makeFullPageBase64Image > should handle landscape mode with rotation 1`] = ` [ [ - "cropped-screenshot-data", + "screenshot1-data", 2, ], -] -`; - -exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 2`] = ` -[ [ - { - "h": 500, - "w": 900, - "x": 100, - "y": 50, - }, + "screenshot2-data", + 2, ], -] -`; - -exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 3`] = ` -[ [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 500, - "w": 900, - "x": 100, - "y": 50, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, + "screenshot3-data", + 2, ], ] `; -exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 4`] = `"fullPageImageData"`; +exports[`makeFullPageBase64Image > should handle landscape mode with rotation 2`] = `"croppedImageData"`; -exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 1`] = ` +exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 1`] = ` [ [ - "wide-screenshot-data", + "screenshot1-data", 2, ], [ - "tall-screenshot-data", + "screenshot2-data", 2, ], -] -`; - -exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 2`] = ` -[ - [ - { - "h": 600, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; - -exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 3`] = ` -[ - [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 600, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, - ], [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 600, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 600, + "screenshot3-data", + 2, ], ] `; -exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 4`] = `"fullPageImageData"`; +exports[`makeFullPageBase64Image > should handle landscape mode without rotation when width >= height 2`] = `"croppedImageData"`; -exports[`makeFullPageBase64Image > should handle single screenshot 1`] = ` +exports[`makeFullPageBase64Image > should handle large canvas dimensions 1`] = ` [ [ - "single-screenshot-data", + "large-screenshot-data", 2, ], ] `; -exports[`makeFullPageBase64Image > should handle single screenshot 2`] = ` -[ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], -] -`; +exports[`makeFullPageBase64Image > should handle large canvas dimensions 2`] = `"croppedImageData"`; -exports[`makeFullPageBase64Image > should handle single screenshot 3`] = ` +exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 1`] = ` [ [ - { - "bitmap": { - "height": 800, - "width": 1000, - }, - "composite": [MockFunction spy], - "crop": [MockFunction spy] { - "calls": [ - [ - { - "h": 800, - "w": 1000, - "x": 0, - "y": 0, - }, - ], - ], - "results": [ - { - "type": "return", - "value": [Circular], - }, - ], - }, - "getBase64": [MockFunction spy], - "opacity": [MockFunction spy], - "rotate": [MockFunction spy], - }, - 0, - 0, + "cropped-screenshot-data", + 2, ], ] `; -exports[`makeFullPageBase64Image > should handle single screenshot 4`] = `"fullPageImageData"`; - -exports[`rotateBase64Image > should handle different base64 input 1`] = `"differentRotatedData"`; - -exports[`rotateBase64Image > should handle different base64 input 2`] = ` -[ - [ - { - "data": [ - 118, - 39, - 223, - 122, - 183, - 167, - 180, - 137, - 154, - 129, - 224, - 218, - 181, - ], - "type": "Buffer", - }, - ], -] -`; +exports[`makeFullPageBase64Image > should handle screenshots with cropping positions 2`] = `"croppedImageData"`; -exports[`rotateBase64Image > should handle different base64 input 3`] = ` +exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 1`] = ` [ [ - 270, + "wide-screenshot-data", + 2, ], -] -`; - -exports[`rotateBase64Image > should rotate image by 180 degrees 1`] = `"rotatedImageData"`; - -exports[`rotateBase64Image > should rotate image by 180 degrees 2`] = ` -[ [ - 180, + "tall-screenshot-data", + 2, ], ] `; -exports[`rotateBase64Image > should rotate image by specified degrees 1`] = `"rotatedImageData"`; +exports[`makeFullPageBase64Image > should handle screenshots with different dimensions 2`] = `"croppedImageData"`; -exports[`rotateBase64Image > should rotate image by specified degrees 2`] = ` -[ - [ - { - "data": [ - 162, - 184, - 160, - 138, - 118, - 165, - 34, - 102, - 160, - 120, - 54, - 173, - ], - "type": "Buffer", - }, - ], -] -`; - -exports[`rotateBase64Image > should rotate image by specified degrees 3`] = ` +exports[`makeFullPageBase64Image > should handle single screenshot 1`] = ` [ [ - 90, + "single-screenshot-data", + 2, ], ] `; -exports[`rotateBase64Image > should rotate image by specified degrees 4`] = ` -[ - [ - "image/png", - ], -] -`; +exports[`makeFullPageBase64Image > should handle single screenshot 2`] = `"croppedImageData"`; exports[`takeBase64ElementScreenshot > should fallback to takeResizedBase64Screenshot when takeElementScreenshot throws an error 1`] = ` [ diff --git a/packages/image-comparison-core/src/methods/compareReport.interfaces.ts b/packages/image-comparison-core/src/methods/compareReport.interfaces.ts index 28659c7b9..30f92f8ae 100644 --- a/packages/image-comparison-core/src/methods/compareReport.interfaces.ts +++ b/packages/image-comparison-core/src/methods/compareReport.interfaces.ts @@ -1,4 +1,4 @@ -import type { CompareData } from 'src/resemble/compare.interfaces.js' +import type { CompareData } from 'src/pixelmatch/compare.interfaces.js' import type { WicImageCompareOptions } from './images.interfaces.js' import type { BoundingBoxes, ReportFileSizes } from './rectangles.interfaces.js' import type { FilePaths, FolderPaths } from 'src/base.interfaces.js' diff --git a/packages/image-comparison-core/src/methods/createCompareReport.test.ts b/packages/image-comparison-core/src/methods/createCompareReport.test.ts index f0a39b053..fd8ef6529 100644 --- a/packages/image-comparison-core/src/methods/createCompareReport.test.ts +++ b/packages/image-comparison-core/src/methods/createCompareReport.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { writeFileSync, readFileSync } from 'node:fs' import { createCompareReport, createJsonReportIfNeeded } from './createCompareReport.js' -import type { CompareData } from '../resemble/compare.interfaces.js' +import type { CompareData } from '../pixelmatch/compare.interfaces.js' import type { BoundingBox, IgnoreBoxes } from './rectangles.interfaces.js' import type { BaseDimensions } from '../base.interfaces.js' import { getBase64ScreenshotSize } from '../helpers/utils.js' @@ -19,7 +19,8 @@ describe('createCompareReport', () => { const createMockData = (misMatchPercentage = 0): CompareData => ({ misMatchPercentage, rawMisMatchPercentage: misMatchPercentage, - getBuffer: () => Buffer.from(''), + getRawPixels: () => ({ data: new Uint8Array(4), width: 1, height: 1 }), + getBuffer: () => Promise.resolve(Buffer.from('')), diffBounds: { top: 0, left: 0, bottom: 0, right: 0 }, analysisTime: 0, diffPixels: [], @@ -144,7 +145,8 @@ describe('createJsonReportIfNeeded', () => { const createMockData = (misMatchPercentage = 0): CompareData => ({ misMatchPercentage, rawMisMatchPercentage: misMatchPercentage, - getBuffer: () => Buffer.from(''), + getRawPixels: () => ({ data: new Uint8Array(4), width: 1, height: 1 }), + getBuffer: () => Promise.resolve(Buffer.from('')), diffBounds: { top: 0, left: 0, bottom: 0, right: 0 }, analysisTime: 0, diffPixels: [], diff --git a/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts b/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts index fa16e368f..b5a434e5d 100644 --- a/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts +++ b/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts @@ -6,7 +6,7 @@ import * as utils from '../helpers/utils.js' import * as rectangles from './rectangles.js' import * as processDiffPixels from './processDiffPixels.js' import * as createCompareReport from './createCompareReport.js' -import * as compareImages from '../resemble/compareImages.js' +import * as compareImagesPixelmatch from '../pixelmatch/compareImages.js' const log = logger('test') @@ -79,7 +79,7 @@ vi.mock('./createCompareReport.js', () => ({ createCompareReport: vi.fn(), createJsonReportIfNeeded: vi.fn() })) -vi.mock('../resemble/compareImages.js', () => ({ +vi.mock('../pixelmatch/compareImages.js', () => ({ default: vi.fn() })) vi.mock('../helpers/constants.js', () => ({ @@ -165,28 +165,6 @@ describe('executeImageCompare', () => { beforeEach(async () => { vi.clearAllMocks() - const jimp = await import('jimp') - const jimpReadMock = vi.mocked(jimp.Jimp.read) - const mockImage = { - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mock-image-data'), - opacity: vi.fn().mockReturnThis(), - width: 100, - height: 200, - bitmap: { width: 100, height: 200 }, - background: 0, - formats: [], - inspect: vi.fn().mockReturnValue('MockImage'), - toString: vi.fn().mockReturnValue('MockImage'), - scanIterator: vi.fn(), - scan: vi.fn(), - scanQuiet: vi.fn(), - scanIteratorQuiet: vi.fn(), - scanQuietIterator: vi.fn(), - scanQuietIteratorQuiet: vi.fn(), - } as any - jimpReadMock.mockResolvedValue(mockImage) - vi.mocked(fsPromises.access).mockResolvedValue(undefined) vi.mocked(fsPromises.unlink).mockResolvedValue(undefined) vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined) @@ -217,14 +195,16 @@ describe('executeImageCompare', () => { }) vi.mocked(createCompareReport.createCompareReport).mockReturnValue(undefined) vi.mocked(createCompareReport.createJsonReportIfNeeded).mockResolvedValue(undefined) - vi.mocked(compareImages.default).mockResolvedValue({ + const mockCompareData = { rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, diffPixels: [] - }) + } + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue(mockCompareData) vi.mocked(images.checkBaselineImageExists).mockResolvedValue(undefined) vi.mocked(images.removeDiffImageIfExists).mockResolvedValue(undefined) vi.mocked(images.saveBase64Image).mockResolvedValue(undefined) @@ -258,7 +238,7 @@ describe('executeImageCompare', () => { savePerInstance: false, fileName: 'test.png' }) - expect(compareImages.default).toHaveBeenCalledWith( + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( Buffer.from('mock-image-data'), Buffer.from('mock-image-data'), { @@ -454,9 +434,10 @@ describe('executeImageCompare', () => { } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, @@ -556,9 +537,10 @@ describe('executeImageCompare', () => { } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.123456, misMatchPercentage: 0.12, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, @@ -598,9 +580,10 @@ describe('executeImageCompare', () => { autoSaveBaseline: false, } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0, misMatchPercentage: 0, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -616,7 +599,7 @@ describe('executeImageCompare', () => { }) expect(images.saveBase64Image).not.toHaveBeenCalled() - expect(compareImages.default).toHaveBeenCalledWith( + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( expect.any(Buffer), Buffer.from(base64Image, 'base64'), expect.any(Object), @@ -673,9 +656,10 @@ describe('executeImageCompare', () => { return undefined }) - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0, misMatchPercentage: 0, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -711,9 +695,10 @@ describe('executeImageCompare', () => { }, }, } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -748,9 +733,10 @@ describe('executeImageCompare', () => { }, }, } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -784,9 +770,10 @@ describe('executeImageCompare', () => { }, }, } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.05, misMatchPercentage: 0.05, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -820,9 +807,10 @@ describe('executeImageCompare', () => { }, }, } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.2, misMatchPercentage: 0.2, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -850,9 +838,10 @@ describe('executeImageCompare', () => { alwaysSaveActualImage: false, } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0, misMatchPercentage: 0, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -930,7 +919,7 @@ describe('executeImageCompare', () => { testContext: mockTestContext }) - expect(compareImages.default).toHaveBeenCalledWith( + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( expect.any(Buffer), expect.any(Buffer), { @@ -979,7 +968,7 @@ describe('executeImageCompare', () => { testContext: mockTestContext }) - expect(compareImages.default).toHaveBeenCalledWith( + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( expect.any(Buffer), expect.any(Buffer), { @@ -1012,7 +1001,7 @@ describe('executeImageCompare', () => { testContext: mockTestContext }) - expect(compareImages.default).toHaveBeenCalledWith( + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( expect.any(Buffer), expect.any(Buffer), { @@ -1057,9 +1046,10 @@ describe('executeImageCompare', () => { } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, @@ -1104,9 +1094,10 @@ describe('executeImageCompare', () => { } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, @@ -1150,9 +1141,10 @@ describe('executeImageCompare', () => { } } - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0.5, misMatchPercentage: 0.5, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 100, bottom: 200 }, analysisTime: 100, @@ -1189,9 +1181,10 @@ describe('executeImageCompare', () => { return Promise.resolve() }) - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0, misMatchPercentage: 0, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, @@ -1232,9 +1225,10 @@ describe('executeImageCompare', () => { vi.mocked(images.checkBaselineImageExists).mockImplementation(checkBaselineImageExists) - vi.mocked(compareImages.default).mockResolvedValue({ + vi.mocked(compareImagesPixelmatch.default).mockResolvedValue({ rawMisMatchPercentage: 0, misMatchPercentage: 0, + getRawPixels: vi.fn().mockReturnValue({ data: new Uint8Array(4), width: 1, height: 1 }), getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')), diffBounds: { left: 0, top: 0, right: 0, bottom: 0 }, analysisTime: 10, diff --git a/packages/image-comparison-core/src/methods/images.test.ts b/packages/image-comparison-core/src/methods/images.test.ts index 54bbbb7a7..a6fd692c3 100644 --- a/packages/image-comparison-core/src/methods/images.test.ts +++ b/packages/image-comparison-core/src/methods/images.test.ts @@ -14,27 +14,31 @@ import { cropAndConvertToDataURL, makeCroppedBase64Image, makeFullPageBase64Image, + addBlockOuts, rotateBase64Image, takeResizedBase64Screenshot, } from './images.js' import type { WicElement } from '../commands/element.interfaces.js' +import * as imageUtils from '../utils/imageUtils.js' const log = logger('test') -vi.mock('jimp', () => ({ - Jimp: Object.assign(vi.fn().mockImplementation(() => ({ - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), - opacity: vi.fn().mockReturnThis(), - rotate: vi.fn().mockReturnThis(), - crop: vi.fn().mockReturnThis(), - })), { - read: vi.fn(), - MIME_PNG: 'image/png', - }), - JimpMime: { - png: 'image/png', - }, +const makeRawImage = (width = 1000, height = 800) => ({ + data: new Uint8Array(width * height * 4), + width, + height, +}) +vi.mock('../utils/imageUtils.js', () => ({ + decodeImage: vi.fn().mockImplementation(() => makeRawImage()), + cropImage: vi.fn().mockImplementation(() => makeRawImage(200, 100)), + compositeImage: vi.fn(), + createCanvas: vi.fn().mockImplementation((w: number, h: number) => makeRawImage(w, h)), + setOpacity: vi.fn(), + toBase64Png: vi.fn().mockReturnValue('croppedImageData'), + rotate90CW: vi.fn().mockImplementation(() => makeRawImage(800, 1000)), + rotate90CCW: vi.fn().mockImplementation(() => makeRawImage(800, 1000)), + rotate180: vi.fn().mockImplementation(() => makeRawImage()), + encodeImage: vi.fn().mockReturnValue(Buffer.from('encoded')), })) vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('node:fs', async () => { @@ -287,66 +291,66 @@ describe('checkBaselineImageExists', () => { }) describe('rotateBase64Image', () => { - let jimpReadMock: ReturnType + afterEach(() => { vi.clearAllMocks() }) - beforeEach(async () => { - const jimp = await import('jimp') - jimpReadMock = vi.mocked(jimp.Jimp.read) + it('calls rotate90CW for 90 degrees and returns toBase64Png result', () => { + const result = rotateBase64Image({ base64Image: 'originalImageData', degrees: 90 }) + + expect(vi.mocked(imageUtils.rotate90CW)).toHaveBeenCalledTimes(1) + expect(vi.mocked(imageUtils.rotate180)).not.toHaveBeenCalled() + expect(result).toBe('croppedImageData') }) - afterEach(() => { - vi.clearAllMocks() + it('calls rotate180 for 180 degrees', () => { + rotateBase64Image({ base64Image: 'originalImageData', degrees: 180 }) + + expect(vi.mocked(imageUtils.rotate180)).toHaveBeenCalledTimes(1) + expect(vi.mocked(imageUtils.rotate90CW)).not.toHaveBeenCalled() }) - it('should rotate image by specified degrees', async () => { - const mockImage = { - rotate: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,rotatedImageData') - } - jimpReadMock.mockResolvedValue(mockImage) + it('calls rotate90CCW for 270 degrees', () => { + rotateBase64Image({ base64Image: 'differentImageData', degrees: 270 }) - const result = await rotateBase64Image({ - base64Image: 'originalImageData', - degrees: 90 - }) + expect(vi.mocked(imageUtils.rotate90CCW)).toHaveBeenCalledTimes(1) + expect(vi.mocked(imageUtils.rotate90CW)).not.toHaveBeenCalled() + }) - expect(result).toMatchSnapshot() - expect(jimpReadMock.mock.calls).toMatchSnapshot() - expect(mockImage.rotate.mock.calls).toMatchSnapshot() - expect(mockImage.getBase64.mock.calls).toMatchSnapshot() + it('calls rotate90CW for any other degree value', () => { + rotateBase64Image({ base64Image: 'differentImageData', degrees: 45 }) + + expect(vi.mocked(imageUtils.rotate90CW)).toHaveBeenCalledTimes(1) }) +}) - it('should rotate image by 180 degrees', async () => { - const mockImage = { - rotate: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,rotatedImageData') - } - jimpReadMock.mockResolvedValue(mockImage) +describe('addBlockOuts', () => { + afterEach(() => { vi.clearAllMocks() }) - const result = await rotateBase64Image({ - base64Image: 'originalImageData', - degrees: 180 - }) + it('returns the image unchanged when there are no ignored boxes', async () => { + const result = await addBlockOuts('someBase64', []) - expect(result).toMatchSnapshot() - expect(mockImage.rotate.mock.calls).toMatchSnapshot() + expect(vi.mocked(imageUtils.decodeImage)).toHaveBeenCalledTimes(1) + expect(vi.mocked(imageUtils.compositeImage)).not.toHaveBeenCalled() + expect(vi.mocked(imageUtils.toBase64Png)).toHaveBeenCalledTimes(1) + expect(result).toBe('croppedImageData') }) - it('should handle different base64 input', async () => { - const mockImage = { - rotate: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,differentRotatedData') - } - jimpReadMock.mockResolvedValue(mockImage) + it('creates a semi-transparent green overlay for each ignored box', async () => { + const boxes = [ + { left: 10, top: 20, right: 110, bottom: 120 }, + { left: 200, top: 50, right: 300, bottom: 150 }, + ] - const result = await rotateBase64Image({ - base64Image: 'differentImageData', - degrees: 270 - }) + await addBlockOuts('someBase64', boxes) - expect(result).toMatchSnapshot() - expect(jimpReadMock.mock.calls).toMatchSnapshot() - expect(mockImage.rotate.mock.calls).toMatchSnapshot() + expect(vi.mocked(imageUtils.createCanvas)).toHaveBeenCalledTimes(2) + // First box: width=100, height=100, green (#39aa56 = 57,170,86,255) + expect(vi.mocked(imageUtils.createCanvas)).toHaveBeenCalledWith(100, 100, 57, 170, 86, 255) + expect(vi.mocked(imageUtils.setOpacity)).toHaveBeenCalledTimes(2) + expect(vi.mocked(imageUtils.setOpacity)).toHaveBeenCalledWith(expect.any(Object), 0.5) + expect(vi.mocked(imageUtils.compositeImage)).toHaveBeenCalledTimes(2) + expect(vi.mocked(imageUtils.compositeImage)).toHaveBeenCalledWith( + expect.any(Object), expect.any(Object), 10, 20 + ) }) }) @@ -640,7 +644,8 @@ describe('handleIOSBezelCorners', () => { let getIosBezelImageNamesMock: ReturnType let readFileSyncMock: ReturnType let getBase64ScreenshotSizeMock: ReturnType - let mockImage: any + let mockImage: { data: Uint8Array; width: number; height: number } + let compositeImageFn: ReturnType beforeEach(async () => { logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {}) @@ -652,12 +657,8 @@ describe('handleIOSBezelCorners', () => { const fsModule = vi.mocked(await import('node:fs')) readFileSyncMock = vi.spyOn(fsModule, 'readFileSync') - mockImage = { - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), - opacity: vi.fn().mockReturnThis(), - rotate: vi.fn().mockReturnThis(), - } + mockImage = { data: new Uint8Array(4), width: 100, height: 100 } + compositeImageFn = vi.mocked(imageUtils.compositeImage) }) afterEach(() => { @@ -701,7 +702,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).toHaveBeenCalledTimes(2) - expect(mockImage.composite).toHaveBeenCalledTimes(2) + expect(compositeImageFn).toHaveBeenCalledTimes(2) expect(logWarnSpy).not.toHaveBeenCalled() }) @@ -725,7 +726,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).toHaveBeenCalledTimes(2) - expect(mockImage.composite).toHaveBeenCalledTimes(2) + expect(compositeImageFn).toHaveBeenCalledTimes(2) expect(logWarnSpy).not.toHaveBeenCalled() }) @@ -749,7 +750,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).toHaveBeenCalledTimes(2) - expect(mockImage.composite).toHaveBeenCalledTimes(2) + expect(compositeImageFn).toHaveBeenCalledTimes(2) expect(logWarnSpy).not.toHaveBeenCalled() }) @@ -789,7 +790,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).toHaveBeenCalledTimes(2) - expect(mockImage.composite).toHaveBeenCalledTimes(2) + expect(compositeImageFn).toHaveBeenCalledTimes(2) expect(logWarnSpy).not.toHaveBeenCalled() }) @@ -827,7 +828,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).not.toHaveBeenCalled() - expect(mockImage.composite).not.toHaveBeenCalled() + expect(compositeImageFn).not.toHaveBeenCalled() expect(logWarnSpy.mock.calls).toMatchSnapshot() }) @@ -849,7 +850,7 @@ describe('handleIOSBezelCorners', () => { expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot() expect(readFileSyncMock).not.toHaveBeenCalled() - expect(mockImage.composite).not.toHaveBeenCalled() + expect(compositeImageFn).not.toHaveBeenCalled() expect(logWarnSpy.mock.calls).toMatchSnapshot() }) @@ -871,9 +872,6 @@ describe('handleIOSBezelCorners', () => { }) describe('cropAndConvertToDataURL', () => { - let mockImage: any - let mockCroppedImage: any - const defaultCropOptions = { addIOSBezelCorners: false, base64Image: 'originalImageData', @@ -887,125 +885,52 @@ describe('cropAndConvertToDataURL', () => { width: 200, } - beforeEach(async () => { - mockCroppedImage = { - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,croppedImageData'), - } - mockImage = { - crop: vi.fn().mockReturnValue(mockCroppedImage), - } - - const jimpModule = vi.mocked(await import('jimp')) - vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage) - }) - - afterEach(() => { - vi.clearAllMocks() - }) + afterEach(() => { vi.clearAllMocks() }) it('should crop image and return base64 data without iOS bezel corners', async () => { const result = await cropAndConvertToDataURL(defaultCropOptions) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + expect(vi.mocked(imageUtils.cropImage)).toHaveBeenCalledWith(expect.any(Object), 50, 25, 200, 100) expect(result).toMatchSnapshot() }) it('should crop image and add iOS bezel corners when isIOS is true', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - addIOSBezelCorners: true, - isIOS: true, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, isIOS: true }) expect(result).toMatchSnapshot() }) it('should handle landscape orientation with iOS bezel corners', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - addIOSBezelCorners: true, - isIOS: true, - isLandscape: true, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, isIOS: true, isLandscape: true }) expect(result).toMatchSnapshot() }) it('should handle Android device (isIOS false) without bezel corners', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - addIOSBezelCorners: true, - deviceName: 'Samsung Galaxy S21', - isIOS: false, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, deviceName: 'Samsung Galaxy S21', isIOS: false }) expect(result).toMatchSnapshot() }) it('should handle zero dimensions', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - height: 0, - sourceX: 0, - sourceY: 0, - width: 0, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, height: 0, sourceX: 0, sourceY: 0, width: 0 }) expect(result).toMatchSnapshot() }) it('should handle large crop dimensions', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - height: 2000, - sourceX: 1000, - sourceY: 500, - width: 3000, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, height: 2000, sourceX: 1000, sourceY: 500, width: 3000 }) expect(result).toMatchSnapshot() }) it('should handle different base64 input data', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - base64Image: 'differentImageData123', - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, base64Image: 'differentImageData123' }) expect(result).toMatchSnapshot() }) it('should handle different device pixel ratios', async () => { - const result = await cropAndConvertToDataURL({ - ...defaultCropOptions, - addIOSBezelCorners: true, - devicePixelRatio: 2, - isIOS: true, - }) - - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() + const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, devicePixelRatio: 2, isIOS: true }) expect(result).toMatchSnapshot() }) }) describe('makeCroppedBase64Image', () => { let getBase64ScreenshotSizeMock: ReturnType - let mockImage: any - let mockCroppedImage: any const defaultCropOptions = { addIOSBezelCorners: false, @@ -1027,22 +952,6 @@ describe('makeCroppedBase64Image', () => { beforeEach(async () => { const utilsModule = vi.mocked(await import('../helpers/utils.js')) getBase64ScreenshotSizeMock = vi.spyOn(utilsModule, 'getBase64ScreenshotSize') - mockCroppedImage = { - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,finalCroppedImageData'), - composite: vi.fn().mockReturnThis(), - opacity: vi.fn().mockReturnThis(), - } - mockImage = { - crop: vi.fn().mockReturnValue(mockCroppedImage), - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), - opacity: vi.fn().mockReturnThis(), - rotate: vi.fn().mockReturnThis(), - } - - const jimpModule = vi.mocked(await import('jimp')) - vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage) - getBase64ScreenshotSizeMock.mockReturnValue({ width: 1000, height: 2000 }) }) @@ -1054,8 +963,6 @@ describe('makeCroppedBase64Image', () => { const result = await makeCroppedBase64Image(defaultCropOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1066,7 +973,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1077,7 +983,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1089,7 +994,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1100,7 +1004,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1116,7 +1019,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1126,7 +1028,6 @@ describe('makeCroppedBase64Image', () => { const result = await makeCroppedBase64Image(defaultCropOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1137,7 +1038,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1153,7 +1053,6 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1170,15 +1069,12 @@ describe('makeCroppedBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) }) describe('makeFullPageBase64Image', () => { let getBase64ScreenshotSizeMock: ReturnType - let mockCanvas: any - let mockImage: any const defaultScreenshotsData = { fullPageHeight: 2000, @@ -1222,31 +1118,6 @@ describe('makeFullPageBase64Image', () => { beforeEach(async () => { const utilsModule = vi.mocked(await import('../helpers/utils.js')) getBase64ScreenshotSizeMock = vi.spyOn(utilsModule, 'getBase64ScreenshotSize') - mockCanvas = { - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,fullPageImageData'), - } - mockImage = { - bitmap: { - width: 1000, - height: 800, - }, - crop: vi.fn().mockReturnThis(), - composite: vi.fn().mockReturnThis(), - getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), - opacity: vi.fn().mockReturnThis(), - rotate: vi.fn().mockReturnThis(), - } - const jimpModule = vi.mocked(await import('jimp')) - - vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage) - vi.mocked(jimpModule.Jimp).mockImplementation((options: any) => { - if (options && (options.width || options.height)) { - return mockCanvas - } - return mockImage - }) - getBase64ScreenshotSizeMock.mockReturnValue({ width: 1000, height: 800 }) }) @@ -1258,9 +1129,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() - expect(mockCanvas.getBase64.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1273,8 +1141,6 @@ describe('makeFullPageBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1298,8 +1164,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(singleScreenshotData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1310,8 +1174,6 @@ describe('makeFullPageBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1348,8 +1210,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(mixedScreenshotsData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1373,8 +1233,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(croppedScreenshotsData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1387,8 +1245,6 @@ describe('makeFullPageBase64Image', () => { }) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1402,9 +1258,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(emptyScreenshotsData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() - expect(mockCanvas.getBase64.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1430,8 +1283,6 @@ describe('makeFullPageBase64Image', () => { const result = await makeFullPageBase64Image(largeScreenshotsData, defaultOptions) expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot() - expect(mockImage.crop.mock.calls).toMatchSnapshot() - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() expect(result).toMatchSnapshot() }) @@ -1445,7 +1296,7 @@ describe('makeFullPageBase64Image', () => { it('should handle canvas Y positions correctly', async () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, defaultOptions) - expect(mockCanvas.composite.mock.calls).toMatchSnapshot() + expect(vi.mocked(imageUtils.compositeImage).mock.calls.length).toBeGreaterThan(0) expect(result).toMatchSnapshot() }) }) diff --git a/packages/image-comparison-core/src/methods/images.ts b/packages/image-comparison-core/src/methods/images.ts index 34ca9b16d..0af8e99fe 100644 --- a/packages/image-comparison-core/src/methods/images.ts +++ b/packages/image-comparison-core/src/methods/images.ts @@ -1,9 +1,9 @@ import { fileURLToPath } from 'node:url' import { readFileSync, writeFileSync, promises as fsPromises, constants } from 'node:fs' import { dirname, join } from 'node:path' -import { Jimp, JimpMime } from 'jimp' +import { decodeImage, toBase64Png, createCanvas, cropImage, compositeImage, setOpacity, rotate90CW, rotate90CCW, rotate180 } from '../utils/imageUtils.js' import logger from '@wdio/logger' -import compareImages from '../resemble/compareImages.js' +import compareImagesPixelmatch from '../pixelmatch/compareImages.js' import { calculateDprData, getIosBezelImageNames, getBase64ScreenshotSize, prepareComparisonFilePaths, updateVisualBaseline } from '../helpers/utils.js' import { prepareIgnoreOptions } from '../helpers/options.js' import { DEFAULT_RESIZE_DIMENSIONS, supportedIosBezelDevices } from '../helpers/constants.js' @@ -25,7 +25,7 @@ import type { } from './images.interfaces.js' import type { IgnoreBoxes } from './rectangles.interfaces.js' import type { FullPageScreenshotsData } from './screenshots.interfaces.js' -import type { CompareData, ComparisonOptions } from '../resemble/compare.interfaces.js' +import type { CompareData, ComparisonOptions } from '../pixelmatch/compare.interfaces.js' import { generateAndSaveDiff } from './processDiffPixels.js' import { createJsonReportIfNeeded } from './createCompareReport.js' import { takeBase64Screenshot } from './screenshots.js' @@ -125,7 +125,7 @@ export async function getRotatedImageIfNeeded({ isWebDriverElementScreenshot, is const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(base64Image) const isRotated = !isWebDriverElementScreenshot && isLandscape && screenshotHeight > screenshotWidth - return isRotated ? await rotateBase64Image({ base64Image, degrees: 90 }) : base64Image + return isRotated ? rotateBase64Image({ base64Image, degrees: 90 }) : base64Image } /** @@ -214,11 +214,11 @@ export async function handleIOSBezelCorners({ const topImage = readFileSync(join(__dirname, '..', '..', 'assets', 'ios', `${topImageName}.png`), { encoding: 'base64' }) const bottomImage = readFileSync(join(__dirname, '..', '..', 'assets', 'ios', `${bottomImageName}.png`), { encoding: 'base64' }) - const topBase64Image = isLandscape ? await rotateBase64Image({ base64Image: topImage, degrees: 90 }) : topImage - const bottomBase64Image = isLandscape ? await rotateBase64Image({ base64Image: bottomImage, degrees: 90 }) : bottomImage + const topBase64Image = isLandscape ? rotateBase64Image({ base64Image: topImage, degrees: 270 }) : topImage + const bottomBase64Image = isLandscape ? rotateBase64Image({ base64Image: bottomImage, degrees: 270 }) : bottomImage - image.composite(await Jimp.read(Buffer.from(topBase64Image, 'base64')), 0, 0) - image.composite(await Jimp.read(Buffer.from(bottomBase64Image, 'base64')), + compositeImage(image, decodeImage(Buffer.from(topBase64Image, 'base64')), 0, 0) + compositeImage(image, decodeImage(Buffer.from(bottomBase64Image, 'base64')), isLandscape ? width - getBase64ScreenshotSize(bottomImage).height : 0, isLandscape ? 0 : height - getBase64ScreenshotSize(bottomImage).height ) @@ -262,15 +262,14 @@ export async function cropAndConvertToDataURL({ sourceY, width, }: CropAndConvertToDataURL): Promise { - const image = await Jimp.read(Buffer.from(base64Image, 'base64')) - const croppedImage = image.crop({ x:sourceX, y:sourceY, w:width, h:height }) + const image = decodeImage(Buffer.from(base64Image, 'base64')) + const croppedImage = cropImage(image, sourceX, sourceY, width, height) if (isIOS) { await handleIOSBezelCorners({ addIOSBezelCorners, image: croppedImage, deviceName, devicePixelRatio, height, isLandscape, width }) } - const base64CroppedImage = await croppedImage.getBase64(JimpMime.png) - return base64CroppedImage.replace(/^data:image\/png;base64,/, '') + return toBase64Png(croppedImage) } /** @@ -446,7 +445,7 @@ export async function executeImageCompare( } // 5. Execute the compare and retrieve the data - const data: CompareData = await compareImages(readFileSync(baselineFilePath), actualImageBuffer, compareOptions) + const data: CompareData = await compareImagesPixelmatch(readFileSync(baselineFilePath), actualImageBuffer, compareOptions) const rawMisMatchPercentage = data.rawMisMatchPercentage const reportMisMatchPercentage = imageCompareOptions.rawMisMatchPercentage ? rawMisMatchPercentage @@ -524,21 +523,21 @@ export async function makeFullPageBase64Image( ): Promise { const amountOfScreenshots = screenshotsData.data.length const { fullPageHeight: canvasHeight, fullPageWidth: canvasWidth } = screenshotsData - const canvas = await new Jimp({ width: canvasWidth, height: canvasHeight }) + const canvas = createCanvas(canvasWidth, canvasHeight) // Load all the images for (let i = 0; i < amountOfScreenshots; i++) { const currentScreenshot = screenshotsData.data[i].screenshot const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(currentScreenshot, devicePixelRatio) const isRotated = isLandscape && screenshotHeight > screenshotWidth - const newBase64Image = isRotated ? await rotateBase64Image({ base64Image: currentScreenshot, degrees: 90 }) : currentScreenshot + const newBase64Image = isRotated ? rotateBase64Image({ base64Image: currentScreenshot, degrees: 90 }) : currentScreenshot const { canvasYPosition, imageHeight, imageXPosition, imageYPosition } = screenshotsData.data[i] - const image = await Jimp.read(Buffer.from(newBase64Image, 'base64')) + const image = decodeImage(Buffer.from(newBase64Image, 'base64')) // Clamp crop dimensions to fit within the actual image bounds // This is especially important for the last image where the calculated height might exceed available pixels - const actualImageWidth = image.bitmap.width - const actualImageHeight = image.bitmap.height + const actualImageWidth = image.width + const actualImageHeight = image.height const clampedCropX = Math.max(0, Math.min(imageXPosition, actualImageWidth - 1)) const clampedCropY = Math.max(0, Math.min(imageYPosition, actualImageHeight - 1)) // Ensure the cropped width matches the canvas width to avoid 1px gaps due to rounding @@ -547,15 +546,10 @@ export async function makeFullPageBase64Image( const clampedCropWidth = Math.min(canvasWidth, maxAvailableWidth) const clampedCropHeight = Math.min(imageHeight, actualImageHeight - clampedCropY) - canvas.composite( - image.crop({ x: clampedCropX, y: clampedCropY, w: clampedCropWidth, h: clampedCropHeight }), - 0, - canvasYPosition - ) + compositeImage(canvas, cropImage(image, clampedCropX, clampedCropY, clampedCropWidth, clampedCropHeight), 0, canvasYPosition) } - const base64FullPageImage = await canvas.getBase64(JimpMime.png) - return base64FullPageImage.replace(/^data:image\/png;base64,/, '') + return toBase64Png(canvas) } /** @@ -566,35 +560,36 @@ export async function saveBase64Image(base64Image: string, filePath: string) { await fsPromises.writeFile(filePath, Buffer.from(base64Image, 'base64')) } +export async function savePngBuffer(buffer: Buffer, filePath: string): Promise { + await fsPromises.mkdir(dirname(filePath), { recursive: true }) + await fsPromises.writeFile(filePath, buffer) +} + /** * Create a canvas with the ignore boxes if they are present */ export async function addBlockOuts(screenshot: string, ignoredBoxes: IgnoreBoxes[]): Promise { - const image = await Jimp.read(Buffer.from(screenshot, 'base64')) + const image = decodeImage(Buffer.from(screenshot, 'base64')) // Loop over all ignored areas and add them to the current canvas for (const ignoredBox of ignoredBoxes) { const { right: ignoredBoxWidth, bottom: ignoredBoxHeight, left: x, top: y } = ignoredBox - const ignoreCanvas = new Jimp({ width: ignoredBoxWidth - x, height: ignoredBoxHeight - y, color: '#39aa56' }) - ignoreCanvas.opacity(0.5) - - image.composite(ignoreCanvas, x, y) + const ignoreCanvas = createCanvas(ignoredBoxWidth - x, ignoredBoxHeight - y, 57, 170, 86, 255) + setOpacity(ignoreCanvas, 0.5) + compositeImage(image, ignoreCanvas, x, y) } - const base64ImageWithBlockOuts = await image.getBase64(JimpMime.png) - return base64ImageWithBlockOuts.replace(/^data:image\/png;base64,/, '') + return toBase64Png(image) } /** * Rotate a base64 image * Tnx to https://gist.github.com/Zyndoras/6897abdf53adbedf02564808aaab94db */ -export async function rotateBase64Image({ base64Image, degrees }: RotateBase64ImageOptions): Promise { - const image = await Jimp.read(Buffer.from(base64Image, 'base64')) - const rotatedImage = image.rotate(degrees) - const base64RotatedImage = await rotatedImage.getBase64(JimpMime.png) - - return base64RotatedImage.replace(/^data:image\/png;base64,/, '') +export function rotateBase64Image({ base64Image, degrees }: RotateBase64ImageOptions): string { + const image = decodeImage(Buffer.from(base64Image, 'base64')) + const rotated = degrees === 180 ? rotate180(image) : degrees === 270 ? rotate90CCW(image) : rotate90CW(image) + return toBase64Png(rotated) } /** diff --git a/packages/image-comparison-core/src/methods/processDiffPixels.ts b/packages/image-comparison-core/src/methods/processDiffPixels.ts index 4ac86995f..f1e81830b 100644 --- a/packages/image-comparison-core/src/methods/processDiffPixels.ts +++ b/packages/image-comparison-core/src/methods/processDiffPixels.ts @@ -47,8 +47,9 @@ import logger from '@wdio/logger' import type { Pixel, WicImageCompareOptions } from 'src/methods/images.interfaces.js' import type { BoundingBox, IgnoreBoxes } from './rectangles.interfaces.js' -import type { CompareData } from '../resemble/compare.interfaces.js' -import { saveBase64Image, addBlockOuts } from './images.js' +import type { CompareData } from '../pixelmatch/compare.interfaces.js' +import { savePngBuffer } from './images.js' +import { compositeImage, createCanvas, encodeImage, setOpacity } from '../utils/imageUtils.js' const log = logger('@wdio/visual-service:@wdio/image-comparison-core:pixelDiffProcessing') @@ -271,7 +272,17 @@ export async function generateAndSaveDiff( diffBoundingBoxes.push(...processDiffPixels(data.diffPixels, imageCompareOptions.diffPixelBoundingBoxProximity)) } - await saveBase64Image(await addBlockOuts(Buffer.from(await data.getBuffer()).toString('base64'), ignoredBoxes), diffFilePath) + const rawDiff = data.getRawPixels() + + if (ignoredBoxes.length > 0) { + for (const box of ignoredBoxes) { + const overlay = createCanvas(box.right - box.left, box.bottom - box.top, 57, 170, 86, 255) + setOpacity(overlay, 0.5) + compositeImage(rawDiff, overlay, box.left, box.top) + } + } + + await savePngBuffer(encodeImage(rawDiff), diffFilePath) log.warn( '\x1b[33m%s\x1b[0m', diff --git a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts index 0c755be49..607730a7a 100644 --- a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts +++ b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts @@ -136,7 +136,7 @@ export interface PrepareIgnoreRectanglesOptions { } export interface PreparedIgnoreRectangles { - /** The final ignored boxes ready for resemble comparison */ + /** The final ignored boxes for image comparison */ ignoredBoxes: any[]; /** Whether any ignore rectangles were found */ hasIgnoreRectangles: boolean; diff --git a/packages/image-comparison-core/src/methods/rectangles.ts b/packages/image-comparison-core/src/methods/rectangles.ts index 8d98a258b..86b49432a 100644 --- a/packages/image-comparison-core/src/methods/rectangles.ts +++ b/packages/image-comparison-core/src/methods/rectangles.ts @@ -1,4 +1,5 @@ -import { Jimp } from 'jimp' +import { readFileSync } from 'node:fs' +import { decodeImage } from '../utils/imageUtils.js' import { ANDROID_OFFSETS, IOS_OFFSETS } from '../helpers/constants.js' import { calculateDprData, getBase64ScreenshotSize, isObject } from '../helpers/utils.js' import { getElementPositionAndroid, getElementPositionDesktop, getElementWebviewPosition } from './elementPosition.js' @@ -505,10 +506,14 @@ export async function determineWebElementIgnoreRegions( // to reduce 1px boundary differences on high-DPR / BiDi. let result = [...regions, ...regionsFromElements] .map((region: RectanglesOutput) => { + // Floor position (x/y) to include the start pixel, ceil size (width/height) + // to include the end pixel. Flooring size can lose 1px when CSS * DPR has + // a fractional part, causing the last row or column of an element to fall + // outside the ignored region. let x = Math.floor(region.x * devicePixelRatio) let y = Math.floor(region.y * devicePixelRatio) - let width = Math.floor(region.width * devicePixelRatio) - let height = Math.floor(region.height * devicePixelRatio) + let width = Math.ceil(region.width * devicePixelRatio) + let height = Math.ceil(region.height * devicePixelRatio) if (padding > 0) { x = Math.max(0, x - padding) y = Math.max(0, y - padding) @@ -667,10 +672,8 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp } if (webStatusAddressToolBarOptions.length > 0) { - // There's an issue with the resemble lib when all the rectangles are 0,0,0,0, it will see this as a full - // blockout of the image and the comparison will succeed with 0 % difference. - // Additionally, rectangles with either width or height equal to 0 will result in an entire axis being ignored - // due to how resemble handles falsy values. Filter those out up front. + // Filter out zero-dimension rectangles: a 0,0,0,0 rect would block out the entire image, + // and rects with width or height of 0 produce undefined axis behaviour. Remove them upfront. webStatusAddressToolBarOptions = webStatusAddressToolBarOptions .filter((rectangle) => !(rectangle.x === 0 && rectangle.y === 0 && rectangle.width === 0 && rectangle.height === 0)) .filter((rectangle) => rectangle.width > 0 && rectangle.height > 0) @@ -682,8 +685,8 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp try { // For iOS: block out home bar if (!isAndroid && deviceRectangles.homeBar.height > 0) { - const image = await Jimp.read(actualFilePath) - const imageHeightDevicePixels = image.bitmap.height + const image = decodeImage(readFileSync(actualFilePath)) + const imageHeightDevicePixels = image.height const imageHeightCssPixels = imageHeightDevicePixels / devicePixelRatio // Adjust home bar X position relative to the viewport (full page image only contains viewport) const viewportXCssPixels = deviceRectangles.viewport.x diff --git a/packages/image-comparison-core/src/pixelmatch/compare.interfaces.ts b/packages/image-comparison-core/src/pixelmatch/compare.interfaces.ts new file mode 100644 index 000000000..d7966e998 --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compare.interfaces.ts @@ -0,0 +1,46 @@ +import type { BaseBoundingBox, BaseCoordinates } from '../base.interfaces.js' +import type { RawImage } from '../utils/imageUtils.js' + +export interface CompareData { + /** The mismatch percentage like 0.12345567 */ + rawMisMatchPercentage: number; + /** The mismatch percentage like 0.12 */ + misMatchPercentage: number; + /** Raw RGBA pixel data of the diff composited on the actual screenshot, with dimensions */ + getRawPixels: () => RawImage; + /** The diff image encoded as a PNG buffer */ + getBuffer: () => Promise; + /** The bounds of the diff area */ + diffBounds: BaseBoundingBox; + /** The analysis time in milliseconds */ + analysisTime: number; + /** The diff pixels location(s) and color(s) */ + diffPixels: BaseCoordinates[]; +} + +type Box = { + /** Left boundary of the box */ + left: number; + /** Top boundary of the box */ + top: number; + /** Right boundary of the box */ + right: number; + /** Bottom boundary of the box */ + bottom: number; +}; + +type OutputSettings = { + /** Box area to ignore during comparison */ + ignoredBoxes?: Box[] | undefined; +}; + +export type ComparisonIgnoreOption = 'nothing' | 'less' | 'antialiasing' | 'colors' | 'alpha'; + +export interface ComparisonOptions { + /** Output settings for the comparison */ + output?: OutputSettings | undefined; + /** Whether to scale images to the same size before comparison */ + scaleToSameSize?: boolean | undefined; + /** What aspects to ignore during comparison */ + ignore?: ComparisonIgnoreOption | ComparisonIgnoreOption[] | undefined; +} diff --git a/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts b/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts new file mode 100644 index 000000000..392da6db0 --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts @@ -0,0 +1,375 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('pixelmatch', () => ({ + default: vi.fn() +})) + +vi.mock('../utils/imageUtils.js', () => { + const makeImage = (width = 100, height = 100) => ({ + data: new Uint8Array(width * height * 4).fill(128), + width, + height, + }) + + return { + decodeImage: vi.fn().mockImplementation(() => makeImage()), + resizeBilinear: vi.fn().mockImplementation((_img: unknown, w: number, h: number) => makeImage(w, h)), + encodeImage: vi.fn().mockReturnValue(Buffer.from('png-data')), + } +}) + +import compareImages from './compareImages.js' +import pixelmatch from 'pixelmatch' +import * as imageUtils from '../utils/imageUtils.js' + +const pixelmatchFn = vi.mocked(pixelmatch) +const decodeImageFn = vi.mocked(imageUtils.decodeImage) +const resizeBilinearFn = vi.mocked(imageUtils.resizeBilinear) + +describe('pixelmatch adapter - compareImages', () => { + beforeEach(() => { + vi.clearAllMocks() + decodeImageFn.mockReturnValue({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 }) + }) + + describe('basic comparison', () => { + it('returns zero mismatch percentage when images are identical', async () => { + pixelmatchFn.mockImplementation(() => 0) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.rawMisMatchPercentage).toBe(0) + expect(result.misMatchPercentage).toBe(0) + expect(result.diffPixels).toHaveLength(0) + }) + + it('returns correct mismatch percentage for a known diff count', async () => { + // 100x100 = 10000 total pixels, 100 diff pixels = 1% + pixelmatchFn.mockImplementation(() => 100) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.rawMisMatchPercentage).toBeCloseTo(1, 5) + expect(result.misMatchPercentage).toBe(1) + }) + + it('returns analysisTime greater than or equal to 0', async () => { + pixelmatchFn.mockImplementation(() => 0) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.analysisTime).toBeGreaterThanOrEqual(0) + }) + + it('resolves getBuffer to a Buffer', async () => { + pixelmatchFn.mockImplementation(() => 0) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + const buf = await result.getBuffer() + + expect(Buffer.isBuffer(buf)).toBe(true) + }) + + it('getRawPixels paints diff pixels magenta and keeps actual pixels for matches', async () => { + const actual = new Uint8Array(100 * 100 * 4).fill(200) // distinctive actual colour + decodeImageFn + .mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 }) // baseline + .mockReturnValueOnce({ data: actual, width: 100, height: 100 }) // actual + + pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array) => { + // Mark pixel at (0,0) as a diff (magenta) + output[0] = 255 + output[1] = 0 + output[2] = 255 + output[3] = 255 + // Pixel at (1,0) remains 0 - match + return 1 + }) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + const raw = result.getRawPixels() + + // Diff pixel → magenta + expect(raw.data[0]).toBe(255) + expect(raw.data[1]).toBe(0) + expect(raw.data[2]).toBe(255) + expect(raw.data[3]).toBe(255) + + // Match pixel → actual screenshot value (200) + expect(raw.data[4]).toBe(200) + expect(raw.data[5]).toBe(200) + expect(raw.data[6]).toBe(200) + + expect(raw.width).toBe(100) + expect(raw.height).toBe(100) + }) + }) + + describe('diffPixels and diffBounds', () => { + it('collects magenta pixels as diff pixel coordinates', async () => { + pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => { + // Place a magenta pixel at x=5, y=3 + const pos = (3 * width + 5) * 4 + output[pos] = 255 + output[pos + 1] = 0 + output[pos + 2] = 255 + output[pos + 3] = 255 + return 1 + }) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.diffPixels).toHaveLength(1) + expect(result.diffPixels[0]).toEqual({ x: 5, y: 3 }) + }) + + it('does not count grayscale matching pixels as diff pixels', async () => { + pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => { + const pos = (2 * width + 10) * 4 + output[pos] = 200 + output[pos + 1] = 200 + output[pos + 2] = 200 + output[pos + 3] = 255 + return 0 + }) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.diffPixels).toHaveLength(0) + }) + + it('computes correct diffBounds from multiple diff pixels', async () => { + pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => { + const mark = (x: number, y: number) => { + const pos = (y * width + x) * 4 + output[pos] = 255 + output[pos + 1] = 0 + output[pos + 2] = 255 + output[pos + 3] = 255 + } + mark(10, 5) + mark(30, 20) + mark(20, 10) + return 3 + }) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.diffBounds).toEqual({ left: 10, top: 5, right: 30, bottom: 20 }) + }) + + it('returns sentinel diffBounds when there are no diff pixels', async () => { + pixelmatchFn.mockImplementation(() => 0) + + const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(result.diffBounds.left).toBeGreaterThan(result.diffBounds.right) + expect(result.diffBounds.top).toBeGreaterThan(result.diffBounds.bottom) + }) + }) + + describe('ignore option mapping', () => { + it('passes threshold=0 and includeAA=true for ignore: nothing', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'nothing' }) + + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.any(Number), expect.any(Number), + expect.objectContaining({ threshold: 0, includeAA: true }) + ) + }) + + it('passes threshold=0.063 and includeAA=false for ignore: less', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'less' }) + + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.any(Number), expect.any(Number), + expect.objectContaining({ threshold: 0.063, includeAA: false }) + ) + }) + + it('passes threshold=0.13 and includeAA=false for ignore: antialiasing', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'antialiasing' }) + + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.any(Number), expect.any(Number), + expect.objectContaining({ threshold: 0.13, includeAA: false }) + ) + }) + + it('passes threshold=0.13 and includeAA=false when no ignore option is given', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.any(Number), expect.any(Number), + expect.objectContaining({ threshold: 0.13, includeAA: false }) + ) + }) + + it('accepts ignore as an array and uses the highest-priority mode', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { + ignore: ['antialiasing', 'less'] + }) + + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.anything(), expect.anything(), expect.anything(), + expect.any(Number), expect.any(Number), + expect.objectContaining({ threshold: 0.063 }) + ) + }) + }) + + describe('pixel transformations', () => { + it('grayscales both pixel arrays when ignore includes colors', async () => { + let capturedPixels1: Uint8Array | undefined + + pixelmatchFn.mockImplementation((img1: Uint8Array) => { + capturedPixels1 = new Uint8Array(img1) + return 0 + }) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'colors' }) + + // After grayscale, R=G=B for every pixel (luma of 128,128,128 = 128) + expect(capturedPixels1![0]).toBe(capturedPixels1![1]) + expect(capturedPixels1![1]).toBe(capturedPixels1![2]) + }) + + it('sets all alpha channels to 255 when ignore includes alpha', async () => { + // Use image data with alpha < 255 + decodeImageFn.mockReturnValue({ + data: new Uint8Array(100 * 100 * 4).fill(0), // all zeros, including alpha + width: 100, + height: 100, + }) + let capturedPixels1: Uint8Array | undefined + + pixelmatchFn.mockImplementation((img1: Uint8Array) => { + capturedPixels1 = new Uint8Array(img1) + return 0 + }) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'alpha' }) + + // After opaqueAlphaChannel, every 4th byte should be 255 + expect(capturedPixels1![3]).toBe(255) + expect(capturedPixels1![7]).toBe(255) + }) + + it('pads img2 to canvas size when img1 is larger', async () => { + decodeImageFn + .mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 }) + .mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4).fill(64), width: 50, height: 50 }) + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + // Canvas is 100×100; img2 (50×50) is padded to fill it + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.any(Object), expect.any(Object), expect.any(Uint8Array), + 100, 100, expect.any(Object) + ) + }) + + it('pads img1 to canvas size when img2 is larger', async () => { + decodeImageFn + .mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4).fill(64), width: 50, height: 50 }) + .mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 }) + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + + // Canvas is 100×100; img1 (50×50) is padded to fill it + expect(pixelmatchFn).toHaveBeenCalledWith( + expect.any(Object), expect.any(Object), expect.any(Uint8Array), + 100, 100, expect.any(Object) + ) + }) + }) + + describe('ignoredBoxes', () => { + it('zeroes out the specified box regions in both pixel arrays before comparison', async () => { + let capturedImg1: Uint8Array | undefined + let capturedImg2: Uint8Array | undefined + + pixelmatchFn.mockImplementation((img1: Uint8Array, img2: Uint8Array) => { + capturedImg1 = new Uint8Array(img1) + capturedImg2 = new Uint8Array(img2) + return 0 + }) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { + output: { + ignoredBoxes: [{ left: 0, top: 0, right: 0, bottom: 0 }] + } + }) + + expect(capturedImg1![0]).toBe(0) + expect(capturedImg1![1]).toBe(0) + expect(capturedImg1![2]).toBe(0) + expect(capturedImg1![3]).toBe(0) + expect(capturedImg2![0]).toBe(0) + }) + }) + + describe('scaleToSameSize', () => { + it('calls resizeBilinear on the smaller image when images differ in size', async () => { + decodeImageFn + .mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4), width: 100, height: 100 }) + .mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4), width: 50, height: 50 }) + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true }) + + expect(resizeBilinearFn).toHaveBeenCalledTimes(1) + expect(resizeBilinearFn).toHaveBeenCalledWith( + expect.objectContaining({ width: 50, height: 50 }), + 100, 100 + ) + }) + + it('resizes img1 when img2 is larger', async () => { + decodeImageFn + .mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4), width: 50, height: 50 }) + .mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4), width: 100, height: 100 }) + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true }) + + expect(resizeBilinearFn).toHaveBeenCalledTimes(1) + expect(resizeBilinearFn).toHaveBeenCalledWith( + expect.objectContaining({ width: 50, height: 50 }), + 100, 100 + ) + }) + + it('does not call resizeBilinear when scaleToSameSize is false', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: false }) + + expect(resizeBilinearFn).not.toHaveBeenCalled() + }) + + it('does not call resizeBilinear when images are the same size', async () => { + pixelmatchFn.mockImplementation(() => 0) + + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true }) + + expect(resizeBilinearFn).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/image-comparison-core/src/pixelmatch/compareImages.ts b/packages/image-comparison-core/src/pixelmatch/compareImages.ts new file mode 100644 index 000000000..a33ad1bbf --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compareImages.ts @@ -0,0 +1,198 @@ +import pixelmatch from 'pixelmatch' +import { decodeImage, resizeBilinear, encodeImage, type RawImage } from '../utils/imageUtils.js' +import type { CompareData, ComparisonOptions, ComparisonIgnoreOption } from './compare.interfaces.js' + +function resolveIgnoreList(ignore: ComparisonOptions['ignore']): ComparisonIgnoreOption[] { + if (!ignore) { + return [] + } + + return Array.isArray(ignore) ? ignore : [ignore] +} + +function toPixelmatchOptions(ignoreList: ComparisonIgnoreOption[]): { threshold: number; includeAA: boolean } { + if (ignoreList.includes('nothing')) { + return { threshold: 0, includeAA: true } + } + if (ignoreList.includes('less')) { + // 16/255 per channel in resemble maps roughly to ~6.3% of max YIQ distance + return { threshold: 0.063, includeAA: false } + } + // 'antialiasing', 'alpha', 'colors' and the default. + // Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance which + // corresponds to ~0.13 in YIQ perceptual distance. Using 0.1 is stricter + // and causes invisible sub-pixel differences to register as failures. + return { threshold: 0.13, includeAA: false } +} + +function grayscalePixels(pixels: Buffer, totalPixels: number): void { + for (let i = 0; i < totalPixels * 4; i += 4) { + const luma = Math.round(0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]) + pixels[i] = luma + pixels[i + 1] = luma + pixels[i + 2] = luma + } +} + +function opaqueAlphaChannel(pixels: Buffer, totalPixels: number): void { + for (let i = 3; i < totalPixels * 4; i += 4) { + pixels[i] = 255 + } +} + +// Pad a raw RGBA pixel buffer to a larger canvas size, placing the source at +// position (0, 0) and filling the remaining area with opaque white. +// Pad source at (0, 0) and fill the remaining area with opaque white. +function padToSize(src: Buffer, srcW: number, srcH: number, dstW: number, dstH: number): Buffer { + const dst = Buffer.alloc(dstW * dstH * 4, 255) // opaque white + for (let y = 0; y < srcH; y++) { + src.copy(dst, y * dstW * 4, y * srcW * 4, (y + 1) * srcW * 4) + } + return dst +} + +function zeroIgnoredBoxes( + pixels: Buffer, + width: number, + boxes: Array<{ left: number; top: number; right: number; bottom: number }> +): void { + for (const box of boxes) { + for (let y = box.top; y <= box.bottom; y++) { + for (let x = box.left; x <= box.right; x++) { + const offset = (y * width + x) * 4 + pixels[offset] = 0 + pixels[offset + 1] = 0 + pixels[offset + 2] = 0 + pixels[offset + 3] = 0 + } + } + } +} + +export default async function compareImages( + image1: Buffer, + image2: Buffer, + options: ComparisonOptions +): Promise { + const start = Date.now() + + let img1 = decodeImage(image1) + let img2 = decodeImage(image2) + + if (options.scaleToSameSize) { + const size1 = img1.width * img1.height + const size2 = img2.width * img2.height + if (size1 > size2) { + img2 = resizeBilinear(img2, img1.width, img1.height) + } else if (size2 > size1) { + img1 = resizeBilinear(img1, img2.width, img2.height) + } + } + + // Determine the target canvas size (max of both dimensions). + const width = Math.max(img1.width, img2.width) + const height = Math.max(img1.height, img2.height) + const totalPixels = width * height + + // Copy bitmap data into mutable buffers, padding smaller images at (0,0) + // with opaque white so content is not shifted by centering. + const pixels1 = img1.width === width && img1.height === height + ? Buffer.from(img1.data) + : padToSize(Buffer.from(img1.data), img1.width, img1.height, width, height) + const pixels2 = img2.width === width && img2.height === height + ? Buffer.from(img2.data) + : padToSize(Buffer.from(img2.data), img2.width, img2.height, width, height) + + // Snapshot the original actual pixels before any comparison transforms (grayscale, + // alpha-opaque, zero-out). The diff image uses this as its background so the real + // screenshot content is always visible, including inside blockout regions. + const displayPixels2 = Buffer.from(pixels2) + + const ignoreList = resolveIgnoreList(options.ignore) + + if (ignoreList.includes('colors')) { + grayscalePixels(pixels1, totalPixels) + grayscalePixels(pixels2, totalPixels) + } + + if (ignoreList.includes('alpha')) { + opaqueAlphaChannel(pixels1, totalPixels) + opaqueAlphaChannel(pixels2, totalPixels) + } + + const ignoredBoxes = options.output?.ignoredBoxes ?? [] + if (ignoredBoxes.length > 0) { + zeroIgnoredBoxes(pixels1, width, ignoredBoxes) + zeroIgnoredBoxes(pixels2, width, ignoredBoxes) + } + + const { threshold, includeAA } = toPixelmatchOptions(ignoreList) + const outputPixels = new Uint8Array(totalPixels * 4) + + // Use magenta [255, 0, 255] for both diff and AA pixels. + const diffCount: number = pixelmatch(pixels1, pixels2, outputPixels, width, height, { + threshold, + includeAA, + diffColor: [255, 0, 255], + aaColor: [255, 0, 255], + }) + + // Collect diff pixel coordinates from the output buffer. + // Both diff and AA pixels are drawn in magenta [255, 0, 255]; grayscale pixels are matches. + const diffPixels: Array<{ x: number; y: number }> = [] + let left = width + let top = height + let right = 0 + let bottom = 0 + + for (let i = 0; i < outputPixels.length; i += 4) { + if (outputPixels[i] === 255 && outputPixels[i + 1] === 0 && outputPixels[i + 2] === 255) { + const pixelIndex = i / 4 + const x = pixelIndex % width + const y = Math.floor(pixelIndex / width) + diffPixels.push({ x, y }) + if (x < left) { left = x } + if (x > right) { right = x } + if (y < top) { top = y } + if (y > bottom) { bottom = y } + } + } + + const diffBounds = diffPixels.length > 0 + ? { left, top, right, bottom } + : { left: width, top: height, right: 0, bottom: 0 } + + // Single-pass blend: paint diff pixels (magenta) on top of the actual screenshot. + // pixels2 is already in memory and normalised to the canvas size, so no extra decode needed. + const getRawPixels = (): RawImage => { + const data = new Uint8Array(totalPixels * 4) + for (let i = 0; i < data.length; i += 4) { + if (outputPixels[i] === 255 && outputPixels[i + 1] === 0 && outputPixels[i + 2] === 255) { + data[i] = 255 + data[i + 1] = 0 + data[i + 2] = 255 + data[i + 3] = 255 + } else { + data[i] = displayPixels2[i] + data[i + 1] = displayPixels2[i + 1] + data[i + 2] = displayPixels2[i + 2] + data[i + 3] = displayPixels2[i + 3] + } + } + return { data, width, height } + } + + const getBuffer = async (): Promise => encodeImage(getRawPixels()) + + const rawMisMatchPercentage = (diffCount / totalPixels) * 100 + + return { + rawMisMatchPercentage, + misMatchPercentage: Number(rawMisMatchPercentage.toFixed(2)), + getRawPixels, + getBuffer, + diffBounds, + analysisTime: Date.now() - start, + diffPixels, + } +} diff --git a/packages/image-comparison-core/src/resemble/compare.interfaces.ts b/packages/image-comparison-core/src/resemble/compare.interfaces.ts deleted file mode 100644 index 54cc72bdb..000000000 --- a/packages/image-comparison-core/src/resemble/compare.interfaces.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { BaseBoundingBox, BaseCoordinates, BaseDimensions } from '../base.interfaces.js' - -export interface CompareData { - /** The mismatch percentage like 0.12345567 */ - rawMisMatchPercentage: number; - /** The mismatch percentage like 0.12 */ - misMatchPercentage: number; - /** The image buffer */ - getBuffer: () => Buffer; - /** The bounds of the diff area */ - diffBounds: BaseBoundingBox; - /** The analysis time in milliseconds */ - analysisTime: number; - /** The diff pixels location(s) and color(s) */ - diffPixels: BaseCoordinates[]; - -} - -/** - * Src: @types/resemblejs - */ -type OutputSettings = { - /** Color to use for highlighting errors */ - errorColor?: - | { - /** Red color component (0-255) */ - red: number; - /** Green color component (0-255) */ - green: number; - /** Blue color component (0-255) */ - blue: number; - } - | undefined; - /** Type of error highlighting to use */ - errorType?: OutputErrorType | undefined; - /** Custom error pixel processing function */ - errorPixel?: ((px: number[], offset: number, d1: Color, d2: Color) => void) | undefined; - /** Transparency level for the output image */ - transparency?: number | undefined; - /** Threshold for large image processing */ - largeImageThreshold?: number | undefined; - /** Whether to use cross-origin for image loading */ - useCrossOrigin?: boolean | undefined; - /** Bounding box to focus comparison on */ - boundingBox?: Box | undefined; - /** Box area to ignore during comparison */ - ignoredBox?: Box | undefined; - /** Multiple bounding boxes to focus comparison on */ - boundingBoxes?: Box[] | undefined; - /** Multiple box areas to ignore during comparison */ - ignoredBoxes?: Box[] | undefined; - /** Color to ignore during comparison */ - ignoreAreasColoredWith?: Color | undefined; -}; - -type Box = { - /** Left boundary of the box */ - left: number; - /** Top boundary of the box */ - top: number; - /** Right boundary of the box */ - right: number; - /** Bottom boundary of the box */ - bottom: number; -}; - -type Color = { - /** Red color component (0-255) */ - r: number; - /** Green color component (0-255) */ - g: number; - /** Blue color component (0-255) */ - b: number; - /** Alpha transparency component (0-255) */ - a: number; -}; - -type Tolerance = { - /** Tolerance for red color component */ - red?: number; - /** Tolerance for green color component */ - green?: number; - /** Tolerance for blue color component */ - blue?: number; - /** Tolerance for alpha transparency component */ - alpha?: number; - /** Minimum brightness tolerance */ - minBrightness?: number; - /** Maximum brightness tolerance */ - maxBrightness?: number; -}; - -type OutputErrorType = 'flat' | 'movement' | 'flatDifferenceIntensity' | 'movementDifferenceIntensity' | 'diffOnly'; - -export type ComparisonIgnoreOption = 'nothing' | 'less' | 'antialiasing' | 'colors' | 'alpha'; -export interface ComparisonOptions { - /** Output settings for the comparison */ - output?: OutputSettings | undefined; - /** Threshold to return early if mismatch exceeds this value */ - returnEarlyThreshold?: number | undefined; - /** Whether to scale images to the same size before comparison */ - scaleToSameSize?: boolean | undefined; - /** What aspects to ignore during comparison */ - ignore?: ComparisonIgnoreOption | ComparisonIgnoreOption[] | undefined; - /** Tolerance settings for color differences */ - tolerance?: Tolerance | undefined; -} -export interface ComparisonResult { - /** - * Error information if error encountered - * - * Note: If error encountered, other properties will be undefined - */ - error?: unknown | undefined; - - /** - * Time consumed by the comparison (in milliseconds) - */ - analysisTime: number; - - /** - * Do the two images have the same dimensions? - */ - isSameDimensions: boolean; - - /** - * The difference in width and height between the dimensions of the two compared images - */ - dimensionDifference: BaseDimensions; - - /** - * The percentage of pixels which do not match between the images - */ - rawMisMatchPercentage: number; - - /** - * Same as `rawMisMatchPercentage` but fixed to 2-digit after the decimal point - */ - misMatchPercentage: number; - - diffBounds: Box; - - /** - * Get a data URL for the comparison image - */ - getImageDataUrl(): string; - - /** - * Get data buffer - */ - getBuffer?: (includeOriginal: boolean) => Buffer; -} diff --git a/packages/image-comparison-core/src/resemble/compareImages.d.ts b/packages/image-comparison-core/src/resemble/compareImages.d.ts deleted file mode 100644 index 30d79cf6d..000000000 --- a/packages/image-comparison-core/src/resemble/compareImages.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ComparisonOptions, ComparisonResult } from './compare.interfaces.ts' - -/** - * The API under Node is the same as on the `resemble.compare` but promise based - */ -declare function compareImages( - image1: string | ImageData | Buffer, - image2: string | ImageData | Buffer, - options: ComparisonOptions, -): Promise; - -export default compareImages diff --git a/packages/image-comparison-core/src/resemble/compareImages.ts b/packages/image-comparison-core/src/resemble/compareImages.ts deleted file mode 100644 index 1ab4a3766..000000000 --- a/packages/image-comparison-core/src/resemble/compareImages.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-ignore: Ignoring type checking for this module import -import * as resembleJimp from './resemble.jimp.cjs' -import type { CompareData, ComparisonOptions } from './compare.interfaces.js' - -export default async function compareImages( - image1: Buffer, - image2: Buffer, - options: ComparisonOptions -): Promise { - /** - * Resemble.js implemented in the way that scales 2nd images to the size of 1st. - * Experimentally proven that downscaling images produces more accurate result than upscaling - */ - const { imageToCompare1, imageToCompare2 } = - options.scaleToSameSize && image1.length > image2.length - ? { - imageToCompare1: image2, - imageToCompare2: image1, - } - : { imageToCompare1: image1, imageToCompare2: image2 } - - try { - const data = await resembleJimp.default.compare(imageToCompare1, imageToCompare2, options) - return data - } catch (err) { - throw err - } -} diff --git a/packages/image-comparison-core/src/resemble/resemble.jimp.cjs b/packages/image-comparison-core/src/resemble/resemble.jimp.cjs deleted file mode 100644 index 353bfb349..000000000 --- a/packages/image-comparison-core/src/resemble/resemble.jimp.cjs +++ /dev/null @@ -1,1183 +0,0 @@ -/* -James Cryer / Huddle -URL: https://github.com/Huddle/Resemble.js -Modified by: @wswebcreation -Reason: The node-canvas library was producing a lot of issues due to system dependency errors. - The Jimp library is a pure JavaScript image processing library that can be used in a Node.js environment. - The browser is not needed anymore, so the API is deliberately broken to only work in a Node.js environment. - The old code is still in the file, but commented out. This way, the code can easily be compared when needed with - the original resemble.js file. -*/ - -const { Jimp, JimpMime } = require('jimp') - -const naiveFallback = function () { - // ISC (c) 2011-2019 https://github.com/medikoo/es5-ext/blob/master/global.js - if (typeof self === 'object' && self) { - return self - } - if (typeof window === 'object' && window) { - return window - } - throw new Error('Unable to resolve global `this`') -} - -const getGlobalThis = function () { - // ISC (c) 2011-2019 https://github.com/medikoo/es5-ext/blob/master/global.js - // Fallback to standard globalThis if available - if (typeof globalThis === 'object' && globalThis) { - return globalThis - } - - try { - Object.defineProperty(Object.prototype, '__global__', { - get: function () { - return this - }, - configurable: true, - }) - } catch (error) { - return naiveFallback() - } - try { - // eslint-disable-next-line no-undef - if (!__global__) { - return naiveFallback() - } - return __global__ // eslint-disable-line no-undef - } finally { - delete Object.prototype.__global__ - } -} - -const isNode = function () { - const globalPolyfill = getGlobalThis() - return ( - typeof globalPolyfill.process !== 'undefined' && - globalPolyfill.process.versions && - globalPolyfill.process.versions.node - ) -}; - -(function (root, factory) { - 'use strict' - if (typeof define === 'function' && define.amd) { - define([], factory) - } else if (typeof module === 'object' && module.exports) { - module.exports = factory() - } else { - root.resemble = factory() - } -})(this /* eslint-disable-line no-invalid-this*/, function () { - 'use strict' - - let Img - // var Canvas; - // var loadNodeCanvasImage; - - // if (isNode()) { - // Canvas = require("canvas"); // eslint-disable-line global-require - // Img = Canvas.Image; - // loadNodeCanvasImage = Canvas.loadImage; - // } else { - // Img = Image; - // } - - function createCanvas(width, height) { - // if (isNode()) { - // return Canvas.createCanvas(width, height); - // } - - // var cnvs = document.createElement("canvas"); - // cnvs.width = width; - // cnvs.height = height; - // return cnvs; - - return new Jimp({ width, height }) - } - - const oldGlobalSettings = {} - let globalOutputSettings = oldGlobalSettings - - const resemble = function (fileData) { - let pixelTransparency = 1 - - const errorPixelColor = { - // Color for Error Pixels. Between 0 and 255. - red: 255, - green: 0, - blue: 255, - alpha: 255, - } - - const targetPix = { r: 0, g: 0, b: 0, a: 0 } // isAntialiased - - const errorPixelTransform = { - flat: function (px, offset) { - px[offset] = errorPixelColor.red - px[offset + 1] = errorPixelColor.green - px[offset + 2] = errorPixelColor.blue - px[offset + 3] = errorPixelColor.alpha - }, - movement: function (px, offset, d1, d2) { - px[offset] = (d2.r * (errorPixelColor.red / 255) + errorPixelColor.red) / 2 - px[offset + 1] = (d2.g * (errorPixelColor.green / 255) + errorPixelColor.green) / 2 - px[offset + 2] = (d2.b * (errorPixelColor.blue / 255) + errorPixelColor.blue) / 2 - px[offset + 3] = d2.a - }, - flatDifferenceIntensity: function (px, offset, d1, d2) { - px[offset] = errorPixelColor.red - px[offset + 1] = errorPixelColor.green - px[offset + 2] = errorPixelColor.blue - px[offset + 3] = colorsDistance(d1, d2) - }, - movementDifferenceIntensity: function (px, offset, d1, d2) { - const ratio = (colorsDistance(d1, d2) / 255) * 0.8 - - px[offset] = - (1 - ratio) * (d2.r * (errorPixelColor.red / 255)) + - ratio * errorPixelColor.red - px[offset + 1] = - (1 - ratio) * (d2.g * (errorPixelColor.green / 255)) + - ratio * errorPixelColor.green - px[offset + 2] = - (1 - ratio) * (d2.b * (errorPixelColor.blue / 255)) + - ratio * errorPixelColor.blue - px[offset + 3] = d2.a - }, - diffOnly: function (px, offset, d1, d2) { - px[offset] = d2.r - px[offset + 1] = d2.g - px[offset + 2] = d2.b - px[offset + 3] = d2.a - }, - } - - let errorPixel = errorPixelTransform.flat - let errorType - let boundingBoxes - let ignoredBoxes - let ignoreAreasColoredWith - let largeImageThreshold = 1200 - let useCrossOrigin = true - let data = {} - let images = [] - const updateCallbackArray = [] - - const tolerance = { - // between 0 and 255 - red: 16, - green: 16, - blue: 16, - alpha: 16, - minBrightness: 16, - maxBrightness: 240, - } - - let ignoreAntialiasing = false - let ignoreColors = false - let scaleToSameSize = false - let compareOnly = false - let returnEarlyThreshold - - function colorsDistance(c1, c2) { - return ( - (Math.abs(c1.r - c2.r) + Math.abs(c1.g - c2.g) + Math.abs(c1.b - c2.b)) / 3 - ) - } - - function withinBoundingBox(x, y, width, height, box) { - return ( - x >= (box.left || 0) && - x <= (box.right || width) && - y >= (box.top || 0) && - y <= (box.bottom || height) - ) - } - - function withinComparedArea(x, y, width, height, pixel2) { - let isIncluded = true - let i - let boundingBox - let ignoredBox - let selected - let ignored - - if (boundingBoxes instanceof Array) { - selected = false - for (i = 0; i < boundingBoxes.length; i++) { - boundingBox = boundingBoxes[i] - if (withinBoundingBox(x, y, width, height, boundingBox)) { - selected = true - break - } - } - } - if (ignoredBoxes instanceof Array) { - ignored = true - for (i = 0; i < ignoredBoxes.length; i++) { - ignoredBox = ignoredBoxes[i] - if (withinBoundingBox(x, y, width, height, ignoredBox)) { - ignored = false - break - } - } - } - - if (ignoreAreasColoredWith) { - return colorsDistance(pixel2, ignoreAreasColoredWith) !== 0 - } - - if (selected === undefined && ignored === undefined) { - return true - } - if (selected === false && ignored === true) { - return false - } - if (selected === true || ignored === true) { - isIncluded = true - } - if (selected === false || ignored === false) { - isIncluded = false - } - return isIncluded - } - - function triggerDataUpdate() { - const len = updateCallbackArray.length - let i - for (i = 0; i < len; i++) { - if (typeof updateCallbackArray[i] === 'function') { - updateCallbackArray[i](data) - } - } - } - - function loop(w, h, callback) { - let x - let y - - for (x = 0; x < w; x++) { - for (y = 0; y < h; y++) { - callback(x, y) - } - } - } - - function parseImage(sourceImageData, width, height) { - let pixelCount = 0 - let redTotal = 0 - let greenTotal = 0 - let blueTotal = 0 - let alphaTotal = 0 - let brightnessTotal = 0 - let whiteTotal = 0 - let blackTotal = 0 - - loop(width, height, function (horizontalPos, verticalPos) { - const offset = (verticalPos * width + horizontalPos) * 4 - const red = sourceImageData[offset] - const green = sourceImageData[offset + 1] - const blue = sourceImageData[offset + 2] - const alpha = sourceImageData[offset + 3] - const brightness = getBrightness(red, green, blue) - - if (red === green && red === blue && alpha) { - if (red === 0) { - blackTotal++ - } else if (red === 255) { - whiteTotal++ - } - } - - pixelCount++ - - redTotal += (red / 255) * 100 - greenTotal += (green / 255) * 100 - blueTotal += (blue / 255) * 100 - alphaTotal += ((255 - alpha) / 255) * 100 - brightnessTotal += (brightness / 255) * 100 - }) - - data.red = Math.floor(redTotal / pixelCount) - data.green = Math.floor(greenTotal / pixelCount) - data.blue = Math.floor(blueTotal / pixelCount) - data.alpha = Math.floor(alphaTotal / pixelCount) - data.brightness = Math.floor(brightnessTotal / pixelCount) - data.white = Math.floor((whiteTotal / pixelCount) * 100) - data.black = Math.floor((blackTotal / pixelCount) * 100) - - triggerDataUpdate() - } - - function onLoadImage(hiddenImage, callback) { - // don't assign to hiddenImage, see https://github.com/Huddle/Resemble.js/pull/87/commits/300d43352a2845aad289b254bfbdc7cd6a37e2d7 - let width = hiddenImage.bitmap.width - let height = hiddenImage.bitmap.height - - if (scaleToSameSize && images.length === 1) { - width = images[0].bitmap.width - height = images[0].bitmap.height - } - - // var hiddenCanvas = createCanvas(width, height); - // var imageData; - - // hiddenCanvas.getContext("2d").drawImage(hiddenImage, 0, 0, width, height); - // imageData = hiddenCanvas - // .getContext("2d") - // .getImageData(0, 0, width, height); - - // images.push(imageData); - - images.push(hiddenImage) - - callback(hiddenImage, width, height) - } - - function loadImageData(fileDataForImage, callback) { - // var fileReader; - // var hiddenImage = new Img(); - - // if (!hiddenImage.setAttribute) { - // hiddenImage.setAttribute = function setAttribute() {}; - // } - - // if (useCrossOrigin) { - // hiddenImage.setAttribute("crossorigin", "anonymous"); - // } - - // hiddenImage.onerror = function (event) { - // hiddenImage.onload = null; - // hiddenImage.onerror = null; // fixes pollution between calls - // const error = event ? event + "" : "Unknown error"; - // images.push({ - // error: `Failed to load image '${fileDataForImage}'. ${error}`, - // }); - // callback(); - // }; - - // hiddenImage.onload = function () { - // hiddenImage.onload = null; // fixes pollution between calls - // hiddenImage.onerror = null; - // onLoadImage(hiddenImage, callback); - // }; - - // if (typeof fileDataForImage === "string") { - // hiddenImage.src = fileDataForImage; - // if (!isNode() && hiddenImage.complete && hiddenImage.naturalWidth > 0) { - // hiddenImage.onload(); - // } - // } else if ( - // typeof fileDataForImage.data !== "undefined" && - // typeof fileDataForImage.width === "number" && - // typeof fileDataForImage.height === "number" - // ) { - // images.push(fileDataForImage); - - // callback( - // fileDataForImage, - // fileDataForImage.width, - // fileDataForImage.height - // ); - // } else if ( - // typeof Buffer !== "undefined" && - // fileDataForImage instanceof Buffer - // ) { - // // If we have Buffer, assume we're on Node+Canvas and its supported - // // hiddenImage.src = fileDataForImage; - - // loadNodeCanvasImage(fileDataForImage) - // .then(function (image) { - // hiddenImage.onload = null; // fixes pollution between calls - // hiddenImage.onerror = null; - // onLoadImage(image, callback); - // }) - // .catch(function (err) { - // images.push({ - // error: err ? err + "" : "Image load error.", - // }); - // callback(); - // }); - // } else { - // fileReader = new FileReader(); - // fileReader.onload = function (event) { - // hiddenImage.src = event.target.result; - // }; - // fileReader.readAsDataURL(fileDataForImage); - // } - - Jimp.read(fileDataForImage) - .then((image) => { - onLoadImage(image, callback) - }) - .catch((err) => { - images.push({ - error: `Failed to load image '${fileDataForImage}'. ${err}`, - }) - callback() - }) - } - - function isColorSimilar(a, b, color) { - const absDiff = Math.abs(a - b) - - if (typeof a === 'undefined') { - return false - } - if (typeof b === 'undefined') { - return false - } - - if (a === b) { - return true - } else if (absDiff < tolerance[color]) { - return true - } - return false - } - - function isPixelBrightnessSimilar(d1, d2) { - const alpha = isColorSimilar(d1.a, d2.a, 'alpha') - const brightness = isColorSimilar( - d1.brightness, - d2.brightness, - 'minBrightness' - ) - return brightness && alpha - } - - function getBrightness(r, g, b) { - return 0.3 * r + 0.59 * g + 0.11 * b - } - - function isRGBSame(d1, d2) { - const red = d1.r === d2.r - const green = d1.g === d2.g - const blue = d1.b === d2.b - return red && green && blue - } - - function isRGBSimilar(d1, d2) { - const red = isColorSimilar(d1.r, d2.r, 'red') - const green = isColorSimilar(d1.g, d2.g, 'green') - const blue = isColorSimilar(d1.b, d2.b, 'blue') - const alpha = isColorSimilar(d1.a, d2.a, 'alpha') - - return red && green && blue && alpha - } - - function isContrasting(d1, d2) { - return Math.abs(d1.brightness - d2.brightness) > tolerance.maxBrightness - } - - function getHue(red, green, blue) { - const r = red / 255 - const g = green / 255 - const b = blue / 255 - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - let h - let d - - if (max === min) { - h = 0 // achromatic - } else { - d = max - min - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0) - break - case g: - h = (b - r) / d + 2 - break - case b: - h = (r - g) / d + 4 - break - default: - h /= 6 - } - } - - return h - } - - function isAntialiased( - sourcePix, - pix, - cacheSet, - verticalPos, - horizontalPos, - width - ) { - let offset - const distance = 1 - let i - let j - let hasHighContrastSibling = 0 - let hasSiblingWithDifferentHue = 0 - let hasEquivalentSibling = 0 - - addHueInfo(sourcePix) - - for (i = distance * -1; i <= distance; i++) { - for (j = distance * -1; j <= distance; j++) { - if (i === 0 && j === 0) { - // ignore source pixel - continue - } else { - offset = ((verticalPos + j) * width + (horizontalPos + i)) * 4 - - if (!getPixelInfo(targetPix, pix, offset, cacheSet)) { - continue - } - - addBrightnessInfo(targetPix) - addHueInfo(targetPix) - - if (isContrasting(sourcePix, targetPix)) { - hasHighContrastSibling++ - } - - if (isRGBSame(sourcePix, targetPix)) { - hasEquivalentSibling++ - } - - if (Math.abs(targetPix.h - sourcePix.h) > 0.3) { - hasSiblingWithDifferentHue++ - } - - if (hasSiblingWithDifferentHue > 1 || hasHighContrastSibling > 1) { - return true - } - } - } - } - - if (hasEquivalentSibling < 2) { - return true - } - - return false - } - - function copyPixel(px, offset, pix) { - if (errorType === 'diffOnly') { - return - } - - px[offset] = pix.r // r - px[offset + 1] = pix.g // g - px[offset + 2] = pix.b // b - px[offset + 3] = pix.a * pixelTransparency // a - } - - function copyGrayScalePixel(px, offset, pix) { - if (errorType === 'diffOnly') { - return - } - - px[offset] = pix.brightness // r - px[offset + 1] = pix.brightness // g - px[offset + 2] = pix.brightness // b - px[offset + 3] = pix.a * pixelTransparency // a - } - - function getPixelInfo(dst, pix, offset) { - if (pix.length > offset) { - dst.r = pix[offset] - dst.g = pix[offset + 1] - dst.b = pix[offset + 2] - dst.a = pix[offset + 3] - - return true - } - - return false - } - - function addBrightnessInfo(pix) { - pix.brightness = getBrightness(pix.r, pix.g, pix.b) // 'corrected' lightness - } - - function addHueInfo(pix) { - pix.h = getHue(pix.r, pix.g, pix.b) - } - - function analyseImages(img1, img2, width, height) { - const data1 = img1.bitmap.data - const data2 = img2.bitmap.data - let hiddenCanvas - // var context; - // var imgd; - let pix - - if (!compareOnly) { - hiddenCanvas = createCanvas(width, height) - - // context = hiddenCanvas.getContext("2d"); - // imgd = context.createImageData(width, height); - pix = hiddenCanvas.bitmap.data - } - - let mismatchCount = 0 - const diffBounds = { - top: height, - left: width, - bottom: 0, - right: 0, - } - const diffPixels = [] - const updateBounds = function (x, y) { - // Update the big box - diffBounds.left = Math.min(x, diffBounds.left) - diffBounds.right = Math.max(x, diffBounds.right) - diffBounds.top = Math.min(y, diffBounds.top) - diffBounds.bottom = Math.max(y, diffBounds.bottom) - // Update the diffPixels array - diffPixels.push({ x, y }) - } - - const time = Date.now() - - let skip - - if ( - !!largeImageThreshold && - ignoreAntialiasing && - (width > largeImageThreshold || height > largeImageThreshold) - ) { - skip = 6 - } - - const pixel1 = { r: 0, g: 0, b: 0, a: 0 } - const pixel2 = { r: 0, g: 0, b: 0, a: 0 } - - let skipTheRest = false - - loop(width, height, function (horizontalPos, verticalPos) { - if (skipTheRest) { - return - } - - if (skip) { - // only skip if the image isn't small - if (verticalPos % skip === 0 || horizontalPos % skip === 0) { - return - } - } - - const offset = (verticalPos * width + horizontalPos) * 4 - if ( - !getPixelInfo(pixel1, data1, offset, 1) || - !getPixelInfo(pixel2, data2, offset, 2) - ) { - return - } - - const isWithinComparedArea = withinComparedArea( - horizontalPos, - verticalPos, - width, - height, - pixel2 - ) - - if (ignoreColors) { - addBrightnessInfo(pixel1) - addBrightnessInfo(pixel2) - - if ( isPixelBrightnessSimilar(pixel1, pixel2) || !isWithinComparedArea ) { - if (!compareOnly) { - copyGrayScalePixel(pix, offset, pixel2) - } - } else { - if (!compareOnly) { - errorPixel(pix, offset, pixel1, pixel2) - } - - mismatchCount++ - updateBounds(horizontalPos, verticalPos) - } - return - } - - if (isRGBSimilar(pixel1, pixel2) || !isWithinComparedArea) { - if (!compareOnly) { - copyPixel(pix, offset, pixel1) - } - } else if ( - ignoreAntialiasing && - ( - addBrightnessInfo(pixel1), // jit pixel info augmentation looks a little weird, sorry. - addBrightnessInfo(pixel2), - isAntialiased(pixel1, data1, 1, verticalPos, horizontalPos, width) || - isAntialiased(pixel2, data2, 2, verticalPos, horizontalPos, width) - ) - ) { - if ( isPixelBrightnessSimilar(pixel1, pixel2) || !isWithinComparedArea ) { - if (!compareOnly) { - copyGrayScalePixel(pix, offset, pixel2) - } - } else { - if (!compareOnly) { - errorPixel(pix, offset, pixel1, pixel2) - } - - mismatchCount++ - updateBounds(horizontalPos, verticalPos) - } - } else { - if (!compareOnly) { - errorPixel(pix, offset, pixel1, pixel2) - } - - mismatchCount++ - updateBounds(horizontalPos, verticalPos) - } - - if (compareOnly) { - const currentMisMatchPercent = (mismatchCount / (height * width)) * 100 - - if (currentMisMatchPercent > returnEarlyThreshold) { - skipTheRest = true - } - } - }) - - data.rawMisMatchPercentage = (mismatchCount / (height * width)) * 100 - data.misMatchPercentage = data.rawMisMatchPercentage.toFixed(2) - data.diffBounds = diffBounds - data.analysisTime = Date.now() - time - // Add diffPixels array to the data object - data.diffPixels = diffPixels - - data.getImageDataUrl = function (text) { - if (compareOnly) { - throw Error('No diff image available - ran in compareOnly mode') - } - - let barHeight = 0 - - if (text) { - barHeight = addLabel(text, hiddenCanvas) - } - - // context.putImageData(imgd, 0, barHeight); - - return hiddenCanvas.getBase64(JimpMime.png) - } - - if (!compareOnly) { - data.getBuffer = function (includeOriginal) { - if (includeOriginal) { - const imageWidth = hiddenCanvas.bitmap.width + 2 - hiddenCanvas.resize(imageWidth * 3, hiddenCanvas.bitmap.height) - hiddenCanvas.composite(img1, 0, 0) - hiddenCanvas.composite(img2, imageWidth, 0) - hiddenCanvas.composite(hiddenCanvas, imageWidth * 2, 0) - } - return hiddenCanvas.getBuffer(JimpMime.png) - } - } - } - - function addLabel(text, hiddenCanvas) { - const textPadding = 2 - - // context.font = "12px sans-serif"; - - // var textWidth = context.measureText(text).width + textPadding * 2; - // var barHeight = 22; - - // if (textWidth > hiddenCanvas.width) { - // hiddenCanvas.width = textWidth; - // } - - // hiddenCanvas.height += barHeight; - - // context.fillStyle = "#666"; - // context.fillRect(0, 0, hiddenCanvas.width, barHeight - 4); - // context.fillStyle = "#fff"; - // context.fillRect(0, barHeight - 4, hiddenCanvas.width, 4); - - // context.fillStyle = "#fff"; - // context.textBaseline = "top"; - // context.font = "12px sans-serif"; - // context.fillText(text, textPadding, 1); - - // return barHeight; - - return Jimp.loadFont(Jimp.FONT_SANS_12_WHITE).then((font) => { - const textWidth = Jimp.measureText(font, text) + textPadding * 2 - const barHeight = 22 - - if (textWidth > hiddenCanvas.bitmap.width) { - hiddenCanvas.resize(textWidth, hiddenCanvas.bitmap.height) - } - - const context = hiddenCanvas.clone() - context.print(font, textPadding, 1, text) - - return barHeight - }) - } - - function normalise(img, w, h) { - // var c; - // var context; - - if (img.bitmap.height < h || img.bitmap.width < w) { - // c = createCanvas(w, h); - // context = c.getContext("2d"); - // context.putImageData(img, 0, 0); - return img.contain({w, h}) - } - - return img - } - - function outputSettings(options) { - let key - - if (options.errorColor) { - for (key in options.errorColor) { - if (options.errorColor.hasOwnProperty(key)) { - errorPixelColor[key] = options.errorColor[key] === void 0 - ? errorPixelColor[key] - : options.errorColor[key] - } - } - } - - if (options.errorType && errorPixelTransform[options.errorType]) { - errorPixel = errorPixelTransform[options.errorType] - errorType = options.errorType - } - - if (options.errorPixel && typeof options.errorPixel === 'function') { - errorPixel = options.errorPixel - } - - pixelTransparency = isNaN(Number(options.transparency)) - ? pixelTransparency - : options.transparency - - if (options.largeImageThreshold !== undefined) { - largeImageThreshold = options.largeImageThreshold - } - - if (options.useCrossOrigin !== undefined) { - useCrossOrigin = options.useCrossOrigin - } - - if (options.boundingBox !== undefined) { - boundingBoxes = [options.boundingBox] - } - - if (options.ignoredBox !== undefined) { - ignoredBoxes = [options.ignoredBox] - } - - if (options.boundingBoxes !== undefined) { - boundingBoxes = options.boundingBoxes - } - - if (options.ignoredBoxes !== undefined) { - ignoredBoxes = options.ignoredBoxes - } - - if (options.ignoreAreasColoredWith !== undefined) { - ignoreAreasColoredWith = options.ignoreAreasColoredWith - } - } - - function compare(one, two) { - if (globalOutputSettings !== oldGlobalSettings) { - outputSettings(globalOutputSettings) - } - - function onceWeHaveBoth() { - let width - let height - if (images.length === 2) { - if (images[0].error || images[1].error) { - data = {} - data.error = images[0].error ? images[0].error : images[1].error - triggerDataUpdate() - return - } - width = images[0].bitmap.width > images[1].bitmap.width - ? images[0].bitmap.width - : images[1].bitmap.width - height = images[0].bitmap.height > images[1].bitmap.height - ? images[0].bitmap.height - : images[1].bitmap.height - - data.isSameDimensions = images[0].bitmap.width === images[1].bitmap.width && - images[0].bitmap.height === images[1].bitmap.height ? true : false - - data.dimensionDifference = { - width: images[0].bitmap.width - images[1].bitmap.width, - height: images[0].bitmap.height - images[1].bitmap.height, - } - - analyseImages( - normalise(images[0], width, height), - normalise(images[1], width, height), - width, - height - ) - - triggerDataUpdate() - } - } - - images = [] - loadImageData(one, onceWeHaveBoth) - loadImageData(two, onceWeHaveBoth) - } - - function getCompareApi(param) { - let secondFileData - const hasMethod = typeof param === 'function' - - if (!hasMethod) { - // assume it's file data - secondFileData = param - } - - var self = { - setReturnEarlyThreshold: function (threshold) { - if (threshold) { - compareOnly = true - returnEarlyThreshold = threshold - } - return self - }, - scaleToSameSize: function () { - scaleToSameSize = true - - if (hasMethod) { - param() - } - return self - }, - useOriginalSize: function () { - scaleToSameSize = false - - if (hasMethod) { - param() - } - return self - }, - ignoreNothing: function () { - tolerance.red = 0 - tolerance.green = 0 - tolerance.blue = 0 - tolerance.alpha = 0 - tolerance.minBrightness = 0 - tolerance.maxBrightness = 255 - - ignoreAntialiasing = false - ignoreColors = false - - if (hasMethod) { - param() - } - return self - }, - ignoreLess: function () { - tolerance.red = 16 - tolerance.green = 16 - tolerance.blue = 16 - tolerance.alpha = 16 - tolerance.minBrightness = 16 - tolerance.maxBrightness = 240 - - ignoreAntialiasing = false - ignoreColors = false - - if (hasMethod) { - param() - } - return self - }, - ignoreAntialiasing: function () { - tolerance.red = 32 - tolerance.green = 32 - tolerance.blue = 32 - tolerance.alpha = 32 - tolerance.minBrightness = 64 - tolerance.maxBrightness = 96 - - ignoreAntialiasing = true - ignoreColors = false - - if (hasMethod) { - param() - } - return self - }, - ignoreColors: function () { - tolerance.alpha = 16 - tolerance.minBrightness = 16 - tolerance.maxBrightness = 240 - - ignoreAntialiasing = false - ignoreColors = true - - if (hasMethod) { - param() - } - return self - }, - ignoreAlpha: function () { - tolerance.red = 16 - tolerance.green = 16 - tolerance.blue = 16 - tolerance.alpha = 255 - tolerance.minBrightness = 16 - tolerance.maxBrightness = 240 - - ignoreAntialiasing = false - ignoreColors = false - - if (hasMethod) { - param() - } - return self - }, - repaint: function () { - if (hasMethod) { - param() - } - return self - }, - outputSettings: function (options) { - outputSettings(options) - return self - }, - onComplete: function (callback) { - updateCallbackArray.push(callback) - - const wrapper = function () { - compare(fileData, secondFileData) - } - - wrapper() - - return getCompareApi(wrapper) - }, - setupCustomTolerance: function (customSettings) { - for (const property in tolerance) { - if (!customSettings.hasOwnProperty(property)) { - continue - } - - tolerance[property] = customSettings[property] - } - }, - } - - return self - } - - var rootSelf = { - onComplete: function (callback) { - updateCallbackArray.push(callback) - loadImageData(fileData, function (imageData, width, height) { - parseImage(imageData, width, height) - }) - }, - compareTo: function (secondFileData) { - return getCompareApi(secondFileData) - }, - outputSettings: function (options) { - outputSettings(options) - return rootSelf - }, - } - - return rootSelf - } - - function setGlobalOutputSettings(settings) { - globalOutputSettings = settings - return resemble - } - - function applyIgnore(api, ignore, customTolerance) { - switch (ignore) { - case 'nothing': - api.ignoreNothing() - break - case 'less': - api.ignoreLess() - break - case 'antialiasing': - api.ignoreAntialiasing() - break - case 'colors': - api.ignoreColors() - break - case 'alpha': - api.ignoreAlpha() - break - default: - throw new Error('Invalid ignore: ' + ignore) - } - - api.setupCustomTolerance(customTolerance) - } - - resemble.compare = function (image1, image2, options) { - return new Promise((resolve, reject) => { - let opt - - if (typeof options !== 'object') { - opt = {} - } else { - opt = options - } - - const res = resemble(image1) - let compare - - if (opt.output) { - res.outputSettings(opt.output) - } - - compare = res.compareTo(image2) - - if (opt.returnEarlyThreshold) { - compare.setReturnEarlyThreshold(opt.returnEarlyThreshold) - } - - if (opt.scaleToSameSize) { - compare.scaleToSameSize() - } - - const toleranceSettings = opt.tolerance || {} - if (typeof opt.ignore === 'string') { - applyIgnore(compare, opt.ignore, toleranceSettings) - } else if (opt.ignore && opt.ignore.forEach) { - opt.ignore.forEach(function (v) { - applyIgnore(compare, v, toleranceSettings) - }) - } - - compare.onComplete(function (data) { - if (data.error) { - reject(data.error) - } else { - resolve(data) - } - }) - }) - } - - - resemble.outputSettings = setGlobalOutputSettings - return resemble -}) diff --git a/packages/image-comparison-core/src/utils/imageUtils.test.ts b/packages/image-comparison-core/src/utils/imageUtils.test.ts new file mode 100644 index 000000000..a88512d7e --- /dev/null +++ b/packages/image-comparison-core/src/utils/imageUtils.test.ts @@ -0,0 +1,317 @@ +import { describe, it, expect } from 'vitest' +import { encode } from 'fast-png' +import { + decodeImage, + encodeImage, + toBase64Png, + createCanvas, + cropImage, + compositeImage, + resizeBilinear, + rotate90CW, + rotate90CCW, + rotate180, + setOpacity, +} from './imageUtils.js' +import type { RawImage } from './imageUtils.js' + +// Build a minimal 2x2 RGBA PNG buffer for use as test input +function makePng(pixels: number[][]): Buffer { + // pixels: [[r,g,b,a], ...] in row-major order, width = sqrt(pixels.length) + const side = Math.sqrt(pixels.length) + const data = new Uint8Array(pixels.length * 4) + pixels.forEach(([r, g, b, a], i) => { + data[i * 4] = r + data[i * 4 + 1] = g + data[i * 4 + 2] = b + data[i * 4 + 3] = a + }) + return Buffer.from(encode({ data, width: side, height: side, channels: 4, depth: 8 })) +} + +describe('decodeImage', () => { + it('decodes an RGBA PNG buffer into a RawImage', () => { + const buf = makePng([[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 0, 255]]) + const img = decodeImage(buf) + expect(img.width).toBe(2) + expect(img.height).toBe(2) + expect(img.data[0]).toBe(255) // R + expect(img.data[1]).toBe(0) // G + expect(img.data[2]).toBe(0) // B + expect(img.data[3]).toBe(255) // A + }) + + it('converts an RGB (3-channel) PNG to RGBA by adding full alpha', () => { + const data = new Uint8Array([100, 150, 200, 50, 60, 70]) + const buf = Buffer.from(encode({ data, width: 2, height: 1, channels: 3, depth: 8 })) + const img = decodeImage(buf) + expect(img.width).toBe(2) + expect(img.height).toBe(1) + expect(img.data[0]).toBe(100) // R + expect(img.data[1]).toBe(150) // G + expect(img.data[2]).toBe(200) // B + expect(img.data[3]).toBe(255) // A added + expect(img.data[4]).toBe(50) + expect(img.data[7]).toBe(255) // A added for second pixel + }) + + it('converts a grayscale (1-channel) PNG to RGBA', () => { + const data = new Uint8Array([128]) + const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 1, depth: 8 })) + const img = decodeImage(buf) + expect(img.width).toBe(1) + expect(img.data[0]).toBe(128) // R = gray value + expect(img.data[1]).toBe(128) // G = gray value + expect(img.data[2]).toBe(128) // B = gray value + expect(img.data[3]).toBe(255) // A = full + }) + + it('converts a grayscale+alpha (2-channel) PNG to RGBA', () => { + const data = new Uint8Array([200, 128]) // gray=200, alpha=128 + const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 2, depth: 8 })) + const img = decodeImage(buf) + expect(img.data[0]).toBe(200) // R = gray + expect(img.data[1]).toBe(200) // G = gray + expect(img.data[2]).toBe(200) // B = gray + expect(img.data[3]).toBe(128) // A preserved + }) + + it('converts a 16-bit RGBA PNG by downsampling to 8-bit', () => { + // 16-bit value 0xFF00 >> 8 = 255, 0x8000 >> 8 = 128 + const data = new Uint16Array([0xFF00, 0x8000, 0x0000, 0xFF00]) + const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 4, depth: 16 })) + const img = decodeImage(buf) + expect(img.data[0]).toBe(255) // R: 0xFF00 >> 8 + expect(img.data[1]).toBe(128) // G: 0x8000 >> 8 + expect(img.data[2]).toBe(0) // B: 0x0000 >> 8 + expect(img.data[3]).toBe(255) // A: 0xFF00 >> 8 + }) +}) + +describe('encodeImage / toBase64Png', () => { + it('round-trips a RawImage through encode → decode', () => { + const original = createCanvas(2, 2, 100, 150, 200, 255) + const buf = encodeImage(original) + const decoded = decodeImage(buf) + expect(decoded.width).toBe(2) + expect(decoded.height).toBe(2) + expect(decoded.data[0]).toBe(100) + expect(decoded.data[1]).toBe(150) + expect(decoded.data[2]).toBe(200) + expect(decoded.data[3]).toBe(255) + }) + + it('toBase64Png returns a valid base64 string that decodes back correctly', () => { + const img = createCanvas(1, 1, 10, 20, 30, 255) + const b64 = toBase64Png(img) + expect(typeof b64).toBe('string') + const decoded = decodeImage(Buffer.from(b64, 'base64')) + expect(decoded.data[0]).toBe(10) + expect(decoded.data[1]).toBe(20) + expect(decoded.data[2]).toBe(30) + }) +}) + +describe('createCanvas', () => { + it('creates a zero-filled canvas by default', () => { + const img = createCanvas(3, 3) + expect(img.width).toBe(3) + expect(img.height).toBe(3) + expect(img.data.every(v => v === 0)).toBe(true) + }) + + it('fills all pixels with the provided RGBA color', () => { + const img = createCanvas(2, 2, 57, 170, 86, 255) + for (let i = 0; i < 4; i++) { + expect(img.data[i * 4]) .toBe(57) + expect(img.data[i * 4 + 1]).toBe(170) + expect(img.data[i * 4 + 2]).toBe(86) + expect(img.data[i * 4 + 3]).toBe(255) + } + }) +}) + +describe('cropImage', () => { + it('extracts the correct rectangular region', () => { + // 4x1 image: red, green, blue, yellow + const img: RawImage = { + data: new Uint8Array([ + 255, 0, 0, 255, // red + 0, 255, 0, 255, // green + 0, 0, 255, 255, // blue + 255, 255, 0, 255, // yellow + ]), + width: 4, + height: 1, + } + const cropped = cropImage(img, 1, 0, 2, 1) + expect(cropped.width).toBe(2) + expect(cropped.height).toBe(1) + // First pixel = green + expect(cropped.data[0]).toBe(0) + expect(cropped.data[1]).toBe(255) + expect(cropped.data[2]).toBe(0) + // Second pixel = blue + expect(cropped.data[4]).toBe(0) + expect(cropped.data[5]).toBe(0) + expect(cropped.data[6]).toBe(255) + }) +}) + +describe('compositeImage', () => { + it('copies an opaque overlay exactly onto the base', () => { + const base = createCanvas(2, 2, 0, 0, 0, 255) + const overlay = createCanvas(1, 1, 255, 0, 0, 255) + compositeImage(base, overlay, 1, 1) + const di = (1 * 2 + 1) * 4 + expect(base.data[di]) .toBe(255) + expect(base.data[di + 1]).toBe(0) + expect(base.data[di + 2]).toBe(0) + expect(base.data[di + 3]).toBe(255) + // Top-left should be untouched + expect(base.data[0]).toBe(0) + }) + + it('blends a semi-transparent overlay with opacity', () => { + const base = createCanvas(1, 1, 0, 0, 0, 255) // black opaque + const overlay = createCanvas(1, 1, 255, 0, 0, 255) // red opaque + compositeImage(base, overlay, 0, 0, 0.5) + // src_a = 0.5, dst_a = 1, out_a = 1 + // R = round((255 * 0.5 + 0 * 1 * 0.5) / 1) = 128 (approximately) + expect(base.data[0]).toBeCloseTo(128, 0) + expect(base.data[3]).toBe(255) + }) + + it('skips blending when both src and dst alpha are zero', () => { + const base = createCanvas(1, 1, 255, 0, 0, 0) // red but fully transparent + const overlay = createCanvas(1, 1, 0, 255, 0, 0) // green but fully transparent + compositeImage(base, overlay, 0, 0) + // outA = 0 → pixel unchanged: base stays (255, 0, 0, 0) + expect(base.data[0]).toBe(255) + expect(base.data[3]).toBe(0) + }) + + it('skips pixels outside the base bounds', () => { + const base = createCanvas(2, 2, 0, 0, 0, 255) + const overlay = createCanvas(3, 3, 255, 0, 0, 255) + // Offset so overlay extends beyond base - should not throw + expect(() => compositeImage(base, overlay, 1, 1)).not.toThrow() + }) +}) + +describe('resizeBilinear', () => { + it('doubles a 1x1 image to 2x2 with the same color', () => { + const img = createCanvas(1, 1, 100, 200, 50, 255) + const resized = resizeBilinear(img, 2, 2) + expect(resized.width).toBe(2) + expect(resized.height).toBe(2) + expect(resized.data[0]).toBe(100) + expect(resized.data[1]).toBe(200) + expect(resized.data[2]).toBe(50) + }) + + it('halves a 4x4 image to 2x2', () => { + const img = createCanvas(4, 4, 200, 100, 50, 255) + const resized = resizeBilinear(img, 2, 2) + expect(resized.width).toBe(2) + expect(resized.height).toBe(2) + }) +}) + +describe('rotate90CW', () => { + it('swaps width and height', () => { + const img = createCanvas(4, 2, 0, 0, 0, 255) + const rotated = rotate90CW(img) + expect(rotated.width).toBe(2) + expect(rotated.height).toBe(4) + }) + + it('moves top-left pixel to top-right', () => { + // 2x1: [red | green] + const img: RawImage = { + data: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]), + width: 2, + height: 1, + } + const rotated = rotate90CW(img) + // 90° CW: new 1x2 - top pixel comes from bottom-left of src (only 1 row, so left=red) + expect(rotated.width).toBe(1) + expect(rotated.height).toBe(2) + // top of rotated should be red (was left in src) + expect(rotated.data[0]).toBe(255) // R + expect(rotated.data[1]).toBe(0) // G + }) +}) + +describe('rotate90CCW', () => { + it('swaps width and height', () => { + const img = createCanvas(4, 2, 0, 0, 0, 255) + const rotated = rotate90CCW(img) + expect(rotated.width).toBe(2) + expect(rotated.height).toBe(4) + }) + + it('is the inverse of rotate90CW', () => { + const img = createCanvas(3, 2, 0, 0, 0, 255) + img.data[0] = 42 // mark top-left + const cw = rotate90CW(img) + const back = rotate90CCW(cw) + expect(back.width).toBe(3) + expect(back.height).toBe(2) + expect(back.data[0]).toBe(42) + }) +}) + +describe('rotate180', () => { + it('preserves dimensions', () => { + const img = createCanvas(3, 4, 0, 0, 0, 255) + const rotated = rotate180(img) + expect(rotated.width).toBe(3) + expect(rotated.height).toBe(4) + }) + + it('flips the pixel order', () => { + const img: RawImage = { + data: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]), + width: 2, + height: 1, + } + const rotated = rotate180(img) + // First pixel should now be green (was last) + expect(rotated.data[0]).toBe(0) + expect(rotated.data[1]).toBe(255) + expect(rotated.data[2]).toBe(0) + // Second pixel should be red + expect(rotated.data[4]).toBe(255) + expect(rotated.data[5]).toBe(0) + }) + + it('is its own inverse', () => { + const img = createCanvas(2, 2, 0, 0, 0, 255) + img.data[0] = 77 + const twice = rotate180(rotate180(img)) + expect(twice.data[0]).toBe(77) + }) +}) + +describe('setOpacity', () => { + it('halves the alpha channel of each pixel', () => { + const img = createCanvas(2, 2, 57, 170, 86, 255) + setOpacity(img, 0.5) + for (let i = 0; i < 4; i++) { + expect(img.data[i * 4 + 3]).toBe(128) + } + }) + + it('sets full opacity to 255', () => { + const img = createCanvas(1, 1, 0, 0, 0, 128) + setOpacity(img, 1) + expect(img.data[3]).toBe(128) // unchanged - 128 * 1 = 128 + }) + + it('sets zero opacity to 0', () => { + const img = createCanvas(1, 1, 0, 0, 0, 200) + setOpacity(img, 0) + expect(img.data[3]).toBe(0) + }) +}) diff --git a/packages/image-comparison-core/src/utils/imageUtils.ts b/packages/image-comparison-core/src/utils/imageUtils.ts new file mode 100644 index 000000000..8a2543c24 --- /dev/null +++ b/packages/image-comparison-core/src/utils/imageUtils.ts @@ -0,0 +1,210 @@ +import { decode, encode } from 'fast-png' +import type { PngDataArray } from 'fast-png' + +export interface RawImage { + data: Uint8Array + width: number + height: number +} + +// Convert any channel count to RGBA 8-bit. fast-png may decode RGB, grayscale, or clamped PNGs. +function toRGBA(data: PngDataArray, channels: number, width: number, height: number): Uint8Array { + if (channels === 4 && (data instanceof Uint8Array || data instanceof Uint8ClampedArray)) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + const pixels = width * height + const out = new Uint8Array(pixels * 4) + for (let i = 0; i < pixels; i++) { + if (channels === 3) { + out[i * 4] = data[i * 3] + out[i * 4 + 1] = data[i * 3 + 1] + out[i * 4 + 2] = data[i * 3 + 2] + out[i * 4 + 3] = 255 + } else if (channels === 1) { + const v = data[i] + out[i * 4] = v + out[i * 4 + 1] = v + out[i * 4 + 2] = v + out[i * 4 + 3] = 255 + } else if (channels === 2) { + const v = data[i * 2] + out[i * 4] = v + out[i * 4 + 1] = v + out[i * 4 + 2] = v + out[i * 4 + 3] = data[i * 2 + 1] + } else { + // 4-channel Uint16Array: downsample 16-bit → 8-bit + out[i * 4] = data[i * 4] >> 8 + out[i * 4 + 1] = data[i * 4 + 1] >> 8 + out[i * 4 + 2] = data[i * 4 + 2] >> 8 + out[i * 4 + 3] = data[i * 4 + 3] >> 8 + } + } + return out +} + +export function decodeImage(buffer: Buffer): RawImage { + const png = decode(buffer) + return { + data: toRGBA(png.data, png.channels, png.width, png.height), + width: png.width, + height: png.height, + } +} + +export function encodeImage(img: RawImage): Buffer { + return Buffer.from(encode({ data: img.data, width: img.width, height: img.height, channels: 4, depth: 8 })) +} + +export function toBase64Png(img: RawImage): string { + return encodeImage(img).toString('base64') +} + +export function createCanvas(width: number, height: number, r = 0, g = 0, b = 0, a = 0): RawImage { + const data = new Uint8Array(width * height * 4) + if (r !== 0 || g !== 0 || b !== 0 || a !== 0) { + for (let i = 0; i < data.length; i += 4) { + data[i] = r + data[i + 1] = g + data[i + 2] = b + data[i + 3] = a + } + } + return { data, width, height } +} + +export function cropImage(img: RawImage, x: number, y: number, w: number, h: number): RawImage { + const data = new Uint8Array(w * h * 4) + const rowBytes = w * 4 + for (let row = 0; row < h; row++) { + const srcOffset = ((y + row) * img.width + x) * 4 + data.set(img.data.subarray(srcOffset, srcOffset + rowBytes), row * rowBytes) + } + return { data, width: w, height: h } +} + +// Porter-Duff "over" compositing. Mutates base in place. +export function compositeImage(base: RawImage, overlay: RawImage, offsetX: number, offsetY: number, opacity = 1): void { + for (let oy = 0; oy < overlay.height; oy++) { + const by = oy + offsetY + if (by < 0 || by >= base.height) { continue } + for (let ox = 0; ox < overlay.width; ox++) { + const bx = ox + offsetX + if (bx < 0 || bx >= base.width) { continue } + + const si = (oy * overlay.width + ox) * 4 + const di = (by * base.width + bx) * 4 + + const srcA = (overlay.data[si + 3] / 255) * opacity + const dstA = base.data[di + 3] / 255 + const outA = srcA + dstA * (1 - srcA) + + if (outA === 0) { continue } + + base.data[di] = Math.round((overlay.data[si] * srcA + base.data[di] * dstA * (1 - srcA)) / outA) + base.data[di + 1] = Math.round((overlay.data[si + 1] * srcA + base.data[di + 1] * dstA * (1 - srcA)) / outA) + base.data[di + 2] = Math.round((overlay.data[si + 2] * srcA + base.data[di + 2] * dstA * (1 - srcA)) / outA) + base.data[di + 3] = Math.round(outA * 255) + } + } +} + +export function resizeBilinear(img: RawImage, newW: number, newH: number): RawImage { + const data = new Uint8Array(newW * newH * 4) + const xRatio = img.width / newW + const yRatio = img.height / newH + + for (let dy = 0; dy < newH; dy++) { + const sy = dy * yRatio + const y0 = Math.floor(sy) + const y1 = Math.min(y0 + 1, img.height - 1) + const yFrac = sy - y0 + + for (let dx = 0; dx < newW; dx++) { + const sx = dx * xRatio + const x0 = Math.floor(sx) + const x1 = Math.min(x0 + 1, img.width - 1) + const xFrac = sx - x0 + + const i00 = (y0 * img.width + x0) * 4 + const i10 = (y0 * img.width + x1) * 4 + const i01 = (y1 * img.width + x0) * 4 + const i11 = (y1 * img.width + x1) * 4 + const di = (dy * newW + dx) * 4 + + const w00 = (1 - xFrac) * (1 - yFrac) + const w10 = xFrac * (1 - yFrac) + const w01 = (1 - xFrac) * yFrac + const w11 = xFrac * yFrac + + data[di] = Math.round(img.data[i00] * w00 + img.data[i10] * w10 + img.data[i01] * w01 + img.data[i11] * w11) + data[di + 1] = Math.round(img.data[i00 + 1] * w00 + img.data[i10 + 1] * w10 + img.data[i01 + 1] * w01 + img.data[i11 + 1] * w11) + data[di + 2] = Math.round(img.data[i00 + 2] * w00 + img.data[i10 + 2] * w10 + img.data[i01 + 2] * w01 + img.data[i11 + 2] * w11) + data[di + 3] = Math.round(img.data[i00 + 3] * w00 + img.data[i10 + 3] * w10 + img.data[i01 + 3] * w01 + img.data[i11 + 3] * w11) + } + } + return { data, width: newW, height: newH } +} + +// 90° clockwise: new width = srcHeight, new height = srcWidth +// dst(dx, dy) ← src(col=dy, row=srcH-1-dx) +export function rotate90CW(img: RawImage): RawImage { + const { width: srcW, height: srcH } = img + const data = new Uint8Array(srcW * srcH * 4) + const newW = srcH + const newH = srcW + + for (let dy = 0; dy < newH; dy++) { + for (let dx = 0; dx < newW; dx++) { + const si = ((srcH - 1 - dx) * srcW + dy) * 4 + const di = (dy * newW + dx) * 4 + data[di] = img.data[si] + data[di + 1] = img.data[si + 1] + data[di + 2] = img.data[si + 2] + data[di + 3] = img.data[si + 3] + } + } + return { data, width: newW, height: newH } +} + +// 90° counter-clockwise: new width = srcHeight, new height = srcWidth +// dst(dx, dy) ← src(col=srcW-1-dy, row=dx) +export function rotate90CCW(img: RawImage): RawImage { + const { width: srcW, height: srcH } = img + const data = new Uint8Array(srcW * srcH * 4) + const newW = srcH + const newH = srcW + + for (let dy = 0; dy < newH; dy++) { + for (let dx = 0; dx < newW; dx++) { + const si = (dx * srcW + (srcW - 1 - dy)) * 4 + const di = (dy * newW + dx) * 4 + data[di] = img.data[si] + data[di + 1] = img.data[si + 1] + data[di + 2] = img.data[si + 2] + data[di + 3] = img.data[si + 3] + } + } + return { data, width: newW, height: newH } +} + +export function rotate180(img: RawImage): RawImage { + const data = new Uint8Array(img.data.length) + const total = img.width * img.height + for (let i = 0; i < total; i++) { + const si = i * 4 + const di = (total - 1 - i) * 4 + data[di] = img.data[si] + data[di + 1] = img.data[si + 1] + data[di + 2] = img.data[si + 2] + data[di + 3] = img.data[si + 3] + } + return { data, width: img.width, height: img.height } +} + +// Multiply every pixel's alpha channel by opacity (0–1). Mutates in place. +export function setOpacity(img: RawImage, opacity: number): void { + for (let i = 3; i < img.data.length; i += 4) { + img.data[i] = Math.round(img.data[i] * opacity) + } +} diff --git a/packages/image-comparison-core/tsconfig.json b/packages/image-comparison-core/tsconfig.json index 25c263166..97ce9e0b4 100644 --- a/packages/image-comparison-core/tsconfig.json +++ b/packages/image-comparison-core/tsconfig.json @@ -2,10 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "allowSyntheticDefaultImports": true, - "allowJs": true, "rootDir": "./src", "outDir": "./dist", "baseUrl": "." }, - "include": ["./src/**/*.ts", "./src/resemble/resemble.jimp.cjs"] + "include": ["./src/**/*.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ba125cba..5cd159b21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,9 +131,12 @@ importers: '@wdio/types': specifier: ^9.27.0 version: 9.27.0 - jimp: - specifier: ^1.6.1 - version: 1.6.1 + fast-png: + specifier: ^8.0.0 + version: 8.0.0 + pixelmatch: + specifier: ^7.2.0 + version: 7.2.0 devDependencies: webdriverio: specifier: ^9.27.0 @@ -3874,6 +3877,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@8.0.0: + resolution: {integrity: sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==} + fast-xml-parser@5.3.0: resolution: {integrity: sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==} hasBin: true @@ -4443,6 +4449,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + iobuffer@6.0.1: + resolution: {integrity: sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -5862,6 +5871,10 @@ packages: resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} hasBin: true + pixelmatch@7.2.0: + resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -12008,6 +12021,11 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@8.0.0: + dependencies: + fflate: 0.8.2 + iobuffer: 6.0.1 + fast-xml-parser@5.3.0: dependencies: strnum: 2.1.1 @@ -12628,6 +12646,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + iobuffer@6.0.1: {} + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -14260,6 +14280,10 @@ snapshots: dependencies: pngjs: 6.0.0 + pixelmatch@7.2.0: + dependencies: + pngjs: 7.0.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 diff --git a/tests/configs/lambdatest.android.emus.web.ts b/tests/configs/lambdatest.android.emus.web.ts index abb310906..c164b7349 100644 --- a/tests/configs/lambdatest.android.emus.web.ts +++ b/tests/configs/lambdatest.android.emus.web.ts @@ -82,6 +82,7 @@ function createCaps({ build: string, w3c: boolean, queueTimeout: number, + idleTimeout: number, }, specs: string[]; 'wdio-ics:options': { @@ -107,6 +108,7 @@ function createCaps({ build, w3c: true, queueTimeout: 900, + idleTimeout: 90, ...(Number(platformVersion) > 14 ? { appiumVersion: '3.0.2' } : {}), }, specs: [mobileSpecs], diff --git a/tests/configs/lambdatest.ios.sims.web.ts b/tests/configs/lambdatest.ios.sims.web.ts index c7657183e..9892d8aef 100644 --- a/tests/configs/lambdatest.ios.sims.web.ts +++ b/tests/configs/lambdatest.ios.sims.web.ts @@ -36,6 +36,7 @@ export function lambdaTestIosSimWeb({ buildName }: { buildName: string }) { deviceOrientation:orientation, w3c: true, queueTimeout: 900, + idleTimeout: 90, }, 'wdio-ics:options': { logName: `${deviceName diff --git a/tests/configs/wdio.lambdatest.shared.conf.ts b/tests/configs/wdio.lambdatest.shared.conf.ts index 3c03ec851..022ccadc0 100644 --- a/tests/configs/wdio.lambdatest.shared.conf.ts +++ b/tests/configs/wdio.lambdatest.shared.conf.ts @@ -7,7 +7,7 @@ export const config: WebdriverIO.Config = { // =================== // Test Configurations // =================== - specFileRetries: 8, + specFileRetries: 4, // Wait for 8 min, then a new session should be created // and the queue should be empty connectionRetryTimeout: 8 * 60 * 1000, @@ -44,7 +44,6 @@ export const config: WebdriverIO.Config = { blockOutSideBar: true, createJsonReportFiles: false, rawMisMatchPercentage: !!process.env.RAW_MISMATCH || false, - enableLayoutTesting: true, ignoreAntialiasing: true, alwaysSaveActualImage: false, } satisfies VisualServiceOptions, diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedFullPage-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedFullPage-chrome-latest-320x658.png deleted file mode 100644 index 316f08469..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedFullPage-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedViewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedViewportScreenshot-chrome-latest-320x658.png index 6e7ef79a5..4ed1e58db 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedViewportScreenshot-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedViewportScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedWdioLogo-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedWdioLogo-chrome-latest-320x658.png index c8162df59..a377bfa81 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedWdioLogo-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/bidiEmulatedWdioLogo-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png index 839cb9bb5..583e467d8 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiLegacyEmulatedFullPage-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiLegacyEmulatedFullPage-chrome-latest-320x658.png index 5e6a10304..d9516f0f3 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/bidiLegacyEmulatedFullPage-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/bidiLegacyEmulatedFullPage-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-aboveThreshold-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-aboveThreshold-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-aboveThreshold-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-addedElement-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-addedElement-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-addedElement-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-edgeOutline-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-edgeOutline-chrome-latest-1366x768.png new file mode 100644 index 000000000..a2b4fc4a3 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-edgeOutline-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-match-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-match-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-match-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-notIgnoredSignificant-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-notIgnoredSignificant-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-notIgnoredSignificant-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-significantAdded-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-significantAdded-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-significantAdded-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-solidFill-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-solidFill-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/diffAnalysis-solidFill-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/examplePage-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/examplePage-chrome-latest-1366x768.png new file mode 100644 index 000000000..88b2d2018 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/examplePage-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/examplePageFail-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/examplePageFail-chrome-latest-1366x768.png index 1e2d5d880..88b2d2018 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/examplePageFail-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/examplePageFail-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-1366x768.png index d22b44489..683302a4f 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-320x658.png deleted file mode 100644 index 5e6a10304..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/fullPage-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png index e450c0e7a..ec5553d05 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png index d22b44489..5ba40a950 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png index 14cd80de0..3f131ab64 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png deleted file mode 100644 index 84c82a271..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/legacyEmulatedViewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/legacyEmulatedViewportScreenshot-chrome-latest-320x658.png index 5a2a2ba89..c67c7dead 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/legacyEmulatedViewportScreenshot-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/legacyEmulatedViewportScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png index fb910775b..b53740589 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png and b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-1366x768.png index 14cd80de0..88b2d2018 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-320x658.png deleted file mode 100644 index 84c82a271..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/noActualStoredOnDiff-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png index 270e99c21..2aa5cb119 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-320x658.png deleted file mode 100644 index e91b26222..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-1366x768.png index 1e2d5d880..88b2d2018 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-320x658.png deleted file mode 100644 index 84c82a271..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/viewportScreenshot-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-1366x768.png index c568d7861..df3427d86 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-320x658.png deleted file mode 100644 index a0bce2d5d..000000000 Binary files a/tests/lambdaTestBaseline/desktop_chrome/wdioLogo-chrome-latest-320x658.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/fullPage-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/fullPage-Firefox_latest-1366x768.png index e242ae27b..e8a8e598b 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/fullPage-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/fullPage-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png index 53c01c60c..cb9d02f6f 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png index 24cc8ad18..2be1fa16b 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png index ea5bf6fa1..476e899e6 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/noActualStoredOnDiff-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/noActualStoredOnDiff-Firefox_latest-1366x768.png index ea5bf6fa1..5d7f04715 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/noActualStoredOnDiff-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/noActualStoredOnDiff-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/tabbable-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/tabbable-Firefox_latest-1366x768.png index 591a4367e..faf9d4d82 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/tabbable-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/tabbable-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/viewportScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/viewportScreenshot-Firefox_latest-1366x768.png index f91fdd925..5d7f04715 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/viewportScreenshot-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/viewportScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/wdioLogo-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/wdioLogo-Firefox_latest-1366x768.png index 47a2e681a..d69849fff 100644 Binary files a/tests/lambdaTestBaseline/desktop_firefox/wdioLogo-Firefox_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_firefox/wdioLogo-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/fullPage-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/fullPage-Microsoft_Edge_latest-1366x768.png index 91c6de583..cc128b7fb 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/fullPage-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/fullPage-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png index 6966c0f13..3b643c999 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png index 6d6b2864d..5ba54d9e1 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png index 235ecf973..99c657271 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/noActualStoredOnDiff-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/noActualStoredOnDiff-Microsoft_Edge_latest-1366x768.png index 4823e5146..74982bea5 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/noActualStoredOnDiff-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/noActualStoredOnDiff-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/tabbable-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/tabbable-Microsoft_Edge_latest-1366x768.png index a7d7c95c4..53c88c75d 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/tabbable-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/tabbable-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/viewportScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/viewportScreenshot-Microsoft_Edge_latest-1366x768.png index 827f084cb..4e8cddc87 100644 Binary files a/tests/lambdaTestBaseline/desktop_microsoftedge/viewportScreenshot-Microsoft_Edge_latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_microsoftedge/viewportScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png index 969b7b077..5b70c4b7f 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png index ea7f6c9d4..c2d18b4be 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png index a13e756df..e5adf6702 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png index 83c01b534..c8e3aea5a 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/noActualStoredOnDiff-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/noActualStoredOnDiff-SafariLatest-1366x768.png index ab304908e..ea1ce3520 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/noActualStoredOnDiff-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/noActualStoredOnDiff-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png index bc0b87c67..e3aeb97b9 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png index 83c01b534..d1adea130 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png index f4a0c2cb7..8b8eb9ecb 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png index f6ad62ebc..6d9e05e98 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png index f27bd6d81..177be72b4 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png index 3b544822c..467650642 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png index 6bb25ab7b..781ac41c1 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png index f6ad62ebc..38a4bb73d 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png index ddabb8a9e..6273757cf 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png index 8f9f1ab76..7c89ed068 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png index c71ff93fe..6273757cf 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png index 8f9f1ab76..7c89ed068 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png index c93206191..6273757cf 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png index d7d4cd5f3..736f8ecc9 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxPortrait18-430x932.png index 56df7dafa..a79d0a3c6 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png index 1c5603cdb..e793891fc 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png index c38539ed0..7cc2735cc 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png index d7d4cd5f3..ee4d1b9c2 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png index 56df7dafa..ff292345b 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png index 7d0388dd9..90a49c3fd 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png index 6dd6dbff8..7114d31d1 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png index 6205bb847..017c5405d 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png index 6dd6dbff8..7114d31d1 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png index c61923049..90a49c3fd 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxLandscape26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxLandscape26-440x956.png index 275cd365f..6c8a3206a 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxLandscape26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxLandscape26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxPortrait26-440x956.png index a28cf72f8..9ef1d491e 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/fullPage-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxLandscape26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxLandscape26-440x956.png index 708ed630e..048170f43 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxLandscape26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxLandscape26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxPortrait26-440x956.png index bc363cb07..3d865b184 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsElementScreenshot-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxLandscape26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxLandscape26-440x956.png index 275cd365f..ee66172ee 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxLandscape26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxLandscape26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxPortrait26-440x956.png index a28cf72f8..7c20c70fa 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsFullPageScreenshot-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsScreenshot-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsScreenshot-Iphone17ProMaxPortrait26-440x956.png index 885f305fe..8ae407814 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsScreenshot-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/ignoredElementsScreenshot-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxLandscape26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxLandscape26-440x956.png index 748283f90..61df4d48c 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxLandscape26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxLandscape26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxPortrait26-440x956.png index 73158fa50..8ae407814 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-landscape-Iphone17ProMaxPortrait26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-landscape-Iphone17ProMaxPortrait26-440x956.png index 748283f90..61df4d48c 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-landscape-Iphone17ProMaxPortrait26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-landscape-Iphone17ProMaxPortrait26-440x956.png differ diff --git a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-portrait-Iphone17ProMaxLandscape26-440x956.png b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-portrait-Iphone17ProMaxLandscape26-440x956.png index 73158fa50..8ae407814 100644 Binary files a/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-portrait-Iphone17ProMaxLandscape26-440x956.png and b/tests/lambdaTestBaseline/iphone_17_pro_max/screenshot-portrait-Iphone17ProMaxLandscape26-440x956.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png deleted file mode 100644 index 293a7c0ee..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png deleted file mode 100644 index daae30736..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png deleted file mode 100644 index 9e85b325c..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png deleted file mode 100644 index 9cbe3e426..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png deleted file mode 100644 index 05c6e1bd6..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png deleted file mode 100644 index 8bb3174ed..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png deleted file mode 100644 index f12b1e8dd..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png index f487d34bb..6398ed656 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png index f487d34bb..6398ed656 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png deleted file mode 100644 index 2eae258b2..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 51b26411f..40d4fcecb 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png index 51b26411f..40d4fcecb 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png index 65c2a4971..cc119f80b 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png index 7443fa53d..91a5bc2b9 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png index 7443fa53d..91a5bc2b9 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png index c47fb0782..3fb43430a 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 384fedfad..7dd5194ea 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png index 384fedfad..7dd5194ea 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png deleted file mode 100644 index f12b1e8dd..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png index f487d34bb..916aaa50f 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png index f487d34bb..916aaa50f 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png deleted file mode 100644 index 2eae258b2..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 51b26411f..42f594f82 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png index 51b26411f..42f594f82 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png deleted file mode 100644 index bd1ce2a29..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index f0ebf8d8b..a9d9aaec4 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png index 7b0146b41..f83ec356b 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png deleted file mode 100644 index b700d8aed..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png index 34714f955..23b0c8fc6 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png index 86fe352c7..f8fda0082 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot16-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png deleted file mode 100644 index 175337ea2..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 6d711d742..d7bab1808 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png index ca9a92cd1..a8e5a328c 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot16-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png deleted file mode 100644 index 1de1f0d7a..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png index 5f9d27f16..23b0c8fc6 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot16-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot16-952x427.png index 86fe352c7..38db3762f 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot16-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot16-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png deleted file mode 100644 index f2acc63fe..000000000 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png index 788e6d567..a9d9aaec4 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot16-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot16-427x952.png index 7b0146b41..f83ec356b 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot16-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot16-427x952.png differ diff --git a/tests/specs/basics.spec.ts b/tests/specs/basics.spec.ts index 50ff8f80a..6d815f8db 100644 --- a/tests/specs/basics.spec.ts +++ b/tests/specs/basics.spec.ts @@ -2,6 +2,8 @@ import { join } from 'node:path' import { browser, expect } from '@wdio/globals' import { fileExists } from '../helpers/fileExists.ts' +const isBaselineSetup = process.env.BASELINE_SETUP === 'true' + describe('@wdio/visual-service basics', () => { beforeEach(async () => { await browser.url('') @@ -47,12 +49,14 @@ describe('@wdio/visual-service basics', () => { describe('check methods', () => { it('should fail comparing with a baseline', async () => { - const tag = 'examplePageFail' + if (!isBaselineSetup) { + await browser.execute( + 'arguments[0].innerHTML = "Test Demo Page";', + await $('.hero__subtitle') + ) + } - await browser.execute( - 'arguments[0].innerHTML = "Test Demo Page";', - await $('.hero__subtitle') - ) + const tag = 'examplePageFail' await expect(await browser.checkScreen(tag, { enableLayoutTesting: false })).toBeGreaterThan(0) }) diff --git a/tests/specs/desktop.bidi.emulated.spec.ts b/tests/specs/desktop.bidi.emulated.spec.ts index 51aaa6d07..3da809f57 100644 --- a/tests/specs/desktop.bidi.emulated.spec.ts +++ b/tests/specs/desktop.bidi.emulated.spec.ts @@ -40,7 +40,7 @@ describe('@wdio/visual-service desktop bidi emulated', () => { ], // Some padding to make sure that we cover the element, // with BiDi we sometimes miss the element due to internal calculations - ignoreRegionPadding: 2, + ignoreRegionPadding: 4, // Don't comment this out, it's needed to hide the navbar hideElements: [await $('nav.navbar')] } diff --git a/tests/specs/desktop.ocr.spec.ts b/tests/specs/desktop.ocr.spec.ts index a162a78f9..20dc321e5 100644 --- a/tests/specs/desktop.ocr.spec.ts +++ b/tests/specs/desktop.ocr.spec.ts @@ -63,6 +63,7 @@ describe('@wdio/visual-service:ocr desktop', () => { text: 'WebdriverIO?', relativePosition: { left: 150, + above: 10, } }) @@ -70,8 +71,11 @@ describe('@wdio/visual-service:ocr desktop', () => { }) it(`should click on a button based on text inside of a haystack of coordinates on ${environment}`, async function () { + // Scope the haystack to the single "Why WebdriverIO?" button. The full button + // row is a very wide, short strip which defeats Tesseract's automatic page + // segmentation, so we crop tightly around the button instead. await driver.ocrClickOnText({ - haystack: { height: 44, width: 1108, x: 129, y: 590 }, + haystack: { height: 44, width: 200, x: 470, y: 590 }, text: 'WebdriverIO?', // With the default contrast of 0.25, the text is not found contrast: 0.5, diff --git a/tests/specs/desktop.spec.ts b/tests/specs/desktop.spec.ts index 54208a4f8..1a6ce16f1 100644 --- a/tests/specs/desktop.spec.ts +++ b/tests/specs/desktop.spec.ts @@ -2,6 +2,8 @@ import type { ImageCompareResult } from '@wdio/image-comparison-core' import { browser, expect } from '@wdio/globals' import { fileExists } from '../helpers/fileExists.ts' +const isBaselineSetup = process.env.BASELINE_SETUP === 'true' + describe('@wdio/visual-service desktop', () => { // @TODO // @ts-ignore @@ -24,24 +26,24 @@ describe('@wdio/visual-service desktop', () => { it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { await $('.features_vqN4').scrollIntoView() - // When running a new set of images then first comment out block 1 and 2. Then run the test. - // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. - // If so, then uncomment block 2 and check if pass with the same arguments. - // Block 1 - await browser.execute(() => { - document.querySelectorAll('.feature_G9wp h3').forEach(heading => { - (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + // Block 1 introduces visual differences to verify ignore regions. Skipped when BASELINE_SETUP=true. + if (!isBaselineSetup) { + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) }) - }) + } await expect($('.features_vqN4')).toMatchElementSnapshot( 'ignoredElementsElementScreenshot', { - // Block 2 - ignore: [ - await $$('.feature_G9wp h3'), - ], - // Don't comment this out, it's needed to hide the navbar + // Block 2 ignores the modified regions. Skipped when BASELINE_SETUP=true. + ...(!isBaselineSetup ? { + ignore: [ + await $$('.feature_G9wp h3'), + ], + } : {}), hideElements: [await $('nav.navbar')] } ) @@ -52,24 +54,25 @@ describe('@wdio/visual-service desktop', () => { }) it(`should compare a viewport screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { - // When running a new set of images then first comment out block 1 and 2. Then run the test. - // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. - // If so, then uncomment block 2 and check if pass with the same arguments. - // Block 1 - await browser.execute(() => { - document.querySelectorAll('.navbar__items--right a.navbar__item, .feature_G9wp').forEach(link => { - (link as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + // Block 1 introduces visual differences to verify ignore regions. Skipped when BASELINE_SETUP=true. + if (!isBaselineSetup) { + await browser.execute(() => { + document.querySelectorAll('.navbar__items--right a.navbar__item, .feature_G9wp').forEach(link => { + (link as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) }) - }) + } await expect(browser).toMatchScreenSnapshot( 'ignoredElementsViewportScreenshot', { - // Block 2 - ignore: [ - await $$('.navbar__items--right a.navbar__item'), - await $$('.feature_G9wp'), - ], + // Block 2 ignores the modified regions. Skipped when BASELINE_SETUP=true. + ...(!isBaselineSetup ? { + ignore: [ + await $$('.navbar__items--right a.navbar__item'), + await $$('.feature_G9wp'), + ], + } : {}), } ) }) @@ -84,25 +87,26 @@ describe('@wdio/visual-service desktop', () => { }) it(`should compare a full page screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { - // When running a new set of images then first comment out block 1 and 2. Then run the test. - // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. - // If so, then uncomment block 2 and check if pass with the same arguments. - // Block 1 - await browser.execute(() => { - document.querySelectorAll('.feature_G9wp h3').forEach(heading => { - (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + // Block 1 introduces visual differences to verify ignore regions. Skipped when BASELINE_SETUP=true. + if (!isBaselineSetup) { + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) }) - }) + } await expect(browser).toMatchFullPageSnapshot('ignoredElementsFullPageScreenshot', { fullPageScrollTimeout: 1500, hideAfterFirstScroll: [ await $('nav.navbar'), ], - // // Block 2 - ignore: [ - await $$('.feature_G9wp h3'), - ], + // Block 2 ignores the modified regions. Skipped when BASELINE_SETUP=true. + ...(!isBaselineSetup ? { + ignore: [ + await $$('.feature_G9wp h3'), + ], + } : {}), }) }) @@ -117,19 +121,24 @@ describe('@wdio/visual-service desktop', () => { it(`should not store an actual image for '${browserName}' when the diff is below the threshold (#1115)`, async function () { const tag = 'noActualStoredOnDiff' - await browser.execute(() => { - const el = document.createElement('div') - el.id = 'test-diff-element' - el.style.cssText = 'position:fixed;top:10px;left:10px;width:500px;height:500px;background:red;z-index:9999;' - document.body.appendChild(el) - }) + // Introduces a small diff below the threshold. Skipped when BASELINE_SETUP=true. + if (!isBaselineSetup) { + await browser.execute(() => { + const el = document.createElement('div') + el.id = 'test-diff-element' + el.style.cssText = 'position:fixed;top:10px;left:10px;width:500px;height:500px;background:red;z-index:9999;' + document.body.appendChild(el) + }) + } const result = await browser.checkScreen(tag, { returnAllCompareData: true, }) as ImageCompareResult - expect(result.misMatchPercentage).toBeGreaterThan(0) - expect(result.misMatchPercentage).toBeLessThanOrEqual(70) - expect(fileExists(result.folders.actual)).toBe(false) + if (!isBaselineSetup) { + expect(result.misMatchPercentage).toBeGreaterThan(0) + expect(result.misMatchPercentage).toBeLessThanOrEqual(70) + expect(fileExists(result.folders.actual)).toBe(false) + } }) })