From 35e8eb42baedd2a16a4d63a233bb3d679e98fd3f Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Tue, 27 Oct 2020 14:37:33 +0100 Subject: [PATCH 01/14] Start adding docs via typedoc --- packages/bigtest/.gitignore | 1 + packages/bigtest/package.json | 1 + packages/bigtest/src/index.ts | 2 +- packages/bigtest/typedoc.json | 5 + packages/interactor/src/create-interactor.ts | 25 +++- packages/interactor/src/definitions/button.ts | 104 +++++++++++--- packages/interactor/src/index.ts | 5 +- packages/interactor/src/specification.ts | 23 ++++ yarn.lock | 128 +++++++++++++++--- 9 files changed, 254 insertions(+), 40 deletions(-) create mode 100644 packages/bigtest/.gitignore create mode 100644 packages/bigtest/typedoc.json diff --git a/packages/bigtest/.gitignore b/packages/bigtest/.gitignore new file mode 100644 index 000000000..d8f8d4692 --- /dev/null +++ b/packages/bigtest/.gitignore @@ -0,0 +1 @@ +docs diff --git a/packages/bigtest/package.json b/packages/bigtest/package.json index 53969ff5b..a6f44610f 100644 --- a/packages/bigtest/package.json +++ b/packages/bigtest/package.json @@ -23,6 +23,7 @@ "@frontside/typescript": "^1.1.0", "@types/node": "^13.13.4", "ts-node": "*", + "typedoc": "^0.17.0-3", "typescript": "3.9.7" }, "dependencies": { diff --git a/packages/bigtest/src/index.ts b/packages/bigtest/src/index.ts index ac38878ba..a774f483e 100644 --- a/packages/bigtest/src/index.ts +++ b/packages/bigtest/src/index.ts @@ -1,2 +1,2 @@ export * from '@bigtest/interactor'; -export * from '@bigtest/suite'; +export { test } from '@bigtest/suite'; diff --git a/packages/bigtest/typedoc.json b/packages/bigtest/typedoc.json new file mode 100644 index 000000000..120ebb63f --- /dev/null +++ b/packages/bigtest/typedoc.json @@ -0,0 +1,5 @@ +{ + "mode": "library", + "out": "docs", + "name": "BigTest" +} diff --git a/packages/interactor/src/create-interactor.ts b/packages/interactor/src/create-interactor.ts index ce35ff0c9..52bdb9f0f 100644 --- a/packages/interactor/src/create-interactor.ts +++ b/packages/interactor/src/create-interactor.ts @@ -1,5 +1,5 @@ import { bigtestGlobals } from '@bigtest/globals'; -import { InteractorSpecification, FilterParams, Filters, Actions, InteractorInstance, LocatorFn } from './specification'; +import { InteractorSpecification, InteractorBuilder, InteractorConstructor, FilterParams, Filters, Actions, InteractorInstance, LocatorFn } from './specification'; import { Locator } from './locator'; import { Filter } from './filter'; import { Interactor } from './interactor'; @@ -7,8 +7,27 @@ import { interaction } from './interaction'; const defaultLocator: LocatorFn = (element) => element.textContent || ""; -export function createInteractor(interactorName: string) { - return function = {}, A extends Actions = {}>(specification: InteractorSpecification) { + +/** + * Create a custom interactor. Due to TypeInference issues, this creates an + * {@link InteractorBuilder}, which you will need to create the actual + * interactor. See {@link InteractorSpecification} for detailed breakdown of + * available options for the builder. + * + * ### Creating a simple interactor + * + * ``` typescript + * let Paragraph = createInteractor('paragraph')({ selector: 'p' }); + * ``` + * + * Note the double function call! + * + * @param interactorName The human readable name of the interactor, used mainly for debugging purposes and error messages + * @typeParam E The type of DOM Element that this interactor operates on. By specifying the element type, actions and filters defined for the interactor can be type checked against the actual element type. + * @returns You will need to call the returned builder to create an interactor. + */ +export function createInteractor(interactorName: string): InteractorBuilder { + return function = {}, A extends Actions = {}>(specification: InteractorSpecification): InteractorConstructor { let InteractorClass = class extends Interactor {}; for(let [actionName, action] of Object.entries(specification.actions || {})) { diff --git a/packages/interactor/src/definitions/button.ts b/packages/interactor/src/definitions/button.ts index 764997358..84733356f 100644 --- a/packages/interactor/src/definitions/button.ts +++ b/packages/interactor/src/definitions/button.ts @@ -1,11 +1,92 @@ -import { createInteractor, perform, focused } from '../index'; -import { isVisible } from 'element-is-visible'; +import { Interactor, Interaction, InteractorConstructor, createInteractor, perform, focused, isVisible } from '../index'; +import { FilterParams, ActionMethods } from '../specification'; function isButtonElement(element: HTMLInputElement | HTMLButtonElement): element is HTMLButtonElement { return element.tagName === 'BUTTON'; } -export const Button = createInteractor('button')({ +type ButtonElement = HTMLInputElement | HTMLButtonElement; + +const filters = { + title: (element: ButtonElement) => element.title, + id: (element: ButtonElement) => element.id, + visible: { apply: isVisible, default: true }, + disabled: { + apply: (element: ButtonElement) => element.disabled, + default: false + }, + focused +}; + +const actions = { + click: perform((element: ButtonElement) => { element.click(); }), + focus: perform((element: ButtonElement) => { element.focus(); }), + blur: perform((element: ButtonElement) => { element.blur(); }), +}; + +type ButtonConstructor = InteractorConstructor; + +export interface ButtonFilters extends FilterParams { + /** + * Filter by title + */ + title?: string; + /** + * Filter by id + */ + id?: string; + /** + * Filter by visibility. See {@link isVisible}. + */ + visible?: boolean; + /** + * Filter by whether the button is disabled. + */ + disabled?: boolean; + /** + * Filter by whether the button is focused. + */ + focused?: boolean; +} + +export interface ButtonActions extends ActionMethods { + /** + * Click on the button + */ + click(): Interaction; + /** + * Move focus to the button + */ + focus(): Interaction; + /** + * Move focus away from the button + */ + blur(): Interaction; +} + +/** + * Call this {@link InteractorConstructor} to initialize a button interactor. + * The button interactor can be used to interact with buttons on the page and + * to assert on their state. + * + * The button is located by the visible text on the button. + * + * ### Example + * + * ``` typescript + * await Button('Submit').click(); + * await Button('Submit').is({ disabled: true }); + * await Button({ id: 'submit-button', disabled: true }).exists(); + * ``` + * + * ### See also + * + * - {@link ButtonFilters}: filters defined for this interactor + * - {@link ButtonActions}: actions callable on instances of this interactor + * - {@link InteractorConstructor}: how to create an interactor instance from this constructor + * - {@link Interactor}: interface of instances of this interactor in addition to its actions + */ +export const Button: ButtonConstructor = createInteractor('button')({ selector: 'button,input[type=button],input[type=submit],input[type=reset],input[type=image]', locator(element) { if(isButtonElement(element)) { @@ -16,19 +97,6 @@ export const Button = createInteractor('bu return element.value; } }, - filters: { - title: (element) => element.title, - id: (element) => element.id, - visible: { apply: isVisible, default: true }, - disabled: { - apply: (element) => element.disabled, - default: false - }, - focused - }, - actions: { - click: perform((element) => { element.click(); }), - focus: perform((element) => { element.focus(); }), - blur: perform((element) => { element.blur(); }), - }, + filters, + actions, }); diff --git a/packages/interactor/src/index.ts b/packages/interactor/src/index.ts index 1004b6e95..3aea13401 100644 --- a/packages/interactor/src/index.ts +++ b/packages/interactor/src/index.ts @@ -1,14 +1,17 @@ +export { InteractorConstructor, InteractorBuilder, InteractorSpecification } from './specification'; export { Interactor } from './interactor'; +export { Interaction, ReadonlyInteraction } from './interaction'; export { createInteractor } from './create-interactor'; export { Page } from './page'; export { App } from './app'; export { perform } from './perform'; export { fillIn } from './fill-in'; export { focused } from './focused'; +export { isVisible } from 'element-is-visible'; export { Link } from './definitions/link'; export { Heading } from './definitions/heading'; -export { Button } from './definitions/button'; +export { Button, ButtonFilters, ButtonActions } from './definitions/button'; export { TextField } from './definitions/text-field'; export { CheckBox } from './definitions/check-box'; export { RadioButton } from './definitions/radio-button'; diff --git a/packages/interactor/src/specification.ts b/packages/interactor/src/specification.ts index f94f7fd09..f0237005d 100644 --- a/packages/interactor/src/specification.ts +++ b/packages/interactor/src/specification.ts @@ -41,3 +41,26 @@ export type FilterParams> = keyof F exte } export type InteractorInstance, A extends Actions> = Interactor & ActionMethods; + +export interface InteractorConstructor, A extends Actions> { + (filters?: FilterParams): InteractorInstance; + (value: string, filters?: FilterParams): InteractorInstance; +} + +/** + * When calling {@link createInteractor}, this is the intermediate object that + * is returned. See {@link InteractorSpecification} for a detailed list of all + * available options. + * + * @typeParam E The type of DOM Element that this interactor operates on. By specifying the element type, actions and filters defined for the interactor can be type checked against the actual element type. + */ +export interface InteractorBuilder { + /** + * Calling the builder will create an interactor. + * + * @param specification The specification of this interactor + * @typeParam F the filters of this interactor, this is usually inferred from the specification + * @typeParam A the actions of this interactor, this is usually inferred from the specification + */ + = {}, A extends Actions = {}>(specification: InteractorSpecification): InteractorConstructor; +} diff --git a/yarn.lock b/yarn.lock index 45f6698ef..f4bc7904a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3619,7 +3619,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== -"@types/minimatch@*": +"@types/minimatch@*", "@types/minimatch@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -4845,6 +4845,13 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backbone@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" + integrity sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ== + dependencies: + underscore ">=1.8.3" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -8578,7 +8585,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -8750,7 +8757,7 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== -handlebars@^4.7.6: +handlebars@^4.7.2, handlebars@^4.7.6: version "4.7.6" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -8885,6 +8892,11 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highlight.js@^9.18.0: + version "9.18.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" + integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -9380,6 +9392,11 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -9494,6 +9511,13 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" +is-core-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" + integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== + dependencies: + has "^1.0.3" + is-core-module@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -10344,6 +10368,11 @@ joi@^17.1.1: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" +jquery@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" + integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -10974,6 +11003,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lunr@^2.3.8: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" @@ -11054,6 +11088,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +marked@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355" + integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -11246,7 +11285,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -13433,7 +13472,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.0: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -13941,6 +13980,13 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -14230,6 +14276,14 @@ resolve@1.17.0, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13. dependencies: path-parse "^1.0.6" +resolve@^1.1.6: + version "1.18.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" + integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== + dependencies: + is-core-module "^2.0.0" + path-parse "^1.0.6" + resolve@^1.17.0: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" @@ -14334,17 +14388,17 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-inject-process-env@^1.3.0: +rollup-plugin-inject-process-env@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/rollup-plugin-inject-process-env/-/rollup-plugin-inject-process-env-1.3.1.tgz#2d7660fe76f2b221b976cb35597763ffcaad3db3" integrity sha512-kKDoL30IZr0wxbNVJjq+OS92RJSKRbKV6B5eNW4q3mZTFqoWDh6lHy+mPDYuuGuERFNKXkG+AKxvYqC9+DRpKQ== dependencies: magic-string "^0.25.7" -rollup-plugin-typescript2@^0.27.3: - version "0.27.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz#cd9455ac026d325b20c5728d2cc54a08a771b68b" - integrity sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg== +rollup-plugin-typescript2@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" + integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== dependencies: "@rollup/pluginutils" "^3.1.0" find-cache-dir "^3.3.1" @@ -14352,10 +14406,10 @@ rollup-plugin-typescript2@^0.27.3: resolve "1.17.0" tslib "2.0.1" -rollup@^2.29.0: - version "2.34.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.34.0.tgz#ecc7f1d4ce2cb88bb51bec2f56b984f3c35b8271" - integrity sha512-dW5iLvttZzdVehjEuNJ1bWvuMEJjOWGmnuFS82WeKHTGXDkRHQeq/ExdifkSyJv9dLcR86ysKRmrIDyR6O0X8g== +rollup@^2.33.3: + version "2.34.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.34.1.tgz#a387230df02c58b242794a213dfb68b42de2c8fb" + integrity sha512-tGveB6NU5x4MS/iXaIsjfUkEv4hxzJJ4o0FRy5LO62Ndx3R2cmE1qsLYlSfRkvHUUPqWiFoxEm8pRftzh1a5HA== optionalDependencies: fsevents "~2.1.2" @@ -14682,6 +14736,15 @@ shell-quote@1.7.2: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shelljs@^0.8.3: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -15914,6 +15977,32 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typedoc-default-themes@0.8.0-0: + version "0.8.0-0" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.8.0-0.tgz#80b7080837b2c9eba36c2fe06601ebe01973a0cd" + integrity sha512-blFWppm5aKnaPOa1tpGO9MLu+njxq7P3rtkXK4QxJBNszA+Jg7x0b+Qx0liXU1acErur6r/iZdrwxp5DUFdSXw== + dependencies: + backbone "^1.4.0" + jquery "^3.4.1" + lunr "^2.3.8" + underscore "^1.9.1" + +typedoc@^0.17.0-3: + version "0.17.0-3" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.17.0-3.tgz#91996e77427ff3a208ab76595a927ee11b75e9e8" + integrity sha512-DO2djkR4NHgzAWfNbJb2eQKsFMs+gOuYBXlQ8dOSCjkAK5DRI7ZywDufBGPUw7Ue9Qwi2Cw1DxLd3reDq8wFuQ== + dependencies: + "@types/minimatch" "3.0.3" + fs-extra "^8.1.0" + handlebars "^4.7.2" + highlight.js "^9.18.0" + lodash "^4.17.15" + marked "^0.8.0" + minimatch "^3.0.0" + progress "^2.0.3" + shelljs "^0.8.3" + typedoc-default-themes "0.8.0-0" + typescript@3.9.7: version "3.9.7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" @@ -15940,9 +16029,9 @@ typical@^5.0.0, typical@^5.2.0: integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== uglify-js@^3.1.4: - version "3.11.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.5.tgz#d6788bc83cf35ff18ea78a65763e480803409bc6" - integrity sha512-btvv/baMqe7HxP7zJSF7Uc16h1mSfuuSplT0/qdjxseesDU+yYzH33eHBH+eMdeRXwujXspaCTooWHQVVBh09w== + version "3.11.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.4.tgz#b47b7ae99d4bd1dca65b53aaa69caa0909e6fadf" + integrity sha512-FyYnoxVL1D6+jDGQpbK5jW6y/2JlVfRfEeQ67BPCUg5wfCjaKOpr2XeceE4QL+MkhxliLtf5EbrMDZgzpt2CNw== uncss@^0.17.2: version "0.17.3" @@ -15959,6 +16048,11 @@ uncss@^0.17.2: postcss-selector-parser "6.0.2" request "^2.88.0" +underscore@>=1.8.3, underscore@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e" + integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" From 08be6a80c5ab9a4775a90c2529fef8d6b557429d Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Mon, 2 Nov 2020 12:21:06 +0100 Subject: [PATCH 02/14] Upgrade to TypeDoc beta --- packages/bigtest/package.json | 2 +- packages/bigtest/typedoc.json | 4 +- packages/interactor/src/definitions/button.ts | 2 +- yarn.lock | 81 +++++++------------ 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/packages/bigtest/package.json b/packages/bigtest/package.json index a6f44610f..051b95427 100644 --- a/packages/bigtest/package.json +++ b/packages/bigtest/package.json @@ -23,7 +23,7 @@ "@frontside/typescript": "^1.1.0", "@types/node": "^13.13.4", "ts-node": "*", - "typedoc": "^0.17.0-3", + "typedoc": "^0.20.0-beta.8", "typescript": "3.9.7" }, "dependencies": { diff --git a/packages/bigtest/typedoc.json b/packages/bigtest/typedoc.json index 120ebb63f..907fc4260 100644 --- a/packages/bigtest/typedoc.json +++ b/packages/bigtest/typedoc.json @@ -1,5 +1,5 @@ { - "mode": "library", "out": "docs", - "name": "BigTest" + "name": "BigTest", + "entryPoints": "src/index.ts" } diff --git a/packages/interactor/src/definitions/button.ts b/packages/interactor/src/definitions/button.ts index 84733356f..5fe1a9aec 100644 --- a/packages/interactor/src/definitions/button.ts +++ b/packages/interactor/src/definitions/button.ts @@ -1,4 +1,4 @@ -import { Interactor, Interaction, InteractorConstructor, createInteractor, perform, focused, isVisible } from '../index'; +import { Interaction, InteractorConstructor, createInteractor, perform, focused, isVisible } from '../index'; import { FilterParams, ActionMethods } from '../specification'; function isButtonElement(element: HTMLInputElement | HTMLButtonElement): element is HTMLButtonElement { diff --git a/yarn.lock b/yarn.lock index f4bc7904a..99ebc954b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3619,7 +3619,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== -"@types/minimatch@*", "@types/minimatch@3.0.3": +"@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -4845,13 +4845,6 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== -backbone@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" - integrity sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ== - dependencies: - underscore ">=1.8.3" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -8757,7 +8750,7 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== -handlebars@^4.7.2, handlebars@^4.7.6: +handlebars@^4.7.6: version "4.7.6" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -8892,10 +8885,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@^9.18.0: - version "9.18.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" - integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== +highlight.js@^10.3.1: + version "10.3.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.3.2.tgz#135fd3619a00c3cbb8b4cd6dbc78d56bfcbc46f1" + integrity sha512-3jRT7OUYsVsKvukNKZCtnvRcFyCJqSEIuIMsEybAXRiFSwpt65qjPd/Pr+UOdYt7WJlt+lj3+ypUsHiySBp/Jw== hmac-drbg@^1.0.0: version "1.0.1" @@ -10368,11 +10361,6 @@ joi@^17.1.1: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" -jquery@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" - integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -10921,7 +10909,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.19, "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5: +lodash@4.17.19, "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== @@ -11003,7 +10991,7 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lunr@^2.3.8: +lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== @@ -11088,10 +11076,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.8.0: - version "0.8.2" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355" - integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw== +marked@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.2.tgz#5d77ffb789c4cb0ae828bfe76250f7140b123f70" + integrity sha512-5jjKHVl/FPo0Z6ocP3zYhKiJLzkwJAw4CZoLjv57FkvbUuwOX4LIBBGGcXjAY6ATcd1q9B8UTj5T9Umauj0QYQ== md5.js@^1.3.4: version "1.3.5" @@ -14736,7 +14724,7 @@ shell-quote@1.7.2: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.3: +shelljs@^0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -15977,31 +15965,27 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typedoc-default-themes@0.8.0-0: - version "0.8.0-0" - resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.8.0-0.tgz#80b7080837b2c9eba36c2fe06601ebe01973a0cd" - integrity sha512-blFWppm5aKnaPOa1tpGO9MLu+njxq7P3rtkXK4QxJBNszA+Jg7x0b+Qx0liXU1acErur6r/iZdrwxp5DUFdSXw== - dependencies: - backbone "^1.4.0" - jquery "^3.4.1" - lunr "^2.3.8" - underscore "^1.9.1" +typedoc-default-themes@0.12.0-beta.6: + version "0.12.0-beta.6" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.0-beta.6.tgz#b8a0b49e00f85b0a906f3f33b13d83cf877b44de" + integrity sha512-3B6gxssa+cecFwKNNx28dTQu/csjXnZlwv+yi2AOj/hYUdXVqC4FUGuPdYiDC3eBKiumx8H7do7VmBA7R2C57A== -typedoc@^0.17.0-3: - version "0.17.0-3" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.17.0-3.tgz#91996e77427ff3a208ab76595a927ee11b75e9e8" - integrity sha512-DO2djkR4NHgzAWfNbJb2eQKsFMs+gOuYBXlQ8dOSCjkAK5DRI7ZywDufBGPUw7Ue9Qwi2Cw1DxLd3reDq8wFuQ== +typedoc@^0.20.0-beta.8: + version "0.20.0-beta.8" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.20.0-beta.8.tgz#6fc92f37eff43901fd9f5ec9fd6690671363d8db" + integrity sha512-2NSno7Q+En69EkI7v7w3UhKBZT6B6bT8t6yK5C84eTNYk2oyE6NUvflEsd4Ezpp66o/spqRmICnUb0Ig6vNAzg== dependencies: - "@types/minimatch" "3.0.3" - fs-extra "^8.1.0" - handlebars "^4.7.2" - highlight.js "^9.18.0" - lodash "^4.17.15" - marked "^0.8.0" + fs-extra "^9.0.1" + handlebars "^4.7.6" + highlight.js "^10.3.1" + lodash "^4.17.20" + lunr "^2.3.9" + marked "^1.2.2" minimatch "^3.0.0" progress "^2.0.3" - shelljs "^0.8.3" - typedoc-default-themes "0.8.0-0" + semver "^7.3.2" + shelljs "^0.8.4" + typedoc-default-themes "0.12.0-beta.6" typescript@3.9.7: version "3.9.7" @@ -16048,11 +16032,6 @@ uncss@^0.17.2: postcss-selector-parser "6.0.2" request "^2.88.0" -underscore@>=1.8.3, underscore@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e" - integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw== - unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" From 392901a9d125694a8841518e873cfaaff9be6f06 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Thu, 12 Nov 2020 10:30:42 +0100 Subject: [PATCH 03/14] Add more docs --- packages/bigtest/src/index.ts | 2 +- packages/interactor/src/create-interactor.ts | 2 +- packages/interactor/src/specification.ts | 23 +++++ packages/suite/src/dsl.ts | 99 +++++++++++++++++++- 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/packages/bigtest/src/index.ts b/packages/bigtest/src/index.ts index a774f483e..b5f45bac2 100644 --- a/packages/bigtest/src/index.ts +++ b/packages/bigtest/src/index.ts @@ -1,2 +1,2 @@ export * from '@bigtest/interactor'; -export { test } from '@bigtest/suite'; +export { test, TestBuilder } from '@bigtest/suite'; diff --git a/packages/interactor/src/create-interactor.ts b/packages/interactor/src/create-interactor.ts index 52bdb9f0f..aff0b46cb 100644 --- a/packages/interactor/src/create-interactor.ts +++ b/packages/interactor/src/create-interactor.ts @@ -9,7 +9,7 @@ const defaultLocator: LocatorFn = (element) => element.textContent || " /** - * Create a custom interactor. Due to TypeInference issues, this creates an + * Create a custom interactor. Due to TypeScript inference issues, this creates an * {@link InteractorBuilder}, which you will need to create the actual * interactor. See {@link InteractorSpecification} for detailed breakdown of * available options for the builder. diff --git a/packages/interactor/src/specification.ts b/packages/interactor/src/specification.ts index f0237005d..4cd20d9a4 100644 --- a/packages/interactor/src/specification.ts +++ b/packages/interactor/src/specification.ts @@ -7,6 +7,20 @@ export type ActionFn = (interactor: InteractorInstance = (element: E) => T; +/** + * A function which given an element returns a string which can be used to + * locate the element, for example by returning the elements text content, or + * an attribute value. + * + * ### Example + * + * ``` typescript + * const inputValue: LocatorFn = (element) => element.value; + * ``` + * + * @param element The element to extract a locator out of + * @typeParam E The type of the element that the locator function operates on + */ export type LocatorFn = (element: E) => string; export type FilterObject = { @@ -19,9 +33,18 @@ export type Filters = Record | F export type Actions = Record>; export type InteractorSpecification, A extends Actions> = { + /** + * The CSS selector that this interactor uses to find matching elements + */ selector?: string; actions?: A; filters?: F; + /** + * A function which returns a string value for a matched element, which can + * be used to locate a specific instance of this interactor. The `value` + * parameter of an {@link InteractorConstructor} must match the value + * returned from the locator function. + */ locator?: LocatorFn; } diff --git a/packages/suite/src/dsl.ts b/packages/suite/src/dsl.ts index f6d4f9e18..ffbca124e 100644 --- a/packages/suite/src/dsl.ts +++ b/packages/suite/src/dsl.ts @@ -1,6 +1,14 @@ import { TestImplementation, Context, Step, Assertion } from './interfaces'; -export function test(description: string): TestBuilder { +/** + * This provides a builder API to construct BigTest tests. The builder API + * makes it more consise to write tests, as well as providing type safety if + * you're using TypeScript. + * + * @param description The description to apply to the test, a human readable text which describes the test's purpose. + * @typeParam C test steps and assertions receive a context as an argument, and can extend this context through their return values, the context usually starts out empty. + */ +export function test(description: string): TestBuilder { return new TestBuilder({ description, steps: [], @@ -35,21 +43,106 @@ class TestStructureError extends Error { type TestBuilderState = 'step' | 'assertion' | 'child'; +/** + * A builder API for constructing BigTest tests. This is usually created via the {@link test} function. + * + * @typeParam C test steps and assertions receive a context as an argument, and can extend this context through their return values. + */ export class TestBuilder implements TestImplementation { + /** @hidden */ public description: string; + /** @hidden */ public steps: Step[]; + /** @hidden */ public assertions: Assertion[]; + /** @hidden */ public children: TestImplementation[]; - constructor(test: TestImplementation, private state = 'step') { + /** @hidden */ + private state: TestBuilderState; + + /** @hidden */ + constructor(test: TestImplementation, state: TestBuilderState = 'step') { this.description = test.description; this.steps = test.steps; this.assertions = test.assertions; this.children = test.children; + this.state = state; } - step(...steps: StepList): TestBuilder; + /** + * Add one or more steps to this test. The arguments to this function should + * be either a description and an action, or one or more objects which have + * `description` and `action` properties. Interactor actions and assertions + * can both be used as a step directly. + * + * The action is an async function which can return either void or an object. + * If it returns an object, then this object is merged into the context, + * + * ### With description and action + * + * ``` typescript + * .step("do the thing", async() { + * await thing.do(); + * }); + * ``` + * + * ### With step objects + * + * ``` typescript + * .step( + * { + * description: "do the thing", + * action: async() { + * await things.do(); + * } + * }, + * { + * description: "another thing", + * action: async() { + * await anotherThing.do(); + * } + * } + * ); + * ``` + * + * ### With interactor + * + * Interactor actions and assertions implement the step object interface, so + * they can be used like this: + * + * ``` typescript + * .step( + * Link('New post').click(), + * TextField('Text').fillIn('BigTest is cool!'), + * Button('Submit').click(), + * Headline('BigTest is cool!').exists() + * ) + * ``` + * + * ### Returning from context + * + * ``` typescript + * .step("given a user", async() { + * return { user: await User.create() } + * }) + * .step("and the user has a post", async({ user }) { + * return { post: await Post.create({ user }) } + * }) + * .step("when I visit the post", async({ user, post }) { + * await Page.visit(`/users/${user.id}/posts/${post.id}`); + * }) + * ``` + * + * @param description The description of this step + * @param action An async function which receives the current context and may return an object which extends the context + * @typeParam R The return type of the action function can either be + */ step(description: string, action: Action): TestBuilder; + /** + * @param steps A list of step objects, each of which must have a `description` and `action` property. + */ + step(...steps: StepList): TestBuilder; step(...args: [string, Action] | StepList): TestBuilder { if(this.state === 'assertion' || this.state === 'child') { throw new TestStructureError(`Cannot add step after adding ${this.state}`); From 5021f7a01e2176e63811ed18637337a4992e6d66 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Mon, 16 Nov 2020 16:43:32 +0100 Subject: [PATCH 04/14] Skip README --- packages/bigtest/typedoc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bigtest/typedoc.json b/packages/bigtest/typedoc.json index 907fc4260..accff3f8e 100644 --- a/packages/bigtest/typedoc.json +++ b/packages/bigtest/typedoc.json @@ -1,5 +1,6 @@ { "out": "docs", "name": "BigTest", - "entryPoints": "src/index.ts" + "entryPoints": "src/index.ts", + "readme": "none" } From c318bf1ec0d53255cfc35ebb738717227764fb52 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Tue, 17 Nov 2020 09:41:28 +0100 Subject: [PATCH 05/14] Add dedicated start page for API docs Doing this allows us to link stuff from the API docs and have a better introduction. We'll have getting started guides separate anyway. --- packages/bigtest/API.md | 3 +++ packages/bigtest/typedoc.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/bigtest/API.md diff --git a/packages/bigtest/API.md b/packages/bigtest/API.md new file mode 100644 index 000000000..f0d1724b7 --- /dev/null +++ b/packages/bigtest/API.md @@ -0,0 +1,3 @@ +# BigTest + +This is cool stuff {@link createInteractor}, yeah! diff --git a/packages/bigtest/typedoc.json b/packages/bigtest/typedoc.json index accff3f8e..091e92c5a 100644 --- a/packages/bigtest/typedoc.json +++ b/packages/bigtest/typedoc.json @@ -2,5 +2,6 @@ "out": "docs", "name": "BigTest", "entryPoints": "src/index.ts", - "readme": "none" + "readme": "API.md", + "includeVersion": true } From fbed19a9703a3d78d7ec0c070671a47502cbc816 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Tue, 17 Nov 2020 09:42:14 +0100 Subject: [PATCH 06/14] Start adding docs for other interactor --- packages/interactor/src/definitions/button.ts | 2 + .../interactor/src/definitions/check-box.ts | 127 ++++++++++++++---- 2 files changed, 106 insertions(+), 23 deletions(-) diff --git a/packages/interactor/src/definitions/button.ts b/packages/interactor/src/definitions/button.ts index 5fe1a9aec..d03254fac 100644 --- a/packages/interactor/src/definitions/button.ts +++ b/packages/interactor/src/definitions/button.ts @@ -85,6 +85,8 @@ export interface ButtonActions extends ActionMethods('button')({ selector: 'button,input[type=button],input[type=submit],input[type=reset],input[type=image]', diff --git a/packages/interactor/src/definitions/check-box.ts b/packages/interactor/src/definitions/check-box.ts index a66c655e4..a17ce520e 100644 --- a/packages/interactor/src/definitions/check-box.ts +++ b/packages/interactor/src/definitions/check-box.ts @@ -1,28 +1,109 @@ -import { createInteractor, perform, focused } from '../index'; +import { Interaction, InteractorConstructor, createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; +import { FilterParams, ActionMethods } from '../specification'; -export const CheckBox = createInteractor('check box')({ - selector: 'input[type=checkbox]', - locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', - filters: { - title: (element) => element.title, - id: (element) => element.id, - valid: (element) => element.validity.valid, - checked: (element) => element.checked, - visible: { - apply: (element) => isVisible(element) || (element.labels && Array.from(element.labels).some(isVisible)), - default: true - }, - disabled: { - apply: (element) => element.disabled, - default: false - }, - focused +const filters = { + title: (element: HTMLInputElement) => element.title, + id: (element: HTMLInputElement) => element.id, + valid: (element: HTMLInputElement) => element.validity.valid, + checked: (element: HTMLInputElement) => element.checked, + visible: { + apply: (element: HTMLInputElement) => isVisible(element) || (element.labels && Array.from(element.labels).some(isVisible)), + default: true }, - actions: { - click: perform((element) => { element.click(); }), - check: perform((element) => { if(!element.checked) element.click(); }), - uncheck: perform((element) => { if(element.checked) element.click(); }), - toggle: perform((element) => { element.click(); }), + disabled: { + apply: (element: HTMLInputElement) => element.disabled, + default: false }, + focused +}; + +const actions = { + click: perform((element: HTMLInputElement) => { element.click(); }), + check: perform((element: HTMLInputElement) => { if(!element.checked) element.click(); }), + uncheck: perform((element: HTMLInputElement) => { if(element.checked) element.click(); }), + toggle: perform((element: HTMLInputElement) => { element.click(); }), +}; + +type CheckboxConstructor = InteractorConstructor; + +export interface CheckboxFilters extends FilterParams { + /** + * Filter by title + */ + title?: string; + /** + * Filter by id + */ + id?: string; + /** + * Filter by visibility. See {@link isVisible}. + */ + valid?: boolean; + /** + * Filter by whether the checkbox is valid. + */ + checked?: boolean; + /** + * Filter by whether the checkbox is checked. + */ + visible?: boolean; + /** + * Filter by whether the checkbox is disabled. + */ + disabled?: boolean; + /** + * Filter by whether the checkbox is focused. + */ + focused?: boolean; +} + +export interface CheckboxActions extends ActionMethods { + /** + * Click on the checkbox + */ + click(): Interaction; + /** + * Check the checkbox if it is not checked + */ + check(): Interaction; + /** + * Uncheck the checkbox if it is checked + */ + uncheck(): Interaction; + /** + * Toggle the checkbox + */ + toggle(): Interaction; +} + +/** + * Call this {@link InteractorConstructor} to initialize a checkbox interactor. + * The checkbox interactor can be used to interact with checkboxes on the page and + * to assert on their state. + * + * The checkbox is located by the text of its label. + * + * ### Example + * + * ``` typescript + * await Checbox('Submit').click(); + * await Checbox('Submit').is({ disabled: true }); + * await Checbox({ id: 'submit-button', disabled: true }).exists(); + * ``` + * + * ### See also + * + * - {@link ButtonFilters}: filters defined for this interactor + * - {@link ButtonActions}: actions callable on instances of this interactor + * - {@link InteractorConstructor}: how to create an interactor instance from this constructor + * - {@link Interactor}: interface of instances of this interactor in addition to its actions + * + * @category Interactor + */ +export const CheckBox: CheckboxConstructor = createInteractor('check box')({ + selector: 'input[type=checkbox]', + locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', + filters, + actions, }); From 0e9a23eac305dc938a82c25b62bc1f4a1b927ede Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Thu, 26 Nov 2020 16:47:10 +0100 Subject: [PATCH 07/14] Add typedoc support for sub-packages This makes it easier to work on documentation for each package individually. --- .gitignore | 1 + packages/bigtest/.gitignore | 1 - packages/interactor/API.md | 9 +++++++++ packages/interactor/package.json | 2 ++ packages/interactor/typedoc.json | 7 +++++++ packages/suite/API.md | 14 ++++++++++++++ packages/suite/package.json | 1 + packages/suite/typedoc.json | 7 +++++++ 8 files changed, 41 insertions(+), 1 deletion(-) delete mode 100644 packages/bigtest/.gitignore create mode 100644 packages/interactor/API.md create mode 100644 packages/interactor/typedoc.json create mode 100644 packages/suite/API.md create mode 100644 packages/suite/typedoc.json diff --git a/.gitignore b/.gitignore index 74c87725c..7b22e0728 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules yarn-error.log /packages/*/dist +/packages/*/docs diff --git a/packages/bigtest/.gitignore b/packages/bigtest/.gitignore deleted file mode 100644 index d8f8d4692..000000000 --- a/packages/bigtest/.gitignore +++ /dev/null @@ -1 +0,0 @@ -docs diff --git a/packages/interactor/API.md b/packages/interactor/API.md new file mode 100644 index 000000000..0f7acb32a --- /dev/null +++ b/packages/interactor/API.md @@ -0,0 +1,9 @@ +# Interactors + +Interactors are ... + +## Custom interactors + +You can create your own interactors with {@link createInteractor}. + + diff --git a/packages/interactor/package.json b/packages/interactor/package.json index 370cc39c6..d18c592b0 100644 --- a/packages/interactor/package.json +++ b/packages/interactor/package.json @@ -42,6 +42,8 @@ "express": "^4.17.1", "jsdom": "^16.2.2", "mocha": "^6.2.2", + "typedoc": "^0.20.0-beta.8", + "typescript": "3.9.7", "ts-node": "*", "wait-on": "^5.2.0" }, diff --git a/packages/interactor/typedoc.json b/packages/interactor/typedoc.json new file mode 100644 index 000000000..b84c3075b --- /dev/null +++ b/packages/interactor/typedoc.json @@ -0,0 +1,7 @@ +{ + "out": "docs", + "name": "@bigtest/interactor", + "entryPoints": "src/index.ts", + "readme": "API.md", + "includeVersion": true +} diff --git a/packages/suite/API.md b/packages/suite/API.md new file mode 100644 index 000000000..263e4173e --- /dev/null +++ b/packages/suite/API.md @@ -0,0 +1,14 @@ +# BigTest Suite + +Tools for working with BigTest test suites, including typings for all of the +elements of a test suite, helpers to validate a suite, as well as a DSL for +creating test suites. + +## Using the DSL + +When using the DSL you will usually import the {@link test} function and use it +as a starting point. + +## Suite validation + +See {@link validateTest}. diff --git a/packages/suite/package.json b/packages/suite/package.json index 80d905c8e..13f270d74 100644 --- a/packages/suite/package.json +++ b/packages/suite/package.json @@ -30,6 +30,7 @@ "expect": "^24.9.0", "mocha": "^6.2.2", "ts-node": "*", + "typedoc": "^0.20.0-beta.8", "typescript": "3.9.7" }, "volta": { diff --git a/packages/suite/typedoc.json b/packages/suite/typedoc.json new file mode 100644 index 000000000..b8ce44175 --- /dev/null +++ b/packages/suite/typedoc.json @@ -0,0 +1,7 @@ +{ + "out": "docs", + "name": "@bigtest/suite", + "entryPoints": "src/index.ts", + "readme": "API.md", + "includeVersion": true +} From e2031e3b914446fd3dcff7d6b6ebd515484436f3 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Thu, 26 Nov 2020 18:02:42 +0100 Subject: [PATCH 08/14] Improve suite docs --- packages/suite/API.md | 15 +++ packages/suite/src/dsl.ts | 142 ++++++++++++++++++++++++---- packages/suite/src/index.ts | 2 +- packages/suite/src/interfaces.ts | 86 +++++++++-------- packages/suite/src/validate-test.ts | 27 ++++-- 5 files changed, 207 insertions(+), 65 deletions(-) diff --git a/packages/suite/API.md b/packages/suite/API.md index 263e4173e..2e513330d 100644 --- a/packages/suite/API.md +++ b/packages/suite/API.md @@ -4,6 +4,21 @@ Tools for working with BigTest test suites, including typings for all of the elements of a test suite, helpers to validate a suite, as well as a DSL for creating test suites. +## Typings + +BigTest test suites are represented as a tree-like data structure. The exact +format of this data structure is described by the types in this packages. There +are three variations of this structure: + +- {@link Test}: the baseline, which describes the common structure of a test suite, + but does not include the ability to execute the test suite, or its results. +- {@link TestImplementation}: has the same structure as {@link Test}, but also + includes the ability to execute the test suite. This is what is normally + exported from a test file. +- {@link TestResult}: has the same structure as {@link Test}, but represents + the result of running the test suite, and includes the results of each step + and assertion as well as the aggregate results of test nodes. + ## Using the DSL When using the DSL you will usually import the {@link test} function and use it diff --git a/packages/suite/src/dsl.ts b/packages/suite/src/dsl.ts index ffbca124e..e2fa807f8 100644 --- a/packages/suite/src/dsl.ts +++ b/packages/suite/src/dsl.ts @@ -82,8 +82,8 @@ export class TestBuilder implements TestImplementation { * ### With description and action * * ``` typescript - * .step("do the thing", async() { - * await thing.do(); + * .step("click the new post link", async () { + * await Link('New post').click(); * }); * ``` * @@ -92,15 +92,15 @@ export class TestBuilder implements TestImplementation { * ``` typescript * .step( * { - * description: "do the thing", - * action: async() { - * await things.do(); + * description: "click the new post link", + * action: async () { + * await Link('New post').click(); * } * }, * { - * description: "another thing", - * action: async() { - * await anotherThing.do(); + * description: "fill in the text", + * action: async () { + * await TextField('Title').fillIn('An introduction to BigTest'); * } * } * ); @@ -112,31 +112,39 @@ export class TestBuilder implements TestImplementation { * they can be used like this: * * ``` typescript + * .step(Link('New post').click()) + * .step(TextField('Title').fillIn('An introduction to BigTest')) + * ``` + * + * Or like this if you prefer: + * + * ``` typescript * .step( * Link('New post').click(), - * TextField('Text').fillIn('BigTest is cool!'), - * Button('Submit').click(), - * Headline('BigTest is cool!').exists() + * TextField('Title').fillIn('An introduction to BigTest'), * ) * ``` * * ### Returning from context * + * Returning an object from a step merges it into the context, and it can + * then be used in subsequent steps. + * * ``` typescript - * .step("given a user", async() { + * .step("given a user", async () { * return { user: await User.create() } * }) - * .step("and the user has a post", async({ user }) { + * .step("and the user has a post", async ({ user }) { * return { post: await Post.create({ user }) } * }) - * .step("when I visit the post", async({ user, post }) { + * .step("when I visit the post", async ({ user, post }) { * await Page.visit(`/users/${user.id}/posts/${post.id}`); * }) * ``` * * @param description The description of this step - * @param action An async function which receives the current context and may return an object which extends the context - * @typeParam R The return type of the action function can either be + * @param action An async function which receives the current context and may return an object which extends the context. See {@link Action}. + * @typeParam R The return type of the action function can optionally be an object which will be merged into the context. */ step(description: string, action: Action): TestBuilder; /** @@ -166,8 +174,69 @@ export class TestBuilder implements TestImplementation { }); } - assertion(...assertions: AssertionList): TestBuilder; + /** + * Add one or more assertions to this test. The arguments to this function + * should be either a description and a check, or one or more objects which + * have `description` and `check` properties. Interactor assertions can be + * used as an assertion directly, but interactor actions cannot. + * + * The check is an async function, if it completes without error, the + * assertion passes. Its return value is ignored. + * + * ### With description and check + * + * ``` typescript + * .assertion("the new post link is no longer shown", async () { + * await Link('New post').absent(); + * }); + * ``` + * + * ### With assertion objects + * + * ``` typescript + * .step( + * { + * description: "the new post link is no longer shown", + * check: async () { + * await Link('New post').absent(); + * } + * }, + * { + * description: "the heading has changed", + * check: async () { + * await Heading('Create a new post').exists(); + * } + * } + * ); + * ``` + * + * ### With interactor + * + * Interactor assertions implement the assertion object interface, so they + * can be used like this: + * + * ``` typescript + * .assertion(Link('New post').absent()) + * .assertion(Headline('Create a new post').exists()) + * ``` + * + * Or like this if you prefer: + * + * ``` typescript + * .assertion( + * Link('New post').absent(), + * Headline('Create a new post').exists() + * ) + * ``` + * + * @param description The description of this assertion + * @param check An async function which receives the context. See {@link Check}. + */ assertion(description: string, check: Check): TestBuilder; + /** + * @param steps A list of assertion objects, each of which must have a `description` and `check` property. + */ + assertion(...assertions: AssertionList): TestBuilder; assertion(...args: [string, Check] | AssertionList): TestBuilder { if(this.state === 'child') { throw new TestStructureError(`Cannot add step after adding ${this.state}`); @@ -191,6 +260,45 @@ export class TestBuilder implements TestImplementation { }, 'assertion'); } + /** + * Add a child test to this test. Child tests are executed after all of a + * tests steps have run. Each child test may in turn add steps and + * assertions. + * + * A callback function must be provided as the second argument. This function + * receives a {@link TestBuilder} which you can use to construct the child + * test. The finished child test is then returned from the callback + * function. + * + * ### Example + * + * ``` typescript + * .child("signing in", (test) => { + * return ( + * test + * .step(TextField('Email').fillIn('jonas@example.com')) + * .step(TextField('Password').fillIn('password123')) + * .step(Button('Submit').click() + * ) + * }); + * ``` + * + * Or more consisely: + * + * ``` typescript + * .child("signing in", (test) => test + * .step(TextField('Email').fillIn('jonas@example.com')) + * .step(TextField('Password').fillIn('password123')) + * .step(Button('Submit').click() + * ); + * ``` + * + * *Note: the odd signature using a callback function was chosen because it + * improves type checking and inference when using TypeScript.* + * + * @param description The description of this child test + * @param childFn a callback function + */ child(description: string, childFn: (inner: TestBuilder) => TestBuilder): TestBuilder { let child = childFn(test(description)); return new TestBuilder({ diff --git a/packages/suite/src/index.ts b/packages/suite/src/index.ts index 481430380..4d981b937 100644 --- a/packages/suite/src/index.ts +++ b/packages/suite/src/index.ts @@ -1,3 +1,3 @@ export * from './interfaces'; export { test, TestBuilder } from './dsl'; -export { validateTest, MAXIMUM_DEPTH } from './validate-test'; +export { validateTest, TestValidationError, MAXIMUM_DEPTH } from './validate-test'; diff --git a/packages/suite/src/interfaces.ts b/packages/suite/src/interfaces.ts index f36fa8e0e..4c7bfb3ae 100644 --- a/packages/suite/src/interfaces.ts +++ b/packages/suite/src/interfaces.ts @@ -1,13 +1,19 @@ /** - * A tree of metadata describing a test and all of its children. By - * design, this tree is as stripped down as possible so that it can be - * seamlessly passed around from system to system. It does not include - * any references to the functions which comprise actions and - * assertions since they are not serializable, and cannot be shared - * between processes. + * A common base type for various nodes in test trees. */ -export interface Test extends Node { +export interface Node { + /** The human readable description of the test */ description: string; +} + +/** + * A tree which describes a test and all of its children. This interface + * describes the shape of a test without including any of the functions which + * comprise actions and assertions. This allows the test to be serialized and + * shared. + */ +export interface Test extends Node { + /** @hidden */ path?: string; steps: Node[]; assertions: Node[]; @@ -16,59 +22,51 @@ export interface Test extends Node { /** - * A tree of tests that is like the `Test` interface in every way + * A tree that is like the {@link Test} interface in every way * except that it contains the actual steps and assertions that will - * be run. Most of the time this interface is not necessary and - * components of the system will be working with the `Test` API, but - * in the case of the harness which actually consumes the test - * implementation, and in the case of the DSL which produces the test - * implementation, it will be needed. + * be run. + * + * It represents the full implementation of a test and is is what is normally + * exported from a test file. */ export interface TestImplementation extends Test { - description: string; - path?: string; steps: Step[]; assertions: Assertion[]; children: TestImplementation[]; } +/** + * An `async` function that accepts the current test context. If it resolves to + * another context, that context will be merged into the current context, + * otherwise, the context will be left alone. + */ export type Action = (context: Context) => Promise; /** - * A single operation that is part of the test. It contains an Action - * which is an `async` function that accepts the current test - * context. If it resolves to another context, that context will be - * merged into the current context, otherwise, the context will be - * left alone. + * A step which forms part of a test. Steps are executed in sequence. If one + * step fails, subsequent steps will be disregarded. Once all steps complete + * successfully, any assertions will run. */ export interface Step extends Node { - description: string; action: Action; } export type Check = (context: Context) => Promise; /** - * A single assertion that is part of a test case. It accepts the - * current text context which has been built up to this point. It - * should throw an exception if the test is failing. Any non-error - * result will be considered a pass. + * A single assertion that is part of a test case. It accepts the current text + * context which has been built up to this point. It should throw an exception + * if the test is failing. Any non-error result will be considered a pass. */ export interface Assertion extends Node { - description: string; check: Check; } /** - * Passed down the line from step to step and to each assertion of a - * test. + * Passed down the line from step to step and to each assertion of a test. */ export type Context = Record; -interface Node { - description: string; -} - /** * State indicator for various results. * - pending: not yet evaluating @@ -80,37 +78,43 @@ interface Node { export type ResultStatus = 'pending' | 'running' | 'failed' | 'ok' | 'disregarded'; /** - * Represents the result for a single test in the tree. A TestResult is ok even if - * one of its children is not, as long as all of its own steps and assertions pass. + * Represents the result of running a {@link Test}. The status of the test is + * an aggregate of the steps and assertions it contains. Only if all steps and + * assertions pass is the test marked as `ok`. + * + * A TestResult is ok even if one of its children is not, as long as all of its + * own steps and assertions pass. */ export interface TestResult extends Test { - description: string; - path?: string; + status: ResultStatus; steps: StepResult[]; assertions: AssertionResult[]; children: TestResult[]; - status: ResultStatus; } /** - * The result of a single step + * The result of a single {@link Step}. */ export interface StepResult extends Node { - description: string; status: ResultStatus; + /** If the status was `failed` then this may provide further details about the cause of failure */ error?: ErrorDetails; + /** True if the failure was caused by a timeout */ timeout?: boolean; + /** Any log events which are generated through uncaught errors, or log messages written to the console */ logEvents?: LogEvent[]; } /** - * The result of a single assertion + * The result of a single {@link Assertion}. */ export interface AssertionResult extends Node { - description: string; status: ResultStatus; + /** If the status was `failed` then this may provide further details about the cause of failure */ error?: ErrorDetails; + /** True if the failure was caused by a timeout */ timeout?: boolean; + /** Any log events which are generated through uncaught errors, or log messages written to the console */ logEvents?: LogEvent[]; } diff --git a/packages/suite/src/validate-test.ts b/packages/suite/src/validate-test.ts index dbcf73990..84892a648 100644 --- a/packages/suite/src/validate-test.ts +++ b/packages/suite/src/validate-test.ts @@ -1,14 +1,16 @@ import { Test } from './interfaces'; -type Loc = { - file: string; -} - -class TestValidationError extends Error { +export class TestValidationError extends Error { name = 'TestValidationError' - public loc?: Loc; + /** + * The location where this error occurred. + */ + public loc?: { file: string }; + /** + * @hidden + */ constructor(message: string, file?: string) { super(message); if(file) { @@ -27,8 +29,21 @@ function findDuplicates(array: T[], callback: (value: T) => void) { } } +/** + * The maximum nesting depth of a test suite. + */ export const MAXIMUM_DEPTH = 10; +/** + * Checks whether the given {@link Test} is well formed. Note that this only + * checks the format of the test structure, and not whether the test is + * succeeds, or contains any other errors, such as type or logic errors. + * + * If the test is invalid, it will throw a {@link TestValidationError}. + * + * @param test The test to validate + * @returns `true` if the test is valid, otherwise it will throw an exception + */ export function validateTest(test: Test): true { function validateTestInner(test: Test, path: string[] = [], file?: string, depth = 0): true { if(depth > MAXIMUM_DEPTH) { From a21aefbbfabfcc04f665d0e89c4c9cd4d433af6b Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Thu, 26 Nov 2020 18:30:50 +0100 Subject: [PATCH 09/14] Document interactor class --- packages/interactor/API.md | 6 +- packages/interactor/src/interactor.ts | 88 +++++++++++++++++++++++++-- packages/interactor/typedoc.json | 3 +- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/interactor/API.md b/packages/interactor/API.md index 0f7acb32a..75317436c 100644 --- a/packages/interactor/API.md +++ b/packages/interactor/API.md @@ -1,9 +1,9 @@ # Interactors -Interactors are ... +## Built in interactors + +We provide a set of interactors built in. ## Custom interactors You can create your own interactors with {@link createInteractor}. - - diff --git a/packages/interactor/src/interactor.ts b/packages/interactor/src/interactor.ts index 1220793b9..c99c674d5 100644 --- a/packages/interactor/src/interactor.ts +++ b/packages/interactor/src/interactor.ts @@ -9,10 +9,16 @@ import { formatTable } from './format-table'; import { FilterNotMatchingError } from './errors'; import { interaction, check, Interaction, ReadonlyInteraction } from './interaction'; +/** + * Instances of an interactor returned by an {@link InteractorConstructor}, use + * this class as its base. They are also extended with any additional actions + * defined in their {@link InteractorSpecification}. + */ export class Interactor, A extends Actions> { // eslint-disable-next-line @typescript-eslint/no-explicit-any private ancestors: Array> = []; + /** @hidden */ constructor( public name: string, public specification: InteractorSpecification, @@ -25,6 +31,22 @@ export class Interactor, A extends Actio return [...this.ancestors, this]; } + /** + * Returns a copy of the given interactor which is scoped to this interactor. + * When there are multiple matches for an interactor, this makes it possible + * to make them more specific by limiting the interactor to a section of the + * page. + * + * ## Example + * + * ``` typescript + * await Fieldset('Owner').find(TextField('Name')).fillIn('Jonas'); + * await Fieldset('Brand').find(TextField('Name')).fillIn('Volkswagen'); + * ``` + * @param interactor the interactor which should be scoped + * @returns a scoped copy of the initial interactor + * @typeParam T the type of the interactor that we are going to scope + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any find>(interactor: T): T { return Object.create(interactor, { @@ -42,6 +64,9 @@ export class Interactor, A extends Actio } } + /** + * @returns a human readable description of this interactor + */ get description(): string { return this.ancestorsAndSelf.reverse().map((interactor) => interactor.ownDescription).join(' within '); } @@ -54,7 +79,20 @@ export class Interactor, A extends Actio return resolveUnique(this.unsafeSyncResolveParent(), this) as E; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + /** + * Perform a one-off action on the given interactor. Takes a function which + * receives an element. This function converges, which means that it is rerun + * in a loop until it does not throw an error or times out. + * + * We recommend using this function for debugging only. You should normally + * define an action in an {@link InteractorSpecification}. + * + * ## Example + * + * ``` typescript + * await Link('Next').perform((e) => e.click()); + * ``` + */ perform(fn: (element: E) => void): Interaction { return interaction(`${this.description} performs`, () => { return converge(() => { @@ -63,6 +101,16 @@ export class Interactor, A extends Actio }); } + /** + * An assertion which checks that an element matching the interactor exists. + * Throws an error if the element does not exist. + * + * ## Example + * + * ``` typescript + * await Link('Next').exists(); + * ``` + */ exists(): ReadonlyInteraction { return check(`${this.description} exists`, () => { return converge(() => { @@ -71,6 +119,16 @@ export class Interactor, A extends Actio }); } + /** + * An assertion which checks that an element matching the interactor does not + * exist. Throws an error if the element exists. + * + * ## Example + * + * ``` typescript + * await Link('Next').absent(); + * ``` + */ absent(): ReadonlyInteraction { return check(`${this.description} does not exist`, () => { return converge(() => { @@ -79,6 +137,30 @@ export class Interactor, A extends Actio }); } + /** + * Checks that there is one element matching the interactor, and that this + * element matches the given filters. The available filters are defined by + * the {@link InteractorSpecification}. + * + * ## Example + * + * ``` typescript + * await Link('Home').has({ href: '/' }) + * ``` + */ + has(filters: FilterParams): ReadonlyInteraction { + return this.is(filters); + } + + /** + * Identical to {@link has}, but reads better with some filters. + * + * ## Example + * + * ``` typescript + * await CheckBox('Accept conditions').is({ checked: true }) + * ``` + */ is(filters: FilterParams): ReadonlyInteraction { let filter = new Filter(this.specification, filters); return check(`${this.description} matches filters: ${filter.description}`, () => { @@ -92,8 +174,4 @@ export class Interactor, A extends Actio }); }); } - - has(filters: FilterParams): ReadonlyInteraction { - return this.is(filters); - } } diff --git a/packages/interactor/typedoc.json b/packages/interactor/typedoc.json index b84c3075b..aa3a04ddf 100644 --- a/packages/interactor/typedoc.json +++ b/packages/interactor/typedoc.json @@ -3,5 +3,6 @@ "name": "@bigtest/interactor", "entryPoints": "src/index.ts", "readme": "API.md", - "includeVersion": true + "includeVersion": true, + "excludePrivate": true } From c9ccbcbda3a999855ec5976e58a88ebbf51dd513 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Sat, 28 Nov 2020 18:37:35 +0100 Subject: [PATCH 10/14] Add documentation for built in interactors --- packages/interactor/src/definitions/button.ts | 121 ++++++---------- .../interactor/src/definitions/check-box.ts | 133 ++++++------------ .../interactor/src/definitions/heading.ts | 22 ++- packages/interactor/src/definitions/link.ts | 33 ++++- .../src/definitions/multi-select.ts | 68 +++++++-- .../src/definitions/radio-button.ts | 36 ++++- packages/interactor/src/definitions/select.ts | 44 +++++- .../interactor/src/definitions/text-field.ts | 42 +++++- packages/interactor/src/index.ts | 2 +- 9 files changed, 311 insertions(+), 190 deletions(-) diff --git a/packages/interactor/src/definitions/button.ts b/packages/interactor/src/definitions/button.ts index d03254fac..f17be13fc 100644 --- a/packages/interactor/src/definitions/button.ts +++ b/packages/interactor/src/definitions/button.ts @@ -1,71 +1,40 @@ -import { Interaction, InteractorConstructor, createInteractor, perform, focused, isVisible } from '../index'; -import { FilterParams, ActionMethods } from '../specification'; +import { createInteractor, perform, focused } from '../index'; +import { isVisible } from 'element-is-visible'; function isButtonElement(element: HTMLInputElement | HTMLButtonElement): element is HTMLButtonElement { return element.tagName === 'BUTTON'; } -type ButtonElement = HTMLInputElement | HTMLButtonElement; - -const filters = { - title: (element: ButtonElement) => element.title, - id: (element: ButtonElement) => element.id, - visible: { apply: isVisible, default: true }, - disabled: { - apply: (element: ButtonElement) => element.disabled, - default: false +const ButtonInteractor = createInteractor('button')({ + selector: 'button,input[type=button],input[type=submit],input[type=reset],input[type=image]', + locator(element) { + if(isButtonElement(element)) { + return element.textContent || ''; + } else if(element.type === 'image') { + return element.alt; + } else { + return element.value; + } }, - focused -}; - -const actions = { - click: perform((element: ButtonElement) => { element.click(); }), - focus: perform((element: ButtonElement) => { element.focus(); }), - blur: perform((element: ButtonElement) => { element.blur(); }), -}; - -type ButtonConstructor = InteractorConstructor; - -export interface ButtonFilters extends FilterParams { - /** - * Filter by title - */ - title?: string; - /** - * Filter by id - */ - id?: string; - /** - * Filter by visibility. See {@link isVisible}. - */ - visible?: boolean; - /** - * Filter by whether the button is disabled. - */ - disabled?: boolean; - /** - * Filter by whether the button is focused. - */ - focused?: boolean; -} - -export interface ButtonActions extends ActionMethods { - /** - * Click on the button - */ - click(): Interaction; - /** - * Move focus to the button - */ - focus(): Interaction; - /** - * Move focus away from the button - */ - blur(): Interaction; -} + filters: { + title: (element) => element.title, + id: (element) => element.id, + visible: { apply: isVisible, default: true }, + disabled: { + apply: (element) => element.disabled, + default: false + }, + focused + }, + actions: { + click: perform((element) => { element.click(); }), + focus: perform((element) => { element.focus(); }), + blur: perform((element) => { element.blur(); }), + }, +}); /** - * Call this {@link InteractorConstructor} to initialize a button interactor. + * Call this {@link InteractorConstructor} to initialize a button {@link Interactor}. * The button interactor can be used to interact with buttons on the page and * to assert on their state. * @@ -79,26 +48,20 @@ export interface ButtonActions extends ActionMethods('button')({ - selector: 'button,input[type=button],input[type=submit],input[type=reset],input[type=image]', - locator(element) { - if(isButtonElement(element)) { - return element.textContent || ''; - } else if(element.type === 'image') { - return element.alt; - } else { - return element.value; - } - }, - filters, - actions, -}); +export const Button = ButtonInteractor; diff --git a/packages/interactor/src/definitions/check-box.ts b/packages/interactor/src/definitions/check-box.ts index a17ce520e..b4f4c010b 100644 --- a/packages/interactor/src/definitions/check-box.ts +++ b/packages/interactor/src/definitions/check-box.ts @@ -1,84 +1,34 @@ -import { Interaction, InteractorConstructor, createInteractor, perform, focused } from '../index'; +import { createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; -import { FilterParams, ActionMethods } from '../specification'; -const filters = { - title: (element: HTMLInputElement) => element.title, - id: (element: HTMLInputElement) => element.id, - valid: (element: HTMLInputElement) => element.validity.valid, - checked: (element: HTMLInputElement) => element.checked, - visible: { - apply: (element: HTMLInputElement) => isVisible(element) || (element.labels && Array.from(element.labels).some(isVisible)), - default: true +const CheckBoxInteractor = createInteractor('check box')({ + selector: 'input[type=checkbox]', + locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', + filters: { + title: (element) => element.title, + id: (element) => element.id, + valid: (element) => element.validity.valid, + checked: (element) => element.checked, + visible: { + apply: (element) => isVisible(element) || (element.labels && Array.from(element.labels).some(isVisible)), + default: true + }, + disabled: { + apply: (element) => element.disabled, + default: false + }, + focused }, - disabled: { - apply: (element: HTMLInputElement) => element.disabled, - default: false + actions: { + click: perform((element) => { element.click(); }), + check: perform((element) => { if(!element.checked) element.click(); }), + uncheck: perform((element) => { if(element.checked) element.click(); }), + toggle: perform((element) => { element.click(); }), }, - focused -}; - -const actions = { - click: perform((element: HTMLInputElement) => { element.click(); }), - check: perform((element: HTMLInputElement) => { if(!element.checked) element.click(); }), - uncheck: perform((element: HTMLInputElement) => { if(element.checked) element.click(); }), - toggle: perform((element: HTMLInputElement) => { element.click(); }), -}; - -type CheckboxConstructor = InteractorConstructor; - -export interface CheckboxFilters extends FilterParams { - /** - * Filter by title - */ - title?: string; - /** - * Filter by id - */ - id?: string; - /** - * Filter by visibility. See {@link isVisible}. - */ - valid?: boolean; - /** - * Filter by whether the checkbox is valid. - */ - checked?: boolean; - /** - * Filter by whether the checkbox is checked. - */ - visible?: boolean; - /** - * Filter by whether the checkbox is disabled. - */ - disabled?: boolean; - /** - * Filter by whether the checkbox is focused. - */ - focused?: boolean; -} - -export interface CheckboxActions extends ActionMethods { - /** - * Click on the checkbox - */ - click(): Interaction; - /** - * Check the checkbox if it is not checked - */ - check(): Interaction; - /** - * Uncheck the checkbox if it is checked - */ - uncheck(): Interaction; - /** - * Toggle the checkbox - */ - toggle(): Interaction; -} +}); /** - * Call this {@link InteractorConstructor} to initialize a checkbox interactor. + * Call this {@link InteractorConstructor} to initialize a checkbox {@link Interactor}. * The checkbox interactor can be used to interact with checkboxes on the page and * to assert on their state. * @@ -87,23 +37,28 @@ export interface CheckboxActions extends ActionMethods('check box')({ - selector: 'input[type=checkbox]', - locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', - filters, - actions, -}); +export const CheckBox = CheckBoxInteractor; diff --git a/packages/interactor/src/definitions/heading.ts b/packages/interactor/src/definitions/heading.ts index bc876378b..e64602c94 100644 --- a/packages/interactor/src/definitions/heading.ts +++ b/packages/interactor/src/definitions/heading.ts @@ -1,10 +1,30 @@ import { createInteractor } from '../create-interactor'; import { isVisible } from 'element-is-visible'; -export const Heading = createInteractor('heading')({ +const HeadingInteractor = createInteractor('heading')({ selector: 'h1,h2,h3,h4,h5,h6', filters: { level: (element) => parseInt(element.tagName[1]), visible: { apply: isVisible, default: true }, } }); + +/** + * Call this {@link InteractorConstructor} to initialize a heading {@link Interactor}. + * The heading interactor can be used to assert on the state of headings on the page, + * represented by the `h1` through `h6` tags. + * + * ### Example + * + * ``` typescript + * await Heading('Welcome!').exists(); + * ``` + * + * ### Filters + * + * - `level`: *number* – The level of the heading, for example, the level of `h3` is `3`. + * - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}. + * + * @category Interactor + */ +export const Heading = HeadingInteractor; diff --git a/packages/interactor/src/definitions/link.ts b/packages/interactor/src/definitions/link.ts index 32e318bcd..3e5382252 100644 --- a/packages/interactor/src/definitions/link.ts +++ b/packages/interactor/src/definitions/link.ts @@ -1,7 +1,7 @@ import { createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; -export const Link = createInteractor('link')({ +const LinkInteractor = createInteractor('link')({ selector: 'a[href]', filters: { title: (element) => element.title, @@ -14,3 +14,34 @@ export const Link = createInteractor('link')({ click: perform((element) => { element.click(); }) }, }); + +/** + * Call this {@link InteractorConstructor} to initialize a link {@link Interactor}. + * The link interactor can be used to interact with links on the page and + * to assert on their state. + * + * The link is located by its text. + * + * ### Example + * + * ``` typescript + * await Link('Home').click(); + * await Link('Home').has({ href: '/' }); + * await Link({ id: 'home-link', href: '/' }).exists(); + * ``` + * + * ### Filters + * + * - `title`: *string* – Filter by title + * - `id`: *string* – Filter by id + * - `href`: *string* – The value of the href attribute that the link points to + * - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}. + * - `focused`: *boolean* – Filter by whether the link is focused. See {@link focused}. + * + * ### Actions + * + * - `click()`: *{@link Interaction}* – Click on the link + * + * @category Interactor + */ +export const Link = LinkInteractor; diff --git a/packages/interactor/src/definitions/multi-select.ts b/packages/interactor/src/definitions/multi-select.ts index 5701f7999..86f1472d6 100644 --- a/packages/interactor/src/definitions/multi-select.ts +++ b/packages/interactor/src/definitions/multi-select.ts @@ -1,9 +1,9 @@ -import { createInteractor, perform } from '../index'; +import { createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; import { dispatchChange, dispatchInput } from '../dispatch'; import { getSelect } from '../get-select'; -const SelectOption = createInteractor('option')({ +const MultiSelectOption = createInteractor('option')({ selector: 'option', locator: (element) => element.label, filters: { @@ -43,7 +43,7 @@ const SelectOption = createInteractor('option')({ }, }); -export const MultiSelect = createInteractor('select box')({ +const MultiSelectInteractor = createInteractor('select box')({ selector: 'select[multiple]', locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', filters: { @@ -51,27 +51,67 @@ export const MultiSelect = createInteractor('select box')({ id: (element) => element.id, valid: (element) => element.validity.valid, values: (element) => Array.from(element.selectedOptions).map((o) => o.label), - visible: { - apply: (element) => isVisible(element) || (element.labels && Array.from(element.labels).some(isVisible)), - default: true - }, + visible: { apply: isVisible, default: true }, disabled: { apply: (element) => element.disabled, default: false - } + }, + focused }, actions: { click: perform((element) => { element.click(); }), focus: perform((element) => { element.focus(); }), blur: perform((element) => { element.blur(); }), - choose: async (interactor, value: string) => { - await interactor.find(SelectOption(value)).choose(); + choose: async (interactor, text: string) => { + await interactor.find(MultiSelectOption(text)).choose(); }, - select: async (interactor, value: string) => { - await interactor.find(SelectOption(value)).select(); + select: async (interactor, text: string) => { + await interactor.find(MultiSelectOption(text)).select(); }, - deselect: async (interactor, value: string) => { - await interactor.find(SelectOption(value)).deselect(); + deselect: async (interactor, text: string) => { + await interactor.find(MultiSelectOption(text)).deselect(); }, }, }); + +/** + * Call this {@link InteractorConstructor} to initialize an {@link Interactor} + * for select boxes with multiple select. The multi select interactor can be + * used to interact with select boxes with the `multiple` attribute and to + * assert on their state. + * + * See {@link Select} for an interactor for single select boxes. + * + * The multi select is located by the text of its label. + * + * ### Example + * + * ``` typescript + * await MultiSelect('Language').select('English'); + * await MultiSelect('Language').select('German'); + * await MultiSelect('Language').deselect('Swedish'); + * await MultiSelect('Language').has({ values: ['English', 'German'] }); + * ``` + * + * ### Filters + * + * - `title`: *string* – Filter by title + * - `id`: *string* – Filter by id + * - `valid`: *boolean* – Filter by whether the checkbox is valid. + * - `values`: *string[]* – Filter by the text of the selected options. + * - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}. + * - `disabled`: *boolean* – Filter by whether the checkbox is disabled. Defaults to `false`. + * - `focused`: *boolean* – Filter by whether the checkbox is focused. See {@link focused}. + * + * ### Actions + * + * - `click()`: *{@link Interaction}* – Click on the multi select + * - `focus()`: *{@link Interaction}* – Move focus to the multi select + * - `blur()`: *{@link Interaction}* – Move focus away from the multi select + * - `choose(text: string)`: *{@link Interaction}* – Choose the option with the given text from the multi select. Will deselect all other selected options. + * - `select(text: string)`: *{@link Interaction}* – Add the option with the given text to the selection. + * - `deselect(text: string)`: *{@link Interaction}* – Remove the option with the given text from the selection. + * + * @category Interactor + */ +export const MultiSelect = MultiSelectInteractor; diff --git a/packages/interactor/src/definitions/radio-button.ts b/packages/interactor/src/definitions/radio-button.ts index 355050c93..9565c87ed 100644 --- a/packages/interactor/src/definitions/radio-button.ts +++ b/packages/interactor/src/definitions/radio-button.ts @@ -1,7 +1,7 @@ import { createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; -export const RadioButton = createInteractor('radio button')({ +const RadioButtonInteractor = createInteractor('radio button')({ selector: 'input[type=radio]', locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', filters: { @@ -24,3 +24,37 @@ export const RadioButton = createInteractor('radio button')({ choose: perform((element) => { element.click(); }), }, }); + +/** + * Call this {@link InteractorConstructor} to initialize a radio button {@link Interactor}. + * The radio button interactor can be used to interact with radio buttons on the page and + * to assert on their state. + * + * The radio button is located by the text of its label. + * + * ### Example + * + * ``` typescript + * await RadioButton('Public').click(); + * await RadioButton('Private').is({ disabled: true }); + * await RadioButton({ id: 'privacy-public', disabled: true }).exists(); + * ``` + * + * ### Filters + * + * - `title`: *string* – Filter by title + * - `id`: *string* – Filter by id + * - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}. + * - `valid`: *boolean* – Filter by whether the radio button is valid. + * - `checked`: *boolean* – Filter by whether the radio button is checked. + * - `disabled`: *boolean* – Filter by whether the radio button is disabled. Defaults to `false`. + * - `focused`: *boolean* – Filter by whether the radio button is focused. See {@link focused}. + * + * ### Actions + * + * - `click()`: *{@link Interaction}* – Click on the radio button + * - `choose()`: *{@link Interaction}* – Click on the radio button + * + * @category Interactor + */ +export const RadioButton = RadioButtonInteractor; diff --git a/packages/interactor/src/definitions/select.ts b/packages/interactor/src/definitions/select.ts index c818c2912..f8ae01c0c 100644 --- a/packages/interactor/src/definitions/select.ts +++ b/packages/interactor/src/definitions/select.ts @@ -1,4 +1,4 @@ -import { createInteractor, perform } from '../index'; +import { createInteractor, perform, focused } from '../index'; import { isVisible } from 'element-is-visible'; import { dispatchChange, dispatchInput } from '../dispatch'; import { getSelect } from '../get-select'; @@ -25,7 +25,7 @@ const SelectOption = createInteractor('option')({ }, }); -export const Select = createInteractor('select box')({ +const SelectInteractor = createInteractor('select box')({ selector: 'select:not([multiple])', locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', filters: { @@ -40,7 +40,8 @@ export const Select = createInteractor('select box')({ disabled: { apply: (element) => element.disabled, default: false - } + }, + focused }, actions: { click: perform((element) => { element.click(); }), @@ -51,3 +52,40 @@ export const Select = createInteractor('select box')({ }, }, }); + +/** + * Call this {@link InteractorConstructor} to initialize an {@link Interactor} + * for select boxes. The select interactor can be used to interact with select + * boxes and to assert on their state. + * + * For interacting with multiple select boxes, see {@link MultiSelect}. + * + * The select box is located by the text of its label. + * + * ### Example + * + * ``` typescript + * await Select('Language').select('English'); + * await Select('Language').has({ value: 'English' }); + * ``` + * + * ### Filters + * + * - `title`: *string* – Filter by title + * - `id`: *string* – Filter by id + * - `valid`: *boolean* – Filter by whether the checkbox is valid. + * - `value`: *string* – Filter by the text of the selected option. + * - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}. + * - `disabled`: *boolean* – Filter by whether the checkbox is disabled. Defaults to `false`. + * - `focused`: *boolean* – Filter by whether the checkbox is focused. See {@link focused}. + * + * ### Actions + * + * - `click()`: *{@link Interaction}* – Click on the select box + * - `focus()`: *{@link Interaction}* – Move focus to the select box + * - `blur()`: *{@link Interaction}* – Move focus away from the select box + * - `choose(text: string)`: *{@link Interaction}* – Choose the option with the given text from the select box. + * + * @category Interactor + */ +export const Select = SelectInteractor; diff --git a/packages/interactor/src/definitions/text-field.ts b/packages/interactor/src/definitions/text-field.ts index beefca6d7..889511a5e 100644 --- a/packages/interactor/src/definitions/text-field.ts +++ b/packages/interactor/src/definitions/text-field.ts @@ -6,7 +6,7 @@ const selector = 'textarea, input' + [ 'image', 'month', 'radio', 'range', 'reset', 'submit', 'time', 'datetime' ].map((t) => `:not([type=${t}])`).join(''); -export const TextField = createInteractor('text field')({ +const TextFieldInteractor = createInteractor('text field')({ selector, locator: (element) => element.labels ? (Array.from(element.labels)[0]?.textContent || '') : '', filters: { @@ -29,3 +29,43 @@ export const TextField = createInteractor Date: Tue, 1 Dec 2020 09:56:47 +0100 Subject: [PATCH 11/14] Hide deprecated App interactor in docs --- packages/interactor/src/app.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/interactor/src/app.ts b/packages/interactor/src/app.ts index 6464505bd..73c5a5dc5 100644 --- a/packages/interactor/src/app.ts +++ b/packages/interactor/src/app.ts @@ -1,6 +1,9 @@ import { Interaction } from './interaction'; import { Page } from './page'; +/** + * @hidden + */ export const App = { visit(path = '/'): Interaction { console.warn('App.visit is deprecated, please use Page.visit instead'); From 5272efc899b05b34b672cc5836d157786f6caa47 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Tue, 1 Dec 2020 10:02:05 +0100 Subject: [PATCH 12/14] Document helper functions --- packages/interactor/src/fill-in.ts | 8 ++++++++ packages/interactor/src/focused.ts | 3 +++ 2 files changed, 11 insertions(+) diff --git a/packages/interactor/src/fill-in.ts b/packages/interactor/src/fill-in.ts index 15d794dbc..db7ff62e9 100644 --- a/packages/interactor/src/fill-in.ts +++ b/packages/interactor/src/fill-in.ts @@ -52,6 +52,14 @@ function setValue(element: TextFieldElement, value: string): void { } } +/** + * Fill in text into an element by emulating how a user would do it, first + * focusing the element, then filling in the text letter by letter, generating + * the appropriate keyboard events. + * + * @param element The element to fill in text in + * @param value The text value to fill in + */ export function fillIn(element: TextFieldElement, value: string) { let originalValue = element.value; diff --git a/packages/interactor/src/focused.ts b/packages/interactor/src/focused.ts index 742206506..220297217 100644 --- a/packages/interactor/src/focused.ts +++ b/packages/interactor/src/focused.ts @@ -1,3 +1,6 @@ +/** + * Helper function for focused filters, returns whether the given element is focused. + */ export function focused(element: Element): boolean { return element.ownerDocument.activeElement === element; } From 4540c9e340a88d25d58bf7b38afd2c7e7d73cab2 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 2 Dec 2020 12:48:44 +0100 Subject: [PATCH 13/14] Document `Page` --- packages/interactor/src/page.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/interactor/src/page.ts b/packages/interactor/src/page.ts index ab7067364..f0b80bc02 100644 --- a/packages/interactor/src/page.ts +++ b/packages/interactor/src/page.ts @@ -10,7 +10,7 @@ const PageInteractor = createInteractor('page')({ } }); -export const Page = Object.assign(PageInteractor(), { +const PageInteractorInstance = Object.assign(PageInteractor(), { visit(path = '/'): Interaction { return interaction(`visiting ${JSON.stringify(path)}`, async () => { let appUrl = bigtestGlobals.appUrl; @@ -42,3 +42,33 @@ export const Page = Object.assign(PageInteractor(), { }); } }); + +/** + * This {@link Interactor} can be used to assert on global properties of the + * page. When using the BigTest test runner, it can also be used for + * interacting with the page itself, for example through nagivation. + * + * ### Example + * + * ``` typescript + * await Page.has({ title: 'Welcome to my app!' }); + * ``` + * + * Navigation, for BigTest test runner only: + * + * ``` typescript + * await Page.visit('/archive'); + * ``` + * + * ### Filters + * + * - `title`: *string* – the title of the document + * - `url`: *string* – the URL of the document + * + * ### Actions + * + * - `visit(path: string)`: *{@link Interaction}* – visit the given path in the test frame, BigTest runner only. + * + * @category Interactor + */ +export const Page = PageInteractorInstance; From a1c4df8cccaff7858a3743413281f45364968858 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 2 Dec 2020 13:10:28 +0100 Subject: [PATCH 14/14] Some more docs --- packages/interactor/src/interaction.ts | 25 ++++++++++++++ packages/interactor/src/interactor.ts | 4 +++ packages/interactor/src/specification.ts | 43 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/packages/interactor/src/interaction.ts b/packages/interactor/src/interaction.ts index 66084071b..a5f63dde6 100644 --- a/packages/interactor/src/interaction.ts +++ b/packages/interactor/src/interaction.ts @@ -1,9 +1,34 @@ +/** + * An interaction represents some type of action or assertion that can be + * taken on an {@link Interactor}. + * + * The interaction can function as a lazy promise. That means that calling + * `then` on the interaction or awaiting it using `await`, will run the + * interaction and the promise will resolve once the action is complete. + * However, an interaction which is not awaited will not run by itself. + * + * @typeParam T the return value of the promise that this interaction evaluates to. + */ export interface Interaction extends Promise { + /** + * Return a description of the interaction + */ description: string; + /** + * Perform the interaction + */ action: () => Promise; } +/** + * Like {@link Interaction}, except that it is used for assertions only. + * + * @typeParam T the return value of the promise that this interaction evaluates to. + */ export interface ReadonlyInteraction extends Interaction { + /** + * Perform the check + */ check: () => Promise; } diff --git a/packages/interactor/src/interactor.ts b/packages/interactor/src/interactor.ts index c99c674d5..abd955e66 100644 --- a/packages/interactor/src/interactor.ts +++ b/packages/interactor/src/interactor.ts @@ -20,9 +20,13 @@ export class Interactor, A extends Actio /** @hidden */ constructor( + /** @hidden */ public name: string, + /** @hidden */ public specification: InteractorSpecification, + /** @hidden */ public filter: Filter, + /** @hidden */ public locator?: Locator, ) {} diff --git a/packages/interactor/src/specification.ts b/packages/interactor/src/specification.ts index 4cd20d9a4..3ee506405 100644 --- a/packages/interactor/src/specification.ts +++ b/packages/interactor/src/specification.ts @@ -65,8 +65,51 @@ export type FilterParams> = keyof F exte export type InteractorInstance, A extends Actions> = Interactor & ActionMethods; +/** + * An interactor constructor is a function which can be used to initialize an + * {@link Interactor}. When calling {@link createInteractor}, you will get + * back an interactor constructor. + * + * The constructor can be called with a locator value, and an object of + * filters. Both are optional, and can be omitted. + * + * @typeParam E The type of DOM Element that this interactor operates on. + * @typeParam F the filters of this interactor, this is usually inferred from the specification + * @typeParam A the actions of this interactor, this is usually inferred from the specification + */ export interface InteractorConstructor, A extends Actions> { + /** + * The constructor can be called with filters only: + * + * ``` typescript + * Link({ id: 'home-link', href: '/' }); + * ``` + * + * Or with no arguments, this can be especially useful when finding a nested element. + * + * ``` + * ListItem('JavaScript').find(Link()).click(); // click the only link within a specific list item + * ``` + * + * @param filters An object describing a set of filters to apply, which should match the value of applying the filters defined in the {@link InteractorSpecification} to the element. + */ (filters?: FilterParams): InteractorInstance; + /** + * The constructor can be called with a locator: + * + * ``` typescript + * Link('Home'); + * ``` + * + * Or with a locator and options: + * + * ``` typescript + * Link('Home', { href: '/' }); + * ``` + * + * @param value The locator value, which should match the value of applying the locator function defined in the {@link InteractorSpecification} to the element. + * @param filters An object describing a set of filters to apply, which should match the value of applying the filters defined in the {@link InteractorSpecification} to the element. + */ (value: string, filters?: FilterParams): InteractorInstance; }