From 4193c7c2cb6225df9b5c8109f860c35066f2a376 Mon Sep 17 00:00:00 2001 From: olivierwilkinson Date: Tue, 19 Jan 2021 10:43:54 +0000 Subject: [PATCH] feat: Add Typescript support --- .eslintrc | 9 ++- README.md | 31 +++++++++ package.json | 13 ++-- src/{index.js => index.ts} | 67 ++++++++++++------- src/types.ts | 43 ++++++++++++ test/{configure.e2e.js => configure.e2e.ts} | 4 +- test/{queries.e2e.js => queries.e2e.ts} | 4 +- ...etupBrowser.e2e.js => setupBrowser.e2e.ts} | 18 +++-- test/{within.e2e.js => within.e2e.ts} | 2 +- tsconfig.build.json | 7 ++ tsconfig.json | 12 ++++ wdio.conf.js | 8 ++- 12 files changed, 173 insertions(+), 45 deletions(-) rename src/{index.js => index.ts} (61%) create mode 100644 src/types.ts rename test/{configure.e2e.js => configure.e2e.ts} (89%) rename test/{queries.e2e.js => queries.e2e.ts} (97%) rename test/{setupBrowser.e2e.js => setupBrowser.e2e.ts} (75%) rename test/{within.e2e.js => within.e2e.ts} (95%) create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index 80ac6d1..a9b64b0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,9 @@ { - "extends": "./node_modules/kcd-scripts/eslint.js", + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:@typescript-eslint/recommended", + "./node_modules/kcd-scripts/eslint.js" + ], "rules": { "babel/new-cap": "off", "func-names": "off", @@ -7,7 +11,8 @@ "prefer-arrow-callback": "off", "testing-library/no-await-sync-query": "off", "testing-library/no-dom-import": "off", - "testing-library/prefer-screen-queries": "off" + "testing-library/prefer-screen-queries": "off", + "@typescript-eslint/no-var-requires": "off" }, "overrides": [ { diff --git a/README.md b/README.md index bcf0195..fdb2bdb 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,37 @@ it('lets you configure queries', async () => { }) ``` +### Typescript + +All the above methods are fully typed. To use the Browser and Element commands +added by `setupBrowser` the global `WebdriverIO` namespace will need to be +modified. Add the following to a typescript module: + +``` +import {WebdriverIOQueries} from 'webdriverio-testing-library'; + +declare global { + namespace WebdriverIO { + interface Browser extends WebdriverIOQueries {} + interface Element extends WebdriverIOQueries {} + } +} +``` + +If you are using the `@wdio/sync` framework you will need to use the +`WebdriverIOQueriesSync` type to extend the interfaces: + +``` +import {WebdriverIOQueriesSync} from 'webdriverio-testing-library'; + +declare global { + namespace WebdriverIO { + interface Browser extends WebdriverIOQueriesSync {} + interface Element extends WebdriverIOQueriesSync {} + } +} +``` + ## Other Solutions I'm not aware of any, if you are please [make a pull request][prs] and add it diff --git a/package.json b/package.json index 05cd549..b3d8be1 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "1.1.0", "description": "", "main": "dist/index.js", - "typings": "typings", + "types": "dist/index.d.ts", "scripts": { "add-contributor": "kcd-scripts contributors add", - "build": "kcd-scripts build", + "build": "tsc -p tsconfig.build.json", "lint": "kcd-scripts lint", "test:unit": "kcd-scripts test --no-watch --config=jest.config.js", "validate": "kcd-scripts validate build,lint,test", @@ -14,8 +14,7 @@ "semantic-release": "semantic-release" }, "files": [ - "dist", - "typings" + "dist" ], "keywords": [], "author": "", @@ -28,15 +27,19 @@ "webdriverio": "*" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.14.0", + "@typescript-eslint/parser": "^4.14.0", "@wdio/cli": "^6.11.3", "@wdio/local-runner": "^6.12.0", "@wdio/mocha-framework": "^6.11.0", "@wdio/spec-reporter": "^6.11.0", "@wdio/sync": "^6.11.0", "chromedriver": "^87.0.5", - "eslint": "^6.5.1", + "eslint": "^7.6.0", "kcd-scripts": "^5.0.0", "semantic-release": "^17.0.2", + "ts-node": "^9.1.1", + "typescript": "^4.1.3", "wdio-chromedriver-service": "^6.0.4", "webdriverio": "^6.12.0" }, diff --git a/src/index.js b/src/index.ts similarity index 61% rename from src/index.js rename to src/index.ts index c51a82c..a144e51 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,6 +1,19 @@ -const path = require('path') -const fs = require('fs') -const {queries: baseQueries} = require('@testing-library/dom') +/* eslint-disable @typescript-eslint/no-implied-eval babel/no-invalid-this */ + +import path from 'path' +import fs from 'fs' +import {queries as baseQueries} from '@testing-library/dom' +import {Element, BrowserObject, MultiRemoteBrowserObject} from 'webdriverio' + +import {Config, QueryName, WebdriverIOQueries} from './types' + +declare global { + interface Window { + TestingLibraryDom: typeof baseQueries & { + configure: typeof configure + } + } +} const DOM_TESTING_LIBRARY_UMD_PATH = path.join( require.resolve('@testing-library/dom'), @@ -11,9 +24,9 @@ const DOM_TESTING_LIBRARY_UMD = fs .readFileSync(DOM_TESTING_LIBRARY_UMD_PATH) .toString() -let _config +let _config: Partial -async function injectDOMTestingLibrary(container) { +async function injectDOMTestingLibrary(container: Element) { await container.execute(DOM_TESTING_LIBRARY_UMD) if (_config) { @@ -23,7 +36,7 @@ async function injectDOMTestingLibrary(container) { } } -function serializeArgs(args) { +function serializeArgs(args: any[]) { return args.map((arg) => { if (arg instanceof RegExp) { return {RegExp: arg.toString()} @@ -35,10 +48,12 @@ function serializeArgs(args) { }) } -function executeQuery([query, container, ...args], done) { - const deserializedArgs = args.map((arg) => { +function executeQuery( + [query, container, ...args]: [QueryName, HTMLElement, ...any[]], + done: (result: any) => void, +) { + const [matcher, options, waitForOptions] = args.map((arg) => { if (arg && arg.RegExp) { - // eslint-disable-next-line return eval(arg.RegExp) } if (arg && arg.Undefined) { @@ -48,7 +63,12 @@ function executeQuery([query, container, ...args], done) { }) Promise.resolve( - window.TestingLibraryDom[query](container, ...deserializedArgs), + window.TestingLibraryDom[query]( + container, + matcher, + options, + waitForOptions, + ), ) .then(done) .catch((e) => done(e.message)) @@ -62,18 +82,18 @@ Element. There are valid WebElement JSONs that exclude the key but can be turned into Elements, such as { ELEMENT: elementId }; this can happen in setups that aren't generated by @wdio/cli. */ -function createElement(container, elementValue) { +function createElement(container: Element, elementValue: any) { return container.$({ 'element-6066-11e4-a52e-4f735466cecf': '', ...elementValue, }) } -function createQuery(element, queryName) { - return async (...args) => { +function createQuery(element: Element, queryName: string) { + return async (...args: any[]) => { await injectDOMTestingLibrary(element) - const result = await element.executeAsync(executeQuery, [ + const result = await element.executeAsync(executeQuery, [ queryName, element, ...serializeArgs(args), @@ -95,17 +115,17 @@ function createQuery(element, queryName) { } } -function within(element) { +function within(element: Element) { return Object.keys(baseQueries).reduce( (queries, queryName) => ({ ...queries, [queryName]: createQuery(element, queryName), }), {}, - ) + ) as WebdriverIOQueries } -async function setupBrowser(browser) { +async function setupBrowser(browser: BrowserObject | MultiRemoteBrowserObject) { const body = await browser.$('body') const queries = within(body) @@ -117,8 +137,8 @@ async function setupBrowser(browser) { browser.addCommand( queryName, function (...args) { - // eslint-disable-next-line babel/no-invalid-this - return within(this)[queryName](...args) + // @ts-expect-error + return within(this as Element)[queryName](...args) }, true, ) @@ -127,12 +147,9 @@ async function setupBrowser(browser) { return queries } -function configure(config) { +function configure(config: Partial) { _config = config } -module.exports = { - within, - setupBrowser, - configure, -} +export * from './types' +export {within, setupBrowser, configure} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..02ac263 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,43 @@ +import { + Config as BaseConfig, + BoundFunction as BoundFunctionBase, + queries, +} from '@testing-library/dom' +import {Element} from 'webdriverio' + +export type Config = Pick + +export type WebdriverIOQueryReturnType = T extends Promise + ? Element + : T extends HTMLElement + ? Element + : T extends Promise + ? Element[] + : T extends HTMLElement[] + ? Element[] + : T extends null + ? null + : never + +export type WebdriverIOBoundFunction = ( + ...params: Parameters> +) => Promise>>> + +export type WebdriverIOBoundFunctionSync = ( + ...params: Parameters> +) => WebdriverIOQueryReturnType>> + +export type WebdriverIOBoundFunctions = { + [P in keyof T]: WebdriverIOBoundFunction +} + +export type WebdriverIOBoundFunctionsSync = { + [P in keyof T]: WebdriverIOBoundFunctionSync +} + +export type WebdriverIOQueries = WebdriverIOBoundFunctions +export type WebdriverIOQueriesSync = WebdriverIOBoundFunctionsSync< + typeof queries +> + +export type QueryName = keyof typeof queries diff --git a/test/configure.e2e.js b/test/configure.e2e.ts similarity index 89% rename from test/configure.e2e.js rename to test/configure.e2e.ts index 6c64ac3..683f822 100644 --- a/test/configure.e2e.js +++ b/test/configure.e2e.ts @@ -1,11 +1,11 @@ -const {setupBrowser, configure} = require('../src') +import {setupBrowser, configure} from '../src'; describe('configure', () => { beforeEach(() => { configure({testIdAttribute: 'data-automation-id'}) }) afterEach(() => { - configure(null) + configure({}) }) it('supports alternative testIdAttribute', async () => { diff --git a/test/queries.e2e.js b/test/queries.e2e.ts similarity index 97% rename from test/queries.e2e.js rename to test/queries.e2e.ts index c420da7..cddaa0e 100644 --- a/test/queries.e2e.js +++ b/test/queries.e2e.ts @@ -1,11 +1,11 @@ -const {setupBrowser} = require('../src') +import {setupBrowser} from '../src'; describe('queries', () => { it('queryBy resolves with matching element', async () => { const {queryByText} = await setupBrowser(browser) const button = await queryByText('Unique Button Text') - expect(await button.getText()).toEqual('Unique Button Text') + expect(await button?.getText()).toEqual('Unique Button Text') }) it('queryBy resolves with null when there are no matching elements', async () => { diff --git a/test/setupBrowser.e2e.js b/test/setupBrowser.e2e.ts similarity index 75% rename from test/setupBrowser.e2e.js rename to test/setupBrowser.e2e.ts index dda2193..8b2c1b7 100644 --- a/test/setupBrowser.e2e.js +++ b/test/setupBrowser.e2e.ts @@ -1,6 +1,14 @@ -const {queries: baseQueries} = require('@testing-library/dom') +import {queries as baseQueries} from '@testing-library/dom' -const {setupBrowser} = require('../src') +import {setupBrowser} from '../src' +import { WebdriverIOQueries } from '../src/types' + +declare global { + namespace WebdriverIO { + interface Browser extends WebdriverIOQueries {} + interface Element extends WebdriverIOQueries {} + } +} describe('setupBrowser', () => { it('resolves with all queries', async () => { @@ -36,15 +44,15 @@ describe('setupBrowser', () => { }) it('adds queries as browser commands', async () => { - await setupBrowser(browser); + await setupBrowser(browser) expect(await browser.getByText('Page Heading')).toBeDefined() }) it('adds queries as element commands scoped to element', async () => { - await setupBrowser(browser); + await setupBrowser(browser) - const nested = await browser.$('*[data-testid="nested"]'); + const nested = await browser.$('*[data-testid="nested"]') const button = await nested.getByText('Button Text') await button.click() diff --git a/test/within.e2e.js b/test/within.e2e.ts similarity index 95% rename from test/within.e2e.js rename to test/within.e2e.ts index 31765ff..de490e9 100644 --- a/test/within.e2e.js +++ b/test/within.e2e.ts @@ -1,4 +1,4 @@ -const {within, setupBrowser} = require('../src') +import {within, setupBrowser} from '../src'; describe('within', () => { it('scopes queries to element', async () => { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..892aae7 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true + }, + "exclude": ["test", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d80acf0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "es2019", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "./dist", + "skipLibCheck": true + } +} diff --git a/wdio.conf.js b/wdio.conf.js index a6731ab..d567af9 100644 --- a/wdio.conf.js +++ b/wdio.conf.js @@ -18,7 +18,7 @@ exports.config = { // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working // directory is where your package.json resides, so `wdio` will be called from there. // - specs: ['./test/**/*.e2e.js'], + specs: ['./test/**/*.e2e.ts'], // Patterns to exclude. exclude: [ // 'path/to/excluded/files' @@ -140,6 +140,7 @@ exports.config = { mochaOpts: { ui: 'bdd', timeout: 60000, + compiler: ['ts-node/register'] }, // // ===== @@ -183,8 +184,9 @@ exports.config = { * @param {Array.} specs List of spec file paths that are to be run * @param {Object} browser instance of created browser/device session */ - // before: function (capabilities, specs) { - // }, + before: function () { + require("@babel/register")({ extensions: ['.js', '.jsx', '.ts', '.tsx'] }); + }, /** * Runs before a WebdriverIO command gets executed. * @param {String} commandName hook command name