Skip to content

feat: provide proper dimensions based on emulated screens #947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 17, 2025
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
16 changes: 16 additions & 0 deletions .changeset/dull-readers-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"webdriver-image-comparison": patch
"@wdio/visual-service": patch
---

## 🐛 Bug-fixes

- #946: Visual Regression Changes in WDIO v9
- Fixed screen size detection in emulated mode for filenames. Previously used incorrect browser window size.
- Fixed screenshot behavior when `enableLegacyScreenshotMethod: true`, now correctly captures emulated screen instead of complete screen.
- Fixed emulated device handling for Chrome and Edge browsers, now properly sets device metrics based on `deviceMetrics` or `deviceName` capabilities.

## Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))

45 changes: 45 additions & 0 deletions packages/visual-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export default class WdioImageComparisonService extends BaseClass {
} else {
await this.#extendMultiremoteBrowser(capabilities as Capabilities.RequestedMultiremoteCapabilities)
}
// There is an issue with the emulation mode for Chrome or Edge with WebdriverIO v9
// It doesn't set the correct emulation mode for the browser based on the capabilities
// So we need to set the emulation mode manually
// this is a temporary fix until the issue is fixed in WebdriverIO v9 and enough users have upgraded to the latest version
await this.#setEmulation(this.#browser, capabilities)

/**
* add custom matcher for visual comparison when expect has been added.
Expand Down Expand Up @@ -567,4 +572,44 @@ export default class WdioImageComparisonService extends BaseClass {
}
return this._contextManager
}

async #setEmulationForBrowser(browserInstance: WebdriverIO.Browser, capabilities: WebdriverIO.Capabilities) {
if (!browserInstance.isBidi) {
return
}

const chromeMobileEmulation = capabilities['goog:chromeOptions']?.mobileEmulation
const edgeMobileEmulation = capabilities['ms:edgeOptions']?.mobileEmulation
const mobileEmulation = chromeMobileEmulation || edgeMobileEmulation

if (!mobileEmulation) {
return
}

const { deviceName, deviceMetrics } = mobileEmulation

if (deviceName) {
await (browserInstance.emulate as any)('device', deviceName)
return
}

const { pixelRatio: devicePixelRatio = 1, width = 320, height = 658 } = deviceMetrics || {}
await browserInstance.browsingContextSetViewport({
context: await browserInstance.getWindowHandle(),
devicePixelRatio,
viewport: { width, height }
})
}

async #setEmulation(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, capabilities: WebdriverIO.Capabilities) {
if (browser.isMultiremote) {
const multiremoteBrowser = browser as WebdriverIO.MultiRemoteBrowser
for (const browserInstance of Object.values(multiremoteBrowser)) {
await this.#setEmulationForBrowser(browserInstance, browserInstance.capabilities)
}
return
}

await this.#setEmulationForBrowser(browser as WebdriverIO.Browser, capabilities)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,119 @@ describe('getScreenDimensions', () => {

expect(getScreenDimensions()).toMatchSnapshot()
})

it('should detect mobile emulation and return emulated dimensions', () => {
const mockScreen = {
width: 375,
height: 667
}
const originalScreen = window.screen

Object.defineProperty(window, 'screen', {
value: mockScreen,
configurable: true,
writable: true
})
Object.defineProperty(window, 'devicePixelRatio', { value: 3, configurable: true })
Object.defineProperty(window, 'innerWidth', { value: 375, configurable: true })
Object.defineProperty(window, 'innerHeight', { value: 667, configurable: true })
Object.defineProperty(window, 'outerWidth', { value: 375, configurable: true })
Object.defineProperty(window, 'outerHeight', { value: 667, configurable: true })
Object.defineProperty(window, 'matchMedia', {
value: vi.fn().mockImplementation(() => ({
matches: false,
})),
...CONFIGURABLE,
})

const dimensions = getScreenDimensions()

Object.defineProperty(window, 'screen', {
value: originalScreen,
configurable: true,
writable: true
})

expect(dimensions.dimensions.window.screenWidth).toBe(375)
expect(dimensions.dimensions.window.screenHeight).toBe(667)
expect(dimensions.dimensions.window.outerWidth).toBe(375)
expect(dimensions.dimensions.window.outerHeight).toBe(667)
expect(dimensions.dimensions.window.devicePixelRatio).toBe(3)
})

it('should handle desktop (non-emulated) dimensions correctly', () => {
const mockScreen = {
width: 1920,
height: 1080
}
const originalScreen = window.screen

Object.defineProperty(window, 'screen', {
value: mockScreen,
configurable: true,
writable: true
})
Object.defineProperty(window, 'devicePixelRatio', { value: 1, configurable: true })
Object.defineProperty(window, 'innerWidth', { value: 1440, configurable: true })
Object.defineProperty(window, 'innerHeight', { value: 900, configurable: true })
Object.defineProperty(window, 'outerWidth', { value: 1440, configurable: true })
Object.defineProperty(window, 'outerHeight', { value: 900, configurable: true })
Object.defineProperty(window, 'matchMedia', {
value: vi.fn().mockImplementation(() => ({
matches: true,
})),
...CONFIGURABLE,
})

const dimensions = getScreenDimensions()

Object.defineProperty(window, 'screen', {
value: originalScreen,
configurable: true,
writable: true
})

expect(dimensions.dimensions.window.screenWidth).toBe(1920)
expect(dimensions.dimensions.window.screenHeight).toBe(1080)
expect(dimensions.dimensions.window.outerWidth).toBe(1440)
expect(dimensions.dimensions.window.outerHeight).toBe(900)
expect(dimensions.dimensions.window.devicePixelRatio).toBe(1)
})

it('should handle high DPI desktop displays', () => {
const mockScreen = {
width: 2880,
height: 1800
}
const originalScreen = window.screen

Object.defineProperty(window, 'screen', {
value: mockScreen,
configurable: true,
writable: true
})
Object.defineProperty(window, 'devicePixelRatio', { value: 2, configurable: true })
Object.defineProperty(window, 'innerWidth', { value: 1440, configurable: true })
Object.defineProperty(window, 'innerHeight', { value: 900, configurable: true })
Object.defineProperty(window, 'outerWidth', { value: 1440, configurable: true })
Object.defineProperty(window, 'outerHeight', { value: 900, configurable: true })
Object.defineProperty(window, 'matchMedia', {
value: vi.fn().mockImplementation(() => ({
matches: true,
})),
...CONFIGURABLE,
})

const dimensions = getScreenDimensions()

Object.defineProperty(window, 'screen', {
value: originalScreen,
configurable: true,
writable: true
})

expect(dimensions.dimensions.window.devicePixelRatio).toBe(2)
expect(dimensions.dimensions.window.screenWidth).toBe(2880)
expect(dimensions.dimensions.window.screenHeight).toBe(1800)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,93 @@ import type { ScreenDimensions } from './screenDimensions.interfaces.js'
* Get all the screen dimensions
*/
export default function getScreenDimensions(): ScreenDimensions {
// We need to determine if the screen is emulated, because that would return different values
const width = window.innerWidth
const height = window.innerHeight
const dpr = window.devicePixelRatio || 1
const minEdge = Math.min(width, height)
const maxEdge = Math.max(width, height)
const isLikelyEmulated =
dpr >= 2 && // High-DPI signal
minEdge <= 800 && // Catch phones/tablets in portrait/landscape
maxEdge <= 1280 && // Conservative max for emulated tablet sizes
width > 0 && height > 0 // Sanity check

// Other checks
const body = document.body
const html = document.documentElement

const bodyDimensions = {
// On mobile & desktop: Total scrollable height of the body element, including content not visible on screen
scrollHeight: !body ? 0 : body.scrollHeight,
// On mobile & desktop: Height of body element including padding but not margin
offsetHeight: !body ? 0 : body.offsetHeight,
}

const htmlDimensions = {
/** On mobile & desktop: Viewport height excluding scrollbars */
clientHeight: !html ? 0 : html.clientHeight,
/** On mobile & desktop: Viewport width excluding scrollbars */
clientWidth: !html ? 0 : html.clientWidth,
/** On mobile & desktop: Total scrollable height including overflow */
scrollHeight: !html ? 0 : html.scrollHeight,
/** On mobile & desktop: Total scrollable width including overflow */
scrollWidth: !html ? 0 : html.scrollWidth,
/** On mobile & desktop: Height including padding and border */
offsetHeight: !html ? 0 : html.offsetHeight,
}

const windowDimensions = {
/**
* Mobile: Viewport width (changes with zoom)
* Desktop: Viewport width including scrollbars
*/
innerWidth: window.innerWidth,
/**
* Mobile: Viewport height (changes with zoom)
* Desktop: Viewport height including scrollbars
*/
innerHeight: window.innerHeight,
/**
* Mobile: True if device is in landscape orientation
* Desktop: Based on viewport aspect ratio
*/
isLandscape: window.matchMedia('(orientation: landscape)').matches,
outerHeight: window.outerHeight === 0 ? htmlDimensions.clientHeight : window.outerHeight,
outerWidth: window.outerWidth === 0 ? htmlDimensions.clientWidth : window.outerWidth,
/**
* Mobile: Full browser height including UI elements
* Desktop: Browser window height including toolbars/status bar
* Emulated: It will be the same as window.innerHeight
*/
outerHeight: isLikelyEmulated && window.outerHeight > 0?
window.innerHeight :
window.outerHeight === 0 ?
htmlDimensions.clientHeight :
window.outerHeight,
/**
* Mobile: Full browser width
* Desktop: Browser window width
* Emulated: It will be the same as window.innerWidth
*/
outerWidth: isLikelyEmulated && window.outerWidth > 0 ?
window.innerWidth :
window.outerWidth === 0 ?
htmlDimensions.clientWidth :
window.outerWidth,
/**
* Mobile: Physical pixel ratio (typically >1 for high DPI)
* Desktop: Usually 1, or 2 for high DPI displays
*/
devicePixelRatio: window.devicePixelRatio,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
/**
* Mobile: Physical screen width in CSS pixels
* Desktop: Monitor width in pixels
* Emulated: It will be the same as window.innerWidth
*/
screenWidth: isLikelyEmulated ? window.innerWidth : window.screen.width,
/**
* Mobile: Physical screen height in CSS pixels
* Desktop: Monitor height in pixels
* Emulated: It will be the same as window.innerHeight
*/
screenHeight: isLikelyEmulated ? window.innerHeight : window.screen.height,
}

return {
Expand Down
Loading
Loading