diff --git a/README.md b/README.md index 77d6847..abf85e2 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,18 @@ import { sleep } from '@bicycle-codes/dom' await sleep(3000) // wait 3 seconds ``` +### type +```ts +export async function type ( + selector:string|HTMLElement|Element, + value:string, +):Promise +``` + +#### example +```js +``` + ## credits Thanks Jake Verbaten for writing this originally. diff --git a/package.json b/package.json index c88c592..d13e932 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "@bicycle-codes/tapzero": "^0.10.0", + "@bicycle-codes/tapzero": "^0.10.3", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "auto-changelog": "^2.4.0", diff --git a/src/index.ts b/src/index.ts index ee38ba4..6c25cfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { toElement, requestAnimationFrame } from './util.js' export const qs = document.querySelector.bind(document) export const qsa = document.querySelectorAll.bind(document) @@ -122,7 +123,7 @@ export function isElementVisible ( * element: Element, * multipleTags?: boolean, * regex?: RegExp - * }} args + * }|string} args */ export function waitForText (args:{ text?:string, @@ -147,7 +148,7 @@ export function waitForText (args:{ return waitFor( { timeout: opts.timeout }, - () => { + () => { // the lambda const { element, text, @@ -237,7 +238,7 @@ export function waitFor ( lambda?:Lambda ):Promise { let selector:string - let visible:boolean = true + let visible:boolean let timeout = DEFAULT_TIMEOUT if (typeof args === 'string') { selector = args @@ -278,7 +279,7 @@ export function waitFor ( /** * Click the given element. * - * @param {HTMLElement} element + * @param {Element} element */ export function click (element:Element) { event({ @@ -318,3 +319,38 @@ export function event (args:{ element.dispatchEvent(event) } + +/** + * Type the given value into the element, emitting all relevant events, to + * simulate a user typing with a keyboard. + * + * @param {string|HTMLElement|Element} selector - A CSS selector string, or an instance of HTMLElement, or Element. + * @param {string} value - The string to type into the :focus element. + * @returns {Promise} + * + * @example + * ```js + * await type('#my-div', 'Hello World') + * ``` + */ +export async function type ( + selector:string|HTMLElement|Element, + value:string, +):Promise { + const el = toElement(selector) + + if (!('value' in el!)) throw new Error('Element missing value attribute') + + for (const c of value.split('')) { + await requestAnimationFrame() + el.value = el.value != null ? el.value + c : c + el.dispatchEvent( + new Event('input', { + bubbles: true, + cancelable: true + }) + ) + } + + await requestAnimationFrame() +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..4aa4bfd --- /dev/null +++ b/src/util.ts @@ -0,0 +1,41 @@ +/** + * Converts querySelector string to an HTMLElement or validates an + * existing HTMLElement. + * + * + * @param {string|Element|HTMLElement} selector - A CSS selector string, or an + * instance of an Element. + * @returns {Element} The HTMLElement, Element, or Window that corresponds to + * the selector. + * @throws {Error} Throws an error if the `selector` is not a string that + * resolves to an HTMLElement, or not an instance of + * HTMLElement, Element, or Window. + * + */ +export function toElement (_selector:string|HTMLElement|Element) { + let selector:string|Element|null = _selector + if (globalThis.document) { + if (typeof selector === 'string') { + selector = globalThis.document.querySelector(selector) + } + + if (!( + selector instanceof globalThis.HTMLElement || + selector instanceof globalThis.Element + )) { + throw new Error('`stringOrElement` needs to be an instance of ' + + 'HTMLElement or a querySelector that resolves to an HTMLElement') + } + + return selector + } +} + +export async function requestAnimationFrame ():Promise { + if (globalThis.document && globalThis.document.hasFocus()) { + // RAF only works when the window is focused + await new Promise(resolve => globalThis.requestAnimationFrame(resolve)) + } else { + await new Promise((resolve) => setTimeout(resolve, 0)) + } +} diff --git a/test/index.html b/test/index.html index d0fb5d3..9f7bef0 100644 --- a/test/index.html +++ b/test/index.html @@ -6,6 +6,10 @@ +
+ +
+ diff --git a/test/index.ts b/test/index.ts index 4583c56..8d48420 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,4 +1,4 @@ -import { dom, waitForText } from '../src/index.js' +import { dom, qs, waitForText, type } from '../src/index.js' import { test } from '@bicycle-codes/tapzero' import { Terminal } from 'xterm' @@ -256,3 +256,14 @@ test('waitForText', async t => { const text = await waitForText('testing') t.ok(text instanceof HTMLElement, 'should find the p tag given a string') }) + +test('type', async t => { + t.plan(11) + const input = qs('#test-input') + t.plan(11) + input?.addEventListener('input', ev => { + t.ok(ev, 'should dispatch an "input" event 11 times, once for each key') + }) + + await type('#test-input', 'hello world') +})