diff --git a/.changeset/fix-ignore-options-parity.md b/.changeset/fix-ignore-options-parity.md new file mode 100644 index 000000000..7ff2daf9c --- /dev/null +++ b/.changeset/fix-ignore-options-parity.md @@ -0,0 +1,38 @@ +--- +"@wdio/image-comparison-core": patch +"@wdio/visual-service": patch +--- + +fix: ignore* option parity with resemble (pixelmatch) + +After v10 switched to pixelmatch, the public `ignore*` API did not fully match resemble.js preset behaviour. Combined modes such as `ignoreLess` with the default `ignoreAntialiasing: true` still inherited AA forgiveness, and `ignoreColors` used BT.601 grayscale instead of resemble brightness-only comparison. + +**What changed** + +- Multiple `ignore*` flags now follow resemble last-wins ordering (`ignoreAlpha` → `ignoreAntialiasing` → `ignoreColors` → `ignoreLess` → `ignoreNothing`) instead of composing independently +- `ignoreLess`, `ignoreAlpha`, `ignoreColors`, and `ignoreNothing` now apply their own threshold and AA rules when active — they no longer inherit default `ignoreAntialiasing: true` forgiveness +- `ignoreColors` now compares brightness only using resemble luma weights (`0.3/0.59/0.11`), matching resemble v9 behaviour +- Added golden fixture parity tests for all ignore modes +- JSDoc and README document preset mapping, last-wins semantics, and default vs resemble v9 +- Logs a WDIO warning when multiple `ignore*` flags are enabled, naming which option wins + +**Preset reference** + +| Active preset | threshold | AA forgiven | +|---|---|---| +| `ignoreNothing` | 0 | no | +| `ignoreLess` | ~16/255 | no | +| `ignoreColors` | ~16/255 | no (brightness only) | +| `ignoreAlpha` | ~16/255 | no | +| `ignoreAntialiasing` (default) | ~32/255 | yes | + +**Migration** + +- No action needed if you use a single ignore flag or rely on defaults (`ignoreAntialiasing: true`), behaviour is unchanged for typical users +- Set `ignoreAntialiasing: false` when you need strict comparison where anti-aliased pixels count as differences +- Multi-flag combos now match resemble v9 last-wins behaviour; review tests if you combine ignore flags +- `ignoreColors` results may differ slightly from v10 but align with resemble v9 + +### Committers: 1 + +- Wim Selles ([@wswebcreation](https://github.com/wswebcreation)) diff --git a/packages/image-comparison-core/README.md b/packages/image-comparison-core/README.md index eec49436e..2d5e48fd2 100644 --- a/packages/image-comparison-core/README.md +++ b/packages/image-comparison-core/README.md @@ -10,3 +10,40 @@ npm install @wdio/image-comparison-core --save-dev ``` Instructions on how to get started can be found in the [visual testing](https://webdriver.io/docs/visual-testing) docs on the WebdriverIO project page. + +## `ignore*` comparison options (pixelmatch) + +v10 uses [pixelmatch](https://github.com/mapbox/pixelmatch) instead of resemble.js. The public `ignore*` API is preserved and mapped to resemble-style presets via last-wins semantics. + +### Defaults vs resemble v9 + +| | v9 (resemble.js) | v10 default | +|---|---|---| +| AA forgiveness | opt-in (`ignoreAntialiasing: true`) | on by default (`ignoreAntialiasing: true`) | +| Strict comparison | default | set `ignoreAntialiasing: false` | +| Engine | resemble RGB/brightness | pixelmatch YIQ perceptual distance | + +No config change is needed if you rely on forgiving comparison behaviour. + +### Preset mapping + +| Option | Preprocessing | pixelmatch threshold | AA forgiven | +|---|---|---|---| +| *(none, `ignoreAntialiasing: false`)* | — | ~16/255 (`0.063`) | no | +| `ignoreAntialiasing` | — | ~32/255 (`0.13`) | yes | +| `ignoreLess` | — | ~16/255 (`0.063`) | no | +| `ignoreAlpha` | alpha → opaque | ~16/255 (`0.063`) | no | +| `ignoreColors` | resemble luma grayscale | ~16/255 (`0.063`) | no | +| `ignoreNothing` | — | `0` | no | + +Thresholds are calibrated to resemble outcomes; the underlying algorithm is YIQ perceptual distance, not resemble's RGB math. + +### Last-wins semantics + +When multiple `ignore*` flags are enabled, the active preset is the **last** one in this order (matching resemble.js): + +`alpha` → `antialiasing` → `colors` → `less` → `nothing` + +Example: `ignoreLess: true` with the default `ignoreAntialiasing: true` resolves to the `ignoreLess` preset — strict AA, not forgiving. + +Golden fixture tests documenting expected pass/fail behaviour live in [`tests/fixtures/ignore-options/`](./tests/fixtures/ignore-options/). diff --git a/packages/image-comparison-core/src/base.interfaces.ts b/packages/image-comparison-core/src/base.interfaces.ts index 64175acd3..b6b16c4df 100644 --- a/packages/image-comparison-core/src/base.interfaces.ts +++ b/packages/image-comparison-core/src/base.interfaces.ts @@ -90,27 +90,36 @@ export interface BaseMobileWebScreenshotOptions { export interface BaseImageCompareOptions { /** - * Compare images and discard alpha + * Ignore alpha-channel differences during comparison. + * Preprocessing sets all alpha values to opaque before pixelmatch runs. + * Preset: strict threshold (~16/255), AA not forgiven. * @default false */ ignoreAlpha?: boolean; /** - * Compare images and forgive anti-aliasing differences + * Forgive anti-aliased pixels during comparison (pixelmatch `includeAA: false`). + * Preset: relaxed threshold (~32/255), AA forgiven. + * When combined with other ignore flags, last-wins order applies + * (`alpha` → `antialiasing` → `colors` → `less` → `nothing`). * @default true */ ignoreAntialiasing?: boolean; /** - * Compare images in black and white mode + * Compare brightness only, ignoring hue differences. + * Preprocessing converts both images to grayscale using resemble luma (`0.3/0.59/0.11`). + * Preset: strict threshold (~16/255), AA not forgiven. * @default false */ ignoreColors?: boolean; /** - * Compare with reduced color sensitivity + * Use a relaxed RGB tolerance (~16/255 per channel in YIQ space). + * Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness). * @default false */ ignoreLess?: boolean; /** - * Compare with maximum sensitivity + * Use zero tolerance — any pixel difference counts as a mismatch. + * Preset: threshold `0`, AA not forgiven. * @default false */ ignoreNothing?: boolean; diff --git a/packages/image-comparison-core/src/helpers/options.interfaces.ts b/packages/image-comparison-core/src/helpers/options.interfaces.ts index f39514617..65ecf5be2 100644 --- a/packages/image-comparison-core/src/helpers/options.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/options.interfaces.ts @@ -159,31 +159,38 @@ export interface ClassOptions { diffPixelBoundingBoxProximity?: number; /** - * Ignore alpha channel when comparing images. + * Ignore alpha-channel differences during comparison. + * Preprocessing sets all alpha values to opaque before pixelmatch runs. + * Preset: strict threshold (~16/255), AA not forgiven. */ ignoreAlpha?: boolean; /** - * Forgive anti-aliasing differences when comparing images. + * Forgive anti-aliased pixels during comparison (pixelmatch `includeAA: false`). + * Preset: relaxed threshold (~32/255), AA forgiven. + * When combined with other ignore flags, last-wins order applies + * (`alpha` → `antialiasing` → `colors` → `less` → `nothing`). * Defaults to `true` so sub-pixel rendering noise is ignored out of the box. - * Set to `false` for strict pixel comparison where AA pixels count as mismatches. + * Set to `false` for strict comparison where AA pixels count as mismatches. */ ignoreAntialiasing?: boolean; /** - * Compare two images in black and white only. + * Compare brightness only, ignoring hue differences. + * Preprocessing converts both images to grayscale using resemble luma (`0.3/0.59/0.11`). + * Preset: strict threshold (~16/255), AA not forgiven. */ ignoreColors?: boolean; /** - * Compare images with reduced sensitivity. - * red = 16, green = 16, blue = 16, alpha = 16, minBrightness = 16, maxBrightness = 240 + * Use a relaxed RGB tolerance (~16/255 per channel in YIQ space). + * Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness). */ ignoreLess?: boolean; /** - * Compare images with full sensitivity. - * red = 0, green = 0, blue = 0, alpha = 0, minBrightness = 0, maxBrightness = 255 + * Use zero tolerance — any pixel difference counts as a mismatch. + * Preset: threshold `0`, AA not forgiven. */ ignoreNothing?: boolean; @@ -437,29 +444,36 @@ export interface CompareOptions { diffPixelBoundingBoxProximity: number; /** - * Compare images and discard the alpha channel. + * Ignore alpha-channel differences during comparison. + * Preprocessing sets all alpha values to opaque before pixelmatch runs. + * Preset: strict threshold (~16/255), AA not forgiven. */ ignoreAlpha: boolean; /** - * Forgive anti-aliasing differences when comparing images. + * Forgive anti-aliased pixels during comparison (pixelmatch `includeAA: false`). + * Preset: relaxed threshold (~32/255), AA forgiven. + * When combined with other ignore flags, last-wins order applies + * (`alpha` → `antialiasing` → `colors` → `less` → `nothing`). */ ignoreAntialiasing: boolean; /** - * Compare two black-and-white versions of the images, ignoring colors. + * Compare brightness only, ignoring hue differences. + * Preprocessing converts both images to grayscale using resemble luma (`0.3/0.59/0.11`). + * Preset: strict threshold (~16/255), AA not forgiven. */ ignoreColors: boolean; /** - * Use a less sensitive comparison setting: - * red = 16, green = 16, blue = 16, alpha = 16, minBrightness = 16, maxBrightness = 240 + * Use a relaxed RGB tolerance (~16/255 per channel in YIQ space). + * Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness). */ ignoreLess: boolean; /** - * Use the most sensitive comparison setting: - * red = 0, green = 0, blue = 0, alpha = 0, minBrightness = 0, maxBrightness = 255 + * Use zero tolerance — any pixel difference counts as a mismatch. + * Preset: threshold `0`, AA not forgiven. */ ignoreNothing: boolean; diff --git a/packages/image-comparison-core/src/helpers/options.test.ts b/packages/image-comparison-core/src/helpers/options.test.ts index d1977f6ed..44d076446 100644 --- a/packages/image-comparison-core/src/helpers/options.test.ts +++ b/packages/image-comparison-core/src/helpers/options.test.ts @@ -1,11 +1,20 @@ -import { describe, it, expect } from 'vitest' -import { defaultOptions, methodCompareOptions, screenMethodCompareOptions, createBeforeScreenshotOptions, buildAfterScreenshotOptions, prepareIgnoreOptions } from './options.js' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { join } from 'node:path' +import logger from '@wdio/logger' +import { defaultOptions, methodCompareOptions, screenMethodCompareOptions, createBeforeScreenshotOptions, buildAfterScreenshotOptions, prepareIgnoreOptions, resolveActiveIgnorePreset, resolveComparePreset, warnOnMultipleIgnorePresets } from './options.js' import type { ClassOptions } from './options.interfaces.js' import type { ScreenMethodImageCompareCompareOptions } from '../methods/images.interfaces.js' import type { InstanceData } from '../methods/instanceData.interfaces.js' import type { BeforeScreenshotResult } from './beforeScreenshot.interfaces.js' +const log = logger('test') + +vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))) + describe('options', () => { + beforeEach(() => { + vi.clearAllMocks() + }) describe('defaultOptions', () => { it('should return the default options when no options are provided', () => { expect(defaultOptions({})).toMatchSnapshot() @@ -490,4 +499,60 @@ describe('options', () => { })).toEqual(['alpha', 'antialiasing', 'less']) }) }) + + describe('resolveActiveIgnorePreset', () => { + it.each([ + [['alpha'], 'alpha'], + [['antialiasing'], 'antialiasing'], + [['colors'], 'colors'], + [['less'], 'less'], + [['nothing'], 'nothing'], + [[], null], + ] as const)('returns %s for ignore list %j', (ignoreList, expected) => { + expect(resolveActiveIgnorePreset([...ignoreList])).toBe(expected) + }) + + it('uses last-wins order matching prepareIgnoreOptions', () => { + expect(resolveActiveIgnorePreset(['antialiasing', 'less'])).toBe('less') + expect(resolveActiveIgnorePreset(['alpha', 'antialiasing', 'colors', 'less', 'nothing'])).toBe('nothing') + }) + }) + + describe('resolveComparePreset', () => { + it.each([ + [['nothing'], { threshold: 0, includeAA: true }], + [['less'], { threshold: 0.063, includeAA: true }], + [['antialiasing'], { threshold: 0.13, includeAA: false }], + [['alpha'], { threshold: 0.063, includeAA: true }], + [['colors'], { threshold: 0.063, includeAA: true }], + [[], { threshold: 0.063, includeAA: true }], + ] as const)('maps ignore list %j to pixelmatch settings', (ignoreList, expected) => { + expect(resolveComparePreset([...ignoreList])).toEqual(expected) + }) + + it('applies last-wins preset for multi-flag lists', () => { + expect(resolveComparePreset(['antialiasing', 'less'])).toEqual({ + threshold: 0.063, + includeAA: true, + }) + }) + }) + + describe('warnOnMultipleIgnorePresets', () => { + it('does not warn when a single ignore preset is enabled', () => { + warnOnMultipleIgnorePresets(['antialiasing']) + + expect(log.warn).not.toHaveBeenCalled() + }) + + it('warns with the active preset when multiple ignore flags are enabled', () => { + warnOnMultipleIgnorePresets(['antialiasing', 'less']) + + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining('Multiple ignore* compare options are enabled'), + 'ignoreAntialiasing, ignoreLess', + 'ignoreLess', + ) + }) + }) }) diff --git a/packages/image-comparison-core/src/helpers/options.ts b/packages/image-comparison-core/src/helpers/options.ts index 83cf20f6f..03e8eea62 100644 --- a/packages/image-comparison-core/src/helpers/options.ts +++ b/packages/image-comparison-core/src/helpers/options.ts @@ -12,6 +12,7 @@ import type { BeforeScreenshotOptions, BeforeScreenshotResult } from './beforeSc import type { AfterScreenshotOptions } from './afterScreenshot.interfaces.js' import type { InstanceData } from '../methods/instanceData.interfaces.js' import type { ComparisonIgnoreOption } from '../pixelmatch/compare.interfaces.js' +import logger from '@wdio/logger' import { logAllDeprecatedCompareOptions, isStorybook, @@ -20,6 +21,8 @@ import { getMethodOrWicOption, } from './utils.js' +const log = logger('@wdio/visual-service:@wdio/image-comparison-core:options') + /** * Determine the default options by merging user options with sensible defaults */ @@ -264,13 +267,85 @@ export function buildAfterScreenshotOptions({ return afterOptions } -export function prepareIgnoreOptions(imageCompareOptions: MethodImageCompareCompareOptions): ComparisonIgnoreOption[] { - const ignoreDefaults: ComparisonIgnoreOption[] = ['alpha', 'antialiasing', 'colors', 'less', 'nothing'] +/** Resemble last-wins order, must match `prepareIgnoreOptions` filter order. */ +export const IGNORE_PRESET_ORDER: ComparisonIgnoreOption[] = ['alpha', 'antialiasing', 'colors', 'less', 'nothing'] + +const IGNORE_PRESET_OPTION_NAMES: Record = { + alpha: 'ignoreAlpha', + antialiasing: 'ignoreAntialiasing', + colors: 'ignoreColors', + less: 'ignoreLess', + nothing: 'ignoreNothing', +} + +const COMPARE_PRESET_SETTINGS: Record = { + nothing: { threshold: 0, includeAA: true }, + less: { threshold: 0.063, includeAA: true }, + antialiasing: { + // Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance (~0.13 YIQ). + threshold: 0.13, + includeAA: false, + }, + alpha: { threshold: 0.063, includeAA: true }, + colors: { threshold: 0.063, includeAA: true }, +} + +/** + * Returns the active resemble preset from an ignore list using last-wins semantics. + */ +export function resolveActiveIgnorePreset(ignoreList: ComparisonIgnoreOption[]): ComparisonIgnoreOption | null { + let activePreset: ComparisonIgnoreOption | null = null + + for (const preset of IGNORE_PRESET_ORDER) { + if (ignoreList.includes(preset)) { + activePreset = preset + } + } + + return activePreset +} - return ignoreDefaults.filter((option) => +/** + * Maps an ignore list to pixelmatch threshold and AA settings via resemble last-wins presets. + * An empty list yields the strict preset (e.g. `ignoreAntialiasing: false` with no other flags). + */ +export function resolveComparePreset(ignoreList: ComparisonIgnoreOption[]): { threshold: number; includeAA: boolean } { + const activePreset = resolveActiveIgnorePreset(ignoreList) + + if (activePreset) { + return COMPARE_PRESET_SETTINGS[activePreset] + } + + // Default strict tolerance: 16/255 per channel (~6.3% of max YIQ distance). + return { threshold: 0.063, includeAA: true } +} + +export function prepareIgnoreOptions(imageCompareOptions: MethodImageCompareCompareOptions): ComparisonIgnoreOption[] { + return IGNORE_PRESET_ORDER.filter((option) => Object.keys(imageCompareOptions).find( (key: keyof typeof imageCompareOptions) => key.toLowerCase().includes(option) && imageCompareOptions[key], ), ) } +/** + * Logs a warning when multiple ignore* flags are enabled. Only the last preset in + * resemble order is applied for threshold and AA settings. + */ +export function warnOnMultipleIgnorePresets(ignoreList: ComparisonIgnoreOption[]): void { + if (ignoreList.length < 2) { + return + } + + const activePreset = resolveActiveIgnorePreset(ignoreList) + const enabledOptions = ignoreList.map((preset) => IGNORE_PRESET_OPTION_NAMES[preset]).join(', ') + const activeOption = activePreset ? IGNORE_PRESET_OPTION_NAMES[activePreset] : 'none' + + log.warn( + 'Multiple ignore* compare options are enabled (%s). Only one preset is applied per comparison.\n' + + ' Active: %s (last-wins order: ignoreAlpha → ignoreAntialiasing → ignoreColors → ignoreLess → ignoreNothing).', + enabledOptions, + activeOption, + ) +} + 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 d7eb777ce..c3ac2fcb8 100644 --- a/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts +++ b/packages/image-comparison-core/src/methods/images.executeImageCompare.test.ts @@ -925,6 +925,69 @@ describe('executeImageCompare', () => { ) }) + it('should resolve ignoreLess over default antialiasing via last-wins preset order', async () => { + const { resolveComparePreset } = await import('../helpers/options.js') + const optionsWithIgnoreLess = { + ...mockOptions, + compareOptions: { + ...mockOptions.compareOptions, + method: { + ignoreAntialiasing: true, + ignoreLess: true, + } + } + } + + await executeImageCompare({ + isViewPortScreenshot: true, + isNativeContext: false, + options: optionsWithIgnoreLess, + testContext: mockTestContext + }) + + expect(compareImagesPixelmatch.default).toHaveBeenCalledWith( + expect.any(Buffer), + expect.any(Buffer), + { + ignore: ['antialiasing', 'less'], + scaleToSameSize: true + } + ) + expect(resolveComparePreset(['antialiasing', 'less'])).toEqual({ + threshold: 0.063, + includeAA: true, + }) + }) + + it('should warn when multiple ignore options are enabled', async () => { + const optionsWithIgnoreLess = { + ...mockOptions, + compareOptions: { + ...mockOptions.compareOptions, + wic: { + ...mockOptions.compareOptions.wic, + ignoreAntialiasing: true, + }, + method: { + ignoreLess: true, + } + } + } + + await executeImageCompare({ + isViewPortScreenshot: true, + isNativeContext: false, + options: optionsWithIgnoreLess, + testContext: mockTestContext + }) + + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining('Multiple ignore* compare options are enabled'), + 'ignoreAntialiasing, ignoreLess', + 'ignoreLess', + ) + }) + it('should handle ignore options from compareOptions', async () => { const optionsWithIgnore = { ...mockOptions, diff --git a/packages/image-comparison-core/src/methods/images.ts b/packages/image-comparison-core/src/methods/images.ts index 0af8e99fe..17fdb8815 100644 --- a/packages/image-comparison-core/src/methods/images.ts +++ b/packages/image-comparison-core/src/methods/images.ts @@ -5,7 +5,7 @@ import { decodeImage, toBase64Png, createCanvas, cropImage, compositeImage, setO import logger from '@wdio/logger' import compareImagesPixelmatch from '../pixelmatch/compareImages.js' import { calculateDprData, getIosBezelImageNames, getBase64ScreenshotSize, prepareComparisonFilePaths, updateVisualBaseline } from '../helpers/utils.js' -import { prepareIgnoreOptions } from '../helpers/options.js' +import { prepareIgnoreOptions, warnOnMultipleIgnorePresets } from '../helpers/options.js' import { DEFAULT_RESIZE_DIMENSIONS, supportedIosBezelDevices } from '../helpers/constants.js' import { isWdioElement, prepareIgnoreRectangles } from './rectangles.js' import type { @@ -416,6 +416,7 @@ export async function executeImageCompare( // 4. Prepare the compare // 4a.Determine the ignore options const ignore = prepareIgnoreOptions(imageCompareOptions) + warnOnMultipleIgnorePresets(ignore) // 4b. Determine the ignore rectangles for the block outs const { ignoredBoxes } = await prepareIgnoreRectangles({ diff --git a/packages/image-comparison-core/src/pixelmatch/compareBrightness.test.ts b/packages/image-comparison-core/src/pixelmatch/compareBrightness.test.ts new file mode 100644 index 000000000..50c5f97c5 --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compareBrightness.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest' +import { toResembleBrightness, applyResembleGrayscale } from './compareBrightness.js' + +describe('compareBrightness', () => { + describe('toResembleBrightness', () => { + it('uses resemble coefficients 0.3/0.59/0.11', () => { + expect(toResembleBrightness(180, 60, 60)).toBe(96) + expect(toResembleBrightness(96, 96, 96)).toBe(96) + expect(toResembleBrightness(85, 0, 0)).toBe(26) + }) + + it('differs from BT.601 weights for some RGB values', () => { + // BT.601 would round 0.299 * 85 to 25; resemble uses 0.3 → 26 + expect(toResembleBrightness(85, 0, 0)).not.toBe(Math.round(0.299 * 85 + 0.587 * 0 + 0.114 * 0)) + }) + }) + + describe('applyResembleGrayscale', () => { + it('writes equal R/G/B channels using resemble luma', () => { + const pixels = Buffer.from([180, 60, 60, 255]) + applyResembleGrayscale(pixels, 1) + + expect(pixels[0]).toBe(96) + expect(pixels[1]).toBe(96) + expect(pixels[2]).toBe(96) + expect(pixels[3]).toBe(255) + }) + }) +}) diff --git a/packages/image-comparison-core/src/pixelmatch/compareBrightness.ts b/packages/image-comparison-core/src/pixelmatch/compareBrightness.ts new file mode 100644 index 000000000..69f2f2619 --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compareBrightness.ts @@ -0,0 +1,16 @@ +/** Resemble.js ITU-R BT.601 luma weights used for ignoreColors brightness comparison. */ +export const RESEMBLE_LUMA_WEIGHTS = { r: 0.3, g: 0.59, b: 0.11 } as const + +export function toResembleBrightness(r: number, g: number, b: number): number { + const { r: redWeight, g: greenWeight, b: blueWeight } = RESEMBLE_LUMA_WEIGHTS + return Math.round(redWeight * r + greenWeight * g + blueWeight * b) +} + +export function applyResembleGrayscale(pixels: Buffer, totalPixels: number): void { + for (let i = 0; i < totalPixels * 4; i += 4) { + const luma = toResembleBrightness(pixels[i], pixels[i + 1], pixels[i + 2]) + pixels[i] = luma + pixels[i + 1] = luma + pixels[i + 2] = luma + } +} diff --git a/packages/image-comparison-core/src/pixelmatch/compareImages.ignoreColors.test.ts b/packages/image-comparison-core/src/pixelmatch/compareImages.ignoreColors.test.ts new file mode 100644 index 000000000..c9e4c7bc1 --- /dev/null +++ b/packages/image-comparison-core/src/pixelmatch/compareImages.ignoreColors.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import compareImages from './compareImages.js' +import { createCanvas, encodeImage } from '../utils/imageUtils.js' + +function solidColorImage(r: number, g: number, b: number): Buffer { + return encodeImage(createCanvas(1, 1, r, g, b, 255)) +} + +describe('compareImages ignoreColors parity', () => { + it('passes when hue changes but resemble brightness matches', async () => { + const baseline = solidColorImage(180, 60, 60) + const actual = solidColorImage(96, 96, 96) + + const result = await compareImages(baseline, actual, { ignore: 'colors' }) + + expect(result.rawMisMatchPercentage).toBe(0) + expect(result.diffPixels).toHaveLength(0) + }) + + it('fails when brightness differs under ignoreColors', async () => { + const baseline = solidColorImage(96, 96, 96) + const actual = solidColorImage(120, 120, 120) + + const result = await compareImages(baseline, actual, { ignore: 'colors' }) + + expect(result.rawMisMatchPercentage).toBeGreaterThan(0) + expect(result.diffPixels.length).toBeGreaterThan(0) + }) + + it('fails for a color-only diff without ignoreColors', async () => { + const baseline = solidColorImage(180, 60, 60) + const actual = solidColorImage(96, 96, 96) + + const result = await compareImages(baseline, actual, {}) + + expect(result.rawMisMatchPercentage).toBeGreaterThan(0) + }) +}) diff --git a/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts b/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts index 562a88127..bfd98ce85 100644 --- a/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts +++ b/packages/image-comparison-core/src/pixelmatch/compareImages.test.ts @@ -169,22 +169,28 @@ describe('pixelmatch adapter - compareImages', () => { }) describe('ignore option mapping', () => { - it('passes threshold=0 and includeAA=true for ignore: nothing', async () => { + it.each([ + ['nothing', { threshold: 0, includeAA: true }], + ['less', { threshold: 0.063, includeAA: true }], + ['antialiasing', { threshold: 0.13, includeAA: false }], + ['alpha', { threshold: 0.063, includeAA: true }], + ['colors', { threshold: 0.063, includeAA: true }], + ] as const)('passes preset settings for ignore: %s', async (ignore, expected) => { pixelmatchFn.mockImplementation(() => 0) - await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'nothing' }) + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore }) expect(pixelmatchFn).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), expect.any(Number), expect.any(Number), - expect.objectContaining({ threshold: 0, includeAA: true }) + expect.objectContaining(expected) ) }) - it('passes threshold=0.063 and includeAA=true for ignore: less', async () => { + it('passes threshold=0.063 and includeAA=true when no ignore option is given', async () => { pixelmatchFn.mockImplementation(() => 0) - await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'less' }) + await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) expect(pixelmatchFn).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -193,22 +199,12 @@ describe('pixelmatch adapter - compareImages', () => { ) }) - 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.063 and includeAA=true when no ignore option is given', async () => { + it('uses last-wins preset when ignoreLess follows ignoreAntialiasing', async () => { pixelmatchFn.mockImplementation(() => 0) - await compareImages(Buffer.from('img1'), Buffer.from('img2'), {}) + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { + ignore: ['antialiasing', 'less'] + }) expect(pixelmatchFn).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -217,24 +213,26 @@ describe('pixelmatch adapter - compareImages', () => { ) }) - it('accepts ignore as an array and uses less threshold with antialiasing forgiveness', async () => { + it('uses last-wins preset when ignoreNothing follows other flags', async () => { pixelmatchFn.mockImplementation(() => 0) await compareImages(Buffer.from('img1'), Buffer.from('img2'), { - ignore: ['antialiasing', 'less'] + ignore: ['antialiasing', 'less', 'nothing'] }) expect(pixelmatchFn).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), expect.any(Number), expect.any(Number), - expect.objectContaining({ threshold: 0.063, includeAA: false }) + expect.objectContaining({ threshold: 0, includeAA: true }) ) }) - it('passes threshold=0.063 and includeAA=true for ignore: alpha without antialiasing', async () => { + it('uses last-wins preset when ignoreLess follows ignoreAlpha', async () => { pixelmatchFn.mockImplementation(() => 0) - await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore: 'alpha' }) + await compareImages(Buffer.from('img1'), Buffer.from('img2'), { + ignore: ['alpha', 'less'] + }) expect(pixelmatchFn).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -245,7 +243,12 @@ describe('pixelmatch adapter - compareImages', () => { }) describe('pixel transformations', () => { - it('grayscales both pixel arrays when ignore includes colors', async () => { + it('grayscales both pixel arrays with resemble luma when ignore includes colors', async () => { + decodeImageFn.mockReturnValue({ + data: Uint8Array.from([180, 60, 60, 255]), + width: 1, + height: 1, + }) let capturedPixels1: Uint8Array | undefined pixelmatchFn.mockImplementation((img1: Uint8Array) => { @@ -255,9 +258,9 @@ describe('pixelmatch adapter - compareImages', () => { 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]) + expect(capturedPixels1![0]).toBe(96) + expect(capturedPixels1![1]).toBe(96) + expect(capturedPixels1![2]).toBe(96) }) it('sets all alpha channels to 255 when ignore includes alpha', async () => { diff --git a/packages/image-comparison-core/src/pixelmatch/compareImages.ts b/packages/image-comparison-core/src/pixelmatch/compareImages.ts index 1a515faf5..9ac061809 100644 --- a/packages/image-comparison-core/src/pixelmatch/compareImages.ts +++ b/packages/image-comparison-core/src/pixelmatch/compareImages.ts @@ -1,4 +1,6 @@ import pixelmatch from 'pixelmatch' +import { resolveComparePreset } from '../helpers/options.js' +import { applyResembleGrayscale } from './compareBrightness.js' import { decodeImage, resizeBilinear, encodeImage, type RawImage } from '../utils/imageUtils.js' import type { CompareData, ComparisonOptions, ComparisonIgnoreOption } from './compare.interfaces.js' @@ -10,37 +12,6 @@ function resolveIgnoreList(ignore: ComparisonOptions['ignore']): ComparisonIgnor return Array.isArray(ignore) ? ignore : [ignore] } -function toPixelmatchOptions(ignoreList: ComparisonIgnoreOption[]): { threshold: number; includeAA: boolean } { - if (ignoreList.includes('nothing')) { - return { threshold: 0, includeAA: true } - } - - const forgivesAA = ignoreList.includes('antialiasing') - const threshold = ignoreList.includes('less') - ? 0.063 - : forgivesAA - // Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance which - // corresponds to ~0.13 in YIQ perceptual distance. - ? 0.13 - // Default strict tolerance: 16/255 per channel maps to ~6.3% of max YIQ distance. - : 0.063 - - return { - threshold, - // pixelmatch includeAA=true disables AA forgiveness; false enables it. - includeAA: !forgivesAA, - } -} - -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 @@ -118,8 +89,8 @@ export default async function compareImages( const ignoreList = resolveIgnoreList(options.ignore) if (ignoreList.includes('colors')) { - grayscalePixels(pixels1, totalPixels) - grayscalePixels(pixels2, totalPixels) + applyResembleGrayscale(pixels1, totalPixels) + applyResembleGrayscale(pixels2, totalPixels) } if (ignoreList.includes('alpha')) { @@ -133,7 +104,7 @@ export default async function compareImages( zeroIgnoredBoxes(pixels2, width, ignoredBoxes) } - const { threshold, includeAA } = toPixelmatchOptions(ignoreList) + const { threshold, includeAA } = resolveComparePreset(ignoreList) const outputPixels = new Uint8Array(totalPixels * 4) // Use magenta [255, 0, 255] for both diff and AA pixels. diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/README.md b/packages/image-comparison-core/tests/fixtures/ignore-options/README.md new file mode 100644 index 000000000..9d37141de --- /dev/null +++ b/packages/image-comparison-core/tests/fixtures/ignore-options/README.md @@ -0,0 +1,17 @@ +# ignore-options golden fixtures + +PNG pairs documenting expected pixelmatch outcomes for each `ignore*` mode after v10 parity work. + +| Fixture pair | Scenario | Expected pass | Expected fail | +|---|---|---|---| +| `aa-edge-noise-*` | Vertical edge with a single AA column (`170,170,170`) vs hard black/white boundary | `ignoreAntialiasing` | strict (no ignore flags) | +| `font-size-plus-one-*` | 10×12 vs 11×12 black glyph block on white | — | all ignore modes | +| `color-only-diff-*` | `(180,60,60)` vs `(96,96,96)` — same resemble luma | `ignoreColors` | strict | +| `alpha-only-diff-*` | Identical RGB; center pixel alpha `255` vs `120` | `ignoreAlpha` | strict | +| `within-rgb-tolerance-*` | `(100,100,100)` vs `(114,100,100)` — 14/255 channel delta | `ignoreLess` | `ignoreNothing` | + +Regenerate PNGs by running the calibration script in this folder (requires `pnpm --filter @wdio/image-comparison-core build` first): + +```bash +node packages/image-comparison-core/tests/fixtures/ignore-options/calibrate.mjs +``` diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-actual.png b/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-actual.png new file mode 100644 index 000000000..236b793cf Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-actual.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-baseline.png b/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-baseline.png new file mode 100644 index 000000000..4d42b5da8 Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/aa-edge-noise-baseline.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-actual.png b/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-actual.png new file mode 100644 index 000000000..4706d517e Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-actual.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-baseline.png b/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-baseline.png new file mode 100644 index 000000000..794061ea5 Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/alpha-only-diff-baseline.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/calibrate.mjs b/packages/image-comparison-core/tests/fixtures/ignore-options/calibrate.mjs new file mode 100644 index 000000000..fe2b3ae3e --- /dev/null +++ b/packages/image-comparison-core/tests/fixtures/ignore-options/calibrate.mjs @@ -0,0 +1,94 @@ +import { writeFileSync, mkdirSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const root = join(__dirname, '../../..') +const { encodeImage, createCanvas } = await import(join(root, 'dist/utils/imageUtils.js')) +const compareImages = (await import(join(root, 'dist/pixelmatch/compareImages.js'))).default + +function setPixel(data, width, x, y, r, g, b, a = 255) { + const i = (y * width + x) * 4 + data[i] = r + data[i + 1] = g + data[i + 2] = b + data[i + 3] = a +} + +function fillRect(data, width, x, y, w, h, r, g, b, a = 255) { + for (let py = y; py < y + h; py++) { + for (let px = x; px < x + w; px++) { + setPixel(data, width, px, py, r, g, b, a) + } + } +} + +function buildFixtures() { + const colorBaseline = createCanvas(16, 16, 180, 60, 60, 255) + const colorActual = createCanvas(16, 16, 96, 96, 96, 255) + + const alphaBaseline = createCanvas(16, 16, 80, 120, 160, 255) + const alphaActual = createCanvas(16, 16, 80, 120, 160, 255) + setPixel(alphaActual.data, 16, 8, 8, 80, 120, 160, 120) + + const rgbBaseline = createCanvas(16, 16, 100, 100, 100, 255) + const rgbActual = createCanvas(16, 16, 114, 100, 100, 255) + + const fontBaseline = createCanvas(32, 32, 255, 255, 255, 255) + fillRect(fontBaseline.data, 32, 8, 10, 10, 12, 0, 0, 0) + const fontActual = createCanvas(32, 32, 255, 255, 255, 255) + fillRect(fontActual.data, 32, 8, 10, 11, 12, 0, 0, 0) + + const aaBaseline = createCanvas(40, 40, 255, 255, 255, 255) + fillRect(aaBaseline.data, 40, 0, 0, 20, 40, 0, 0, 0) + const aaActual = createCanvas(40, 40, 255, 255, 255, 255) + fillRect(aaActual.data, 40, 0, 0, 20, 40, 0, 0, 0) + for (let y = 0; y < 40; y++) { + setPixel(aaActual.data, 40, 20, y, 170, 170, 170) + } + + return { + 'color-only-diff-baseline.png': encodeImage(colorBaseline), + 'color-only-diff-actual.png': encodeImage(colorActual), + 'alpha-only-diff-baseline.png': encodeImage(alphaBaseline), + 'alpha-only-diff-actual.png': encodeImage(alphaActual), + 'within-rgb-tolerance-baseline.png': encodeImage(rgbBaseline), + 'within-rgb-tolerance-actual.png': encodeImage(rgbActual), + 'font-size-plus-one-baseline.png': encodeImage(fontBaseline), + 'font-size-plus-one-actual.png': encodeImage(fontActual), + 'aa-edge-noise-baseline.png': encodeImage(aaBaseline), + 'aa-edge-noise-actual.png': encodeImage(aaActual), + } +} + +async function runCase(name, baseline, actual, ignore) { + const result = await compareImages(baseline, actual, { ignore }) + const pass = result.rawMisMatchPercentage === 0 + console.log(`${name} ignore=${JSON.stringify(ignore ?? [])} -> ${result.rawMisMatchPercentage.toFixed(4)}% ${pass ? 'PASS' : 'FAIL'}`) +} + +mkdirSync(__dirname, { recursive: true }) +const fixtures = buildFixtures() +for (const [file, buffer] of Object.entries(fixtures)) { + writeFileSync(join(__dirname, file), buffer) +} + +console.log('--- color-only-diff ---') +await runCase('default', fixtures['color-only-diff-baseline.png'], fixtures['color-only-diff-actual.png']) +await runCase('ignoreColors', fixtures['color-only-diff-baseline.png'], fixtures['color-only-diff-actual.png'], 'colors') + +console.log('--- alpha-only-diff ---') +await runCase('default', fixtures['alpha-only-diff-baseline.png'], fixtures['alpha-only-diff-actual.png']) +await runCase('ignoreAlpha', fixtures['alpha-only-diff-baseline.png'], fixtures['alpha-only-diff-actual.png'], 'alpha') + +console.log('--- within rgb ---') +await runCase('ignoreLess', fixtures['within-rgb-tolerance-baseline.png'], fixtures['within-rgb-tolerance-actual.png'], 'less') +await runCase('ignoreNothing', fixtures['within-rgb-tolerance-baseline.png'], fixtures['within-rgb-tolerance-actual.png'], 'nothing') + +console.log('--- font size ---') +await runCase('default', fixtures['font-size-plus-one-baseline.png'], fixtures['font-size-plus-one-actual.png']) +await runCase('ignoreAntialiasing', fixtures['font-size-plus-one-baseline.png'], fixtures['font-size-plus-one-actual.png'], 'antialiasing') + +console.log('--- aa edge ---') +await runCase('strict', fixtures['aa-edge-noise-baseline.png'], fixtures['aa-edge-noise-actual.png']) +await runCase('ignoreAntialiasing', fixtures['aa-edge-noise-baseline.png'], fixtures['aa-edge-noise-actual.png'], 'antialiasing') diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-actual.png b/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-actual.png new file mode 100644 index 000000000..c944ccc8d Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-actual.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-baseline.png b/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-baseline.png new file mode 100644 index 000000000..990f26a2d Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/color-only-diff-baseline.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-actual.png b/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-actual.png new file mode 100644 index 000000000..147aeb486 Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-actual.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-baseline.png b/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-baseline.png new file mode 100644 index 000000000..eb9f59a22 Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/font-size-plus-one-baseline.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-actual.png b/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-actual.png new file mode 100644 index 000000000..ee97d02ee Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-actual.png differ diff --git a/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-baseline.png b/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-baseline.png new file mode 100644 index 000000000..7fefeaa0b Binary files /dev/null and b/packages/image-comparison-core/tests/fixtures/ignore-options/within-rgb-tolerance-baseline.png differ diff --git a/packages/image-comparison-core/tests/ignore-options.parity.test.ts b/packages/image-comparison-core/tests/ignore-options.parity.test.ts new file mode 100644 index 000000000..70d687a56 --- /dev/null +++ b/packages/image-comparison-core/tests/ignore-options.parity.test.ts @@ -0,0 +1,98 @@ +import { readFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, it, expect } from 'vitest' +import compareImages from '../src/pixelmatch/compareImages.js' +import type { ComparisonIgnoreOption } from '../src/pixelmatch/compare.interfaces.js' + +const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/ignore-options') + +function loadFixture(name: string): Buffer { + return readFileSync(join(fixturesDir, name)) +} + +async function compareFixturePair( + baselineName: string, + actualName: string, + ignore?: ComparisonIgnoreOption | ComparisonIgnoreOption[], +) { + return compareImages(loadFixture(baselineName), loadFixture(actualName), { ignore }) +} + +function expectPass(result: Awaited>) { + expect(result.rawMisMatchPercentage).toBe(0) + expect(result.misMatchPercentage).toBe(0) +} + +function expectFail(result: Awaited>) { + expect(result.rawMisMatchPercentage).toBeGreaterThan(0) +} + +describe('ignore-options golden fixture parity', () => { + describe('color-only diff', () => { + const baseline = 'color-only-diff-baseline.png' + const actual = 'color-only-diff-actual.png' + + it('fails with strict comparison (no ignore flags)', async () => { + expectFail(await compareFixturePair(baseline, actual)) + }) + + it('passes with ignoreColors', async () => { + expectPass(await compareFixturePair(baseline, actual, 'colors')) + }) + }) + + describe('alpha-only diff', () => { + const baseline = 'alpha-only-diff-baseline.png' + const actual = 'alpha-only-diff-actual.png' + + it('fails with strict comparison (no ignore flags)', async () => { + expectFail(await compareFixturePair(baseline, actual)) + }) + + it('passes with ignoreAlpha', async () => { + expectPass(await compareFixturePair(baseline, actual, 'alpha')) + }) + }) + + describe('within 16/255 RGB tolerance', () => { + const baseline = 'within-rgb-tolerance-baseline.png' + const actual = 'within-rgb-tolerance-actual.png' + + it('passes with ignoreLess', async () => { + expectPass(await compareFixturePair(baseline, actual, 'less')) + }) + + it('fails with ignoreNothing', async () => { + expectFail(await compareFixturePair(baseline, actual, 'nothing')) + }) + }) + + describe('font size +1px', () => { + const baseline = 'font-size-plus-one-baseline.png' + const actual = 'font-size-plus-one-actual.png' + + it.each([ + 'nothing', + 'less', + 'antialiasing', + 'alpha', + 'colors', + ] as const)('fails with ignore: %s', async (ignore) => { + expectFail(await compareFixturePair(baseline, actual, ignore)) + }) + }) + + describe('sub-pixel AA edge noise', () => { + const baseline = 'aa-edge-noise-baseline.png' + const actual = 'aa-edge-noise-actual.png' + + it('fails with strict comparison (no ignore flags)', async () => { + expectFail(await compareFixturePair(baseline, actual)) + }) + + it('passes with ignoreAntialiasing', async () => { + expectPass(await compareFixturePair(baseline, actual, 'antialiasing')) + }) + }) +}) diff --git a/tests/configs/wdio.lambdatest.shared.conf.ts b/tests/configs/wdio.lambdatest.shared.conf.ts index 022ccadc0..884798b11 100644 --- a/tests/configs/wdio.lambdatest.shared.conf.ts +++ b/tests/configs/wdio.lambdatest.shared.conf.ts @@ -44,7 +44,6 @@ export const config: WebdriverIO.Config = { blockOutSideBar: true, createJsonReportFiles: false, rawMisMatchPercentage: !!process.env.RAW_MISMATCH || false, - ignoreAntialiasing: true, alwaysSaveActualImage: false, } satisfies VisualServiceOptions, ], diff --git a/tests/configs/wdio.saucelabs.shared.conf.ts b/tests/configs/wdio.saucelabs.shared.conf.ts index a8b88e8f2..c757f7c7d 100644 --- a/tests/configs/wdio.saucelabs.shared.conf.ts +++ b/tests/configs/wdio.saucelabs.shared.conf.ts @@ -49,7 +49,6 @@ export const config: WebdriverIO.Config = { createJsonReportFiles: true, rawMisMatchPercentage: !!process.env.RAW_MISMATCH || false, enableLayoutTesting: true, - ignoreAntialiasing: true, } satisfies VisualServiceOptions, ], ],