Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
44 changes: 44 additions & 0 deletions .changeset/fancy-items-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"webdriver-image-comparison": minor
"@wdio/visual-service": minor
---

With BiDi support, passing extra options when capturing screenshots is possible.
For example, full-page screenshots can now be generated more easily — see the [browsingContextCaptureScreenshot](https://webdriver.io/docs/api/webdriverBidi#browsingcontextcapturescreenshot) docs.

This release
- simplifies logic
- speeds up execution with more than 50% 🚀 🤯 ![image](https://github.com/user-attachments/assets/394ad1d6-bbc7-42dd-b93b-ff7eb5a80429)

by using BiDi for Full Page Desktop Web screenshots.

This can be enabled and disabled on the service

```ts
// wdio.conf.ts
export const config = {
// ...
// =====
// Setup
// =====
services: [
[
"visual",
{
// Some options, see the docs for more
createBidiFullPageScreenshots: true // Default is `true
// ... more options
},
],
],
// ...
};
```

or on the test level

```ts
await expect(browser).toMatchFullPageSnapshot('fullPage', {
createBidiFullPageScreenshots: true, // `true` by default
})
```
13 changes: 12 additions & 1 deletion packages/visual-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ import {
} from 'webdriver-image-comparison'
import type { TestContext } from 'webdriver-image-comparison'
import { SevereServiceError } from 'webdriverio'
import { determineNativeContext, enrichTestContext, getFolders, getInstanceData, getNativeContext } from './utils.js'
import {
determineNativeContext,
enrichTestContext,
getFolders,
getInstanceData,
getNativeContext,
isBiDiScreenshotSupported,
} from './utils.js'
import {
toMatchScreenSnapshot,
toMatchFullPageSnapshot,
Expand Down Expand Up @@ -263,12 +270,14 @@ export default class WdioImageComparisonService extends BaseClass {
return command(
{
methods: {
bidiScreenshot: isBiDiScreenshotSupported(browser) ? this.browsingContextCaptureScreenshot.bind(browser) : undefined,
executor: <ReturnValue, InnerArguments extends unknown[]>(
fn: string | ((...args: InnerArguments) => ReturnValue),
...args: InnerArguments): Promise<ReturnValue> => {
return this.execute.bind(browser)(fn, ...args) as Promise<ReturnValue>
},
getElementRect: this.getElementRect.bind(browser),
getWindowHandle: this.getWindowHandle.bind(browser),
screenShot: this.takeScreenshot.bind(browser),
},
instanceData,
Expand Down Expand Up @@ -386,12 +395,14 @@ export default class WdioImageComparisonService extends BaseClass {
returnData[browserName] = await command(
{
methods: {
bidiScreenshot: isBiDiScreenshotSupported(browserInstance) ? browserInstance.browsingContextCaptureScreenshot.bind(browserInstance) : undefined,
executor: <ReturnValue, InnerArguments extends unknown[]>(
fn: string | ((...args: InnerArguments) => ReturnValue),
...args: InnerArguments): Promise<ReturnValue> => {
return this.execute.bind(browser)(fn, ...args) as Promise<ReturnValue>
},
getElementRect: browserInstance.getElementRect.bind(browserInstance),
getWindowHandle: browserInstance.getWindowHandle.bind(browserInstance),
screenShot: browserInstance.takeScreenshot.bind(browserInstance),
},
instanceData,
Expand Down
10 changes: 10 additions & 0 deletions packages/visual-service/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,13 @@ export function enrichTestContext(
}
}

/**
* Check if the current browser supports isBidi screenshots
*/
export function isBiDiScreenshotSupported(driver: WebdriverIO.Browser): boolean {
const { isBidi } = driver
const isBiDiSupported = typeof driver.browsingContextCaptureScreenshot === 'function'

return isBidi && isBiDiSupported
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exports[`BaseClass > initializes default options correctly 1`] = `
"saveAboveTolerance": 0,
"scaleImagesToSameSize": false,
},
"createBidiFullPageScreenshots": true,
"disableBlinkingCursor": false,
"disableCSSAnimation": false,
"enableLayoutTesting": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface SaveFullPageOptions {
export interface SaveFullPageMethodOptions extends Partial<Folders> {
// The padding that needs to be added to the address bar on iOS and Android
addressBarShadowPadding?: number;
// Create fullpage screenshots with the bidi protocol
createBidiFullPageScreenshots?: boolean;
// Disable the blinking cursor
disableBlinkingCursor?: boolean;
// Disable all css animations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ScreenshotOutput, AfterScreenshotOptions } from '../helpers/afterS
import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers/beforeScreenshot.interfaces.js'
import type { FullPageScreenshotDataOptions, FullPageScreenshotsData } from '../methods/screenshots.interfaces.js'
import type { InternalSaveFullPageMethodOptions } from './save.interfaces.js'
import { getMethodOrWicOption } from '../helpers/utils.js'

/**
* Saves an image of the full page
Expand Down Expand Up @@ -34,27 +35,16 @@ export default async function saveFullPageScreen(
} = saveFullPageOptions.wic

// 1c. Set the method options to the right values
const disableBlinkingCursor: boolean = saveFullPageOptions.method.disableBlinkingCursor !== undefined
? Boolean(saveFullPageOptions.method.disableBlinkingCursor)
: saveFullPageOptions.wic.disableBlinkingCursor
const disableCSSAnimation: boolean = saveFullPageOptions.method.disableCSSAnimation !== undefined
? Boolean(saveFullPageOptions.method.disableCSSAnimation)
: saveFullPageOptions.wic.disableCSSAnimation
const enableLayoutTesting: boolean = saveFullPageOptions.method.enableLayoutTesting !== undefined
? Boolean(saveFullPageOptions.method.enableLayoutTesting)
: saveFullPageOptions.wic.enableLayoutTesting
const hideScrollBars: boolean = saveFullPageOptions.method.hideScrollBars !== undefined
? Boolean(saveFullPageOptions.method.hideScrollBars)
: saveFullPageOptions.wic.hideScrollBars
const fullPageScrollTimeout: number = saveFullPageOptions.method.fullPageScrollTimeout !== undefined
? saveFullPageOptions.method.fullPageScrollTimeout!
: saveFullPageOptions.wic.fullPageScrollTimeout
const createBidiFullPageScreenshots = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'createBidiFullPageScreenshots')
const disableBlinkingCursor = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'disableBlinkingCursor')
const disableCSSAnimation = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'disableCSSAnimation')
const enableLayoutTesting = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'enableLayoutTesting')
const fullPageScrollTimeout = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'fullPageScrollTimeout')
const hideAfterFirstScroll: HTMLElement[] = saveFullPageOptions.method.hideAfterFirstScroll || []
const hideElements: HTMLElement[] = saveFullPageOptions.method.hideElements || []
const hideScrollBars = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'hideScrollBars')
const removeElements: HTMLElement[] = saveFullPageOptions.method.removeElements || []
const hideAfterFirstScroll: HTMLElement[] = saveFullPageOptions.method.hideAfterFirstScroll || []
const waitForFontsLoaded: boolean = saveFullPageOptions.method.waitForFontsLoaded !== undefined
? Boolean(saveFullPageOptions.method.waitForFontsLoaded)
: saveFullPageOptions.wic.waitForFontsLoaded
const waitForFontsLoaded = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'waitForFontsLoaded')

// 2. Prepare the beforeScreenshot
const beforeOptions: BeforeScreenshotOptions = {
Expand All @@ -72,35 +62,42 @@ export default async function saveFullPageScreen(
const enrichedInstanceData: BeforeScreenshotResult = await beforeScreenshot(methods.executor, beforeOptions, true)
const devicePixelRatio = enrichedInstanceData.dimensions.window.devicePixelRatio
const isLandscape = enrichedInstanceData.dimensions.window.isLandscape
let fullPageBase64Image: string

// 3. Fullpage screenshots are taken per scrolled viewport
const fullPageScreenshotOptions: FullPageScreenshotDataOptions = {
addressBarShadowPadding: enrichedInstanceData.addressBarShadowPadding,
devicePixelRatio: devicePixelRatio || NaN,
deviceRectangles: instanceData.deviceRectangles,
fullPageScrollTimeout,
hideAfterFirstScroll,
innerHeight: enrichedInstanceData.dimensions.window.innerHeight || NaN,
isAndroid: enrichedInstanceData.isAndroid,
isAndroidChromeDriverScreenshot: enrichedInstanceData.isAndroidChromeDriverScreenshot,
isAndroidNativeWebScreenshot: enrichedInstanceData.isAndroidNativeWebScreenshot,
isIOS: enrichedInstanceData.isIOS,
isLandscape,
screenHeight: enrichedInstanceData.dimensions.window.screenHeight || NaN,
screenWidth: enrichedInstanceData.dimensions.window.screenWidth || NaN,
toolBarShadowPadding: enrichedInstanceData.toolBarShadowPadding,
}
const screenshotsData: FullPageScreenshotsData = await getBase64FullPageScreenshotsData(
methods.screenShot,
methods.executor,
fullPageScreenshotOptions,
)
if (typeof methods.bidiScreenshot === 'function' && typeof methods.getWindowHandle === 'function' && createBidiFullPageScreenshots) {
// 3a. Fullpage screenshots are taken in one go with the Bidi protocol
const contextID = await methods.getWindowHandle()
fullPageBase64Image =( await methods.bidiScreenshot({ context: contextID, origin: 'document' })).data
} else {
// 3b. Fullpage screenshots are taken per scrolled viewport
const fullPageScreenshotOptions: FullPageScreenshotDataOptions = {
addressBarShadowPadding: enrichedInstanceData.addressBarShadowPadding,
devicePixelRatio: devicePixelRatio || NaN,
deviceRectangles: instanceData.deviceRectangles,
fullPageScrollTimeout,
hideAfterFirstScroll,
innerHeight: enrichedInstanceData.dimensions.window.innerHeight || NaN,
isAndroid: enrichedInstanceData.isAndroid,
isAndroidChromeDriverScreenshot: enrichedInstanceData.isAndroidChromeDriverScreenshot,
isAndroidNativeWebScreenshot: enrichedInstanceData.isAndroidNativeWebScreenshot,
isIOS: enrichedInstanceData.isIOS,
isLandscape,
screenHeight: enrichedInstanceData.dimensions.window.screenHeight || NaN,
screenWidth: enrichedInstanceData.dimensions.window.screenWidth || NaN,
toolBarShadowPadding: enrichedInstanceData.toolBarShadowPadding,
}
const screenshotsData: FullPageScreenshotsData = await getBase64FullPageScreenshotsData(
methods.screenShot,
methods.executor,
fullPageScreenshotOptions,
)

// 4. Make a fullpage base64 image
const fullPageBase64Image: string = await makeFullPageBase64Image(screenshotsData, {
devicePixelRatio: devicePixelRatio || NaN,
isLandscape,
})
// 4. Make a fullpage base64 image by scrolling and stitching the images together
fullPageBase64Image = await makeFullPageBase64Image(screenshotsData, {
devicePixelRatio: devicePixelRatio || NaN,
isLandscape,
})
}

// 5. The after the screenshot methods
const afterOptions: AfterScreenshotOptions = {
Expand Down Expand Up @@ -144,3 +141,4 @@ export default async function saveFullPageScreen(
// 6. Return the data
return afterScreenshot(methods.executor, afterOptions!)
}

24 changes: 7 additions & 17 deletions packages/webdriver-image-comparison/src/commands/saveWebElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers
import { DEFAULT_RESIZE_DIMENSIONS } from '../helpers/constants.js'
import type { ResizeDimensions } from '../methods/images.interfaces.js'
import scrollElementIntoView from '../clientSideScripts/scrollElementIntoView.js'
import { getBase64ScreenshotSize, waitFor } from '../helpers/utils.js'
import { getBase64ScreenshotSize, getMethodOrWicOption, waitFor } from '../helpers/utils.js'
import scrollToPosition from '../clientSideScripts/scrollToPosition.js'
import type { InternalSaveElementMethodOptions } from './save.interfaces.js'

Expand All @@ -29,24 +29,14 @@ export default async function saveWebElement(
saveElementOptions.wic
const { executor, screenShot, takeElementScreenshot } = methods
// 1b. Set the method options to the right values
const disableBlinkingCursor: boolean = saveElementOptions.method.disableBlinkingCursor !== undefined
? Boolean(saveElementOptions.method.disableBlinkingCursor)
: saveElementOptions.wic.disableBlinkingCursor
const disableCSSAnimation: boolean = saveElementOptions.method.disableCSSAnimation !== undefined
? Boolean(saveElementOptions.method.disableCSSAnimation)
: saveElementOptions.wic.disableCSSAnimation
const enableLayoutTesting: boolean = saveElementOptions.method.enableLayoutTesting !== undefined
? Boolean(saveElementOptions.method.enableLayoutTesting)
: saveElementOptions.wic.enableLayoutTesting
const hideScrollBars: boolean = saveElementOptions.method.hideScrollBars !== undefined
? Boolean(saveElementOptions.method.hideScrollBars)
: saveElementOptions.wic.hideScrollBars
const resizeDimensions: ResizeDimensions | number = saveElementOptions.method.resizeDimensions || DEFAULT_RESIZE_DIMENSIONS
const disableBlinkingCursor = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'disableBlinkingCursor')
const disableCSSAnimation = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'disableCSSAnimation')
const enableLayoutTesting = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'enableLayoutTesting')
const hideElements: HTMLElement[] = saveElementOptions.method.hideElements || []
const hideScrollBars = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'hideScrollBars')
const removeElements: HTMLElement[] = saveElementOptions.method.removeElements || []
const waitForFontsLoaded: boolean = saveElementOptions.method.waitForFontsLoaded !== undefined
? Boolean(saveElementOptions.method.waitForFontsLoaded)
: saveElementOptions.wic.waitForFontsLoaded
const resizeDimensions: ResizeDimensions | number = saveElementOptions.method.resizeDimensions || DEFAULT_RESIZE_DIMENSIONS
const waitForFontsLoaded = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'waitForFontsLoaded')

// 2. Prepare the beforeScreenshot
const beforeOptions: BeforeScreenshotOptions = {
Expand Down
21 changes: 6 additions & 15 deletions packages/webdriver-image-comparison/src/commands/saveWebScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers
import type { AfterScreenshotOptions, ScreenshotOutput } from '../helpers/afterScreenshot.interfaces.js'
import type { RectanglesOutput, ScreenRectanglesOptions } from '../methods/rectangles.interfaces.js'
import type { InternalSaveScreenMethodOptions } from './save.interfaces.js'
import { getMethodOrWicOption } from '../helpers/utils.js'

/**
* Saves an image of the viewport of the screen
Expand All @@ -26,23 +27,13 @@ export default async function saveWebScreen(
saveScreenOptions.wic

// 1b. Set the method options to the right values
const disableBlinkingCursor: boolean = saveScreenOptions.method.disableBlinkingCursor !== undefined
? Boolean(saveScreenOptions.method.disableBlinkingCursor)
: saveScreenOptions.wic.disableBlinkingCursor
const disableCSSAnimation: boolean = saveScreenOptions.method.disableCSSAnimation !== undefined
? Boolean(saveScreenOptions.method.disableCSSAnimation)
: saveScreenOptions.wic.disableCSSAnimation
const enableLayoutTesting: boolean = saveScreenOptions.method.enableLayoutTesting !== undefined
? Boolean(saveScreenOptions.method.enableLayoutTesting)
: saveScreenOptions.wic.enableLayoutTesting
const hideScrollBars: boolean = saveScreenOptions.method.hideScrollBars !== undefined
? Boolean(saveScreenOptions.method.hideScrollBars)
: saveScreenOptions.wic.hideScrollBars
const disableBlinkingCursor = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'disableBlinkingCursor')
const disableCSSAnimation = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'disableCSSAnimation')
const enableLayoutTesting = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'enableLayoutTesting')
const hideScrollBars = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'hideScrollBars')
const hideElements: HTMLElement[] = saveScreenOptions.method.hideElements || []
const removeElements: HTMLElement[] = saveScreenOptions.method.removeElements || []
const waitForFontsLoaded: boolean = saveScreenOptions.method.waitForFontsLoaded !== undefined
? Boolean(saveScreenOptions.method.waitForFontsLoaded)
: saveScreenOptions.wic.waitForFontsLoaded
const waitForFontsLoaded = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'waitForFontsLoaded')

// 2. Prepare the beforeScreenshot
const beforeOptions: BeforeScreenshotOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exports[`options > defaultOptions > should return the default options when no op
"saveAboveTolerance": 0,
"scaleImagesToSameSize": false,
},
"createBidiFullPageScreenshots": true,
"disableBlinkingCursor": false,
"disableCSSAnimation": false,
"enableLayoutTesting": false,
Expand Down Expand Up @@ -75,6 +76,7 @@ exports[`options > defaultOptions > should return the provided options when opti
"saveAboveTolerance": 12,
"scaleImagesToSameSize": true,
},
"createBidiFullPageScreenshots": true,
"disableBlinkingCursor": true,
"disableCSSAnimation": true,
"enableLayoutTesting": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface ClassOptions {
addIOSBezelCorners?: boolean;
// Delete runtime folder (actual & diff) on initialization
clearRuntimeFolder?: boolean;
// Create fullpage screenshots with the bidi protocol
createBidiFullPageScreenshots?: boolean;
// The naming of the saved images can be customized by passing the parameter `formatImageName` with a format string
formatImageName?: string;
// Is it an hybrid app or not
Expand Down Expand Up @@ -132,6 +134,7 @@ export interface DefaultOptions {
autoSaveBaseline: boolean;
clearFolder: boolean;
compareOptions: CompareOptions;
createBidiFullPageScreenshots: boolean;
disableBlinkingCursor: boolean;
disableCSSAnimation: boolean;
enableLayoutTesting: boolean;
Expand Down
1 change: 1 addition & 0 deletions packages/webdriver-image-comparison/src/helpers/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function defaultOptions(options: ClassOptions): DefaultOptions {
addIOSBezelCorners: options.addIOSBezelCorners ?? false,
autoSaveBaseline: options.autoSaveBaseline ?? true,
clearFolder: options.clearRuntimeFolder ?? false,
createBidiFullPageScreenshots: options.createBidiFullPageScreenshots ?? true,
// Storybook will have it's own default format string
formatImageName: options.formatImageName ?? (isStorybook() ? STORYBOOK_FORMAT_STRING : DEFAULT_FORMAT_STRING),
isHybridApp: options.isHybridApp ?? false,
Expand Down
Loading
Loading