Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .changeset/chore-dependency-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@wdio/visual-service": patch
"@wdio/visual-reporter": patch
---

chore: dependency updates

Updated dependencies to their latest compatible versions:

- `@wdio/visual-service`: `expect-webdriverio` to `^5.7.0`
- `@wdio/visual-reporter`: `sharp` to `^0.35.3`
- Dev tooling: `@typescript-eslint/*` to `^8.63.0`, `vitest` to `^3.2.7`, `eslint` to `^9.39.5`, plus minor bumps for `postcss`, `react-icons`, and `isbot` in the reporter package

No functional or API changes.

### Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))
83 changes: 83 additions & 0 deletions .changeset/fix-ignore-options-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
"@wdio/image-comparison-core": minor
"@wdio/visual-service": minor
---

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.

This release also adds `compareOptions.pixelmatch` so you can pass pixelmatch settings directly instead of using `ignore*` presets.

**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
- WDIO logs a warning when multiple `ignore*` flags are enabled, naming which preset wins
- New `compareOptions.pixelmatch` object for direct pixelmatch control (`threshold`, `includeAA`, `diffColor`, `aaColor`, `diffColorAlt`, `alpha`, `diffMask`, `checkerboard`)

**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 |

**Using `compareOptions.pixelmatch`**

Set it in your service config or on a single `check*` call. Do not put `ignore*` keys and `pixelmatch` on the same options object; that throws, even when an `ignore*` flag is `false`. Service config and method options are separate objects, so a method call can override the service compare mode for that check (a warning is logged when the mode switches).

Service config:

```js
// wdio.conf.js
services: [
['visual', {
compareOptions: {
pixelmatch: {
threshold: 0.063,
includeAA: true,
},
},
}],
]
```

Method override when the service uses `ignore*` presets:

```js
await browser.checkScreen('homepage', {
pixelmatch: { threshold: 0.05 },
})
```

Method override when the service uses `pixelmatch`:

```js
await browser.checkScreen('homepage', {
ignoreLess: true,
})
```

Invalid (throws):

```js
compareOptions: {
ignoreLess: false,
pixelmatch: { threshold: 0.063 },
}
```

See [pixelmatch](https://github.com/mapbox/pixelmatch) for option details.

**What you need to do**

- No change needed if you use a single `ignore*` flag or rely on defaults (`ignoreAntialiasing: true`)
- Set `ignoreAntialiasing: false` when anti-aliased pixels should count as differences
- If you combine multiple `ignore*` flags, review your tests; last-wins ordering now matches resemble v9
- If you use `ignoreColors`, results may differ slightly from early v10 but align with resemble v9
- To tune pixelmatch directly, add `compareOptions.pixelmatch` in your service config or pass `pixelmatch` on individual `check*` calls
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@
"@types/jsdom": "~21.1.7",
"@types/node": "^24",
"@types/xml2js": "~0.4.14",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@wdio/globals": "^9.29.1",
"@wdio/mocha-framework": "^9.29.1",
"@typescript-eslint/parser": "^8.62.0",
"@typescript-eslint/utils": "^8.62.0",
"@vitest/coverage-v8": "^3.2.6",
"@vitest/ui": "^3.2.6",
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/utils": "^8.63.0",
"@vitest/coverage-v8": "^3.2.7",
"@vitest/ui": "^3.2.7",
"@wdio/appium-service": "^9.29.1",
"@wdio/browserstack-service": "^9.29.1",
"@wdio/cli": "^9.29.1",
Expand All @@ -75,7 +75,7 @@
"@wdio/spec-reporter": "^9.29.1",
"@wdio/types": "^9.29.1",
"cross-env": "^7.0.3",
"eslint": "^9.39.4",
"eslint": "^9.39.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-wdio": "^9.29.1",
Expand All @@ -87,7 +87,7 @@
"saucelabs": "^9.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.6",
"vitest": "^3.2.7",
"webdriverio": "^9.29.1",
"wdio-lambdatest-service": "^4.0.1"
},
Expand Down
39 changes: 39 additions & 0 deletions packages/image-comparison-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,42 @@ 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/).

Comparison uses [pixelmatch](https://github.com/mapbox/pixelmatch).
26 changes: 21 additions & 5 deletions packages/image-comparison-core/src/base.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PixelmatchCompareOptions } from './pixelmatch/compare.interfaces.js'

export interface Folders {
/** The actual folder where the current screenshots need to be saved */
actualFolder: string;
Expand Down Expand Up @@ -90,27 +92,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;
Expand All @@ -134,6 +145,11 @@ export interface BaseImageCompareOptions {
* @default false
*/
scaleImagesToSameSize?: boolean;
/**
* Direct pixelmatch comparison settings.
* Mutually exclusive with all `ignore*` options on the same object.
*/
pixelmatch?: PixelmatchCompareOptions;
}

export interface BaseMobileBlockOutOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`options > assertExclusiveCompareMode > ignores empty pixelmatch object 1`] = `false`;

exports[`options > assertExclusiveCompareMode > throws with a descriptive error message 1`] = `
[CompareOptionsConflictError: Cannot combine ignore* options with pixelmatch options.
Present ignore keys: ignoreAntialiasing, ignoreLess (values are ignored).
Use either ignore* (preset mode) or pixelmatch (direct mode), not both.
Context: checkScreen]
`;

exports[`options > buildAfterScreenshotOptions > should build options for native commands (no enriched data) 1`] = `
{
"actualFolder": "/test/actual",
Expand Down Expand Up @@ -187,6 +196,22 @@ exports[`options > defaultOptions > should return the provided options when opti
}
`;

exports[`options > defaultOptions pixelmatch mode > omits ignore* defaults when pixelmatch mode is configured 1`] = `
{
"blockOutSideBar": true,
"blockOutStatusBar": true,
"blockOutToolBar": true,
"createJsonReportFiles": false,
"diffPixelBoundingBoxProximity": 5,
"pixelmatch": {
"threshold": 0.063,
},
"rawMisMatchPercentage": false,
"returnAllCompareData": false,
"scaleImagesToSameSize": false,
}
`;

exports[`options > methodCompareOptions > should not return the method options when no options are provided 1`] = `{}`;

exports[`options > methodCompareOptions > should return the provided options when options are provided 1`] = `
Expand All @@ -211,6 +236,73 @@ exports[`options > methodCompareOptions > should return the provided options whe
}
`;

exports[`options > resolveEffectiveCompareOptions > strips ignore* when method sets pixelmatch 1`] = `
{
"blockOutSideBar": true,
"pixelmatch": {
"threshold": 0.05,
},
"scaleImagesToSameSize": false,
}
`;

exports[`options > resolveEffectiveCompareOptions > strips pixelmatch when method sets ignore* 1`] = `
{
"ignoreLess": true,
"scaleImagesToSameSize": true,
}
`;

exports[`options > resolvePixelmatchOptions > applies defaults for unset fields 1`] = `
{
"aaColor": [
255,
0,
255,
],
"alpha": 0.1,
"checkerboard": true,
"diffColor": [
255,
0,
255,
],
"diffColorAlt": [
255,
0,
255,
],
"diffMask": false,
"includeAA": false,
"threshold": 0.1,
}
`;

exports[`options > resolvePixelmatchOptions > preserves user overrides 1`] = `
{
"aaColor": [
255,
0,
255,
],
"alpha": 0.1,
"checkerboard": true,
"diffColor": [
255,
0,
0,
],
"diffColorAlt": [
255,
0,
255,
],
"diffMask": true,
"includeAA": false,
"threshold": 0.05,
}
`;

exports[`options > screenMethodCompareOptions > should not return the screen method options when no options are provided 1`] = `{}`;

exports[`options > screenMethodCompareOptions > should return the provided options when options are provided 1`] = `
Expand Down
13 changes: 13 additions & 0 deletions packages/image-comparison-core/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ import type { IosOffsets } from './constants.interfaces.js'
import type { ResizeDimensions } from '../methods/images.interfaces.js'
import type { TestContext } from 'src/methods/compareReport.interfaces.js'
import type { DeviceRectangles } from '../methods/rectangles.interfaces.js'
import type { ResolvedPixelmatchOptions } from '../pixelmatch/compare.interfaces.js'

/** Default pixelmatch settings used when direct compare mode fields are unset. */
export const DEFAULT_PIXELMATCH_OPTIONS: ResolvedPixelmatchOptions = {
threshold: 0.1,
includeAA: false,
diffColor: [255, 0, 255],
aaColor: [255, 0, 255],
diffColorAlt: [255, 0, 255],
alpha: 0.1,
diffMask: false,
checkerboard: true,
}

export const DEFAULT_COMPARE_OPTIONS = {
blockOutSideBar: true,
Expand Down
Loading
Loading