Skip to content

Commit 8274749

Browse files
feat: support chaining queries
As of WebdriverIO v7.19.0 it is possible to chain custom queries as long as they end in `$`. See the release notes for v7.19.0 here: https://github.com/webdriverio/webdriverio/blob/v7.19.0/CHANGELOG.md Add chainable queries to the `Browser` and `Element` as commands of the form `{queryName}$`, for example the chainable version of `getByText` is `getByText$`. Infer the types for `ChainablePromiseElement` and `ChainablePromiseArray` from the `Browser` and `Element` types, that way we don't need to lock in a specific version of WebdriverIO's types. To add the types of the chainable commands to the global WebdriverIO types users now need to add `WebdriverIOQueriesChainable` to the global `Browser`, `Element` and `ChainablePromiseElement` interfaces. This should not break existing behaviour or types, and the chainable custom commands should behave the same as the existing commands if the WebdriverIO version is less than v7.19.0. Typescript users need to use at least v4.1 as the types now make use of template literal strings to modify the query name to include `$` at the end. Bump development version of WebdriverIO packages to at least v7.19.0 and add tests for chaining queries.
1 parent 09183e8 commit 8274749

File tree

6 files changed

+145
-50
lines changed

6 files changed

+145
-50
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@
4242
"@types/simmerjs": "^0.5.1",
4343
"@typescript-eslint/eslint-plugin": "^4.14.0",
4444
"@typescript-eslint/parser": "^4.14.0",
45-
"@wdio/cli": "^7.12.1",
46-
"@wdio/local-runner": "^7.12.1",
47-
"@wdio/mocha-framework": "^7.12.0",
48-
"@wdio/selenium-standalone-service": "^7.10.1",
49-
"@wdio/spec-reporter": "^7.10.1",
50-
"@wdio/sync": "^7.12.1",
45+
"@wdio/cli": "^7.19.0",
46+
"@wdio/local-runner": "^7.19.0",
47+
"@wdio/mocha-framework": "^7.19.0",
48+
"@wdio/selenium-standalone-service": "^7.19.0",
49+
"@wdio/spec-reporter": "^7.19.0",
50+
"@wdio/sync": "^7.19.0",
5151
"eslint": "^7.6.0",
5252
"kcd-scripts": "^11.1.0",
5353
"npm-run-all": "^4.1.5",

src/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Config,
1717
QueryName,
1818
WebdriverIOQueries,
19+
WebdriverIOQueriesChainable,
1920
ObjectQueryArg,
2021
SerializedObject,
2122
SerializedArg,
@@ -224,8 +225,8 @@ eslint-disable
224225
@typescript-eslint/no-explicit-any,
225226
@typescript-eslint/no-unsafe-argument
226227
*/
227-
function setupBrowser(browser: BrowserBase): WebdriverIOQueries {
228-
const queries: {[key: string]: WebdriverIOQueries[QueryName]} = {}
228+
function setupBrowser<Browser extends BrowserBase>(browser: Browser): WebdriverIOQueries {
229+
const queries: {[key: string | number | symbol]: WebdriverIOQueries[QueryName]} = {}
229230

230231
Object.keys(baseQueries).forEach((key) => {
231232
const queryName = key as QueryName
@@ -240,20 +241,28 @@ function setupBrowser(browser: BrowserBase): WebdriverIOQueries {
240241
// add query to response queries
241242
queries[queryName] = query as WebdriverIOQueries[QueryName]
242243

243-
// add query to BrowserObject
244+
// add query to BrowserObject and Elements
244245
browser.addCommand(queryName, query as WebdriverIOQueries[QueryName])
245-
246-
// add query to Elements
247246
browser.addCommand(
248247
queryName,
249248
function (this, ...args) {
250249
return within(this)[queryName](...args)
251250
},
252251
true,
253252
)
253+
254+
// add chainable query to BrowserObject and Elements
255+
browser.addCommand(`${queryName}$`, query as WebdriverIOQueriesChainable<Browser>[`${QueryName}$`])
256+
browser.addCommand(
257+
`${queryName}$`,
258+
function (this, ...args) {
259+
return within(this)[queryName](...args)
260+
},
261+
true,
262+
)
254263
})
255264

256-
return queries as WebdriverIOQueries
265+
return queries as unknown as WebdriverIOQueries
257266
}
258267
/*
259268
eslint-enable

src/types.ts

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
SelectorMatcherOptions,
77
MatcherOptions,
88
} from '@testing-library/dom'
9+
import {SelectorsBase} from './wdio-types'
10+
11+
export type Queries = typeof queries
12+
export type QueryName = keyof Queries
913

1014
export type Config = Pick<
1115
BaseConfig,
@@ -16,53 +20,72 @@ export type Config = Pick<
1620
| 'throwSuggestions'
1721
>
1822

19-
export type WebdriverIOQueryReturnType<T> = T extends Promise<HTMLElement>
20-
? WebdriverIO.Element
21-
: T extends HTMLElement
22-
? WebdriverIO.Element
23-
: T extends Promise<HTMLElement[]>
24-
? WebdriverIO.Element[]
25-
: T extends HTMLElement[]
26-
? WebdriverIO.Element[]
27-
: T extends null
28-
? null
29-
: never
23+
export type WebdriverIOQueryReturnType<Element, ElementArray, T> =
24+
T extends Promise<HTMLElement>
25+
? Element
26+
: T extends HTMLElement
27+
? Element
28+
: T extends Promise<HTMLElement[]>
29+
? ElementArray
30+
: T extends HTMLElement[]
31+
? ElementArray
32+
: T extends null
33+
? null
34+
: never
3035

31-
export type WebdriverIOBoundFunction<T> = (
36+
export type WebdriverIOBoundFunction<Element, ElementArray, T> = (
3237
...params: Parameters<BoundFunctionBase<T>>
33-
) => Promise<WebdriverIOQueryReturnType<ReturnType<BoundFunctionBase<T>>>>
38+
) => Promise<
39+
WebdriverIOQueryReturnType<
40+
Element,
41+
ElementArray,
42+
ReturnType<BoundFunctionBase<T>>
43+
>
44+
>
3445

35-
export type WebdriverIOBoundFunctionSync<T> = (
46+
export type WebdriverIOBoundFunctionSync<Element, ElementArray, T> = (
3647
...params: Parameters<BoundFunctionBase<T>>
37-
) => WebdriverIOQueryReturnType<ReturnType<BoundFunctionBase<T>>>
48+
) => WebdriverIOQueryReturnType<
49+
Element,
50+
ElementArray,
51+
ReturnType<BoundFunctionBase<T>>
52+
>
3853

39-
export type WebdriverIOBoundFunctions<T> = {
40-
[P in keyof T]: WebdriverIOBoundFunction<T[P]>
54+
export type WebdriverIOQueries = {
55+
[P in keyof Queries]: WebdriverIOBoundFunction<
56+
WebdriverIO.Element,
57+
WebdriverIO.Element[],
58+
Queries[P]
59+
>
4160
}
4261

43-
export type WebdriverIOBoundFunctionsSync<T> = {
44-
[P in keyof T]: WebdriverIOBoundFunctionSync<T[P]>
62+
export type WebdriverIOQueriesSync = {
63+
[P in keyof Queries]: WebdriverIOBoundFunctionSync<
64+
WebdriverIO.Element,
65+
WebdriverIO.Element[],
66+
Queries[P]
67+
>
4568
}
4669

47-
export type WebdriverIOQueries = WebdriverIOBoundFunctions<typeof queries>
48-
export type WebdriverIOQueriesSync = WebdriverIOBoundFunctionsSync<
49-
typeof queries
50-
>
51-
52-
export type QueryName = keyof typeof queries
70+
export type WebdriverIOQueriesChainable<
71+
Container extends SelectorsBase | undefined,
72+
> = {
73+
[P in keyof Queries as `${string & P}$`]: Container extends SelectorsBase
74+
? WebdriverIOBoundFunctionSync<
75+
ReturnType<Container['$']>,
76+
ReturnType<Container['$$']>,
77+
Queries[P]
78+
>
79+
: undefined
80+
}
5381

5482
export type ObjectQueryArg =
5583
| MatcherOptions
5684
| queries.ByRoleOptions
5785
| SelectorMatcherOptions
5886
| waitForOptions
5987

60-
export type QueryArg =
61-
| ObjectQueryArg
62-
| RegExp
63-
| number
64-
| string
65-
| undefined
88+
export type QueryArg = ObjectQueryArg | RegExp | number | string | undefined
6689

6790
export type SerializedObject = {
6891
serialized: 'object'

src/wdio-types.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@ export type $ = (
2222
| Promise<WebdriverIO.Element>
2323
| WebdriverIO.Element
2424

25+
export type $$ = (
26+
selector: any,
27+
) =>
28+
| ChainablePromiseArrayBase<Promise<WebdriverIO.Element>>
29+
| Promise<WebdriverIO.Element[]>
30+
| WebdriverIO.Element[]
31+
2532
export type ChainablePromiseElementBase<T> = Promise<T> & {$: $}
33+
export type ChainablePromiseArrayBase<T> = Promise<T>
2634

27-
export type ElementBase = {
35+
export type SelectorsBase = {
2836
$: $
37+
$$: $$
38+
}
39+
40+
export type ElementBase = SelectorsBase & {
2941
parent: {
3042
execute<T>(
3143
script: string | ((...args: any[]) => T),
@@ -38,9 +50,7 @@ export type ElementBase = {
3850
}
3951
}
4052

41-
export type BrowserBase = {
42-
$: $
43-
53+
export type BrowserBase = SelectorsBase & {
4454
addCommand<T extends boolean>(
4555
queryName: string,
4656
commandFn: (

test/async/chaining.e2e.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {setupBrowser} from '../../src'
2+
3+
describe('chaining', () => {
4+
it('can chain browser getBy queries', async () => {
5+
setupBrowser(browser)
6+
7+
const button = await browser
8+
.getByTestId$('nested')
9+
.getByText$('Button Text')
10+
11+
await button.click()
12+
13+
expect(await button.getText()).toEqual('Button Clicked')
14+
})
15+
16+
it('can chain element getBy queries', async () => {
17+
const {getByTestId} = setupBrowser(browser)
18+
19+
const nested = await getByTestId('nested')
20+
await nested.getByText$('Button Text').click()
21+
22+
expect(await browser.getByText$('Button Clicked').getText()).toEqual(
23+
'Button Clicked',
24+
)
25+
})
26+
27+
it('can chain browser getAllBy queries', async () => {
28+
setupBrowser(browser)
29+
30+
await browser.getByTestId$('nested').getAllByText$('Button Text')[0].click()
31+
32+
expect(await browser.getAllByText('Button Clicked')).toHaveLength(1)
33+
})
34+
35+
it('can chain element getAllBy queries', async () => {
36+
const {getByTestId} = setupBrowser(browser)
37+
38+
const nested = await getByTestId('nested')
39+
await nested.getAllByText$('Button Text')[0].click()
40+
41+
expect(await nested.getAllByText('Button Clicked')).toHaveLength(1)
42+
})
43+
})

test/async/types.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ eslint-disable
44
@typescript-eslint/no-empty-interface
55
*/
66

7-
import {WebdriverIOQueries} from '../../src'
7+
import {WebdriverIOQueriesChainable, WebdriverIOQueries} from '../../src'
8+
import {SelectorsBase} from '../../src/wdio-types'
89

910
declare global {
1011
namespace WebdriverIO {
11-
interface Browser extends WebdriverIOQueries {}
12-
interface Element extends WebdriverIOQueries {}
12+
interface Browser
13+
extends WebdriverIOQueries,
14+
WebdriverIOQueriesChainable<Browser> {}
15+
interface Element
16+
extends WebdriverIOQueries,
17+
WebdriverIOQueriesChainable<Element> {}
1318
}
1419
}
20+
21+
declare module 'webdriverio' {
22+
interface ChainablePromiseElement<T extends SelectorsBase | undefined>
23+
extends WebdriverIOQueriesChainable<T> {}
24+
}

0 commit comments

Comments
 (0)