import { webdriverMonad, wrapCommand } from '@wdio/utils'
import clone from 'lodash.clonedeep'
import { ELEMENT_KEY } from 'webdriver'
import { getBrowserObject } from '@wdio/utils'
import type { ElementReference } from '@wdio/protocols'
import { getPrototype as getWDIOPrototype, getElementFromResponse } from './index.js'
import { elementErrorHandler } from '../middlewares.js'
import * as browserCommands from '../commands/browser.js'
import type { Selector, AddCommandFn, ExtendedElementReference } from '../types.js'
interface GetElementProps {
isReactElement?: boolean
isShadowElement?: boolean
}
interface WebDriverErrorResponse {
error: string,
message: string
stacktrace: string,
}
class WebDriverError extends Error {
constructor(obj: Error | WebDriverErrorResponse) {
const { name, stack } = obj as Error
const { error, stacktrace } = obj as WebDriverErrorResponse
super(error || name || '')
Object.assign(this, {
message: obj.message,
stack: stacktrace || stack,
})
}
}
* transforms a findElement response into a WDIO element
* @param {string} selector selector that was used to query the element
* @param {Object} res findElement response
* @return {Object} WDIO element object
*/
export function getElement(
this: WebdriverIO.Browser | WebdriverIO.Element,
selector?: Selector,
res?: ElementReference | ExtendedElementReference | Error,
props: GetElementProps = { isReactElement: false, isShadowElement: false }
): WebdriverIO.Element {
const browser = getBrowserObject(this)
const browserCommandKeys = Object.keys(browserCommands)
const propertiesObject: PropertyDescriptorMap = {
* filter out browser commands from object
*/
...(Object.entries(clone(browser.__propertiesObject__)).reduce((commands, [name, descriptor]) => {
if (!browserCommandKeys.includes(name)) {
commands[name] = descriptor
}
return commands
}, {} as Record<string, PropertyDescriptor>)),
...getWDIOPrototype('element'),
scope: { value: 'element' }
}
propertiesObject.emit = { value: this.emit.bind(this) }
const element = webdriverMonad(this.options, (client: WebdriverIO.Element) => {
const elementId = getElementFromResponse(res as ElementReference)
if (elementId) {
* set elementId for easy access
*/
client.elementId = elementId
* set element id with proper key so element can be passed into execute commands
*/
client[ELEMENT_KEY] = elementId
* Attach locator if element was fetched with WebDriver Bidi.
* This allows to later re-fetch the element within the same conditions.
*/
if (res && this.isBidi && 'locator' in res) {
client.locator = res.locator
}
} else {
client.error = res as Error
}
if (selector) {
client.selector = selector
}
client.parent = this
client.isReactElement = props.isReactElement
client.isShadowElement = props.isShadowElement
return client
}, propertiesObject)
const elementInstance = element(this.sessionId as string, elementErrorHandler(wrapCommand))
const origAddCommand = elementInstance.addCommand.bind(elementInstance)
elementInstance.addCommand = (name: string, fn: Function) => {
browser.__propertiesObject__[name] = { value: fn }
origAddCommand(name, fn)
}
return elementInstance
}
* transforms a findElements response into an array of WDIO elements
* @param {string} selector selector that was used to query the element
* @param {Object} res findElements response
* @return {Array} array of WDIO elements
*/
export const getElements = function getElements(
this: WebdriverIO.Browser | WebdriverIO.Element,
selector: Selector | ElementReference[] | WebdriverIO.Element[],
elemResponse: (ElementReference | ExtendedElementReference | Error | WebDriverError)[],
props: GetElementProps = { isReactElement: false, isShadowElement: false }
): WebdriverIO.Element[] {
const browser = getBrowserObject(this as WebdriverIO.Element)
const browserCommandKeys = Object.keys(browserCommands)
const propertiesObject: PropertyDescriptorMap = {
* filter out browser commands from object
*/
...(Object.entries(clone(browser.__propertiesObject__)).reduce((commands, [name, descriptor]) => {
if (!browserCommandKeys.includes(name)) {
commands[name] = descriptor
}
return commands
}, {} as Record<string, PropertyDescriptor>)),
...getWDIOPrototype('element')
}
if (elemResponse.length === 0) {
return []
}
const elements = [elemResponse].flat(1).map((res: ElementReference | ExtendedElementReference | Element | Error | WebDriverError, i) => {
* if we already deal with an element, just return it
*/
if ((res as WebdriverIO.Element).selector && '$$' in res) {
return res as WebdriverIO.Element
}
propertiesObject.scope = { value: 'element' }
propertiesObject.emit = { value: this.emit.bind(this) }
const element = webdriverMonad(this.options, (client: WebdriverIO.Element) => {
const elementId = getElementFromResponse(res as ElementReference)
if (elementId) {
* set elementId for easy access
*/
client.elementId = elementId
* set element id with proper key so element can be passed into execute commands
*/
client[ELEMENT_KEY] = elementId
* Attach locator if element was fetched with WebDriver Bidi.
* This allows to later re-fetch the element within the same conditions.
*/
if (res && this.isBidi && 'locator' in res) {
client.locator = res.locator
}
} else {
res = res as WebDriverError | Error
client.error = res instanceof Error ? res : new WebDriverError(res)
}
client.selector = Array.isArray(selector)
? (selector[i] as WebdriverIO.Element).selector
: selector
client.parent = this
client.index = i
client.isReactElement = props.isReactElement
client.isShadowElement = props.isShadowElement
return client
}, propertiesObject)
const elementInstance: WebdriverIO.Element = element((this as WebdriverIO.Browser).sessionId, elementErrorHandler(wrapCommand))
const origAddCommand = elementInstance.addCommand.bind(elementInstance)
elementInstance.addCommand = (name: string, fn: AddCommandFn) => {
browser.__propertiesObject__[name] = { value: fn }
origAddCommand(name, fn)
}
return elementInstance
})
return elements
}