Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ build

# folders
.tmp
.pixelmatch-tmp/
/__snapshots__/
.idea/
localBaseline/
Expand Down
1 change: 1 addition & 0 deletions packages/image-comparison-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"dependencies": {
"jimp": "^1.6.1",
"pixelmatch": "^7.2.0",
"@wdio/logger": "^9.18.0",
"@wdio/types": "^9.27.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`BaseClass > initializes default options correctly 1`] = `
"blockOutSideBar": true,
"blockOutStatusBar": true,
"blockOutToolBar": true,
"compareEngine": "resemble",
"createJsonReportFiles": false,
"diffPixelBoundingBoxProximity": 5,
"ignoreAlpha": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ exports[`options > defaultOptions > should return the default options when no op
"blockOutSideBar": true,
"blockOutStatusBar": true,
"blockOutToolBar": true,
"compareEngine": "resemble",
"createJsonReportFiles": false,
"diffPixelBoundingBoxProximity": 5,
"ignoreAlpha": false,
Expand Down Expand Up @@ -143,6 +144,7 @@ exports[`options > defaultOptions > should return the provided options when opti
"blockOutSideBar": true,
"blockOutStatusBar": true,
"blockOutToolBar": true,
"compareEngine": "resemble",
"createJsonReportFiles": true,
"diffPixelBoundingBoxProximity": 123,
"ignoreAlpha": true,
Expand Down
1 change: 1 addition & 0 deletions packages/image-comparison-core/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const DEFAULT_COMPARE_OPTIONS = {
blockOutSideBar: true,
blockOutStatusBar: true,
blockOutToolBar: true,
compareEngine: 'resemble' as const,
createJsonReportFiles: false,
diffPixelBoundingBoxProximity: 5,
ignoreAlpha: false,
Expand Down
16 changes: 16 additions & 0 deletions packages/image-comparison-core/src/helpers/options.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ export interface ClassOptions {
*/
scaleImagesToSameSize?: boolean;

/**
* The image comparison engine to use.
* 'pixelmatch' is a beta alternative that uses the YIQ color space and is
* more robust against font rendering noise and anti-aliasing differences.
* Prefer setting this inside compareOptions.
* @default 'resemble'
*/
compareEngine?: 'resemble' | 'pixelmatch';

/**
* Options object passed to the underlying image comparison engine.
*/
Expand Down Expand Up @@ -482,5 +491,12 @@ export interface CompareOptions {
* Scale images to the same size before comparing them.
*/
scaleImagesToSameSize: boolean;
/**
* The image comparison engine to use.
* 'pixelmatch' is a beta alternative that uses the YIQ color space and is
* more robust against font rendering noise and anti-aliasing differences.
* @default 'resemble'
*/
compareEngine?: 'resemble' | 'pixelmatch';
}

4 changes: 3 additions & 1 deletion packages/image-comparison-core/src/helpers/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ export function defaultOptions(options: ClassOptions): DefaultOptions {
* Compare options (merged sequentially):
* 1. Default options (fallback)
* 2. Root compareOptions (deprecated but supported)
* 3. User-provided compareOptions
* 3. Root-level compareEngine shorthand (convenience alias)
* 4. User-provided compareOptions (highest precedence)
*/
compareOptions: {
...DEFAULT_COMPARE_OPTIONS,
...logAllDeprecatedCompareOptions(options),
...(options.compareEngine ? { compareEngine: options.compareEngine } : {}),
...options.compareOptions,
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('createCompareReport', () => {
const createMockData = (misMatchPercentage = 0): CompareData => ({
misMatchPercentage,
rawMisMatchPercentage: misMatchPercentage,
getBuffer: () => Buffer.from(''),
getBuffer: () => Promise.resolve(Buffer.from('')),
diffBounds: { top: 0, left: 0, bottom: 0, right: 0 },
analysisTime: 0,
diffPixels: [],
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('createJsonReportIfNeeded', () => {
const createMockData = (misMatchPercentage = 0): CompareData => ({
misMatchPercentage,
rawMisMatchPercentage: misMatchPercentage,
getBuffer: () => Buffer.from(''),
getBuffer: () => Promise.resolve(Buffer.from('')),
diffBounds: { top: 0, left: 0, bottom: 0, right: 0 },
analysisTime: 0,
diffPixels: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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')

Expand Down Expand Up @@ -82,6 +83,9 @@ vi.mock('./createCompareReport.js', () => ({
vi.mock('../resemble/compareImages.js', () => ({
default: vi.fn()
}))
vi.mock('../pixelmatch/compareImages.js', () => ({
default: vi.fn()
}))
vi.mock('../helpers/constants.js', () => ({
DEFAULT_RESIZE_DIMENSIONS: { top: 0, right: 0, bottom: 0, left: 0 }
}))
Expand Down Expand Up @@ -217,14 +221,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,
getBuffer: vi.fn().mockResolvedValue(Buffer.from('diff-image-data')),
diffBounds: { left: 0, top: 0, right: 100, bottom: 200 },
analysisTime: 100,
diffPixels: []
})
}
vi.mocked(compareImages.default).mockResolvedValue(mockCompareData)
vi.mocked(compareImagesPixelmatch.default).mockResolvedValue(mockCompareData)
vi.mocked(images.checkBaselineImageExists).mockResolvedValue(undefined)
vi.mocked(images.removeDiffImageIfExists).mockResolvedValue(undefined)
vi.mocked(images.saveBase64Image).mockResolvedValue(undefined)
Expand Down Expand Up @@ -268,6 +274,29 @@ describe('executeImageCompare', () => {
)
})

it('should use the pixelmatch adapter when compareEngine is pixelmatch', async () => {
const pixelmatchOptions = {
...mockOptions,
compareOptions: {
...mockOptions.compareOptions,
wic: {
...mockOptions.compareOptions.wic,
compareEngine: 'pixelmatch' as const
}
}
}

await executeImageCompare({
isViewPortScreenshot: true,
isNativeContext: false,
options: pixelmatchOptions,
testContext: mockTestContext
})

expect(compareImagesPixelmatch.default).toHaveBeenCalledTimes(1)
expect(compareImages.default).not.toHaveBeenCalled()
})

it('should handle mobile context with status/address/toolbar rectangles', async () => {
const mobileOptions = {
...mockOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export interface WicImageCompareOptions extends BaseImageCompareOptions, BaseMob
* the higher the number the more pixels will be grouped, the lower the number the less pixels will be grouped due to accuracy.
* Default is 5 pixels */
diffPixelBoundingBoxProximity: number;
/** The image comparison engine to use.
* @default 'resemble'
*/
compareEngine?: 'resemble' | 'pixelmatch';
}

export interface DefaultImageCompareCompareOptions extends MethodImageCompareCompareOptions {
Expand Down
8 changes: 6 additions & 2 deletions packages/image-comparison-core/src/methods/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { readFileSync, writeFileSync, promises as fsPromises, constants } from '
import { dirname, join } from 'node:path'
import { Jimp, JimpMime } from 'jimp'
import logger from '@wdio/logger'
import compareImages from '../resemble/compareImages.js'
import compareImagesResemble 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'
Expand Down Expand Up @@ -446,7 +447,10 @@ export async function executeImageCompare(
}

// 5. Execute the compare and retrieve the data
const data: CompareData = await compareImages(readFileSync(baselineFilePath), actualImageBuffer, compareOptions)
const engine = imageCompareOptions.compareEngine === 'pixelmatch' ? 'pixelmatch' : 'resemble'
log.info(`Using comparison engine: ${engine}`)
const engineFn = engine === 'pixelmatch' ? compareImagesPixelmatch : compareImagesResemble
const data: CompareData = await engineFn(readFileSync(baselineFilePath), actualImageBuffer, compareOptions)
const rawMisMatchPercentage = data.rawMisMatchPercentage
const reportMisMatchPercentage = imageCompareOptions.rawMisMatchPercentage
? rawMisMatchPercentage
Expand Down
8 changes: 6 additions & 2 deletions packages/image-comparison-core/src/methods/rectangles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,10 +505,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)
Expand Down
Loading
Loading