Skip to content

Commit 70c473e

Browse files
fix: pass matcher threshold to core as saveAboveTolerance
When using visual matchers with a threshold (e.g., toMatchScreenSnapshot('tag', 0.9)) and alwaysSaveActualImage: false, images were still saved even when the comparison passed within the threshold. The matcher now passes the expected threshold to the core as saveAboveTolerance, ensuring images are only saved when mismatch exceeds the user's acceptable threshold. Fixes #1111 Co-authored-by: Cursor <[email protected]>
1 parent ad25ea6 commit 70c473e

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@wdio/visual-service": patch
3+
---
4+
5+
# 🐛 Bugfixes
6+
7+
## #1111 Pass matcher threshold to core as saveAboveTolerance
8+
9+
When using visual matchers like `toMatchScreenSnapshot('tag', 0.9)` with `alwaysSaveActualImage: false`, the actual image was still being saved even when the comparison passed within the threshold.
10+
11+
The root cause was that the matcher's expected threshold was not being passed to the core comparison logic. The core used `saveAboveTolerance` (defaulting to 0) to decide whether to save images, while the matcher used the user-provided threshold to determine pass/fail - these were disconnected.
12+
13+
This fix ensures the matcher passes the expected threshold to the core as `saveAboveTolerance`, so images are only saved when the mismatch actually exceeds the user's acceptable threshold.

packages/visual-service/src/matcher.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ function parseMatcherParams (
119119
*/
120120
options.returnAllCompareData = true
121121

122+
/**
123+
* Pass the expected threshold to the core as `saveAboveTolerance` so it knows
124+
* when to save actual images (only when mismatch exceeds the threshold).
125+
* This ensures that when `alwaysSaveActualImage: false`, images are not saved
126+
* if the comparison passes within the user's acceptable threshold.
127+
* Only set if user hasn't explicitly set saveAboveTolerance.
128+
* For numeric thresholds, use that value; otherwise default to 0 (same as comparison default).
129+
* @see https://github.com/webdriverio/visual-testing/issues/1111
130+
*/
131+
if (options.saveAboveTolerance === undefined) {
132+
// Only set saveAboveTolerance for numeric thresholds (including undefined which defaults to 0)
133+
// Asymmetric matchers can't be converted to a numeric tolerance
134+
if (typeof expectedResult === 'number') {
135+
options.saveAboveTolerance = expectedResult
136+
} else if (expectedResult === undefined) {
137+
options.saveAboveTolerance = DEFAULT_EXPECTED_RESULT
138+
}
139+
}
140+
122141
return { expectedResult, options }
123142
}
124143

packages/visual-service/tests/matcher.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,72 @@ describe('custom visual matcher', () => {
145145
expect(results.pass).toBe(false)
146146
expect(results.message()).toMatchSnapshot()
147147
})
148+
149+
describe('saveAboveTolerance threshold passthrough (#1111)', () => {
150+
it('should pass numeric expectedResult as saveAboveTolerance to checkScreen', async () => {
151+
await toMatchScreenSnapshot(browser, 'foo', 0.9, {})
152+
expect(browser.checkScreen).toHaveBeenCalledWith('foo', {
153+
returnAllCompareData: true,
154+
saveAboveTolerance: 0.9
155+
})
156+
})
157+
158+
it('should pass numeric expectedResult as saveAboveTolerance to checkFullPageScreen', async () => {
159+
await toMatchFullPageSnapshot(browser, 'foo', 0.5, {})
160+
expect(browser.checkFullPageScreen).toHaveBeenCalledWith('foo', {
161+
returnAllCompareData: true,
162+
saveAboveTolerance: 0.5
163+
})
164+
})
165+
166+
it('should pass numeric expectedResult as saveAboveTolerance to checkElement', async () => {
167+
await toMatchElementSnapshot(browser as any as WebdriverIO.Element, 'foo', 1.5, {})
168+
expect(browser.checkElement).toHaveBeenCalledWith(browser, 'foo', {
169+
returnAllCompareData: true,
170+
saveAboveTolerance: 1.5
171+
})
172+
})
173+
174+
it('should pass numeric expectedResult as saveAboveTolerance to checkTabbablePage', async () => {
175+
await toMatchTabbablePageSnapshot(browser, 'foo', 2.0, {})
176+
expect(browser.checkTabbablePage).toHaveBeenCalledWith('foo', {
177+
returnAllCompareData: true,
178+
saveAboveTolerance: 2.0
179+
})
180+
})
181+
182+
it('should use default saveAboveTolerance of 0 when no threshold is provided', async () => {
183+
await toMatchScreenSnapshot(browser, 'foo')
184+
expect(browser.checkScreen).toHaveBeenCalledWith('foo', {
185+
returnAllCompareData: true,
186+
saveAboveTolerance: 0
187+
})
188+
})
189+
190+
it('should not override user-provided saveAboveTolerance', async () => {
191+
await toMatchScreenSnapshot(browser, 'foo', 0.9, { saveAboveTolerance: 0.1 })
192+
expect(browser.checkScreen).toHaveBeenCalledWith('foo', {
193+
returnAllCompareData: true,
194+
saveAboveTolerance: 0.1 // User's explicit value is preserved
195+
})
196+
})
197+
198+
it('should not set saveAboveTolerance for asymmetric matchers', async () => {
199+
await toMatchScreenSnapshot(browser, 'foo', expect.any(Number))
200+
expect(browser.checkScreen).toHaveBeenCalledWith('foo', {
201+
returnAllCompareData: true
202+
// No saveAboveTolerance - can't convert asymmetric matcher to number
203+
})
204+
})
205+
206+
it('should set saveAboveTolerance to 0 when options object is passed without threshold', async () => {
207+
// When only options are passed (no expectedResult), threshold defaults to 0
208+
await toMatchScreenSnapshot(browser, 'foo', { hideScrollBars: true })
209+
expect(browser.checkScreen).toHaveBeenCalledWith('foo', {
210+
hideScrollBars: true,
211+
returnAllCompareData: true,
212+
saveAboveTolerance: 0
213+
})
214+
})
215+
})
148216
})

0 commit comments

Comments
 (0)