From 1d52ee34b93f8b4cb7cc666c16ab32b77e1b90de Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 18 Nov 2021 20:57:47 +0000 Subject: [PATCH 1/8] feat!: async APIs --- .eslintrc.js | 1 + src/.eslintrc | 8 - src/click.ts | 58 ---- src/clipboard/copy.ts | 24 +- src/clipboard/cut.ts | 22 +- src/clipboard/paste.ts | 39 +-- src/convenience/click.ts | 49 +++ src/convenience/hover.ts | 22 ++ src/convenience/index.ts | 3 + src/{ => convenience}/tab.ts | 6 +- src/hover.ts | 37 --- src/index.ts | 22 +- src/keyboard/index.ts | 62 +--- src/keyboard/keyboardImplementation.ts | 10 +- src/keyboard/types.ts | 2 +- src/options.ts | 16 + src/pointer/index.ts | 65 +--- src/pointer/pointerAction.ts | 6 +- src/pointer/pointerMove.ts | 3 +- src/pointer/pointerPress.ts | 2 +- src/pointer/types.ts | 8 +- src/setup.ts | 260 --------------- src/setup/api.ts | 44 +++ src/setup/config.ts | 38 +++ src/setup/directApi.ts | 116 +++++++ src/setup/index.ts | 19 ++ src/setup/setup.ts | 81 +++++ src/setup/wrapAsync.ts | 10 + src/type.ts | 93 ------ src/{ => utility}/clear.ts | 9 +- src/utility/index.ts | 4 + src/{ => utility}/selectOptions.ts | 22 +- src/utility/type.ts | 50 +++ src/{ => utility}/upload.ts | 15 +- src/utils/index.ts | 1 + src/utils/misc/getDocumentFromNode.ts | 7 + tests/clear.ts | 50 +-- tests/click/{click.js => click.ts} | 190 +++++------ tests/click/{dblclick.js => dblclick.ts} | 101 ++---- tests/click/tripleClick.ts | 8 +- tests/clipboard/copy.ts | 30 +- tests/clipboard/cut.ts | 30 +- tests/clipboard/{paste.js => paste.ts} | 48 +-- tests/document/index.ts | 10 +- tests/hover/{hover.js => hover.ts} | 42 +-- tests/hover/{unhover.js => unhover.ts} | 22 +- tests/keyboard/index.ts | 47 +-- tests/keyboard/keyboardImplementation.ts | 30 +- tests/keyboard/plugin/arrow.ts | 16 +- tests/keyboard/plugin/character.ts | 12 +- tests/keyboard/plugin/combination.ts | 4 +- tests/keyboard/plugin/control.ts | 24 +- tests/keyboard/plugin/functional.ts | 46 +-- tests/keyboard/shared/fireInputEvent.ts | 10 +- tests/pointer/index.ts | 198 ++++++----- tests/react/keyboard.tsx | 10 +- tests/react/type.tsx | 18 +- .../{deselect.js => deselect.ts} | 32 +- tests/selectOptions/{select.js => select.ts} | 74 ++--- tests/setup.ts | 179 +++------- tests/{tab.js => tab.ts} | 204 ++++++------ tests/type/{index.js => index.ts} | 309 +++++++++--------- tests/type/{modifiers.js => modifiers.ts} | 203 ++++++------ tests/{upload.js => upload.ts} | 117 +++---- tests/utils/dataTransfer/Clipboard.ts | 2 +- tests/utils/dataTransfer/DataTransfer.ts | 14 +- tests/utils/edit/calculateNewValue.ts | 20 +- tests/utils/edit/isContentEditable.ts | 2 +- tests/utils/focus/{blur.js => blur.ts} | 10 +- tests/utils/focus/{focus.js => focus.ts} | 12 +- tests/utils/focus/selectAll.ts | 8 +- tests/utils/focus/selection.ts | 18 +- tests/utils/misc/hasPointerEvents.ts | 2 +- tests/utils/misc/isDescendantOrSelf.ts | 2 +- tests/utils/misc/isElementType.js | 4 +- tests/utils/misc/isVisible.js | 2 +- 76 files changed, 1547 insertions(+), 1847 deletions(-) delete mode 100644 src/.eslintrc delete mode 100644 src/click.ts create mode 100644 src/convenience/click.ts create mode 100644 src/convenience/hover.ts create mode 100644 src/convenience/index.ts rename src/{ => convenience}/tab.ts (58%) delete mode 100644 src/hover.ts create mode 100644 src/options.ts delete mode 100644 src/setup.ts create mode 100644 src/setup/api.ts create mode 100644 src/setup/config.ts create mode 100644 src/setup/directApi.ts create mode 100644 src/setup/index.ts create mode 100644 src/setup/setup.ts create mode 100644 src/setup/wrapAsync.ts delete mode 100644 src/type.ts rename src/{ => utility}/clear.ts (74%) create mode 100644 src/utility/index.ts rename src/{ => utility}/selectOptions.ts (91%) create mode 100644 src/utility/type.ts rename src/{ => utility}/upload.ts (88%) create mode 100644 src/utils/misc/getDocumentFromNode.ts rename tests/click/{click.js => click.ts} (71%) rename tests/click/{dblclick.js => dblclick.ts} (66%) rename tests/clipboard/{paste.js => paste.ts} (72%) rename tests/hover/{hover.js => hover.ts} (78%) rename tests/hover/{unhover.js => unhover.ts} (77%) rename tests/selectOptions/{deselect.js => deselect.ts} (80%) rename tests/selectOptions/{select.js => select.ts} (88%) rename tests/{tab.js => tab.ts} (71%) rename tests/type/{index.js => index.ts} (85%) rename tests/type/{modifiers.js => modifiers.ts} (86%) rename tests/{upload.js => upload.ts} (66%) rename tests/utils/focus/{blur.js => blur.ts} (83%) rename tests/utils/focus/{focus.js => focus.ts} (81%) diff --git a/.eslintrc.js b/.eslintrc.js index 0293c1ce..f9cf0de7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { }, }, rules: { + 'no-await-in-loop': 0, 'testing-library/no-dom-import': 0, '@typescript-eslint/non-nullable-type-assertion-style': 0, }, diff --git a/src/.eslintrc b/src/.eslintrc deleted file mode 100644 index b7d78581..00000000 --- a/src/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rules": { - // everything in this directory is intentionally running in series, not parallel - // because user's cannot fire multiple events at the same time and we need - // all events fired in a predictable order. - "no-await-in-loop": "off" - } -} diff --git a/src/click.ts b/src/click.ts deleted file mode 100644 index 11a3bfeb..00000000 --- a/src/click.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {hasPointerEvents, PointerOptions} from './utils' -import type {UserEvent} from './setup' - -export declare interface clickOptions { - skipHover?: boolean - clickCount?: number -} - -export function click( - this: UserEvent, - element: Element, - { - skipHover = false, - skipPointerEventsCheck = false, - }: clickOptions & PointerOptions = {}, -) { - if (!skipPointerEventsCheck && !hasPointerEvents(element)) { - throw new Error( - 'unable to click element as it has or inherits pointer-events set to "none".', - ) - } - // istanbul ignore else - if (!skipHover) - // We just checked for `pointerEvents`. We can always skip this one in `hover`. - this.hover(element, {skipPointerEventsCheck: true}) - - this.pointer({keys: '[MouseLeft]', target: element}) -} - -export function dblClick( - this: UserEvent, - element: Element, - {skipPointerEventsCheck = false}: clickOptions & PointerOptions = {}, -) { - if (!skipPointerEventsCheck && !hasPointerEvents(element)) { - throw new Error( - 'unable to double-click element as it has or inherits pointer-events set to "none".', - ) - } - this.hover(element, {skipPointerEventsCheck: true}) - - this.pointer({keys: '[MouseLeft][MouseLeft]', target: element}) -} - -export function tripleClick( - this: UserEvent, - element: Element, - {skipPointerEventsCheck = false}: clickOptions & PointerOptions = {}, -) { - if (!skipPointerEventsCheck && !hasPointerEvents(element)) { - throw new Error( - 'unable to triple-click element as it has or inherits pointer-events set to "none".', - ) - } - this.hover(element, {skipPointerEventsCheck: true}) - - this.pointer({keys: '[MouseLeft][MouseLeft][MouseLeft]', target: element}) -} diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index 671ce031..e661cb0a 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,5 +1,5 @@ import {fireEvent} from '@testing-library/dom' -import type {UserEvent} from '../setup' +import {Config, UserEvent} from '../setup' import {copySelection, writeDataTransferToClipboard} from '../utils' export interface copyOptions { @@ -7,18 +7,8 @@ export interface copyOptions { writeToClipboard?: boolean } -export function copy( - this: UserEvent, - options: Omit & {writeToClipboard: true}, -): Promise -export function copy( - this: UserEvent, - options?: Omit & { - writeToClipboard?: boolean - }, -): DataTransfer -export function copy(this: UserEvent, options?: copyOptions) { - const doc = options?.document ?? document +export async function copy(this: UserEvent) { + const doc = this[Config].document const target = doc.activeElement ?? /* istanbul ignore next */ doc.body const clipboardData = copySelection(target) @@ -31,7 +21,9 @@ export function copy(this: UserEvent, options?: copyOptions) { clipboardData, }) - return options?.writeToClipboard - ? writeDataTransferToClipboard(doc, clipboardData).then(() => clipboardData) - : clipboardData + if (this[Config].writeToClipboard) { + await writeDataTransferToClipboard(doc, clipboardData) + } + + return clipboardData } diff --git a/src/clipboard/cut.ts b/src/clipboard/cut.ts index 70bd6f1d..6836e7ce 100644 --- a/src/clipboard/cut.ts +++ b/src/clipboard/cut.ts @@ -1,5 +1,5 @@ import {fireEvent} from '@testing-library/dom' -import type {UserEvent} from '../setup' +import {Config, UserEvent} from '../setup' import { copySelection, isEditable, @@ -12,16 +12,8 @@ export interface cutOptions { writeToClipboard?: boolean } -export function cut( - this: UserEvent, - options: Omit & {writeToClipboard: true}, -): Promise -export function cut( - this: UserEvent, - options?: Omit & {writeToClipboard?: boolean}, -): DataTransfer -export function cut(this: UserEvent, options?: cutOptions) { - const doc = options?.document ?? document +export async function cut(this: UserEvent) { + const doc = this[Config].document const target = doc.activeElement ?? /* istanbul ignore next */ doc.body const clipboardData = copySelection(target) @@ -38,7 +30,9 @@ export function cut(this: UserEvent, options?: cutOptions) { prepareInput('', target, 'deleteByCut')?.commit() } - return options?.writeToClipboard - ? writeDataTransferToClipboard(doc, clipboardData).then(() => clipboardData) - : clipboardData + if (this[Config].writeToClipboard) { + await writeDataTransferToClipboard(doc, clipboardData) + } + + return clipboardData } diff --git a/src/clipboard/paste.ts b/src/clipboard/paste.ts index 15429449..c8ec9dcb 100644 --- a/src/clipboard/paste.ts +++ b/src/clipboard/paste.ts @@ -1,5 +1,5 @@ import {fireEvent} from '@testing-library/dom' -import type {UserEvent} from '../setup' +import {Config, UserEvent} from '../setup' import { createDataTransfer, getSpaceUntilMaxLength, @@ -12,39 +12,24 @@ export interface pasteOptions { document?: Document } -export function paste( - this: UserEvent, - clipboardData?: undefined, - options?: pasteOptions, -): Promise -export function paste( - this: UserEvent, - clipboardData: DataTransfer | string, - options?: pasteOptions, -): void -export function paste( +export async function paste( this: UserEvent, clipboardData?: DataTransfer | string, - options?: pasteOptions, ) { - const doc = options?.document ?? document + const doc = this[Config].document const target = doc.activeElement ?? /* istanbul ignore next */ doc.body - const data: DataTransfer | undefined = - typeof clipboardData === 'string' + const data: DataTransfer = + (typeof clipboardData === 'string' ? getClipboardDataFromString(clipboardData) - : clipboardData - - return data - ? pasteImpl(target, data) - : readDataTransferFromClipboard(doc).then( - dt => pasteImpl(target, dt), - () => { - throw new Error( - '`userEvent.paste()` without `clipboardData` requires the `ClipboardAPI` to be available.', - ) - }, + : clipboardData) ?? + (await readDataTransferFromClipboard(doc).catch(() => { + throw new Error( + '`userEvent.paste()` without `clipboardData` requires the `ClipboardAPI` to be available.', ) + })) + + return pasteImpl(target, data) } function pasteImpl(target: Element, clipboardData: DataTransfer) { diff --git a/src/convenience/click.ts b/src/convenience/click.ts new file mode 100644 index 00000000..efe2aff5 --- /dev/null +++ b/src/convenience/click.ts @@ -0,0 +1,49 @@ +import {hasPointerEvents} from '../utils' +import {Config, UserEvent} from '../setup' +import {PointerInput} from '#src/pointer' + +export declare interface clickOptions { + skipHover?: boolean +} + +export async function click(this: UserEvent, element: Element): Promise { + if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) { + throw new Error( + 'unable to click element as it has or inherits pointer-events set to "none".', + ) + } + + const pointerIn: PointerInput = [] + if (!this[Config].skipHover) { + pointerIn.push({target: element}) + } + pointerIn.push({keys: '[MouseLeft]', target: element}) + + return this.pointer(pointerIn) +} + +export async function dblClick( + this: UserEvent, + element: Element, +): Promise { + if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) { + throw new Error( + 'unable to double-click element as it has or inherits pointer-events set to "none".', + ) + } + + return this.pointer([{target: element}, '[MouseLeft][MouseLeft]']) +} + +export async function tripleClick( + this: UserEvent, + element: Element, +): Promise { + if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) { + throw new Error( + 'unable to triple-click element as it has or inherits pointer-events set to "none".', + ) + } + + return this.pointer([{target: element}, '[MouseLeft][MouseLeft][MouseLeft]']) +} diff --git a/src/convenience/hover.ts b/src/convenience/hover.ts new file mode 100644 index 00000000..dd0fc573 --- /dev/null +++ b/src/convenience/hover.ts @@ -0,0 +1,22 @@ +import {Config, UserEvent} from '../setup' +import {hasPointerEvents} from '../utils' + +export async function hover(this: UserEvent, element: Element) { + if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) { + throw new Error( + 'unable to hover element as it has or inherits pointer-events set to "none".', + ) + } + + return this.pointer({target: element}) +} + +export async function unhover(this: UserEvent, element: Element) { + if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) { + throw new Error( + 'unable to unhover element as it has or inherits pointer-events set to "none".', + ) + } + + return this.pointer({target: element.ownerDocument.body}) +} diff --git a/src/convenience/index.ts b/src/convenience/index.ts new file mode 100644 index 00000000..d29d059f --- /dev/null +++ b/src/convenience/index.ts @@ -0,0 +1,3 @@ +export * from './click' +export * from './hover' +export * from './tab' diff --git a/src/tab.ts b/src/convenience/tab.ts similarity index 58% rename from src/tab.ts rename to src/convenience/tab.ts index 81ef6723..d26807b5 100644 --- a/src/tab.ts +++ b/src/convenience/tab.ts @@ -1,11 +1,11 @@ -import type {UserEvent} from './setup' +import type {UserEvent} from '../setup' export interface tabOptions { shift?: boolean } -export function tab(this: UserEvent, {shift}: tabOptions = {}) { - this.keyboard( +export async function tab(this: UserEvent, {shift}: tabOptions = {}) { + return this.keyboard( shift === true ? '{Shift>}{Tab}{/Shift}' : shift === false diff --git a/src/hover.ts b/src/hover.ts deleted file mode 100644 index b48b230f..00000000 --- a/src/hover.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {createPointerState} from './pointer' -import type {UserEvent} from './setup' -import {hasPointerEvents, PointerOptions} from './utils' - -export function hover( - this: UserEvent, - element: Element, - {skipPointerEventsCheck = false}: PointerOptions = {}, -) { - if (!skipPointerEventsCheck && !hasPointerEvents(element)) { - throw new Error( - 'unable to hover element as it has or inherits pointer-events set to "none".', - ) - } - - const pointerState = createPointerState() - pointerState.position.mouse.target = element.ownerDocument.body - - this.pointer({target: element}, {pointerState}) -} - -export function unhover( - this: UserEvent, - element: Element, - {skipPointerEventsCheck = false}: PointerOptions = {}, -) { - if (!skipPointerEventsCheck && !hasPointerEvents(element)) { - throw new Error( - 'unable to unhover element as it has or inherits pointer-events set to "none".', - ) - } - - const pointerState = createPointerState() - pointerState.position.mouse.target = element - - this.pointer({target: element.ownerDocument.body}, {pointerState}) -} diff --git a/src/index.ts b/src/index.ts index 70796a42..37ac21bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,3 @@ -import {userEventApis, UserEventApis, setup, UserEvent} from './setup' - -const userEvent: UserEventApis & { - setup: typeof setup -} = { - ...(Object.fromEntries( - Object.entries(userEventApis).map(([k, f]) => [ - k, - (...a: unknown[]) => - (f as (this: UserEvent, ...b: unknown[]) => unknown).apply( - userEvent, - a, - ), - ]), - ) as UserEventApis), - setup, -} - -export default userEvent - +export {userEvent as default} from './setup' export type {keyboardKey} from './keyboard' +export type {pointerKey} from './pointer' diff --git a/src/keyboard/index.ts b/src/keyboard/index.ts index 4dab781e..545064a3 100644 --- a/src/keyboard/index.ts +++ b/src/keyboard/index.ts @@ -1,64 +1,12 @@ -import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom' -import {prepareDocument} from '../document' +import {Config, UserEvent} from '../setup' import {keyboardImplementation, releaseAllKeys} from './keyboardImplementation' -import {defaultKeyMap} from './keyMap' import {keyboardState, keyboardOptions, keyboardKey} from './types' -export type {keyboardOptions, keyboardKey} +export {releaseAllKeys} +export type {keyboardOptions, keyboardKey, keyboardState} -export function keyboard( - text: string, - options?: Partial, -): keyboardState -export function keyboard( - text: string, - options: Partial< - keyboardOptions & {keyboardState: keyboardState; delay: number} - >, -): Promise -export function keyboard( - text: string, - options?: Partial, -): keyboardState | Promise { - const {promise, state} = keyboardImplementationWrapper(text, options) - - if ((options?.delay ?? 0) > 0) { - return getDOMTestingLibraryConfig().asyncWrapper(() => - promise.then(() => state), - ) - } else { - // prevent users from dealing with UnhandledPromiseRejectionWarning in sync call - promise.catch(console.error) - - return state - } -} - -export function keyboardImplementationWrapper( - text: string, - config: Partial = {}, -) { - const { - keyboardState: state = createKeyboardState(), - delay = 0, - document: doc = document, - autoModify = false, - keyboardMap = defaultKeyMap, - } = config - const options = { - delay, - document: doc, - autoModify, - keyboardMap, - } - - prepareDocument(document) - - return { - promise: keyboardImplementation(text, options, state), - state, - releaseAllKeys: () => releaseAllKeys(options, state), - } +export async function keyboard(this: UserEvent, text: string): Promise { + return keyboardImplementation(text, this[Config], this[Config].keyboardState) } export function createKeyboardState(): keyboardState { diff --git a/src/keyboard/keyboardImplementation.ts b/src/keyboard/keyboardImplementation.ts index 7fe3b935..b8eda852 100644 --- a/src/keyboard/keyboardImplementation.ts +++ b/src/keyboard/keyboardImplementation.ts @@ -26,13 +26,7 @@ export async function keyboardImplementation( // Release the key automatically if it was pressed before. // Do not release the key on iterations on `state.repeatKey`. if (pressed && !state.repeatKey) { - keyup( - keyDef, - getCurrentElement, - options, - state, - pressed.unpreventedDefault, - ) + keyup(keyDef, getCurrentElement, options, state, pressed.unpreventedDefault) } if (!releasePrevious) { @@ -67,7 +61,7 @@ export async function keyboardImplementation( } if (text.length > consumedLength || repeat > 1) { - if (options.delay > 0) { + if (typeof options.delay === 'number') { await wait(options.delay) } diff --git a/src/keyboard/types.ts b/src/keyboard/types.ts index 3b7a683c..942ac1fa 100644 --- a/src/keyboard/types.ts +++ b/src/keyboard/types.ts @@ -52,7 +52,7 @@ export type keyboardOptions = { /** Document in which to perform the events */ document: Document /** Delay between keystrokes */ - delay: number + delay: number | null /** Add modifiers for given keys - not implemented yet */ autoModify: boolean /** Keyboard layout to use */ diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 00000000..0dfdb528 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,16 @@ +import type {keyboardKey} from './keyboard/types' +import type {pointerKey} from './pointer/types' + +export interface Options { + applyAccept?: boolean + autoModify?: boolean + delay?: number | null + document?: Document + keyboardMap?: keyboardKey[] + pointerMap?: pointerKey[] + skipAutoClose?: boolean + skipClick?: boolean + skipHover?: boolean + skipPointerEventsCheck?: boolean + writeToClipboard?: boolean +} diff --git a/src/pointer/index.ts b/src/pointer/index.ts index 75da2eeb..74b85ece 100644 --- a/src/pointer/index.ts +++ b/src/pointer/index.ts @@ -1,68 +1,31 @@ -import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom' -import {createKeyboardState} from '../keyboard' +import {Config, UserEvent} from '../setup' import {parseKeyDef} from './parseKeyDef' -import {defaultKeyMap} from './keyMap' import { pointerAction, PointerAction, PointerActionTarget, } from './pointerAction' -import type {inputDeviceState, pointerOptions, pointerState} from './types' +import type {pointerOptions, pointerState, pointerKey} from './types' -export function pointer( - input: PointerInput, - options?: Partial, -): pointerState -export function pointer( - input: PointerInput, - options: Partial, -): Promise -export function pointer( - input: PointerInput, - options: Partial = {}, -) { - const {promise, pointerState} = pointerImplementationWrapper(input, options) - - if ((options.delay ?? 0) > 0) { - return getDOMTestingLibraryConfig().asyncWrapper(() => - promise.then(() => pointerState), - ) - } else { - // prevent users from dealing with UnhandledPromiseRejectionWarning in sync call - promise.catch(console.error) - - return pointerState - } -} +export type {pointerOptions, pointerState, pointerKey} type PointerActionInput = | string | ({keys: string} & PointerActionTarget) | PointerAction -type PointerInput = PointerActionInput | Array +export type PointerInput = PointerActionInput | Array -export function pointerImplementationWrapper( +export async function pointer( + this: UserEvent, input: PointerInput, - config: Partial, -) { - const { - pointerState = createPointerState(), - keyboardState = createKeyboardState(), - delay = 0, - pointerMap = defaultKeyMap, - } = config - const options = { - delay, - pointerMap, - } - +): Promise { const actions: PointerAction[] = [] ;(Array.isArray(input) ? input : [input]).forEach(actionInput => { if (typeof actionInput === 'string') { - actions.push(...parseKeyDef(actionInput, options)) + actions.push(...parseKeyDef(actionInput, this[Config])) } else if ('keys' in actionInput) { actions.push( - ...parseKeyDef(actionInput.keys, options).map(i => ({ + ...parseKeyDef(actionInput.keys, this[Config]).map(i => ({ ...actionInput, ...i, })), @@ -72,19 +35,19 @@ export function pointerImplementationWrapper( } }) - return { - promise: pointerAction(actions, options, {pointerState, keyboardState}), - pointerState, - } + return pointerAction(actions, this[Config], this[Config]).then( + () => undefined, + ) } -export function createPointerState(): pointerState { +export function createPointerState(document: Document): pointerState { return { pointerId: 1, position: { mouse: { pointerType: 'mouse', pointerId: 1, + target: document.body, coords: { clientX: 0, clientY: 0, diff --git a/src/pointer/pointerAction.ts b/src/pointer/pointerAction.ts index 8b55c0ea..3eddb411 100644 --- a/src/pointer/pointerAction.ts +++ b/src/pointer/pointerAction.ts @@ -1,8 +1,8 @@ +import {inputDeviceState} from '../setup' import {wait} from '../utils' import {pointerMove, PointerMoveAction} from './pointerMove' import {pointerPress, PointerPressAction} from './pointerPress' import { - inputDeviceState, pointerOptions, pointerState, PointerTarget, @@ -22,7 +22,7 @@ export async function pointerAction( actions: PointerAction[], options: pointerOptions, state: inputDeviceState, -): Promise { +) { const ret: Array> = [] for (let i = 0; i < actions.length; i++) { @@ -51,7 +51,7 @@ export async function pointerAction( ret.push(promise) - if (options.delay > 0) { + if (typeof options.delay === 'number') { await promise if (i < actions.length - 1) { await wait(options.delay) diff --git a/src/pointer/pointerMove.ts b/src/pointer/pointerMove.ts index aed97ca9..d1b78ffb 100644 --- a/src/pointer/pointerMove.ts +++ b/src/pointer/pointerMove.ts @@ -1,4 +1,5 @@ import {setUISelection} from '../document' +import {inputDeviceState} from '../setup' import { PointerCoords, firePointerEvent, @@ -6,7 +7,7 @@ import { isDisabled, } from '../utils' import {resolveSelectionTarget} from './resolveSelectionTarget' -import {inputDeviceState, PointerTarget, SelectionTarget} from './types' +import {PointerTarget, SelectionTarget} from './types' export interface PointerMoveAction extends PointerTarget, SelectionTarget { pointerName?: string diff --git a/src/pointer/pointerPress.ts b/src/pointer/pointerPress.ts index a517464d..a25ad79f 100644 --- a/src/pointer/pointerPress.ts +++ b/src/pointer/pointerPress.ts @@ -10,8 +10,8 @@ import { isFocusable, } from '../utils' import {getUIValue, setUISelection} from '../document' +import {inputDeviceState} from '../setup' import type { - inputDeviceState, pointerKey, pointerState, PointerTarget, diff --git a/src/pointer/types.ts b/src/pointer/types.ts index 10da23e0..fbb4bbea 100644 --- a/src/pointer/types.ts +++ b/src/pointer/types.ts @@ -1,4 +1,3 @@ -import {keyboardState} from '../keyboard/types' import {PointerCoords, MouseButton} from '../utils' /** @@ -44,7 +43,7 @@ export type pointerState = { export type pointerOptions = { /** Delay between keystrokes */ - delay: number + delay: number | null /** Available pointer keys */ pointerMap: pointerKey[] } @@ -78,8 +77,3 @@ export interface SelectionInputRange { start: number end: number } - -export interface inputDeviceState { - pointerState: pointerState - keyboardState: keyboardState -} diff --git a/src/setup.ts b/src/setup.ts deleted file mode 100644 index 14c952e8..00000000 --- a/src/setup.ts +++ /dev/null @@ -1,260 +0,0 @@ -import {clear} from './clear' -import {click, clickOptions, dblClick, tripleClick} from './click' -import {prepareDocument} from './document' -import {hover, unhover} from './hover' -import {createKeyboardState, keyboard, keyboardOptions} from './keyboard' -import type {keyboardState} from './keyboard/types' -import { - copy, - copyOptions, - cut, - cutOptions, - paste, - pasteOptions, -} from './clipboard' -import {createPointerState, pointer} from './pointer' -import type {pointerOptions, pointerState} from './pointer/types' -import {deselectOptions, selectOptions} from './selectOptions' -import {tab, tabOptions} from './tab' -import {type, typeOptions} from './type' -import {upload, uploadOptions} from './upload' -import {PointerOptions, attachClipboardStubToView} from './utils' - -export const userEventApis = { - clear, - click, - copy, - cut, - dblClick, - deselectOptions, - hover, - keyboard, - paste, - pointer, - selectOptions, - tab, - tripleClick, - type, - unhover, - upload, -} -export type UserEventApis = typeof userEventApis -type setup = ReturnType['setup'] -export type UserEvent = UserEventApis & { - setup: setup -} - -type ClickOptions = Omit - -interface ClipboardOptions extends copyOptions, cutOptions, pasteOptions {} - -type KeyboardOptions = Partial - -type PointerApiOptions = Partial - -type TabOptions = Omit - -type TypeOptions = Omit< - typeOptions, - 'initialSelectionStart' | 'initialSelectionEnd' -> - -type UploadOptions = uploadOptions - -interface SetupOptions - extends ClickOptions, - ClipboardOptions, - KeyboardOptions, - PointerOptions, - PointerApiOptions, - TabOptions, - TypeOptions, - UploadOptions {} - -/** - * Start a "session" with userEvent. - * All APIs returned by this function share an input device state and a default configuration. - */ -export function setup(options: SetupOptions = {}) { - const doc = options.document ?? document - prepareDocument(doc) - - const view = doc.defaultView ?? /* istanbul ignore next */ window - attachClipboardStubToView(view) - - return _setup(options, { - keyboardState: createKeyboardState(), - pointerState: createPointerState(), - }) -} - -function _setup( - { - applyAccept, - autoModify, - delay = 0, - document, - keyboardMap, - pointerMap, - skipAutoClose, - skipClick, - skipHover, - skipPointerEventsCheck = false, - // Changing default return type from DataTransfer to Promise - // would require a lot of overloading right now. - // The APIs returned by setup will most likely be changed to async before stable release anyway. - // See https://github.com/testing-library/user-event/issues/504#issuecomment-944883855 - // So the default option can be changed during alpha instead of introducing too much code here. - // TODO: This should default to true - writeToClipboard = false, - }: SetupOptions, - { - keyboardState, - pointerState, - }: { - keyboardState: keyboardState - pointerState: pointerState - }, -): UserEventApis & { - /** - * Create a set of callbacks with different default settings but the same state. - */ - setup(options: SetupOptions): ReturnType -} { - const keyboardDefaults: KeyboardOptions = { - autoModify, - delay, - document, - keyboardMap, - } - const pointerDefaults: PointerOptions = { - skipPointerEventsCheck, - } - const pointerApiDefaults: PointerApiOptions = { - delay, - pointerMap, - } - const clickDefaults: clickOptions = { - skipHover, - } - const clipboardDefaults: ClipboardOptions = { - document, - writeToClipboard, - } - const typeDefaults: TypeOptions = { - delay, - skipAutoClose, - skipClick, - } - const uploadDefaults: UploadOptions = { - applyAccept, - } - - const userEvent: UserEvent = { - clear: (...args: Parameters) => { - return clear.call(userEvent, ...args) - }, - - click: (...args: Parameters) => { - args[1] = {...pointerDefaults, ...clickDefaults, ...args[1]} - return click.call(userEvent, ...args) - }, - - // copy needs typecasting because of the overloading - copy: ((...args: Parameters) => { - args[0] = {...clipboardDefaults, ...args[0]} - return copy.call(userEvent, ...args) - }) as typeof copy, - - // cut needs typecasting because of the overloading - cut: ((...args: Parameters) => { - args[0] = {...clipboardDefaults, ...args[0]} - return cut.call(userEvent, ...args) - }) as typeof cut, - - dblClick: (...args: Parameters) => { - args[1] = {...pointerDefaults, ...clickDefaults, ...args[1]} - return dblClick.call(userEvent, ...args) - }, - - deselectOptions: (...args: Parameters) => { - args[2] = {...pointerDefaults, ...args[2]} - return deselectOptions.call(userEvent, ...args) - }, - - hover: (...args: Parameters) => { - args[1] = {...pointerDefaults, ...args[1]} - return hover.call(userEvent, ...args) - }, - - // keyboard needs typecasting because of the overloading - keyboard: ((...args: Parameters) => { - args[1] = {...keyboardDefaults, ...args[1], keyboardState} - const ret = keyboard(...args) as keyboardState | Promise - if (ret instanceof Promise) { - return ret.then(() => undefined) - } - }) as typeof keyboard, - - // paste needs typecasting because of the overloading - paste: ((...args: Parameters) => { - args[1] = {...clipboardDefaults, ...args[1]} - return paste.call(userEvent, ...args) - }) as typeof paste, - - // pointer needs typecasting because of the overloading - pointer: ((...args: Parameters) => { - args[1] = {...pointerApiDefaults, ...args[1], pointerState, keyboardState} - const ret = pointer(...args) as pointerState | Promise - if (ret instanceof Promise) { - return ret.then(() => undefined) - } - }) as typeof pointer, - - selectOptions: (...args: Parameters) => { - args[2] = {...pointerDefaults, ...args[2]} - return selectOptions.call(userEvent, ...args) - }, - - setup: (options: SetupOptions) => { - return _setup( - { - ...keyboardDefaults, - ...pointerDefaults, - ...clickDefaults, - ...options, - }, - { - keyboardState, - pointerState, - }, - ) - }, - - tab: (...args: Parameters) => { - return tab.call(userEvent, ...args) - }, - - tripleClick: (...args: Parameters) => { - return tripleClick.call(userEvent, ...args) - }, - - // type needs typecasting because of the overloading - type: ((...args: Parameters) => { - args[2] = {...typeDefaults, ...args[2]} - return type.call(userEvent, ...args) - }) as typeof type, - - unhover: (...args: Parameters) => { - args[1] = {...pointerDefaults, ...args[1]} - return unhover.call(userEvent, ...args) - }, - - upload: (...args: Parameters) => { - args[3] = {...uploadDefaults, ...args[3]} - return upload.call(userEvent, ...args) - }, - } - - return userEvent -} diff --git a/src/setup/api.ts b/src/setup/api.ts new file mode 100644 index 00000000..c6073c7b --- /dev/null +++ b/src/setup/api.ts @@ -0,0 +1,44 @@ +import {click, dblClick, tripleClick, hover, unhover, tab} from '../convenience' +import {keyboard} from '../keyboard' +import {copy, cut, paste} from '../clipboard' +import {pointer} from '../pointer' +import {clear, deselectOptions, selectOptions, type, upload} from '../utility' + +export const userEventApi = { + clear, + click, + copy, + cut, + dblClick, + deselectOptions, + hover, + keyboard, + paste, + pointer, + selectOptions, + tab, + tripleClick, + type, + unhover, + upload, +} as const + +/** Which is the options parameter? */ +export const userEventApiOptionsParameter = { + clear: undefined, + click: 1, + copy: 0, + cut: 0, + dblClick: 1, + deselectOptions: 2, + hover: 1, + keyboard: 1, + paste: 1, + pointer: 1, + selectOptions: 2, + tab: 0, + tripleClick: 1, + type: 2, + unhover: 1, + upload: 3, +} as const diff --git a/src/setup/config.ts b/src/setup/config.ts new file mode 100644 index 00000000..25099c00 --- /dev/null +++ b/src/setup/config.ts @@ -0,0 +1,38 @@ +import {keyboardState} from '#src/keyboard/types' +import {pointerState} from '#src/pointer/types' +import {defaultKeyMap as defaultKeyboardMap} from '#src/keyboard/keyMap' +import {defaultKeyMap as defaultPointerMap} from '#src/pointer/keyMap' +import {Options} from '#src/options' + +export interface inputDeviceState { + pointerState: pointerState + keyboardState: keyboardState +} + +export interface Config extends Required, inputDeviceState {} +export const Config = Symbol('Config') + +/** + * Default options applied when API is called per `userEvent.anyApi()` + */ +export const defaultOptionsDirect: Required = { + applyAccept: true, + autoModify: true, + delay: 0, + document: global.document, + keyboardMap: defaultKeyboardMap, + pointerMap: defaultPointerMap, + skipAutoClose: false, + skipClick: false, + skipHover: false, + skipPointerEventsCheck: false, + writeToClipboard: false, +} + +/** + * Default options applied when API is called per `userEvent().anyApi()` + */ +export const defaultOptionsSetup: Required = { + ...defaultOptionsDirect, + writeToClipboard: true, +} diff --git a/src/setup/directApi.ts b/src/setup/directApi.ts new file mode 100644 index 00000000..fb8a134f --- /dev/null +++ b/src/setup/directApi.ts @@ -0,0 +1,116 @@ +import type {copyOptions, cutOptions, pasteOptions} from '../clipboard' +import type {clickOptions, tabOptions} from '../convenience' +import type {keyboardOptions, keyboardState} from '../keyboard' +import type {PointerOptions} from '../utils' +import type {typeOptions, uploadInit, uploadOptions} from '../utility' +import type {PointerInput, pointerOptions} from '../pointer' +import {setupDirect} from './setup' +import {Config, inputDeviceState} from './config' + +export function clear(element: Element) { + return setupDirect().clear(element) +} + +export function click( + element: Element, + options: clickOptions & PointerOptions = {}, +) { + return setupDirect(options, element).click(element) +} + +export function copy(options: copyOptions = {}) { + return setupDirect(options).copy() +} + +export function cut(options: cutOptions = {}) { + return setupDirect(options).cut() +} + +export function dblClick( + element: Element, + options: clickOptions & PointerOptions = {}, +) { + return setupDirect(options).dblClick(element) +} + +export function deselectOptions( + select: Element, + values: HTMLElement | HTMLElement[] | string[] | string, + options: PointerOptions = {}, +) { + return setupDirect(options).deselectOptions(select, values, options) +} + +export function hover(element: Element, options: PointerOptions = {}) { + return setupDirect(options).hover(element) +} + +export async function keyboard( + text: string, + options: Partial = {}, +) { + const instance = setupDirect(options) + const promise = instance.keyboard(text) + + return promise.then(() => instance[Config].keyboardState) +} + +export async function pointer( + input: PointerInput, + options: Partial = {}, +) { + const instance = setupDirect(options) + const promise = instance.pointer(input) + + return promise.then(() => instance[Config].pointerState) +} + +export function paste( + clipboardData?: DataTransfer | string, + options?: pasteOptions, +) { + return setupDirect(options).paste(clipboardData) +} + +export function selectOptions( + select: Element, + values: HTMLElement | HTMLElement[] | string[] | string, + options: PointerOptions = {}, +) { + return setupDirect(options).selectOptions(select, values, options) +} + +export function tripleClick( + element: Element, + options: clickOptions & PointerOptions = {}, +) { + return setupDirect(options).tripleClick(element) +} + +export function type( + element: Element, + text: string, + options: typeOptions = {}, +) { + return setupDirect(options, element).type(element, text, options) +} + +export function unhover(element: Element, options: PointerOptions = {}) { + const instance = setupDirect(options) + instance[Config].pointerState.position.mouse.target = element + + return instance.unhover(element) +} + +export function upload( + element: HTMLElement, + fileOrFiles: File | File[], + init?: uploadInit, + options: uploadOptions = {}, +) { + return setupDirect(options).upload(element, fileOrFiles, init) +} + +export function tab(options: tabOptions = {}) { + return setupDirect().tab(options) +} diff --git a/src/setup/index.ts b/src/setup/index.ts new file mode 100644 index 00000000..d64ec985 --- /dev/null +++ b/src/setup/index.ts @@ -0,0 +1,19 @@ +import {userEventApi} from './api' +import {setupMain, setupSub} from './setup' +import {Config, inputDeviceState} from './config' +import * as directApi from './directApi' + +export type {inputDeviceState} +export {Config} + +export type UserEventApi = typeof userEventApi + +export type UserEvent = UserEventApi & { + readonly setup: typeof setupSub + [Config]: Config +} + +export const userEvent = { + ...directApi, + setup: setupMain, +} as const diff --git a/src/setup/setup.ts b/src/setup/setup.ts new file mode 100644 index 00000000..a272b510 --- /dev/null +++ b/src/setup/setup.ts @@ -0,0 +1,81 @@ +import {prepareDocument} from '../document' +import {createKeyboardState} from '../keyboard' +import {createPointerState} from '../pointer' +import {Options} from '../options' +import {attachClipboardStubToView, getDocumentFromNode} from '../utils' +import type {UserEvent, UserEventApi} from './index' +import {Config, defaultOptionsDirect, defaultOptionsSetup} from './config' +import {userEventApi} from './api' + +/** + * Start a "session" with userEvent. + * All APIs returned by this function share an input device state and a default configuration. + */ +export function setupMain(options: Options = {}) { + const doc = getDocument(options) + prepareDocument(doc) + + const view = doc.defaultView ?? window + attachClipboardStubToView(view) + + return doSetup({ + ...defaultOptionsSetup, + keyboardState: createKeyboardState(), + pointerState: createPointerState(doc), + }) +} + +/** + * Setup in direct call per `userEvent.anyApi()` + */ +export function setupDirect(options: Partial = {}, node?: Node) { + const doc = getDocument(options, node) + prepareDocument(doc) + + return doSetup({ + keyboardState: createKeyboardState(), + pointerState: createPointerState(doc), + ...defaultOptionsDirect, + ...options, + }) +} + +/** + * Create a set of callbacks with different default settings but the same state. + */ +export function setupSub(this: UserEvent, options: Options) { + return doSetup({ + ...this[Config], + ...options, + }) +} + +function wrapImpl< + This extends UserEvent, + Args extends unknown[], + Impl extends (this: This, ...args: Args) => Promise, +>(impl: Impl) { + function method(this: This, ...args: Args) { + return impl.apply(this, args) + // TODO: wrap async + // return wrapAsync(() => impl.apply(this, args)) + } + Object.defineProperty(method, 'name', {get: () => impl.name}) + + return method +} +const wrappedApis = Object.fromEntries( + Object.entries(userEventApi).map(([name, impl]) => [name, wrapImpl(impl)]), +) as UserEventApi + +function doSetup(config: Config) { + return { + ...wrappedApis, + setup: setupSub, + [Config]: config, + } +} + +function getDocument(options: Partial = {}, node?: Node) { + return options.document ?? (node && getDocumentFromNode(node)) ?? document +} diff --git a/src/setup/wrapAsync.ts b/src/setup/wrapAsync.ts new file mode 100644 index 00000000..000b601e --- /dev/null +++ b/src/setup/wrapAsync.ts @@ -0,0 +1,10 @@ +import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom' + +/** + * Wrap an internal Promise + */ +export function wrapAsync Promise) | (() => R)>( + implementation: P, +): Promise { + return getDOMTestingLibraryConfig().asyncWrapper(implementation) +} diff --git a/src/type.ts b/src/type.ts deleted file mode 100644 index 26efb0c0..00000000 --- a/src/type.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom' -import {prepareDocument} from './document' -import type {UserEvent} from './setup' -import {setSelectionRange} from './utils' -import {keyboardImplementationWrapper} from './keyboard' - -export interface typeOptions { - delay?: number - skipClick?: boolean - skipAutoClose?: boolean - initialSelectionStart?: number - initialSelectionEnd?: number -} - -export function type( - this: UserEvent, - element: Element, - text: string, - options?: typeOptions & {delay?: 0}, -): void -export function type( - this: UserEvent, - element: Element, - text: string, - options: typeOptions & {delay: number}, -): Promise -// this needs to be wrapped in the event/asyncWrapper for React's act and angular's change detection -// depending on whether it will be async. -export function type( - this: UserEvent, - element: Element, - text: string, - {delay = 0, ...options}: typeOptions = {}, -): Promise | void { - prepareDocument(element.ownerDocument) - - // we do not want to wrap in the asyncWrapper if we're not - // going to actually be doing anything async, so we only wrap - // if the delay is greater than 0 - - if (delay > 0) { - return getDOMTestingLibraryConfig().asyncWrapper(() => - typeImplementation(this, element, text, {delay, ...options}), - ) - } else { - return void typeImplementation(this, element, text, {delay, ...options}) - // prevents users from dealing with UnhandledPromiseRejectionWarning - .catch(console.error) - } -} - -async function typeImplementation( - userEvent: UserEvent, - element: Element, - text: string, - { - delay, - skipClick = false, - skipAutoClose = false, - initialSelectionStart = undefined, - initialSelectionEnd = undefined, - }: typeOptions & {delay: number}, -): Promise { - // TODO: properly type guard - // we use this workaround for now to prevent changing behavior - if ((element as {disabled?: boolean}).disabled) return - - if (!skipClick) userEvent.click(element) - - if (initialSelectionStart !== undefined) { - setSelectionRange( - element, - initialSelectionStart, - initialSelectionEnd ?? initialSelectionStart, - ) - } - - const {promise, releaseAllKeys} = keyboardImplementationWrapper(text, { - delay, - document: element.ownerDocument, - }) - - if (delay > 0) { - await promise - } - - if (!skipAutoClose) { - releaseAllKeys() - } - - // eslint-disable-next-line consistent-return -- we need to return the internal Promise so that it is catchable if we don't await - return promise -} diff --git a/src/clear.ts b/src/utility/clear.ts similarity index 74% rename from src/clear.ts rename to src/utility/clear.ts index 4809b32a..36b4683c 100644 --- a/src/clear.ts +++ b/src/utility/clear.ts @@ -1,5 +1,4 @@ -import {prepareDocument} from './document' -import type {UserEvent} from './setup' +import type {UserEvent} from '../setup' import { focus, isAllSelected, @@ -7,15 +6,13 @@ import { isEditable, prepareInput, selectAll, -} from './utils' +} from '../utils' -export function clear(this: UserEvent, element: Element) { +export async function clear(this: UserEvent, element: Element) { if (!isEditable(element) || isDisabled(element)) { throw new Error('clear()` is only supported on editable elements.') } - prepareDocument(element.ownerDocument) - focus(element) if (element.ownerDocument.activeElement !== element) { diff --git a/src/utility/index.ts b/src/utility/index.ts new file mode 100644 index 00000000..98bd2f88 --- /dev/null +++ b/src/utility/index.ts @@ -0,0 +1,4 @@ +export * from './clear' +export * from './selectOptions' +export * from './type' +export * from './upload' diff --git a/src/selectOptions.ts b/src/utility/selectOptions.ts similarity index 91% rename from src/selectOptions.ts rename to src/utility/selectOptions.ts index b838fd35..c9de16a4 100644 --- a/src/selectOptions.ts +++ b/src/utility/selectOptions.ts @@ -5,10 +5,10 @@ import { isDisabled, isElementType, PointerOptions, -} from './utils' -import type {UserEvent} from './setup' +} from '../utils' +import type {UserEvent} from '../setup' -export function selectOptions( +export async function selectOptions( this: UserEvent, select: Element, values: HTMLElement | HTMLElement[] | string[] | string, @@ -17,7 +17,7 @@ export function selectOptions( return selectOptionsBase.call(this, true, select, values, options) } -export function deselectOptions( +export async function deselectOptions( this: UserEvent, select: Element, values: HTMLElement | HTMLElement[] | string[] | string, @@ -26,7 +26,7 @@ export function deselectOptions( return selectOptionsBase.call(this, false, select, values, options) } -function selectOptionsBase( +async function selectOptionsBase( this: UserEvent, newValue: boolean, select: Element, @@ -105,7 +105,7 @@ function selectOptionsBase( : hasPointerEvents(select) // the click to open the select options if (withPointerEvents) { - this.click(select, {skipPointerEventsCheck: true}) + await this.click(select) } else { focus(select) } @@ -130,11 +130,11 @@ function selectOptionsBase( ) } } else if (select.getAttribute('role') === 'listbox') { - selectedOptions.forEach(option => { - this.hover(option, {skipPointerEventsCheck}) - this.click(option, {skipPointerEventsCheck}) - this.unhover(option, {skipPointerEventsCheck}) - }) + const sub = this.setup({skipPointerEventsCheck}) + for (const option of selectedOptions) { + await sub.click(option) + await sub.unhover(option) + } } else { throw getConfig().getElementError( `Cannot select options on elements that are neither select nor listbox elements`, diff --git a/src/utility/type.ts b/src/utility/type.ts new file mode 100644 index 00000000..a116c405 --- /dev/null +++ b/src/utility/type.ts @@ -0,0 +1,50 @@ +import type {UserEvent} from '../setup' +import {setSelectionRange} from '../utils' +import {releaseAllKeys} from '../keyboard' +import {Config} from '../setup/config' + +export interface typeOptions { + delay?: number | null + skipClick?: boolean + skipAutoClose?: boolean + initialSelectionStart?: number + initialSelectionEnd?: number +} + +export async function type( + this: UserEvent, + element: Element, + text: string, + { + skipClick = this[Config].skipClick, + skipAutoClose = this[Config].skipAutoClose, + initialSelectionStart, + initialSelectionEnd, + }: typeOptions = {}, +): Promise { + // TODO: properly type guard + // we use this workaround for now to prevent changing behavior + if ((element as {disabled?: boolean}).disabled) return + + if (!skipClick) { + await this.click(element) + } + + if (initialSelectionStart !== undefined) { + setSelectionRange( + element, + initialSelectionStart, + initialSelectionEnd ?? initialSelectionStart, + ) + } + + const promise = this.keyboard(text) + + await promise + + if (!skipAutoClose) { + releaseAllKeys(this[Config], this[Config].keyboardState) + } + + return promise +} diff --git a/src/upload.ts b/src/utility/upload.ts similarity index 88% rename from src/upload.ts rename to src/utility/upload.ts index b203ed25..abe719e2 100644 --- a/src/upload.ts +++ b/src/utility/upload.ts @@ -1,8 +1,8 @@ import {fireEvent, createEvent} from '@testing-library/dom' -import {blur, focus, isDisabled, isElementType} from './utils' -import type {UserEvent} from './setup' +import {blur, focus, isDisabled, isElementType} from '../utils' +import {Config, UserEvent} from '../setup' -interface uploadInit { +export interface uploadInit { changeInit?: EventInit } @@ -10,12 +10,11 @@ export interface uploadOptions { applyAccept?: boolean } -export function upload( +export async function upload( this: UserEvent, element: HTMLElement, fileOrFiles: File | File[], init?: uploadInit, - {applyAccept = false}: uploadOptions = {}, ) { const input = isElementType(element, 'label') ? element.control : element @@ -28,10 +27,12 @@ export function upload( } if (isDisabled(element)) return - this.click(element) + await this.click(element) const files = (Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]) - .filter(file => !applyAccept || isAcceptableFile(file, input.accept)) + .filter( + file => !this[Config].applyAccept || isAcceptableFile(file, input.accept), + ) .slice(0, input.multiple ? undefined : 1) // blur fires when the file selector pops up diff --git a/src/utils/index.ts b/src/utils/index.ts index 8aac89de..3a345bd8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -30,6 +30,7 @@ export * from './keyDef/readNextDescriptor' export * from './misc/eventWrapper' export * from './misc/findClosest' +export * from './misc/getDocumentFromNode' export * from './misc/isDescendantOrSelf' export * from './misc/isElementType' export * from './misc/isVisible' diff --git a/src/utils/misc/getDocumentFromNode.ts b/src/utils/misc/getDocumentFromNode.ts new file mode 100644 index 00000000..9a30d994 --- /dev/null +++ b/src/utils/misc/getDocumentFromNode.ts @@ -0,0 +1,7 @@ +export function getDocumentFromNode(el: Node) { + return isDocument(el) ? el : el.ownerDocument +} + +function isDocument(node: Node): node is Document { + return node.nodeType === 9 +} diff --git a/tests/clear.ts b/tests/clear.ts index c3f5fc0e..c52c368e 100644 --- a/tests/clear.ts +++ b/tests/clear.ts @@ -2,9 +2,9 @@ import userEvent from '#src' import {setup} from '#testHelpers/utils' describe('clear elements', () => { - test('clear text input', () => { + test('clear text input', async () => { const {element, getEventSnapshot} = setup('') - userEvent.clear(element) + await userEvent.clear(element) expect(element).toHaveValue('') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value=""] @@ -16,9 +16,9 @@ describe('clear elements', () => { `) }) - test('clear textarea', () => { + test('clear textarea', async () => { const {element, getEventSnapshot} = setup('') - userEvent.clear(element) + await userEvent.clear(element) expect(element).toHaveValue('') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: textarea[value=""] @@ -30,11 +30,11 @@ describe('clear elements', () => { `) }) - test('clear contenteditable', () => { + test('clear contenteditable', async () => { const {element, getEventSnapshot} = setup( '
hello
', ) - userEvent.clear(element) + await userEvent.clear(element) expect(element).toBeEmptyDOMElement() expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: div @@ -45,7 +45,7 @@ describe('clear elements', () => { `) }) - test('clear inputs that cannot (programmatically) have a selection', () => { + test('clear inputs that cannot (programmatically) have a selection', async () => { const { elements: [email, password, number], } = setup(` @@ -53,19 +53,19 @@ describe('clear elements', () => { `) - userEvent.clear(email) + await userEvent.clear(email) expect(email).toHaveValue('') - userEvent.clear(password) + await userEvent.clear(password) expect(password).toHaveValue('') - userEvent.clear(number) + await userEvent.clear(number) expect(number).toHaveValue(null) }) }) describe('throw error when clear is impossible', () => { - test('only editable elements can be cleared', () => { + test('only editable elements can be cleared', async () => { const { elements: [disabled, readonly, div], } = setup(` @@ -74,35 +74,45 @@ describe('throw error when clear is impossible', () => {
hello
`) - expect(() => userEvent.clear(disabled)).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.clear(disabled), + ).rejects.toThrowErrorMatchingInlineSnapshot( `clear()\` is only supported on editable elements.`, ) - expect(() => userEvent.clear(readonly)).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.clear(readonly), + ).rejects.toThrowErrorMatchingInlineSnapshot( `clear()\` is only supported on editable elements.`, ) - expect(() => userEvent.clear(div)).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.clear(div), + ).rejects.toThrowErrorMatchingInlineSnapshot( `clear()\` is only supported on editable elements.`, ) }) - test('abort if event handler prevents element being focused', () => { + test('abort if event handler prevents element being focused', async () => { const {element} = setup(``) - element.addEventListener('focus', () => element.blur()) + element.addEventListener('focus', async () => element.blur()) - expect(() => userEvent.clear(element)).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.clear(element), + ).rejects.toThrowErrorMatchingInlineSnapshot( `The element to be cleared could not be focused.`, ) }) - test('abort if event handler prevents content being selected', () => { + test('abort if event handler prevents content being selected', async () => { const {element} = setup(``) - element.addEventListener('select', () => { + element.addEventListener('select', async () => { if (element.selectionStart === 0) { element.selectionStart = 1 } }) - expect(() => userEvent.clear(element)).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.clear(element), + ).rejects.toThrowErrorMatchingInlineSnapshot( `The element content to be cleared could not be selected.`, ) }) diff --git a/tests/click/click.js b/tests/click/click.ts similarity index 71% rename from tests/click/click.js rename to tests/click/click.ts index 32981d46..e91c28a9 100644 --- a/tests/click/click.js +++ b/tests/click/click.ts @@ -1,9 +1,9 @@ import userEvent from '#src' import {setup, addEventListener, addListeners} from '#testHelpers/utils' -test('click in button', () => { +test('click in button', async () => { const {element, getEventSnapshot} = setup('`) - const input = element.children[0] - const button = element.children[1] + const input = element.children[0] as HTMLInputElement + const button = element.children[1] as HTMLButtonElement - addEventListener(button, 'click', () => input.focus()) + addEventListener(button, 'click', async () => input.focus()) expect(input).not.toHaveFocus() - userEvent.click(button) + await userEvent.click(button) expect(input).toHaveFocus() - userEvent.click(button) + await userEvent.click(button) expect(input).toHaveFocus() }) -test('gives focus to the form control when clicking the label', () => { +test('gives focus to the form control when clicking the label', async () => { const {element} = setup(`
@@ -258,11 +258,11 @@ test('gives focus to the form control when clicking the label', () => { const label = element.children[0] const input = element.children[1] - userEvent.click(label) + await userEvent.click(label) expect(input).toHaveFocus() }) -test('gives focus to the form control when clicking within a label', () => { +test('gives focus to the form control when clicking within a label', async () => { const {element} = setup(`
@@ -270,16 +270,16 @@ test('gives focus to the form control when clicking within a label', () => {
`) const label = element.children[0] - const span = label.firstChild + const span = label.children[0] const input = element.children[1] - userEvent.click(span) + await userEvent.click(span) expect(input).toHaveFocus() }) -test('fires no events when clicking a label with a nested control that is disabled', () => { +test('fires no events when clicking a label with a nested control that is disabled', async () => { const {element, getEventSnapshot} = setup(``) - userEvent.click(element) + await userEvent.click(element) expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: label @@ -297,12 +297,12 @@ test('fires no events when clicking a label with a nested control that is disabl `) }) -test('does not crash if the label has no control', () => { +test('does not crash if the label has no control', async () => { const {element} = setup(``) - userEvent.click(element) + await userEvent.click(element) }) -test('clicking a label checks the checkbox', () => { +test('clicking a label checks the checkbox', async () => { const {element} = setup(`
@@ -312,12 +312,12 @@ test('clicking a label checks the checkbox', () => { const label = element.children[0] const input = element.children[1] - userEvent.click(label) + await userEvent.click(label) expect(input).toHaveFocus() expect(input).toBeChecked() }) -test('clicking a label checks the radio', () => { +test('clicking a label checks the radio', async () => { const {element} = setup(`
@@ -327,50 +327,50 @@ test('clicking a label checks the radio', () => { const label = element.children[0] const input = element.children[1] - userEvent.click(label) + await userEvent.click(label) expect(input).toHaveFocus() expect(input).toBeChecked() }) -test('submits a form when clicking on a `) - userEvent.click(element.children[0]) + await userEvent.click(element.children[0]) expect(eventWasFired('submit')).toBe(true) }) -test('does not submit a form when clicking on a `) - userEvent.click(element.children[0]) + await userEvent.click(element.children[0]) expect(getEventSnapshot()).not.toContain('submit') }) -test('does not fire blur on current element if is the same as previous', () => { +test('does not fire blur on current element if is the same as previous', async () => { const {element, getEventSnapshot, clearEventCalls} = setup('`) - userEvent.tab() + await userEvent.tab() expect(document.body).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(document.body).toHaveFocus() }) -test('skip consecutive radios of same group', () => { +test('skip consecutive radios of same group', async () => { const { elements: [inputA, radioA, radioB, inputB, radioC, radioD, radioE, inputC], } = setup(` @@ -474,32 +476,32 @@ test('skip consecutive radios of same group', () => { inputA.focus() - userEvent.tab() + await userEvent.tab() expect(radioA).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(inputB).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(radioC).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(radioD).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(inputC).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(radioE).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(radioC).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(inputB).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(radioB).toHaveFocus() - userEvent.tab({shift: true}) + await userEvent.tab({shift: true}) expect(inputA).toHaveFocus() }) -test('skip unchecked radios if that group has a checked one', () => { +test('skip unchecked radios if that group has a checked one', async () => { const { elements: [inputA, , inputB, radioB, inputC, , inputD], } = setup(` @@ -514,17 +516,17 @@ test('skip unchecked radios if that group has a checked one', () => { inputA.focus() - userEvent.tab() + await userEvent.tab() expect(inputB).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(radioB).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(inputC).toHaveFocus() - userEvent.tab() + await userEvent.tab() expect(inputD).toHaveFocus() }) -test('tab from active radio when another one is checked', () => { +test('tab from active radio when another one is checked', async () => { const { elements: [, , , radioB, inputC], } = setup(` @@ -537,12 +539,12 @@ test('tab from active radio when another one is checked', () => { radioB.focus() - userEvent.tab() + await userEvent.tab() expect(inputC).toHaveFocus() }) -test('calls FocusEvents with relatedTarget', () => { +test('calls FocusEvents with relatedTarget', async () => { const { elements: [element0, element1], } = setup('') @@ -551,12 +553,14 @@ test('calls FocusEvents with relatedTarget', () => { const events0 = addListeners(element0) const events1 = addListeners(element1) - userEvent.tab() + await userEvent.tab() - expect(events0.getEvents().find(e => e.type === 'blur').relatedTarget).toBe( - element1, - ) - expect(events1.getEvents().find(e => e.type === 'focus').relatedTarget).toBe( - element0, - ) + expect( + events0.getEvents().find((e): e is FocusEvent => e.type === 'blur') + ?.relatedTarget, + ).toBe(element1) + expect( + events1.getEvents().find((e): e is FocusEvent => e.type === 'focus') + ?.relatedTarget, + ).toBe(element0) }) diff --git a/tests/type/index.js b/tests/type/index.ts similarity index 85% rename from tests/type/index.js rename to tests/type/index.ts index f12c23f1..bb224ba7 100644 --- a/tests/type/index.js +++ b/tests/type/index.ts @@ -1,11 +1,10 @@ import userEvent from '#src' -import {wait} from '#src/utils' import {setup, addListeners} from '#testHelpers/utils' import '#testHelpers/custom-element' -test('types text in input', () => { +test('types text in input', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, 'Sup') + await userEvent.type(element, 'Sup') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value="Sup"] @@ -37,11 +36,11 @@ test('types text in input', () => { `) }) -test('can skip the initial click', () => { +test('can skip the initial click', async () => { const {element, getEventSnapshot, clearEventCalls} = setup('') element.focus() // users MUST focus themselves if they wish to skip the click clearEventCalls() - userEvent.type(element, 'Sup', {skipClick: true}) + await userEvent.type(element, 'Sup', {skipClick: true}) expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value="Sup"] @@ -60,13 +59,15 @@ test('can skip the initial click', () => { `) }) -test('types text inside custom element', () => { +test('types text inside custom element', async () => { const element = document.createElement('custom-el') document.body.append(element) - const inputEl = element.shadowRoot.querySelector('input') + const inputEl = (element.shadowRoot as ShadowRoot).querySelector( + 'input', + ) as HTMLInputElement const {getEventSnapshot} = addListeners(inputEl) - userEvent.type(inputEl, 'Sup') + await userEvent.type(inputEl, 'Sup') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value="Sup"] @@ -98,9 +99,9 @@ test('types text inside custom element', () => { `) }) -test('types text in textarea', () => { +test('types text in textarea', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, 'Sup') + await userEvent.type(element, 'Sup') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: textarea[value="Sup"] @@ -132,12 +133,12 @@ test('types text in textarea', () => { `) }) -test('does not fire input event when keypress calls prevent default', () => { +test('does not fire input event when keypress calls prevent default', async () => { const {element, getEventSnapshot} = setup('', { eventHandlers: {keyPress: e => e.preventDefault()}, }) - userEvent.type(element, 'a') + await userEvent.type(element, 'a') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value=""] @@ -160,12 +161,12 @@ test('does not fire input event when keypress calls prevent default', () => { `) }) -test('does not fire keypress or input events when keydown calls prevent default', () => { +test('does not fire keypress or input events when keydown calls prevent default', async () => { const {element, getEventSnapshot} = setup('', { eventHandlers: {keyDown: e => e.preventDefault()}, }) - userEvent.type(element, 'a') + await userEvent.type(element, 'a') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value=""] @@ -187,19 +188,19 @@ test('does not fire keypress or input events when keydown calls prevent default' `) }) -test('does not fire events when disabled', () => { +test('does not fire events when disabled', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, 'a') + await userEvent.type(element, 'a') expect(getEventSnapshot()).toMatchInlineSnapshot( `No events were fired on: input[value=""]`, ) }) -test('does not fire input when readonly', () => { +test('does not fire input when readonly', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, 'a') + await userEvent.type(element, 'a') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value=""] @@ -225,14 +226,16 @@ test('does not fire input when readonly', () => { test('should delay the typing when opts.delay is not 0', async () => { const inputValues = [{timestamp: Date.now(), value: ''}] const onInput = jest.fn(event => { - inputValues.push({timestamp: Date.now(), value: event.target.value}) + inputValues.push({ + timestamp: Date.now(), + value: ((event as InputEvent).target as HTMLInputElement).value, + }) }) const {element} = setup('', {eventHandlers: {input: onInput}}) const text = 'Hello, world!' const delay = 10 - // eslint-disable-next-line testing-library/no-await-sync-events await userEvent.type(element, text, {delay}) expect(onInput).toHaveBeenCalledTimes(text.length) @@ -245,13 +248,13 @@ test('should delay the typing when opts.delay is not 0', async () => { } }) -test('should fire events on the currently focused element', () => { +test('should fire events on the currently focused element', async () => { const {element} = setup(`
`, { eventHandlers: {keyDown: handleKeyDown}, }) - const input1 = element.children[0] - const input2 = element.children[1] + const input1 = element.children[0] as HTMLInputElement + const input2 = element.children[1] as HTMLInputElement const text = 'Hello, world!' const changeFocusLimit = 7 @@ -261,43 +264,45 @@ test('should fire events on the currently focused element', () => { } } - userEvent.type(input1, text) + await userEvent.type(input1, text) expect(input1).toHaveValue(text.slice(0, changeFocusLimit)) expect(input2).toHaveValue(text.slice(changeFocusLimit)) expect(input2).toHaveFocus() }) -test('should replace selected text', () => { +test('should replace selected text', async () => { const {element} = setup('') - userEvent.type(element, 'friend', { + await userEvent.type(element, 'friend', { initialSelectionStart: 6, initialSelectionEnd: 11, }) expect(element).toHaveValue('hello friend') }) -test('does not continue firing events when disabled during typing', () => { +test('does not continue firing events when disabled during typing', async () => { const {element} = setup('', { - eventHandlers: {input: e => (e.target.disabled = true)}, + eventHandlers: { + input: e => ((e.target as HTMLInputElement).disabled = true), + }, }) - userEvent.type(element, 'hi') + await userEvent.type(element, 'hi') expect(element).toHaveValue('h') }) // https://github.com/testing-library/user-event/issues/346 -test('typing in an empty textarea', () => { +test('typing in an empty textarea', async () => { const {element} = setup('') - userEvent.type(element, '1234') + await userEvent.type(element, '1234') expect(element).toHaveValue('1234') }) // https://github.com/testing-library/user-event/issues/321 -test('typing in a textarea with existing text', () => { +test('typing in a textarea with existing text', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, '12') + await userEvent.type(element, '12') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: textarea[value="Hello, 12"] @@ -328,11 +333,13 @@ test('typing in a textarea with existing text', () => { }) // https://github.com/testing-library/user-event/issues/321 -test('accepts an initialSelectionStart and initialSelectionEnd', () => { - const {element, getEventSnapshot} = setup('') +test('accepts an initialSelectionStart and initialSelectionEnd', async () => { + const {element, getEventSnapshot} = setup( + '', + ) element.setSelectionRange(0, 0) - userEvent.type(element, '12', { + await userEvent.type(element, '12', { initialSelectionStart: element.selectionStart, initialSelectionEnd: element.selectionEnd, }) @@ -370,17 +377,17 @@ test('accepts an initialSelectionStart and initialSelectionEnd', () => { }) // https://github.com/testing-library/user-event/issues/316#issuecomment-640199908 -test('can type into an input with type `email`', () => { +test('can type into an input with type `email`', async () => { const {element} = setup('') const email = 'yo@example.com' - userEvent.type(element, email) + await userEvent.type(element, email) expect(element).toHaveValue(email) }) -test('can type into an input with type `date`', () => { +test('can type into an input with type `date`', async () => { const {element, getEventSnapshot} = setup('') const date = '2020-06-29' - userEvent.type(element, date) + await userEvent.type(element, date) expect(element).toHaveValue(date) expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value="2020-06-29"] @@ -434,10 +441,10 @@ test('can type into an input with type `date`', () => { }) // https://github.com/testing-library/user-event/issues/336 -test('can type "-" into number inputs', () => { +test('can type "-" into number inputs', async () => { const {element, getEventSnapshot} = setup('') const negativeNumber = '-3' - userEvent.type(element, negativeNumber) + await userEvent.type(element, negativeNumber) expect(element).toHaveValue(-3) // NOTE: the input event here does not actually change the value thanks to @@ -471,9 +478,9 @@ test('can type "-" into number inputs', () => { }) // https://github.com/testing-library/user-event/issues/336 -test('can type "." into number inputs', () => { +test('can type "." into number inputs', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, '3.3') + await userEvent.type(element, '3.3') expect(element).toHaveValue(3.3) expect(getEventSnapshot()).toMatchInlineSnapshot(` @@ -507,23 +514,23 @@ test('can type "." into number inputs', () => { `) }) -test('-{backspace}3', () => { +test('-{backspace}3', async () => { const {element} = setup('') const negativeNumber = '-{backspace}3' - userEvent.type(element, negativeNumber) + await userEvent.type(element, negativeNumber) expect(element).toHaveValue(3) }) -test('-a3', () => { +test('-a3', async () => { const {element} = setup('') const negativeNumber = '-a3' - userEvent.type(element, negativeNumber) + await userEvent.type(element, negativeNumber) expect(element).toHaveValue(-3) }) -test('typing an invalid input value', () => { - const {element} = setup('') - userEvent.type(element, '3-3') +test('typing an invalid input value', async () => { + const {element} = setup('') + await userEvent.type(element, '3-3') expect(element).toHaveValue(null) @@ -534,59 +541,59 @@ test('typing an invalid input value', () => { expect(element.validity.badInput).toBe(false) }) -test('should not throw error if we are trying to call type on an element without a value', () => { +test('should not throw error if we are trying to call type on an element without a value', async () => { const {element} = setup('
') - return expect(userEvent.type(element, ':(', {delay: 1})).resolves.toBe( + await expect(userEvent.type(element, ':(', {delay: 1})).resolves.toBe( undefined, ) }) -test('typing on button should not alter its value', () => { +test('typing on button should not alter its value', async () => { const {element} = setup('
`) - const label = element.children[0] - const input = element.children[1] + const label = element.children[0] as HTMLLabelElement + const input = element.children[1] as HTMLInputElement - userEvent.upload(label, files) + await userEvent.upload(label, files) - expect(input.files[0]).toStrictEqual(files[0]) - expect(input.files.item(0)).toStrictEqual(files[0]) - expect(input.files[1]).toStrictEqual(files[1]) - expect(input.files.item(1)).toStrictEqual(files[1]) + expect(input.files?.[0]).toStrictEqual(files[0]) + expect(input.files?.item(0)).toStrictEqual(files[0]) + expect(input.files?.[1]).toStrictEqual(files[1]) + expect(input.files?.item(1)).toStrictEqual(files[1]) expect(input.files).toHaveLength(2) }) -test('should not upload when is disabled', () => { +test('should not upload when is disabled', async () => { const file = new File(['hello'], 'hello.png', {type: 'image/png'}) - const {element} = setup('') + const {element} = setup('') - userEvent.upload(element, file) + await userEvent.upload(element, file) - expect(element.files[0]).toBeUndefined() - expect(element.files.item(0)).toBeNull() + expect(element.files?.[0]).toBeUndefined() + expect(element.files?.item(0)).toBeNull() expect(element.files).toHaveLength(0) }) -test('should call onChange/input bubbling up the event when a file is selected', () => { +test('should call onChange/input bubbling up the event when a file is selected', async () => { const file = new File(['hello'], 'hello.png', {type: 'image/png'}) const {element: form} = setup(` @@ -136,7 +139,7 @@ test('should call onChange/input bubbling up the event when a file is selected', `) - const input = form.querySelector('input') + const input = form.querySelector('input') as HTMLInputElement const onChangeInput = jest.fn() const onChangeForm = jest.fn() @@ -154,7 +157,7 @@ test('should call onChange/input bubbling up the event when a file is selected', expect(onInputInput).toHaveBeenCalledTimes(0) expect(onInputForm).toHaveBeenCalledTimes(0) - userEvent.upload(input, file) + await userEvent.upload(input, file) expect(onChangeForm).toHaveBeenCalledTimes(1) expect(onChangeInput).toHaveBeenCalledTimes(1) @@ -170,28 +173,28 @@ test.each([ [false, 'video/*', 4], ])( 'should filter according to accept attribute applyAccept=%s, acceptAttribute=%s', - (applyAccept, acceptAttribute, expectedLength) => { + async (applyAccept, acceptAttribute, expectedLength) => { const files = [ new File(['hello'], 'hello.png', {type: 'image/png'}), new File(['there'], 'there.jpg', {type: 'audio/mp3'}), new File(['there'], 'there.csv', {type: 'text/csv'}), new File(['there'], 'there.jpg', {type: 'video/mp4'}), ] - const {element} = setup(` + const {element} = setup(` `) - userEvent.upload(element, files, undefined, {applyAccept}) + await userEvent.upload(element, files, undefined, {applyAccept}) expect(element.files).toHaveLength(expectedLength) }, ) -test('should not trigger input event when selected files are the same', () => { - const {element, eventWasFired, clearEventCalls} = setup( +test('should not trigger input event when selected files are the same', async () => { + const {element, eventWasFired, clearEventCalls} = setup( '', ) const files = [ @@ -199,62 +202,64 @@ test('should not trigger input event when selected files are the same', () => { new File(['there'], 'there.png', {type: 'image/png'}), ] - userEvent.upload(element, []) + await userEvent.upload(element, []) expect(eventWasFired('input')).toBe(false) expect(element.files).toHaveLength(0) - userEvent.upload(element, files) + await userEvent.upload(element, files) expect(eventWasFired('input')).toBe(true) expect(element.files).toHaveLength(2) clearEventCalls() - userEvent.upload(element, files) + await userEvent.upload(element, files) expect(eventWasFired('input')).toBe(false) expect(element.files).toHaveLength(2) - userEvent.upload(element, []) + await userEvent.upload(element, []) expect(eventWasFired('input')).toBe(true) expect(element.files).toHaveLength(0) }) -test('input.files implements iterable', () => { - const {element, getEvents} = setup(``) +test('input.files implements iterable', async () => { + const {element, getEvents} = setup( + ``, + ) const files = [ new File(['hello'], 'hello.png', {type: 'image/png'}), new File(['there'], 'there.png', {type: 'image/png'}), ] - userEvent.upload(element, files) - const eventTargetFiles = getEvents('input')[0].target.files + await userEvent.upload(element, files) + const eventTargetFiles = (getEvents('input')[0].target as HTMLInputElement) + .files expect(eventTargetFiles).toBe(element.files) expect(eventTargetFiles).not.toEqual(files) - expect(Array.from(eventTargetFiles)).toEqual(files) + expect(eventTargetFiles && Array.from(eventTargetFiles)).toEqual(files) }) -test('throw error if trying to use upload on an invalid element', () => { +test('throw error if trying to use upload on an invalid element', async () => { const {elements} = setup('
') - expect(() => - userEvent.upload(elements[0], "I'm only a div :("), - ).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.upload(elements[0], new File([], '')), + ).rejects.toThrowErrorMatchingInlineSnapshot( `The given DIV element does not accept file uploads`, ) - expect(() => - userEvent.upload(elements[1], "I'm a checkbox :("), - ).toThrowErrorMatchingInlineSnapshot( + await expect( + userEvent.upload(elements[1], new File([], '')), + ).rejects.toThrowErrorMatchingInlineSnapshot( `The associated INPUT element does not accept file uploads`, ) }) -test('apply init options', () => { +test('apply init options', async () => { const {element, getEvents} = setup('') - userEvent.upload(element, new File([], 'hello.png'), { - clickInit: {shiftKey: true}, + await userEvent.upload(element, new File([], 'hello.png'), { changeInit: {cancelable: true}, }) diff --git a/tests/utils/dataTransfer/Clipboard.ts b/tests/utils/dataTransfer/Clipboard.ts index 1fa7fbb6..9d71b21d 100644 --- a/tests/utils/dataTransfer/Clipboard.ts +++ b/tests/utils/dataTransfer/Clipboard.ts @@ -48,7 +48,7 @@ describe('read from and write to clipboard', () => { await expect(window.navigator.clipboard.readText()).resolves.toBe('') }) - test('detach clipboard', () => { + test('detach clipboard', async () => { expect(window.navigator.clipboard).not.toBe(undefined) detachClipboardStubFromView(window) expect(window.navigator.clipboard).toBe(undefined) diff --git a/tests/utils/dataTransfer/DataTransfer.ts b/tests/utils/dataTransfer/DataTransfer.ts index 0cec5fed..eb619ab0 100644 --- a/tests/utils/dataTransfer/DataTransfer.ts +++ b/tests/utils/dataTransfer/DataTransfer.ts @@ -1,7 +1,7 @@ import {createDataTransfer, getBlobFromDataTransferItem} from '#src/utils' describe('create DataTransfer', () => { - test('plain string', () => { + test('plain string', async () => { const dt = createDataTransfer() dt.setData('text/plain', 'foo') @@ -12,7 +12,7 @@ describe('create DataTransfer', () => { expect(callback).toBeCalledWith('foo') }) - test('multi format', () => { + test('multi format', async () => { const dt = createDataTransfer() dt.setData('text/plain', 'foo') dt.setData('text/html', 'bar') @@ -30,7 +30,7 @@ describe('create DataTransfer', () => { expect(dt.getData('text')).toBe('baz') }) - test('overwrite item', () => { + test('overwrite item', async () => { const dt = createDataTransfer() dt.setData('text/plain', 'foo') dt.setData('text/plain', 'bar') @@ -39,7 +39,7 @@ describe('create DataTransfer', () => { expect(dt.getData('text')).toBe('bar') }) - test('files operation', () => { + test('files operation', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) const f1 = new File(['bar'], 'bar1.txt', {type: 'text/plain'}) const dt = createDataTransfer([f0, f1]) @@ -49,7 +49,7 @@ describe('create DataTransfer', () => { expect(dt.files.length).toBe(2) }) - test('files item', () => { + test('files item', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) const dt = createDataTransfer() dt.setData('text/html', 'foo') @@ -65,7 +65,7 @@ describe('create DataTransfer', () => { expect(callback).not.toBeCalled() }) - test('clear data', () => { + test('clear data', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) const dt = createDataTransfer() dt.setData('text/html', 'foo') @@ -85,7 +85,7 @@ describe('create DataTransfer', () => { }) }) -test('get Blob from DataTransfer', () => { +test('get Blob from DataTransfer', async () => { const dt = createDataTransfer() dt.items.add('foo', 'text/plain') dt.items.add(new File(['bar'], 'bar.txt', {type: 'text/plain'})) diff --git a/tests/utils/edit/calculateNewValue.ts b/tests/utils/edit/calculateNewValue.ts index 0540f439..8d339c44 100644 --- a/tests/utils/edit/calculateNewValue.ts +++ b/tests/utils/edit/calculateNewValue.ts @@ -3,9 +3,9 @@ import {setup} from '#testHelpers/utils' // TODO: focus the maxlength tests on the tested aspects -test('honors maxlength', () => { +test('honors maxlength', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, '123') + await userEvent.type(element, '123') // NOTE: no input event when typing "3" expect(getEventSnapshot()).toMatchInlineSnapshot(` @@ -38,9 +38,9 @@ test('honors maxlength', () => { `) }) -test('honors maxlength="" as if there was no maxlength', () => { +test('honors maxlength="" as if there was no maxlength', async () => { const {element, getEventSnapshot} = setup('') - userEvent.type(element, '123') + await userEvent.type(element, '123') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: input[value="123"] @@ -73,11 +73,11 @@ test('honors maxlength="" as if there was no maxlength', () => { `) }) -test('honors maxlength with existing text', () => { +test('honors maxlength with existing text', async () => { const {element, getEventSnapshot} = setup( '', ) - userEvent.type(element, '3') + await userEvent.type(element, '3') // NOTE: no input event when typing "3" expect(getEventSnapshot()).toMatchInlineSnapshot(` @@ -103,12 +103,12 @@ test('honors maxlength with existing text', () => { `) }) -test('honors maxlength on textarea', () => { +test('honors maxlength on textarea', async () => { const {element, getEventSnapshot} = setup( '', ) - userEvent.type(element, '3') + await userEvent.type(element, '3') expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: textarea[value="12"] @@ -134,10 +134,10 @@ test('honors maxlength on textarea', () => { }) // https://github.com/testing-library/user-event/issues/418 -test('ignores maxlength on input[type=number]', () => { +test('ignores maxlength on input[type=number]', async () => { const {element} = setup(``) - userEvent.type(element, '3') + await userEvent.type(element, '3') expect(element).toHaveValue(123) }) diff --git a/tests/utils/edit/isContentEditable.ts b/tests/utils/edit/isContentEditable.ts index 3d2885a3..070efd67 100644 --- a/tests/utils/edit/isContentEditable.ts +++ b/tests/utils/edit/isContentEditable.ts @@ -1,7 +1,7 @@ import {setup} from '#testHelpers/utils' import {isContentEditable} from '#src/utils' -test('report if element is contenteditable', () => { +test('report if element is contenteditable', async () => { const {elements} = setup( `
`, ) diff --git a/tests/utils/focus/blur.js b/tests/utils/focus/blur.ts similarity index 83% rename from tests/utils/focus/blur.js rename to tests/utils/focus/blur.ts index a92d8e60..ed5286f5 100644 --- a/tests/utils/focus/blur.js +++ b/tests/utils/focus/blur.ts @@ -1,7 +1,7 @@ import {blur, focus} from '#src/utils' import {setup} from '#testHelpers/utils' -test('blur a button', () => { +test('blur a button', async () => { const {element, getEventSnapshot, clearEventCalls} = setup(`