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
11 changes: 11 additions & 0 deletions .changeset/fix-await-element-before-execute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@wdio/image-comparison-core": patch
---

## #1129 Fix `TypeError: element.getBoundingClientRect is not a function` when a `ChainablePromiseElement` is passed to `checkElement`

When `checkElement` (or `saveElement`) was called with a `ChainablePromiseElement`, the lazy promise-based element reference that WebdriverIO's `$()` returns, the element was passed directly as an argument to `browser.execute()` without being awaited first. `browser.execute()` serializes its arguments for transfer to the browser context and cannot handle a pending Promise, so it arrived in the browser as a plain empty object `{}` instead of a WebElement reference. This caused `element.getBoundingClientRect is not a function` because the browser-side `scrollElementIntoView` script received `{}` rather than a DOM element.

# Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ChainablePromiseElement } from 'webdriverio'
import type { DeviceRectangles, RectanglesOutput } from './rectangles.interfaces.js'

// === UNIVERSAL BASE INTERFACES ===
Expand Down Expand Up @@ -250,7 +251,7 @@ export interface ElementScreenshotDataOptions extends
/** Whether to automatically scroll the element into view. */
autoElementScroll: boolean;
/** The element to take a screenshot of. */
element: any;
element: HTMLElement | WebdriverIO.Element | ChainablePromiseElement;
/** The inner height. */
innerHeight?: number;
/** Resize dimensions for the screenshot. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ describe('takeElementScreenshot', () => {
expect(executeMock).toHaveBeenCalledTimes(1)
expect(waitForSpy).toHaveBeenCalledWith(100)
})

it('should resolve a Promise-wrapped element (ChainablePromiseElement) before passing to browser.execute()', async () => {
const resolvedElement = { elementId: 'promise-element' }
const optionsWithPromiseElement = {
...baseOptions,
autoElementScroll: true,
element: Promise.resolve(resolvedElement) as any,
}
executeMock.mockResolvedValueOnce(100)

await takeElementScreenshot(browserInstance, optionsWithPromiseElement, true)

expect(getElementRectMock).toHaveBeenCalledWith('promise-element')
// The resolved element (not the Promise) must be passed to browser.execute()
expect(executeMock.mock.calls[0][1]).toEqual(resolvedElement)
})
})

describe('Legacy screenshots', () => {
Expand Down Expand Up @@ -212,6 +228,25 @@ describe('takeElementScreenshot', () => {
expect(waitForSpy).toHaveBeenCalledWith(100)
})

it('should resolve a Promise-wrapped element (ChainablePromiseElement) before passing to browser.execute()', async () => {
const resolvedElement = { elementId: 'promise-element' }
const optionsWithPromiseElement = {
...baseOptions,
autoElementScroll: true,
element: Promise.resolve(resolvedElement) as any,
}
executeMock.mockResolvedValueOnce(100)

await takeElementScreenshot(browserInstance, optionsWithPromiseElement, false)

// The resolved element (not the Promise) must be passed to browser.execute()
expect(executeMock.mock.calls[0][1]).toEqual(resolvedElement)
// And also passed to takeWebElementScreenshot
expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(
expect.objectContaining({ element: resolvedElement })
)
})

it('should enable fallback when resizeDimensions is provided', async () => {
const optionsWithResize = {
...baseOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,24 @@ async function takeBiDiElementScreenshot(
): Promise<ElementScreenshotData> {
const isWebDriverElementScreenshot = false

// Fix #1129: scrollElementIntoView receives a promise
// The element might be a promise, so we need to resolve it before using it as a browser.execute() argument
// if we need to use it in browser.execute()
const element = await (options.element as unknown as WebdriverIO.Element | Promise<WebdriverIO.Element>)

// Scroll the element into the viewport so any lazy‑load / intersection
// observers are triggered. We always capture from the *document* origin,
// so the clip coordinates are document‑relative and independent of scroll.
let currentPosition: number | undefined
if (options.autoElementScroll) {
currentPosition = await browserInstance.execute(scrollElementIntoView as any, options.element, options.addressBarShadowPadding)
currentPosition = await browserInstance.execute(scrollElementIntoView as any, element, options.addressBarShadowPadding)
await waitFor(100)
}

// Get the element rect and clip the screenshot. WebDriver getElementRect
// returns coordinates relative to the document origin, which matches the
// BiDi `origin: 'document'` coordinate system.
const rect = await browserInstance.getElementRect!((await options.element as WebdriverIO.Element).elementId)
const rect = await browserInstance.getElementRect!(element.elementId)
const clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) }
const base64Image = await takeBase64BiDiScreenshot({
browserInstance,
Expand All @@ -63,10 +68,15 @@ async function takeWebDriverElementScreenshot(
let base64Image: string
let isWebDriverElementScreenshot = false

// Fix #1129: scrollElementIntoView receives a promise
// The element might be a promise, so we need to resolve it before using it as a browser.execute() argument
// if we need to use it in browser.execute()
const element = await (options.element as unknown as WebdriverIO.Element | Promise<WebdriverIO.Element>)

// Scroll the element into top of the viewport and return the current scroll position
let currentPosition: number | undefined
if (options.autoElementScroll) {
currentPosition = await browserInstance.execute(scrollElementIntoView as any, options.element, options.addressBarShadowPadding)
currentPosition = await browserInstance.execute(scrollElementIntoView as any, element, options.addressBarShadowPadding)
// We need to wait for the scroll to finish before taking the screenshot
await waitFor(100)
}
Expand All @@ -77,7 +87,7 @@ async function takeWebDriverElementScreenshot(
browserInstance,
devicePixelRatio: options.devicePixelRatio,
deviceRectangles: options.deviceRectangles,
element: options.element,
element,
initialDevicePixelRatio: options.initialDevicePixelRatio,
isEmulated: options.isEmulated,
innerHeight: options.innerHeight,
Expand Down
Loading