Skip to content

Commit 4193c7c

Browse files
feat: Add Typescript support
1 parent c1ef5c1 commit 4193c7c

12 files changed

+173
-45
lines changed

.eslintrc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
{
2-
"extends": "./node_modules/kcd-scripts/eslint.js",
2+
"parser": "@typescript-eslint/parser",
3+
"extends": [
4+
"plugin:@typescript-eslint/recommended",
5+
"./node_modules/kcd-scripts/eslint.js"
6+
],
37
"rules": {
48
"babel/new-cap": "off",
59
"func-names": "off",
610
"babel/no-unused-expressions": "off",
711
"prefer-arrow-callback": "off",
812
"testing-library/no-await-sync-query": "off",
913
"testing-library/no-dom-import": "off",
10-
"testing-library/prefer-screen-queries": "off"
14+
"testing-library/prefer-screen-queries": "off",
15+
"@typescript-eslint/no-var-requires": "off"
1116
},
1217
"overrides": [
1318
{

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,37 @@ it('lets you configure queries', async () => {
138138
})
139139
```
140140

141+
### Typescript
142+
143+
All the above methods are fully typed. To use the Browser and Element commands
144+
added by `setupBrowser` the global `WebdriverIO` namespace will need to be
145+
modified. Add the following to a typescript module:
146+
147+
```
148+
import {WebdriverIOQueries} from 'webdriverio-testing-library';
149+
150+
declare global {
151+
namespace WebdriverIO {
152+
interface Browser extends WebdriverIOQueries {}
153+
interface Element extends WebdriverIOQueries {}
154+
}
155+
}
156+
```
157+
158+
If you are using the `@wdio/sync` framework you will need to use the
159+
`WebdriverIOQueriesSync` type to extend the interfaces:
160+
161+
```
162+
import {WebdriverIOQueriesSync} from 'webdriverio-testing-library';
163+
164+
declare global {
165+
namespace WebdriverIO {
166+
interface Browser extends WebdriverIOQueriesSync {}
167+
interface Element extends WebdriverIOQueriesSync {}
168+
}
169+
}
170+
```
171+
141172
## Other Solutions
142173

143174
I'm not aware of any, if you are please [make a pull request][prs] and add it

package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@
33
"version": "1.1.0",
44
"description": "",
55
"main": "dist/index.js",
6-
"typings": "typings",
6+
"types": "dist/index.d.ts",
77
"scripts": {
88
"add-contributor": "kcd-scripts contributors add",
9-
"build": "kcd-scripts build",
9+
"build": "tsc -p tsconfig.build.json",
1010
"lint": "kcd-scripts lint",
1111
"test:unit": "kcd-scripts test --no-watch --config=jest.config.js",
1212
"validate": "kcd-scripts validate build,lint,test",
1313
"test": "wdio wdio.conf.js",
1414
"semantic-release": "semantic-release"
1515
},
1616
"files": [
17-
"dist",
18-
"typings"
17+
"dist"
1918
],
2019
"keywords": [],
2120
"author": "",
@@ -28,15 +27,19 @@
2827
"webdriverio": "*"
2928
},
3029
"devDependencies": {
30+
"@typescript-eslint/eslint-plugin": "^4.14.0",
31+
"@typescript-eslint/parser": "^4.14.0",
3132
"@wdio/cli": "^6.11.3",
3233
"@wdio/local-runner": "^6.12.0",
3334
"@wdio/mocha-framework": "^6.11.0",
3435
"@wdio/spec-reporter": "^6.11.0",
3536
"@wdio/sync": "^6.11.0",
3637
"chromedriver": "^87.0.5",
37-
"eslint": "^6.5.1",
38+
"eslint": "^7.6.0",
3839
"kcd-scripts": "^5.0.0",
3940
"semantic-release": "^17.0.2",
41+
"ts-node": "^9.1.1",
42+
"typescript": "^4.1.3",
4043
"wdio-chromedriver-service": "^6.0.4",
4144
"webdriverio": "^6.12.0"
4245
},

src/index.js renamed to src/index.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
const path = require('path')
2-
const fs = require('fs')
3-
const {queries: baseQueries} = require('@testing-library/dom')
1+
/* eslint-disable @typescript-eslint/no-implied-eval babel/no-invalid-this */
2+
3+
import path from 'path'
4+
import fs from 'fs'
5+
import {queries as baseQueries} from '@testing-library/dom'
6+
import {Element, BrowserObject, MultiRemoteBrowserObject} from 'webdriverio'
7+
8+
import {Config, QueryName, WebdriverIOQueries} from './types'
9+
10+
declare global {
11+
interface Window {
12+
TestingLibraryDom: typeof baseQueries & {
13+
configure: typeof configure
14+
}
15+
}
16+
}
417

518
const DOM_TESTING_LIBRARY_UMD_PATH = path.join(
619
require.resolve('@testing-library/dom'),
@@ -11,9 +24,9 @@ const DOM_TESTING_LIBRARY_UMD = fs
1124
.readFileSync(DOM_TESTING_LIBRARY_UMD_PATH)
1225
.toString()
1326

14-
let _config
27+
let _config: Partial<Config>
1528

16-
async function injectDOMTestingLibrary(container) {
29+
async function injectDOMTestingLibrary(container: Element) {
1730
await container.execute(DOM_TESTING_LIBRARY_UMD)
1831

1932
if (_config) {
@@ -23,7 +36,7 @@ async function injectDOMTestingLibrary(container) {
2336
}
2437
}
2538

26-
function serializeArgs(args) {
39+
function serializeArgs(args: any[]) {
2740
return args.map((arg) => {
2841
if (arg instanceof RegExp) {
2942
return {RegExp: arg.toString()}
@@ -35,10 +48,12 @@ function serializeArgs(args) {
3548
})
3649
}
3750

38-
function executeQuery([query, container, ...args], done) {
39-
const deserializedArgs = args.map((arg) => {
51+
function executeQuery(
52+
[query, container, ...args]: [QueryName, HTMLElement, ...any[]],
53+
done: (result: any) => void,
54+
) {
55+
const [matcher, options, waitForOptions] = args.map((arg) => {
4056
if (arg && arg.RegExp) {
41-
// eslint-disable-next-line
4257
return eval(arg.RegExp)
4358
}
4459
if (arg && arg.Undefined) {
@@ -48,7 +63,12 @@ function executeQuery([query, container, ...args], done) {
4863
})
4964

5065
Promise.resolve(
51-
window.TestingLibraryDom[query](container, ...deserializedArgs),
66+
window.TestingLibraryDom[query](
67+
container,
68+
matcher,
69+
options,
70+
waitForOptions,
71+
),
5272
)
5373
.then(done)
5474
.catch((e) => done(e.message))
@@ -62,18 +82,18 @@ Element. There are valid WebElement JSONs that exclude the key but can be turned
6282
into Elements, such as { ELEMENT: elementId }; this can happen in setups that
6383
aren't generated by @wdio/cli.
6484
*/
65-
function createElement(container, elementValue) {
85+
function createElement(container: Element, elementValue: any) {
6686
return container.$({
6787
'element-6066-11e4-a52e-4f735466cecf': '',
6888
...elementValue,
6989
})
7090
}
7191

72-
function createQuery(element, queryName) {
73-
return async (...args) => {
92+
function createQuery(element: Element, queryName: string) {
93+
return async (...args: any[]) => {
7494
await injectDOMTestingLibrary(element)
7595

76-
const result = await element.executeAsync(executeQuery, [
96+
const result = await element.executeAsync<any[], any[]>(executeQuery, [
7797
queryName,
7898
element,
7999
...serializeArgs(args),
@@ -95,17 +115,17 @@ function createQuery(element, queryName) {
95115
}
96116
}
97117

98-
function within(element) {
118+
function within(element: Element) {
99119
return Object.keys(baseQueries).reduce(
100120
(queries, queryName) => ({
101121
...queries,
102122
[queryName]: createQuery(element, queryName),
103123
}),
104124
{},
105-
)
125+
) as WebdriverIOQueries
106126
}
107127

108-
async function setupBrowser(browser) {
128+
async function setupBrowser(browser: BrowserObject | MultiRemoteBrowserObject) {
109129
const body = await browser.$('body')
110130
const queries = within(body)
111131

@@ -117,8 +137,8 @@ async function setupBrowser(browser) {
117137
browser.addCommand(
118138
queryName,
119139
function (...args) {
120-
// eslint-disable-next-line babel/no-invalid-this
121-
return within(this)[queryName](...args)
140+
// @ts-expect-error
141+
return within(this as Element)[queryName](...args)
122142
},
123143
true,
124144
)
@@ -127,12 +147,9 @@ async function setupBrowser(browser) {
127147
return queries
128148
}
129149

130-
function configure(config) {
150+
function configure(config: Partial<Config>) {
131151
_config = config
132152
}
133153

134-
module.exports = {
135-
within,
136-
setupBrowser,
137-
configure,
138-
}
154+
export * from './types'
155+
export {within, setupBrowser, configure}

src/types.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
Config as BaseConfig,
3+
BoundFunction as BoundFunctionBase,
4+
queries,
5+
} from '@testing-library/dom'
6+
import {Element} from 'webdriverio'
7+
8+
export type Config = Pick<BaseConfig, 'testIdAttribute'>
9+
10+
export type WebdriverIOQueryReturnType<T> = T extends Promise<HTMLElement>
11+
? Element
12+
: T extends HTMLElement
13+
? Element
14+
: T extends Promise<HTMLElement[]>
15+
? Element[]
16+
: T extends HTMLElement[]
17+
? Element[]
18+
: T extends null
19+
? null
20+
: never
21+
22+
export type WebdriverIOBoundFunction<T> = (
23+
...params: Parameters<BoundFunctionBase<T>>
24+
) => Promise<WebdriverIOQueryReturnType<ReturnType<BoundFunctionBase<T>>>>
25+
26+
export type WebdriverIOBoundFunctionSync<T> = (
27+
...params: Parameters<BoundFunctionBase<T>>
28+
) => WebdriverIOQueryReturnType<ReturnType<BoundFunctionBase<T>>>
29+
30+
export type WebdriverIOBoundFunctions<T> = {
31+
[P in keyof T]: WebdriverIOBoundFunction<T[P]>
32+
}
33+
34+
export type WebdriverIOBoundFunctionsSync<T> = {
35+
[P in keyof T]: WebdriverIOBoundFunctionSync<T[P]>
36+
}
37+
38+
export type WebdriverIOQueries = WebdriverIOBoundFunctions<typeof queries>
39+
export type WebdriverIOQueriesSync = WebdriverIOBoundFunctionsSync<
40+
typeof queries
41+
>
42+
43+
export type QueryName = keyof typeof queries

test/configure.e2e.js renamed to test/configure.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
const {setupBrowser, configure} = require('../src')
1+
import {setupBrowser, configure} from '../src';
22

33
describe('configure', () => {
44
beforeEach(() => {
55
configure({testIdAttribute: 'data-automation-id'})
66
})
77
afterEach(() => {
8-
configure(null)
8+
configure({})
99
})
1010

1111
it('supports alternative testIdAttribute', async () => {

test/queries.e2e.js renamed to test/queries.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
const {setupBrowser} = require('../src')
1+
import {setupBrowser} from '../src';
22

33
describe('queries', () => {
44
it('queryBy resolves with matching element', async () => {
55
const {queryByText} = await setupBrowser(browser)
66

77
const button = await queryByText('Unique Button Text')
8-
expect(await button.getText()).toEqual('Unique Button Text')
8+
expect(await button?.getText()).toEqual('Unique Button Text')
99
})
1010

1111
it('queryBy resolves with null when there are no matching elements', async () => {

test/setupBrowser.e2e.js renamed to test/setupBrowser.e2e.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
const {queries: baseQueries} = require('@testing-library/dom')
1+
import {queries as baseQueries} from '@testing-library/dom'
22

3-
const {setupBrowser} = require('../src')
3+
import {setupBrowser} from '../src'
4+
import { WebdriverIOQueries } from '../src/types'
5+
6+
declare global {
7+
namespace WebdriverIO {
8+
interface Browser extends WebdriverIOQueries {}
9+
interface Element extends WebdriverIOQueries {}
10+
}
11+
}
412

513
describe('setupBrowser', () => {
614
it('resolves with all queries', async () => {
@@ -36,15 +44,15 @@ describe('setupBrowser', () => {
3644
})
3745

3846
it('adds queries as browser commands', async () => {
39-
await setupBrowser(browser);
47+
await setupBrowser(browser)
4048

4149
expect(await browser.getByText('Page Heading')).toBeDefined()
4250
})
4351

4452
it('adds queries as element commands scoped to element', async () => {
45-
await setupBrowser(browser);
53+
await setupBrowser(browser)
4654

47-
const nested = await browser.$('*[data-testid="nested"]');
55+
const nested = await browser.$('*[data-testid="nested"]')
4856
const button = await nested.getByText('Button Text')
4957
await button.click()
5058

test/within.e2e.js renamed to test/within.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const {within, setupBrowser} = require('../src')
1+
import {within, setupBrowser} from '../src';
22

33
describe('within', () => {
44
it('scopes queries to element', async () => {

tsconfig.build.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"declaration": true
5+
},
6+
"exclude": ["test", "dist"]
7+
}

tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"target": "es2019",
5+
"module": "commonjs",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"declaration": true,
9+
"outDir": "./dist",
10+
"skipLibCheck": true
11+
}
12+
}

0 commit comments

Comments
 (0)