import type { ElementReference } from '@wdio/protocols' import { findElements, enhanceElementsArray, isElement, findElement } from '../../utils/index.js' import { getElements, getElement } from '../../utils/getElementObject.js' import { findDeepElements } from '../../utils/index.js' import { DEEP_SELECTOR } from '../../constants.js' import type { Selector } from '../../types.js' /** * The `$$` command is a short and handy way in order to fetch multiple elements on the page. * It returns a `ChainablePromiseArray` containing a set of WebdriverIO elements. * * Using the wdio testrunner this command is a global variable, see [Globals](https://webdriver.io/docs/api/globals) * for more information. Using WebdriverIO within a [standalone](https://webdriver.io/docs/setuptypes#standalone-mode) * script it will be located on the browser object instead (e.g. `browser.$$`). * * You can chain `$` or `$$` together without wrapping individual commands into `await` in order * to walk down the DOM tree, e.g.: * * ```js * const imageSrc = await $$('div')[1].nextElement().$$('img')[2].getAttribute('src') * ``` * * It is also possible to use async iterators to loop over the result of the query, e.g.: * * ```js * // print all image sources * for await (const img of $$('img')) { * console.log(await img.getAttribute('src')) * } * ``` * * :::info * * For more information on how to select specific elements, check out the [Selectors](/docs/selectors) guide. * * ::: * * @alias $$ * @param {String|Function} selector selector or JS Function to fetch multiple elements * @return {WebdriverIO.ElementArray} * @example https://github.com/webdriverio/example-recipes/blob/59c122c809d44d343c231bde2af7e8456c8f086c/queryElements/example.html * @example https://github.com/webdriverio/example-recipes/blob/59c122c809d44d343c231bde2af7e8456c8f086c/queryElements/multipleElements.js#L6-L7 * @example https://github.com/webdriverio/example-recipes/blob/59c122c809d44d343c231bde2af7e8456c8f086c/queryElements/multipleElements.js#L15-L24 * @example https://github.com/webdriverio/example-recipes/blob/59c122c809d44d343c231bde2af7e8456c8f086c/queryElements/multipleElements.js#L32-L39 * @type utility * */ export async function $$ ( this: WebdriverIO.Browser | WebdriverIO.Element, selector: Selector | ElementReference[] | WebdriverIO.Element[] | HTMLElement[] ): Promise { /** * do a deep lookup if * - we are using Bidi * - have a string selector * - that is not a deep selector */ if (this.isBidi && typeof selector === 'string' && !selector.startsWith(DEEP_SELECTOR)) { /** * run this in Node.js land if we are using browser runner */ if (globalThis.wdio?.execute) { const command = '$$' as const const res = 'elementId' in this ? await globalThis.wdio.executeWithScope(command, this.elementId, selector) as unknown as ElementReference[] : await globalThis.wdio.execute(command, selector) as unknown as ElementReference[] const elements = await getElements.call(this, selector as Selector, res) return enhanceElementsArray(elements, this, selector as Selector) as WebdriverIO.ElementArray } const res = await findDeepElements.call(this, selector) const elements = await getElements.call(this, selector as Selector, res) return enhanceElementsArray(elements, getParent.call(this, res), selector as Selector) as WebdriverIO.ElementArray } let res: (ElementReference | Error)[] = Array.isArray(selector) ? selector as ElementReference[] : await findElements.call(this, selector) /** * allow user to transform a set of HTMLElements into a set of WebdriverIO elements */ if (Array.isArray(selector) && isElement(selector[0])) { res = [] for (const el of selector) { const $el = await findElement.call(this, el) if ($el) { res.push($el) } } } const elements = await getElements.call(this, selector as Selector, res) return enhanceElementsArray(elements, getParent.call(this, res), selector as Selector) as WebdriverIO.ElementArray } function getParent (this: WebdriverIO.Browser | WebdriverIO.Element, res: ElementReference[]) { /** * Define scope of element. In most cases it is `this` but if we pass through * an element object from the browser runner we have to look into the parent * provided by the selector object. Since these objects are passed through * as raw objects without any prototype we have to check if the `$` or `$$` * is defined on the object itself and if not, create a new element object. */ let parent = res.length > 0 ? (res[0] as WebdriverIO.Element).parent || this : this if (typeof parent.$ === 'undefined') { parent = 'selector' in parent ? getElement.call(this, parent.selector, parent) : this } return parent }