Skip to content

Commit

Permalink
breaking(webdriverio): remove Promise<T> adjective from ChainableProm…
Browse files Browse the repository at this point in the history
…iseElement (#13051)

* breaking(webdriverio): remove Promise<T> adjective from ChainablePromiseElement

* fix shim

* ignore error due to issues with expect-webdriverio
  • Loading branch information
christian-bromann committed Jun 20, 2024
1 parent c38781b commit 986cfe8
Show file tree
Hide file tree
Showing 16 changed files with 64 additions and 31 deletions.
2 changes: 1 addition & 1 deletion packages/wdio-browser-runner/src/browser/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const COMMAND_TIMEOUT = 30 * 1000 // 30s
* @returns a matcher result computed in the Node.js environment
*/
function createMatcher (matcherName: string) {
return async function (this: MatcherContext, context: WebdriverIO.Browser | WebdriverIO.Element | ChainablePromiseElement<WebdriverIO.Element> | ChainablePromiseArray, ...args: any[]) {
return async function (this: MatcherContext, context: WebdriverIO.Browser | WebdriverIO.Element | ChainablePromiseElement | ChainablePromiseArray, ...args: any[]) {
const cid = getCID()
if (!import.meta.hot || !cid) {
return {
Expand Down
11 changes: 11 additions & 0 deletions packages/wdio-utils/src/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const ELEMENT_PROPS = [
]
const ACTION_COMMANDS = ['action', 'actions']
const PROMISE_METHODS = ['then', 'catch', 'finally']
const ELEMENT_RETURN_COMMANDS = ['getElement', 'getElements']

const TIME_BUFFER = 3

Expand Down Expand Up @@ -238,6 +239,16 @@ export function wrapCommand<T>(commandName: string, fn: Function): (...args: any
return target[prop as 'then' | 'catch' | 'finally'].bind(target)
}

/**
* Convenience methods to get the element promise. Technically we could just
* await an `ChainablePromiseElement` directly but this causes bad DX when
* chaining commands and e.g. VS Code tries to wrap promises around thenable
* objects.
*/
if (ELEMENT_RETURN_COMMANDS.includes(prop)) {
return () => target
}

/**
* call a command on an element query, e.g.:
* ```js
Expand Down
4 changes: 3 additions & 1 deletion packages/webdriverio/src/commands/element/dragAndDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ElementReference } from '@wdio/protocols'

import { getBrowserObject } from '@wdio/utils'

import type { ChainablePromiseElement } from '../../types.js'

const ACTION_BUTTON = 0 as const

type DragAndDropOptions = {
Expand Down Expand Up @@ -47,7 +49,7 @@ type ElementCoordinates = {
*/
export async function dragAndDrop (
this: WebdriverIO.Element,
target: WebdriverIO.Element | ElementCoordinates,
target: WebdriverIO.Element | ChainablePromiseElement | ElementCoordinates,
{ duration = 10 }: DragAndDropOptions = {}
) {
const moveToCoordinates = target as ElementCoordinates
Expand Down
4 changes: 3 additions & 1 deletion packages/webdriverio/src/commands/element/isExisting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,7 @@ export async function isExisting (this: WebdriverIO.Element) {
: this.isShadowElement
? this.shadow$$.bind(this.parent)
: this.parent.$$.bind(this.parent)
return command(this.selector as string).then((res) => res.length > 0)
return command(this.selector as string)
.getElements()
.then((res) => res.length > 0)
}
19 changes: 13 additions & 6 deletions packages/webdriverio/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type $ElementCommands = typeof ElementCommands
type ElementQueryCommands = '$' | 'custom$' | 'shadow$' | 'react$'
type ElementsQueryCommands = '$$' | 'custom$$' | 'shadow$$' | 'react$$'
type ChainablePrototype = {
[K in ElementQueryCommands]: (...args: Parameters<$ElementCommands[K]>) => ChainablePromiseElement<ThenArg<ReturnType<$ElementCommands[K]>>>
[K in ElementQueryCommands]: (...args: Parameters<$ElementCommands[K]>) => ChainablePromiseElement
} & {
[K in ElementsQueryCommands]: (...args: Parameters<$ElementCommands[K]>) => ChainablePromiseArray<ThenArg<ReturnType<$ElementCommands[K]>>>
[K in ElementsQueryCommands]: (...args: Parameters<$ElementCommands[K]>) => ChainablePromiseArray
}

type AsyncElementProto = {
Expand Down Expand Up @@ -49,11 +49,14 @@ interface ChainablePromiseBaseElement {
* index of the element if fetched with `$$`
*/
index?: Promise<number>
/**
* get the `WebdriverIO.Element` reference
*/
getElement(): Promise<WebdriverIO.Element>
}
export interface ChainablePromiseElement<T> extends
export interface ChainablePromiseElement extends
ChainablePromiseBaseElement,
AsyncElementProto,
Promise<T>,
Omit<WebdriverIO.Element, keyof ChainablePromiseBaseElement | keyof AsyncElementProto> {}

interface AsyncIterators<T> {
Expand All @@ -77,7 +80,7 @@ interface AsyncIterators<T> {
reduce: <T, U>(callback: (accumulator: U, currentValue: WebdriverIO.Element, currentIndex: number, array: T[]) => U | Promise<U>, initialValue?: U) => Promise<U>;
}

export interface ChainablePromiseArray<T> extends Promise<T>, AsyncIterators<T> {
export interface ChainablePromiseArray extends AsyncIterators<WebdriverIO.Element> {
[Symbol.asyncIterator](): AsyncIterableIterator<WebdriverIO.Element>

/**
Expand All @@ -98,7 +101,11 @@ export interface ChainablePromiseArray<T> extends Promise<T>, AsyncIterators<T>
/**
* allow to access a specific index of the element set
*/
[n: number]: ChainablePromiseElement<WebdriverIO.Element | undefined>
[n: number]: ChainablePromiseElement
/**
* get the `WebdriverIO.Element[]` list
*/
getElements(): Promise<WebdriverIO.ElementArray>
}

export type BrowserCommandsType = Omit<$BrowserCommands, keyof ChainablePrototype> & ChainablePrototype
Expand Down
2 changes: 1 addition & 1 deletion packages/webdriverio/src/utils/actions/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const MOVE_PARAM_DEFAULTS = {
x: 0,
y: 0,
duration: 100,
origin: ORIGIN_DEFAULT as (Origin | ElementReference | ChainablePromiseElement<WebdriverIO.Element> | WebdriverIO.Element)
origin: ORIGIN_DEFAULT as (Origin | ElementReference | ChainablePromiseElement | WebdriverIO.Element)
}

type PointerActionParams = Partial<typeof PARAM_DEFAULTS> & Partial<PointerActionUpParams>
Expand Down
2 changes: 1 addition & 1 deletion packages/webdriverio/src/utils/actions/wheel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ScrollParams {
/**
* element origin
*/
origin?: WebdriverIO.Element | ChainablePromiseElement<WebdriverIO.Element>
origin?: WebdriverIO.Element | ChainablePromiseElement
/**
* duration ratio be the ratio of time delta and duration
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/webdriverio/src/utils/implicitWait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default async function implicitWait (currentElement: WebdriverIO.Element,
/**
* if waitForExist was successful requery element and assign elementId to the scope
*/
return (currentElement.parent as WebdriverIO.Element).$(currentElement.selector)
return (currentElement.parent as WebdriverIO.Element).$(currentElement.selector).getElement()
} catch {
if (currentElement.selector.toString().includes('this.previousElementSibling')) {
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion packages/webdriverio/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ export async function hasElementId (element: WebdriverIO.Element) {
: element.isShadowElement
? element.parent.shadow$.bind(element.parent)
: element.parent.$.bind(element.parent)
element.elementId = (await command(element.selector as string)).elementId
element.elementId = (await command(element.selector as string).getElement()).elementId
}

/*
Expand Down
4 changes: 2 additions & 2 deletions packages/webdriverio/src/utils/refetchElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export default async function refetchElement (
*/
return selectors.reduce(async (elementPromise, { selector, index }, currentIndex) => {
const resolvedElement = await elementPromise
let nextElement = index > 0 ? (await resolvedElement.$$(selector as string))[index] : null
nextElement = nextElement || await resolvedElement.$(selector)
let nextElement = index > 0 ? await resolvedElement.$$(selector as string)[index]?.getElement() : null
nextElement = nextElement || await resolvedElement.$(selector).getElement()
/**
* For error purposes, changing command name to '$' if we aren't
* on the last element of the array
Expand Down
8 changes: 6 additions & 2 deletions packages/webdriverio/tests/commands/element/shadow$$.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,19 @@ describe('shadow$$', () => {
})
const errorResponse = { error: 'ups' }
const el = await browser.$('#foo')
// @ts-expect-error mock feature
fetch.setMockResponse([errorResponse, errorResponse, errorResponse, errorResponse])
const queryResult = [{ elem: 123 }]
// @ts-expect-error
queryResult.getElements = vi.fn().mockReturnValue(queryResult)
const mock: any = {
$$: vi.fn().mockReturnValue([{ elem: 123 }]),
options: {},
selector: 'foo',
}
mock.parent = { $: vi.fn().mockReturnValue({}) }
mock.parent = { $: vi.fn().mockReturnValue({ getElement: () => ({}) }) }
mock.waitForExist = vi.fn().mockResolvedValue(mock)
const elem = await el.shadow$$.call(mock, '#shadowfoo')
const elem = await el.shadow$$.call(mock, '#shadowfoo').getElements()
expect(elem).toEqual([{ elem: 123 }])

expect(vi.mocked(fetch).mock.calls[1][0]!.pathname)
Expand Down
9 changes: 5 additions & 4 deletions packages/webdriverio/tests/commands/element/shadow$.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ describe('shadow$', () => {
expect(subElem.elementId).toBe('some-shadow-sub-elem-321')
expect(subElem[ELEMENT_KEY]).toBe('some-shadow-sub-elem-321')

expect(vi.mocked(fetch).mock.calls[2][0]!.pathname)
expect((vi.mocked(fetch).mock.calls[2][0] as any).pathname)
.toBe('/session/foobar-123/element/some-elem-123/shadow')
expect(vi.mocked(fetch).mock.calls[3][0]!.pathname)
expect((vi.mocked(fetch).mock.calls[3][0] as any).pathname)
.toBe('/session/foobar-123/shadow/some-shadow-elem-123/element')
})

Expand Down Expand Up @@ -63,18 +63,19 @@ describe('shadow$', () => {
})
const errorResponse = { error: 'ups' }
const el = await browser.$('#foo')
// @ts-expect-error mock feature
fetch.setMockResponse([errorResponse, errorResponse, errorResponse, errorResponse])
const mock: any = {
$: vi.fn().mockReturnValue({ elem: 123 }),
options: {},
selector: 'foo',
}
mock.parent = { $: vi.fn().mockReturnValue({}) }
mock.parent = { $: vi.fn().mockReturnValue({ getElement: () => ({}) }) }
mock.waitForExist = vi.fn().mockResolvedValue(mock)
const elem = await el.shadow$.call(mock, '#shadowfoo')
expect(elem).toEqual({ elem: 123 })

expect(vi.mocked(fetch).mock.calls[1][0]!.pathname)
expect((vi.mocked(fetch).mock.calls[1][0] as any).pathname)
.toBe('/session/foobar-123/element')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe('waitForClickable', () => {
isClickable : tmpElem.isClickable,
options : { waitforTimeout : 500, waitforInterval: 50 },
}
elem.getElement = () => Promise.resolve(elem)

try {
await elem.waitForClickable({ timeout: duration })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ describe('waitForDisplayed', () => {
isDisplayed: tmpElem.isDisplayed,
options: { waitforTimeout: 500, waitforInterval: 50 },
} as any as WebdriverIO.Element
// @ts-expect-error
elem.getElement = () => Promise.resolve(elem)

try {
await elem.waitForDisplayed({ timeout })
Expand Down
19 changes: 11 additions & 8 deletions tests/typings/webdriverio/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function bar() {
// instances array
expectType<string[]>(mr.instances)

const elements = await browser.$$('foo')
const elements = await browser.$$('foo').getElements()
expectType<string>(elements.foundWith)

////////////////////////////////////////////////////////////////////////////////
Expand All @@ -80,8 +80,11 @@ async function bar() {

const elemA = await remoteBrowser.$('')
const elemB = await remoteBrowser.$('')
const multipleElems = await $$([elemA, elemB])
const multipleElems = await $$([elemA, elemB]).getElements()
const multipleElemsChain = $$([elemA, elemB])
expectType<WebdriverIO.ElementArray>(multipleElems)
// @ts-expect-error
expectType<ChainablePromiseArray>(multipleElemsChain)

////////////////////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -142,7 +145,7 @@ async function bar() {
)
expectType<true | void>(waitUntil)
const waitUntilElems = await browser.waitUntil(async () => {
const elems = await $$('elems')
const elems = await $$('elems').getElements()
if (elems.length < 2) {
return false
}
Expand Down Expand Up @@ -247,7 +250,7 @@ async function bar() {
expectType<number>(ambientResult)

// $
const el1 = await $('')
const el1 = await $('').getElement()
const strFunction = (str: string) => str
strFunction(el1.selector as string)
strFunction(el1.elementId)
Expand Down Expand Up @@ -303,7 +306,7 @@ async function bar() {
expectType<number>(elcResult)

// $$
const elems = await $$('')
const elems = await $$('').getElements()
const el4 = elems[0]
const el5 = await el4.$('')
expectType<string>(await el4.getAttribute('class'))
Expand Down Expand Up @@ -377,7 +380,7 @@ async function bar() {
const ele = await $('')
const touchAction: TouchAction = {
action: 'longPress',
element: await $(''),
element: await $('').getElement(),
ms: 0,
x: 0,
y: 0
Expand Down Expand Up @@ -439,7 +442,7 @@ async function bar() {

// async chain API
expectType<WebdriverIO.Element>(
await browser.$('foo').$('bar').$$('loo')[2].$('foo').$('bar'))
await browser.$('foo').$('bar').$$('loo')[2].$('foo').$('bar').getElement())
expectType<Selector>(
await browser.$('foo').$('bar').selector)
expectType<Error>(
Expand All @@ -457,7 +460,7 @@ async function bar() {

// promise chain API
expectType<string>(
await browser.$('foo').then(_ => _.getText()))
await browser.$('foo').getElement().then(_ => _.getText()))

expectType<void>(
await browser.$$('foo').forEach(() => true)
Expand Down
4 changes: 2 additions & 2 deletions tests/typings/webdriverio/globalImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { $, $$, browser, driver, multiremotebrowser } from '@wdio/globals'
import { fn, spyOn, mock, unmock, mocked } from '@wdio/browser-runner'

;(async () => {
const elem = await $('foo')
const elem = await $('foo').getElement()
expectType<string>(elem.elementId)
const label = await $('foo').$('bar').getComputedLabel()
expectType<string>(label)

const elems = await $$('foo')
const elems = await $$('foo').getElements()
expectType<string>(elems.foundWith)
const tagNames = await $$('foo').map((el) => el.getTagName())
expectType<string[]>(tagNames)
Expand Down

0 comments on commit 986cfe8

Please sign in to comment.