Skip to content

Commit bfe6aca

Browse files
fix: add bidi screenshot support for webscreenshots (#917)
* fix: add bidi screenshot support for webscreenshots * chore: add changeset * chore: two fixes - fix the bidi check - fix fullpage bidi setting * feat: add bidi support for element screenshots * tesT: add UT's * feat: add enableLegacyScreenshotMethod - add bidi tests - add new screenshots * chore: add proper js doc for methods * chore: update changeset * test: fixed more stable tests on chrome
1 parent 426e46f commit bfe6aca

29 files changed

+585
-131
lines changed

.changeset/neat-regions-smell.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"webdriver-image-comparison": major
3+
"@wdio/visual-service": major
4+
---
5+
6+
## 💥 BREAKING CHANGES
7+
8+
### 🧪 Web Screenshot Strategy Now Uses BiDi by Default
9+
10+
#### What was the problem?
11+
12+
Screenshots taken via WebDriver's traditional protocol often lacked precision:
13+
14+
* Emulated devices didn't reflect true resolutions
15+
* Device Pixel Ratio (DPR) was often lost
16+
* Images were cropped or downscaled
17+
18+
#### What changed?
19+
20+
All screenshot-related methods now use the **WebDriver BiDi protocol** by default (if supported by the browser), enabling:
21+
22+
✅ Native support for emulated and high-DPR devices
23+
✅ Better fidelity in screenshot size and clarity
24+
✅ Faster, browser-native screenshots via [`browsingContext.captureScreenshot`](https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot)
25+
26+
The following methods now use BiDi:
27+
28+
* `saveScreen` / `checkScreen`
29+
* `saveElement` / `checkElement`
30+
* `saveFullPageScreen` / `checkFullPageScreen`
31+
32+
#### What’s the impact?
33+
34+
⚠️ **Existing baselines may no longer match.**
35+
Because BiDi screenshots are **sharper** and **match device settings more accurately**, even a small difference in resolution or DPR can cause mismatches.
36+
37+
> If you rely on existing baseline images, you'll need to regenerate them to avoid false positives.
38+
39+
#### Want to keep using the legacy method?
40+
41+
You can disable BiDi screenshots globally or per test using the `enableLegacyScreenshotMethod` flag:
42+
43+
**Globally in `wdio.conf.ts`:**
44+
45+
```ts
46+
import { join } from 'node:path'
47+
48+
export const config = {
49+
services: [
50+
[
51+
'visual',
52+
{
53+
baselineFolder: join(process.cwd(), './localBaseline/'),
54+
debug: true,
55+
formatImageName: '{tag}-{logName}-{width}x{height}',
56+
screenshotPath: join(process.cwd(), '.tmp/'),
57+
enableLegacyScreenshotMethod: true // 👈 fallback to W3C-based screenshots
58+
},
59+
]
60+
],
61+
}
62+
```
63+
64+
**Or per test:**
65+
66+
```ts
67+
it('should compare an element successfully using legacy screenshots', async function () {
68+
await expect($('.hero__title-logo')).toMatchElementSnapshot(
69+
'legacyScreenshotLogo',
70+
{ enableLegacyScreenshotMethod: true } // 👈 fallback to W3C-based screenshots
71+
)
72+
})
73+
```
74+
75+
## 🐛 Bug Fixes
76+
77+
-[#916](https://github.com/webdriverio/visual-testing/issues/916): Visual Testing Screenshot Behavior Changed in Emulated Devices
78+
79+
80+
## Committers: 1
81+
82+
- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))

packages/visual-service/src/service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,15 @@ export default class WdioImageComparisonService extends BaseClass {
227227

228228
return [{
229229
methods: {
230+
bidiScreenshot: isBiDiScreenshotSupported(browser) ? this.browsingContextCaptureScreenshot.bind(browser) : undefined,
230231
executor: <ReturnValue, InnerArguments extends unknown[]>(
231232
fn: string | ((...args: InnerArguments) => ReturnValue),
232233
...args: InnerArguments
233234
): Promise<ReturnValue> => {
234235
return this.execute(fn, ...args) as Promise<ReturnValue>
235236
},
236237
getElementRect: this.getElementRect.bind(this),
238+
getWindowHandle: this.getWindowHandle.bind(browser),
237239
screenShot: this.takeScreenshot.bind(this),
238240
takeElementScreenshot: this.takeElementScreenshot.bind(this),
239241
},
@@ -471,13 +473,15 @@ export default class WdioImageComparisonService extends BaseClass {
471473

472474
return [{
473475
methods: {
476+
bidiScreenshot: isBiDiScreenshotSupported(browserInstance) ? browserInstance.browsingContextCaptureScreenshot.bind(browserInstance) : undefined,
474477
executor: <ReturnValue, InnerArguments extends unknown[]>(
475478
fn: string | ((...args: InnerArguments) => ReturnValue),
476479
...args: InnerArguments
477480
): Promise<ReturnValue> => {
478481
return browserInstance.execute(fn, ...args) as Promise<ReturnValue>
479482
},
480483
getElementRect: browserInstance.getElementRect.bind(browserInstance),
484+
getWindowHandle: browserInstance.getWindowHandle.bind(browserInstance),
481485
screenShot: browserInstance.takeScreenshot.bind(browserInstance),
482486
},
483487
instanceData: updatedInstanceData,

packages/webdriver-image-comparison/src/__snapshots__/base.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ exports[`BaseClass > initializes default options correctly 1`] = `
2626
"disableBlinkingCursor": false,
2727
"disableCSSAnimation": false,
2828
"enableLayoutTesting": false,
29+
"enableLegacyScreenshotMethod": false,
2930
"formatImageName": "{tag}-{browserName}-{width}x{height}-dpr-{dpr}",
3031
"fullPageScrollTimeout": 1500,
3132
"hideScrollBars": true,

packages/webdriver-image-comparison/src/commands/check.interfaces.ts

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,73 @@ import type { CheckScreenOptions } from './screen.interfaces.js'
77
import type { CheckTabbableOptions } from './tabbable.interfaces.js'
88

99
export interface CheckMethodOptions {
10-
// Block out array with x, y, width and height values
10+
/**
11+
* Block out array with x, y, width and height values
12+
*/
1113
blockOut?: RectanglesOutput[];
12-
// Block out the side bar on iOS iPads in landscape mode
14+
/**
15+
* Block out the side bar on iOS iPads in landscape mode
16+
* @default false
17+
*/
1318
blockOutSideBar?: boolean;
14-
// Block out the status bar yes or no
19+
/**
20+
* Block out the status bar yes or no
21+
* @default false
22+
*/
1523
blockOutStatusBar?: boolean;
16-
// Block out the tool bar yes or no
24+
/**
25+
* Block out the tool bar yes or no
26+
* @default false
27+
*/
1728
blockOutToolBar?: boolean;
18-
// Ignore elements and or areas
29+
/**
30+
* Ignore elements and or areas
31+
*/
1932
ignore?: (RectanglesOutput | WebdriverIO.Element | ChainablePromiseElement)[];
20-
// Compare images and discard alpha
33+
/**
34+
* Compare images and discard alpha
35+
* @default false
36+
*/
2137
ignoreAlpha?: boolean;
22-
// Compare images an discard anti aliasing
38+
/**
39+
* Compare images an discard anti aliasing
40+
* @default false
41+
*/
2342
ignoreAntialiasing?: boolean;
24-
// Even though the images are in color, the comparison wil compare 2 black/white images
43+
/**
44+
* Even though the images are in color, the comparison wil compare 2 black/white images
45+
* @default false
46+
*/
2547
ignoreColors?: boolean;
26-
// Compare images and compare with red = 16, green = 16, blue = 16, alpha = 16, minBrightness=16, maxBrightness=240
48+
/**
49+
* Compare images and compare with red = 16, green = 16, blue = 16, alpha = 16, minBrightness=16, maxBrightness=240
50+
* @default false
51+
*/
2752
ignoreLess?: boolean;
28-
// Compare images and compare with red = 0, green = 0, blue = 0, alpha = 0, minBrightness=0, maxBrightness=255
53+
/**
54+
* Compare images and compare with red = 0, green = 0, blue = 0, alpha = 0, minBrightness=0, maxBrightness=255
55+
* @default false
56+
*/
2957
ignoreNothing?: boolean;
30-
// Default false. If true, return percentage will be like 0.12345678, default is 0.12
58+
/**
59+
* Default false. If true, return percentage will be like 0.12345678, default is 0.12
60+
* @default false
61+
*/
3162
rawMisMatchPercentage?: boolean;
32-
// Return all the compare data object
63+
/**
64+
* Return all the compare data object
65+
* @default false
66+
*/
3367
returnAllCompareData?: boolean;
34-
// Allowable value of misMatchPercentage that prevents saving image with
68+
/**
69+
* Allowable value of misMatchPercentage that prevents saving image with differences
70+
* @default 0
71+
*/
3572
saveAboveTolerance?: number;
36-
//Scale images to same size before comparison
73+
/**
74+
* Scale images to same size before comparison
75+
* @default false
76+
*/
3777
scaleImagesToSameSize?: boolean;
3878
}
3979

packages/webdriver-image-comparison/src/commands/checkFullPageScreen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default async function checkFullPageScreen(
3232
disableBlinkingCursor: checkFullPageOptions.method.disableBlinkingCursor,
3333
disableCSSAnimation: checkFullPageOptions.method.disableCSSAnimation,
3434
enableLayoutTesting: checkFullPageOptions.method.enableLayoutTesting,
35+
enableLegacyScreenshotMethod: checkFullPageOptions.method.enableLegacyScreenshotMethod,
3536
fullPageScrollTimeout: checkFullPageOptions.method.fullPageScrollTimeout,
3637
hideAfterFirstScroll: checkFullPageOptions.method.hideAfterFirstScroll || [],
3738
hideScrollBars: checkFullPageOptions.method.hideScrollBars,

packages/webdriver-image-comparison/src/commands/checkWebElement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default async function checkWebElement(
2828
disableBlinkingCursor: checkElementOptions.method.disableBlinkingCursor,
2929
disableCSSAnimation: checkElementOptions.method.disableCSSAnimation,
3030
enableLayoutTesting: checkElementOptions.method.enableLayoutTesting,
31+
enableLegacyScreenshotMethod: checkElementOptions.method.enableLegacyScreenshotMethod,
3132
hideScrollBars: checkElementOptions.method.hideScrollBars,
3233
resizeDimensions: checkElementOptions.method.resizeDimensions,
3334
hideElements: checkElementOptions.method.hideElements || [],

packages/webdriver-image-comparison/src/commands/checkWebScreen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default async function checkWebScreen(
2727
disableBlinkingCursor: checkScreenOptions.method.disableBlinkingCursor,
2828
disableCSSAnimation: checkScreenOptions.method.disableCSSAnimation,
2929
enableLayoutTesting: checkScreenOptions.method.enableLayoutTesting,
30+
enableLegacyScreenshotMethod: checkScreenOptions.method.enableLegacyScreenshotMethod,
3031
hideScrollBars: checkScreenOptions.method.hideScrollBars,
3132
hideElements: checkScreenOptions.method.hideElements || [],
3233
removeElements: checkScreenOptions.method.removeElements || [],

packages/webdriver-image-comparison/src/commands/element.interfaces.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,61 @@ export interface SaveElementOptions {
1010
}
1111

1212
export interface SaveElementMethodOptions extends Partial<Folders> {
13-
// The padding that needs to be added to the address bar on iOS and Android
13+
/**
14+
* The padding that needs to be added to the address bar on iOS and Android
15+
* @default 6
16+
*/
1417
addressBarShadowPadding?: number;
15-
// Disable the blinking cursor
18+
/**
19+
* Disable the blinking cursor
20+
* @default false
21+
*/
1622
disableBlinkingCursor?: boolean;
17-
// Disable all css animations
23+
/**
24+
* Disable all css animations
25+
* @default false
26+
*/
1827
disableCSSAnimation?: boolean;
19-
// Make all text on a page transparent to only focus on the layout
28+
/**
29+
* Make all text on a page transparent to only focus on the layout
30+
* @default false
31+
*/
2032
enableLayoutTesting?: boolean;
21-
// Hide all scrollbars
33+
/**
34+
* By default the screenshots are taken with the BiDi protocol if Bidi is available.
35+
* If you want to use the legacy method, set this to true.
36+
* @default false
37+
*/
38+
enableLegacyScreenshotMethod?: boolean;
39+
/**
40+
* Hide all scrollbars
41+
* @default true
42+
*/
2243
hideScrollBars?: boolean;
23-
// The resizeDimensions
44+
/**
45+
* The resizeDimensions
46+
* @default { top: 0, left: 0, width: 0, height: 0 }
47+
*/
2448
resizeDimensions?: ResizeDimensions;
25-
// The padding that needs to be added to the tool bar on iOS and Android
49+
/**
50+
* The padding that needs to be added to the tool bar on iOS and Android
51+
* @default 6
52+
*/
2653
toolBarShadowPadding?: number;
27-
// Elements that need to be hidden (visibility: hidden) before saving a screenshot
54+
/**
55+
* Elements that need to be hidden (visibility: hidden) before saving a screenshot
56+
* @default []
57+
*/
2858
hideElements?: HTMLElement[];
29-
// Elements that need to be removed (display: none) before saving a screenshot
59+
/**
60+
* Elements that need to be removed (display: none) before saving a screenshot
61+
* @default []
62+
*/
3063
removeElements?: HTMLElement[];
31-
// Wait for the fonts to be loaded
64+
/**
65+
* Wait for the fonts to be loaded
66+
* @default true
67+
*/
3268
waitForFontsLoaded?: boolean;
3369
}
3470

packages/webdriver-image-comparison/src/commands/fullPage.interfaces.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,77 @@ export interface SaveFullPageOptions {
99
}
1010

1111
export interface SaveFullPageMethodOptions extends Partial<Folders> {
12-
// The padding that needs to be added to the address bar on iOS and Android
12+
/**
13+
* The padding that needs to be added to the address bar on iOS and Android
14+
* @default 6
15+
*/
1316
addressBarShadowPadding?: number;
14-
// Create fullpage screenshots with the bidi protocol
17+
/**
18+
* Create fullpage screenshots with the "legacy" protocol which used scrolling and stitching
19+
* @default false
20+
*/
1521
userBasedFullPageScreenshot?: boolean;
16-
// Disable the blinking cursor
22+
/**
23+
* Disable the blinking cursor
24+
* @default false
25+
*/
1726
disableBlinkingCursor?: boolean;
18-
// Disable all css animations
27+
/**
28+
* Disable all css animations
29+
* @default false
30+
*/
1931
disableCSSAnimation?: boolean;
20-
// Make all text on a page transparent to only focus on the layout
32+
/**
33+
* Make all text on a page transparent to only focus on the layout
34+
* @default false
35+
*/
2136
enableLayoutTesting?: boolean;
22-
// Hide all scrollbars
37+
/**
38+
* By default the screenshots are taken with the BiDi protocol if Bidi is available.
39+
* If you want to use the legacy method, set this to true.
40+
* @default false
41+
*/
42+
enableLegacyScreenshotMethod?: boolean;
43+
/**
44+
* Hide all scrollbars
45+
* @default true
46+
*/
2347
hideScrollBars?: boolean;
24-
// The amount of milliseconds to wait for a new scroll
48+
/**
49+
* The amount of milliseconds to wait for a new scroll. This will be used for the legacy
50+
* fullpage screenshot method.
51+
* @default 1500
52+
*/
2553
fullPageScrollTimeout?: number;
26-
// The resizeDimensions
54+
/**
55+
* The resizeDimensions
56+
* @default { top: 0, left: 0, width: 0, height: 0 }
57+
*/
2758
resizeDimensions?: ResizeDimensions;
28-
// The padding that needs to be added to the tool bar on iOS and Android
59+
/**
60+
* The padding that needs to be added to the tool bar on iOS and Android
61+
* @default 6
62+
*/
2963
toolBarShadowPadding?: number;
30-
// Elements that need to be hidden (visibility: hidden) before saving a screenshot
64+
/**
65+
* Elements that need to be hidden (visibility: hidden) before saving a screenshot
66+
* @default []
67+
*/
3168
hideElements?: HTMLElement[];
32-
// Elements that need to be removed (display: none) before saving a screenshot
69+
/**
70+
* Elements that need to be removed (display: none) before saving a screenshot
71+
* @default []
72+
*/
3373
removeElements?: HTMLElement[];
34-
// Elements that need to be hidden after the first scroll for a fullpage scroll
74+
/**
75+
* Elements that need to be hidden after the first scroll for a fullpage scroll
76+
* @default []
77+
*/
3578
hideAfterFirstScroll?: HTMLElement[];
36-
// Wait for the fonts to be loaded
79+
/**
80+
* Wait for the fonts to be loaded
81+
* @default true
82+
*/
3783
waitForFontsLoaded?: boolean;
3884
}
3985

0 commit comments

Comments
 (0)