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
130 changes: 130 additions & 0 deletions .changeset/many-olives-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
"webdriver-image-comparison": major
"@wdio/visual-service": major
"@wdio/visual-reporter": patch
"@wdio/ocr-service": patch
---

## ๐Ÿ’ฅ BREAKING CHANGES

### ๐Ÿ” Viewport Screenshot Logic Reworked for Mobile Web & Hybrid Apps

#### What was the problem?

Screenshots for mobile devices were inconsistent due to platform differences. iOS captures the entire device screen (including status and address bars), while Android (using ChromeDriver) only captures the webview, unless the capability `"appium:nativeWebScreenshot": true` is used.

#### What changed?

Weโ€™ve reimplemented the logic to correctly handle both platforms by default.
This fix addresses [[#747](https://github.com/webdriverio/visual-testing/pull/747)](https://github.com/webdriverio/visual-testing/pull/747).

๐Ÿ’ก Credit to [Benjamin Karran (@ebekebe)](https://github.com/ebekebe) for pointing us in the right direction to improve this logic!

#### Whatโ€™s the advantage?

โœ… More **accurate full-page and element screenshots** on both Android and iOS.
โš ๏ธ But this change may **break your current baselines**, especially on Android and iOS.

---

### ๐Ÿ iOS Element Screenshot Strategy Changed

#### What was the problem?

iOS element screenshots were previously cut from full-device screenshots, which could lead to misalignment or off-by-a-few-pixels issues.

#### What changed?

We now use the element screenshot endpoint directly.

#### Whatโ€™s the advantage?

โœ… More accurate iOS element screenshots.
โš ๏ธ But again, this may affect your existing baselines.

---

### ๐Ÿ–ฅ๏ธ New Full-Page Screenshot Strategy for **Desktop Web**

#### What was the problem?

The "previous" scroll-and-stitch method simulated user interaction by scrolling the page, waiting, taking a screenshot, and repeating until the entire page was captured.
This works well for **lazy-loaded content**, but it is **slow and unstable** on other pages.

#### What changed?

We now use WebDriver BiDiโ€™s [`[browsingContext.captureScreenshot](https://webdriver.io/docs/api/webdriverBidi#browsingcontextcapturescreenshot)`] to capture **full-page screenshots in one go**. This is the new **default strategy for desktop web browsers**.

๐Ÿ“Œ **Mobile platforms (iOS/Android)** still use the scroll-and-stitch approach for now.

#### Whatโ€™s the advantage?
โœ… Execution time reduced by **50%+**
โœ… Logic is greatly simplified
โœ… More consistent and stable results on static or non-lazy pages
๐Ÿ“ธ ![Example](https://github.com/user-attachments/assets/394ad1d6-bbc7-42dd-b93b-ff7eb5a80429)

**Still want the old scroll-and-stitch behavior or need fullpage screenshots for pages who have lazy-loading?**

Use the `userBasedFullPageScreenshot` option to simulate user-like scrolling. This remains the **better choice for pages with lazy-loading**:

```ts
// wdio.conf.ts
services: [
["visual", {
userBasedFullPageScreenshot: true
}]
]
```

Or per test:

```ts
await expect(browser).toMatchFullPageSnapshot('homepage', {
userBasedFullPageScreenshot: true,
})
```

---

## ๐Ÿ’… Polish

### โš ๏ธ Deprecated Root-Level Compare Options

#### What was the problem?

Compare options were allowed at the root level of the service config, making them harder to group or discover.

#### What changed?

You now get a warning if you still use root-level keys. Please move them under the `compareOptions` property instead.

**Example warning:**

```log
WARN The following root-level compare options are deprecated and should be moved under 'compareOptions':
- blockOutStatusBar
- ignoreColors
In the next major version, these options will be removed from the root level.
```

๐Ÿ“˜ See: [[compareOptions docs](https://webdriver.io/docs/visual-testing/service-options#compare-options)](https://webdriver.io/docs/visual-testing/service-options#compare-options)

---

## ๐Ÿ› Bug Fixes

- โœ… [[#747](https://github.com/your-repo/issues/747)](https://github.com/your-repo/issues/747): Fixed incorrect mobile webview context data.

---

## ๐Ÿ”ง Other

- ๐Ÿ†™ Updated dependencies
- ๐Ÿงช Improved test coverage
- ๐Ÿ“ธ Refreshed image baselines

---

## Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ jobs:
LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }}
LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }}
BUILD_PREFIX: true
run: pnpm test.lambdatest.desktop --maxConcurrency=3
run: pnpm test.lambdatest.desktop --maxConcurrency=4

- name: ๐Ÿ“ค Upload artifacts
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -239,7 +239,7 @@ jobs:
LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }}
LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }}
BUILD_PREFIX: true
run: pnpm test.lambdatest.emu.web --maxConcurrency=4
run: pnpm test.lambdatest.emu.web --maxConcurrency=6

- name: ๐Ÿ“ค Upload artifacts
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -302,7 +302,7 @@ jobs:
LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }}
LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }}
BUILD_PREFIX: true
run: pnpm test.lambdatest.sims.web --maxConcurrency=4
run: pnpm test.lambdatest.sims.web --maxConcurrency=6

- name: ๐Ÿ“ค Upload artifacts
uses: actions/upload-artifact@v4
Expand Down
48 changes: 24 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"build": "pnpm run -r build",
"build:reporter": "node ./packages/visual-reporter/dist/cli.js --jsonOutput=.tmp/actual/output.json --reportFolder=.tmp/ --logLevel=debug",
"clean": "rimraf coverage dist .tmp '**/dist'",
"clean": "rimraf coverage dist .tmp **/dist",
"release": "run-s build && changeset publish",
"test": "run-s test:*",
"test:lint": "rimraf .eslintcache && eslint packages tests",
Expand Down Expand Up @@ -56,43 +56,43 @@
"webdriver-image-comparison": "workspace:*"
},
"devDependencies": {
"@changesets/cli": "^2.29.0",
"@changesets/cli": "^2.29.2",
"@tsconfig/node20": "^20.1.5",
"@types/eslint": "^9.6.1",
"@types/inquirer": "^9.0.7",
"@types/jsdom": "~21.1.7",
"@types/node": "^22",
"@types/xml2js": "~0.4.14",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"@typescript-eslint/utils": "^8.29.1",
"@vitest/coverage-v8": "^3.0.8",
"@vitest/ui": "^3.0.8",
"@wdio/appium-service": "^9.12.4",
"@wdio/cli": "^9.12.5",
"@wdio/globals": "^9.9.1",
"@wdio/local-runner": "^9.12.5",
"@wdio/mocha-framework": "^9.12.5",
"@wdio/sauce-service": "^9.12.5",
"@wdio/shared-store-service": "^9.12.5",
"@wdio/spec-reporter": "^9.12.3",
"@wdio/types": "^9.12.2",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@wdio/globals": "^9.12.6",
"@wdio/mocha-framework": "^9.12.6",
"@typescript-eslint/parser": "^8.30.1",
"@typescript-eslint/utils": "^8.30.1",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@wdio/appium-service": "^9.12.6",
"@wdio/cli": "^9.12.6",
"@wdio/local-runner": "^9.12.6",
"@wdio/sauce-service": "^9.12.6",
"@wdio/shared-store-service": "^9.12.6",
"@wdio/spec-reporter": "^9.12.6",
"@wdio/types": "^9.12.6",
"cross-env": "^7.0.3",
"eslint": "^9.23.0",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-wdio": "^9.9.1",
"husky": "^9.1.7",
"jsdom": "^25.0.1",
"jsdom": "^26.1.0",
"npm-run-all2": "^7.0.2",
"release-it": "^17.11.0",
"release-it": "^18.1.2",
"rimraf": "^6.0.1",
"saucelabs": "^8.0.0",
"saucelabs": "^9.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"vitest": "^3.0.8",
"wdio-lambdatest-service": "^4.0.0",
"webdriverio": "^9.12.4"
"typescript": "^5.8.3",
"vitest": "^3.1.1",
"webdriverio": "^9.12.6",
"wdio-lambdatest-service": "^4.0.0"
},
"packageManager": "pnpm@9.15.9+sha256.cf86a7ad764406395d4286a6d09d730711720acc6d93e9dce9ac7ac4dc4a28a7"
}
8 changes: 4 additions & 4 deletions packages/ocr-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
"watch": "pnpm run build:tsc -w"
},
"dependencies": {
"@wdio/globals": "^9.9.1",
"@wdio/globals": "^9.12.6",
"@wdio/logger": "^9.4.4",
"@wdio/types": "^9.12.2",
"@wdio/types": "^9.12.6",
"fuse.js": "^7.1.0",
"@inquirer/prompts": "7.3.2",
"@inquirer/prompts": "7.4.1",
"jimp": "^1.6.0",
"node-tesseract-ocr": "^2.2.1",
"tesseract.js": "^5.1.1",
Expand All @@ -42,4 +42,4 @@
"@types/inquirer": "~9.0.7",
"@types/xml2js": "~0.4.14"
}
}
}
4 changes: 2 additions & 2 deletions packages/ocr-service/src/utils/getData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logger from '@wdio/logger'
import { getNodeOcrData, getSystemOcrData } from './tesseract.js'
import type { GetOcrData, Line, GetData, GetDataOptions, RectReturn, Words } from '../types.js'
import { adjustElementBbox, getScreenshotSize, isRectanglesObject } from './index.js'
import { adjustElementBbox, getBase64ScreenshotSize, isRectanglesObject } from './index.js'
import { drawHighlightedWords, processImage } from './imageProcessing.js'

const log = logger('@wdio/ocr-service:getData')
Expand All @@ -22,7 +22,7 @@ export default async function getData(browser: WebdriverIO.Browser, options: Get
if (!cliFile) {
const screenSize = await browser.getWindowSize()
screenshot = await browser.takeScreenshot()
const { width } = getScreenshotSize(screenshot)
const { width } = getBase64ScreenshotSize(screenshot)
dpr = width / screenSize.width
} else {
screenshot = cliFile
Expand Down
2 changes: 1 addition & 1 deletion packages/ocr-service/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs'
import type { ChainablePromiseElement } from 'webdriverio'
import type { ClickPoint, DetermineClickPointOptions, Rectangles, RectReturn, ScreenshotSize } from '../types.js'

export function getScreenshotSize(screenshot: string): ScreenshotSize {
export function getBase64ScreenshotSize(screenshot: string): ScreenshotSize {
return {
height: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(20)),
width: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(16)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ exports[`getData > processes OCR data correctly using a haystack 1`] = `
"Using system installed version of Tesseract",
],
[
"It took '0.000s' to process the image.",
"It took 'X.XXXs' to process the image.",
],
[
"The following text was found through OCR:
Expand Down
10 changes: 5 additions & 5 deletions packages/ocr-service/tests/utils/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mkdirSync } from 'node:fs'
import { describe, it, expect, vi } from 'vitest'
import { adjustElementBbox, createOcrDir, determineClickPoint, getDprPositions, getScreenshotSize, isRectanglesObject } from '../../src/utils/index.js'
import { adjustElementBbox, createOcrDir, determineClickPoint, getDprPositions, getBase64ScreenshotSize, isRectanglesObject } from '../../src/utils/index.js'
import type { RectReturn } from '../../src/types.js'

vi.mock('node:fs', () => ({
Expand All @@ -15,26 +15,26 @@ function createMockScreenshot(width: number, height: number): string {
return buffer.toString('base64')
}

describe('getScreenshotSize', () => {
describe('getBase64ScreenshotSize', () => {
it('should correctly extract dimensions from a valid screenshot', () => {
const width = 800
const height = 600
const base64 = createMockScreenshot(width, height)
const result = getScreenshotSize(base64)
const result = getBase64ScreenshotSize(base64)

expect(result).toEqual({ width: width, height: height })
})

it('should handle invalid base64 strings gracefully', () => {
const invalidBase64 = 'not-a-real-base64-string'
const action = () => getScreenshotSize(invalidBase64)
const action = () => getBase64ScreenshotSize(invalidBase64)

expect(action).toThrowError()
})

it('should handle unexpected data layout', () => {
const malformedBase64 = Buffer.from([1, 2, 3, 4, 5]).toString('base64')
const action = () => getScreenshotSize(malformedBase64)
const action = () => getBase64ScreenshotSize(malformedBase64)

expect(action).toThrowError()
})
Expand Down
Loading