Skip to content
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

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

Merged
merged 3 commits into from
Jun 20, 2024
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
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