diff --git a/README.md b/README.md index b24c136e..d42d601f 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. diff --git a/package.json b/package.json index e41e26bb..8858bf60 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,16 @@ "node": ">=10" }, "scripts": { - "build": "kcd-scripts build --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --bundle --no-clean", + "build": "npm run build:bundles ; npm run build:types", + "build:bundles": "kcd-scripts build --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --bundle --no-clean", + "build:types": "tsc --build tsconfig.build.json", "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", "test:debug": "node --inspect-brk ./node_modules/.bin/jest --watch --runInBand", "test:update": "npm test -- --updateSnapshot --coverage", "validate": "kcd-scripts validate", - "typecheck": "dtslint ./types/" + "typecheck-DISABLED": "dtslint ./types/" }, "files": [ "dist", @@ -41,16 +43,18 @@ "@babel/runtime": "^7.10.2", "aria-query": "^4.0.2", "dom-accessibility-api": "^0.4.5", + "eslint-import-resolver-typescript": "^2.0.0", "pretty-format": "^25.5.0" }, "devDependencies": { - "dtslint": "^3.6.9", "@testing-library/jest-dom": "^5.9.0", + "dtslint": "^3.6.11", "jest-in-case": "^1.0.2", "jest-serializer-ansi": "^1.0.3", "jest-watch-select-projects": "^2.0.0", "jsdom": "^16.2.2", - "kcd-scripts": "^6.2.0" + "kcd-scripts": "^6.2.3", + "typescript": "^3.9.5" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", @@ -59,6 +63,11 @@ "import/no-unassigned-import": "off", "import/no-useless-path-segments": "off", "no-console": "off" + }, + "settings": { + "import/resolver": { + "typescript": {} + } } }, "eslintIgnore": [ diff --git a/src/__tests__/events.js b/src/__tests__/events.js index f2f8d2a9..37b62c10 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -140,17 +140,21 @@ const eventTypes = [ const allEvents = Object.keys(eventMap) -const bubblingEvents = allEvents - .filter(eventName => eventMap[eventName].defaultInit.bubbles) +const bubblingEvents = allEvents.filter( + eventName => eventMap[eventName].defaultInit.bubbles, +) -const composedEvents = allEvents - .filter(eventName => eventMap[eventName].defaultInit.composed) +const composedEvents = allEvents.filter( + eventName => eventMap[eventName].defaultInit.composed, +) -const nonBubblingEvents = allEvents - .filter(eventName => !bubblingEvents.includes(eventName)) +const nonBubblingEvents = allEvents.filter( + eventName => !bubblingEvents.includes(eventName), +) -const nonComposedEvents = allEvents - .filter(eventName => !composedEvents.includes(eventName)) +const nonComposedEvents = allEvents.filter( + eventName => !composedEvents.includes(eventName), +) eventTypes.forEach(({type, events, elementType}) => { describe(`${type} Events`, () => { @@ -203,7 +207,7 @@ describe(`Composed Events`, () => { const spy = jest.fn() node.addEventListener(event.toLowerCase(), spy) - const shadowRoot = node.attachShadow({ mode: 'closed' }) + const shadowRoot = node.attachShadow({mode: 'closed'}) const innerNode = document.createElement('div') shadowRoot.appendChild(innerNode) @@ -218,7 +222,7 @@ describe(`Composed Events`, () => { const spy = jest.fn() node.addEventListener(event.toLowerCase(), spy) - const shadowRoot = node.attachShadow({ mode: 'closed' }) + const shadowRoot = node.attachShadow({mode: 'closed'}) const innerNode = document.createElement('div') shadowRoot.appendChild(innerNode) @@ -234,7 +238,7 @@ describe(`Aliased Events`, () => { const node = document.createElement('div') const spy = jest.fn() node.addEventListener(eventAliasMap[eventAlias].toLowerCase(), spy) - + fireEvent[eventAlias](node) expect(spy).toHaveBeenCalledTimes(1) }) diff --git a/src/__tests__/role.js b/src/__tests__/role.js index a187a77b..7e3164ba 100644 --- a/src/__tests__/role.js +++ b/src/__tests__/role.js @@ -346,7 +346,9 @@ Here are the accessible roles: test('has no useful error message in findBy', async () => { const {findByRole} = render(`
  • `) - await expect(findByRole('option', {timeout: 1})).rejects.toThrow('Unable to find role="option"') + await expect(findByRole('option', {timeout: 1})).rejects.toThrow( + 'Unable to find role="option"', + ) }) test('explicit role is most specific', () => { diff --git a/src/config.js b/src/config.ts similarity index 76% rename from src/config.js rename to src/config.ts index 44b51146..f012b8a4 100644 --- a/src/config.js +++ b/src/config.ts @@ -1,9 +1,24 @@ import {prettyDOM} from './pretty-dom' +export interface Config { + testIdAttribute: string + asyncWrapper(cb: (...args: any[]) => any): Promise + eventWrapper(cb: (...args: any[]) => any): void + getElementError: (message: string, container: Element) => Error + asyncUtilTimeout: number + defaultHidden: boolean + showOriginalStackTrace: boolean + throwSuggestions: boolean +} + +interface InternalConfig { + _disableExpensiveErrorDiagnostics: boolean +} + // It would be cleaner for this to live inside './queries', but // other parts of the code assume that all exports from // './queries' are query functions. -let config = { +let config: Config & InternalConfig = { testIdAttribute: 'data-testid', asyncUtilTimeout: 1000, // this is to support React's async `act` function. @@ -44,7 +59,11 @@ export function runWithExpensiveErrorDiagnosticsDisabled(callback) { } } -export function configure(newConfig) { +export interface ConfigFn { + (existingConfig: Config): Partial +} + +export function configure(newConfig: Partial | ConfigFn): void { if (typeof newConfig === 'function') { // Pass the existing config out to the provided function // and accept a delta in return diff --git a/src/event-map.js b/src/event-map.js deleted file mode 100644 index 98d78e66..00000000 --- a/src/event-map.js +++ /dev/null @@ -1,350 +0,0 @@ -export const eventMap = { - // Clipboard Events - copy: { - EventType: 'ClipboardEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - cut: { - EventType: 'ClipboardEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - paste: { - EventType: 'ClipboardEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - // Composition Events - compositionEnd: { - EventType: 'CompositionEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - compositionStart: { - EventType: 'CompositionEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - compositionUpdate: { - EventType: 'CompositionEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - // Keyboard Events - keyDown: { - EventType: 'KeyboardEvent', - defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, - }, - keyPress: { - EventType: 'KeyboardEvent', - defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, - }, - keyUp: { - EventType: 'KeyboardEvent', - defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, - }, - // Focus Events - focus: { - EventType: 'FocusEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - blur: { - EventType: 'FocusEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - focusIn: { - EventType: 'FocusEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - focusOut: { - EventType: 'FocusEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - // Form Events - change: { - EventType: 'Event', - defaultInit: {bubbles: true, cancelable: false}, - }, - input: { - EventType: 'InputEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - invalid: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: true}, - }, - submit: { - EventType: 'Event', - defaultInit: {bubbles: true, cancelable: true}, - }, - reset: { - EventType: 'Event', - defaultInit: {bubbles: true, cancelable: true}, - }, - // Mouse Events - click: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true}, - }, - contextMenu: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - dblClick: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - drag: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - dragEnd: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - dragEnter: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - dragExit: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - dragLeave: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - dragOver: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - dragStart: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - drop: { - EventType: 'DragEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - mouseDown: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - mouseEnter: { - EventType: 'MouseEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - mouseLeave: { - EventType: 'MouseEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - mouseMove: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - mouseOut: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - mouseOver: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - mouseUp: { - EventType: 'MouseEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - // Selection Events - select: { - EventType: 'Event', - defaultInit: {bubbles: true, cancelable: false}, - }, - // Touch Events - touchCancel: { - EventType: 'TouchEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - touchEnd: { - EventType: 'TouchEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - touchMove: { - EventType: 'TouchEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - touchStart: { - EventType: 'TouchEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - // UI Events - scroll: { - EventType: 'UIEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - // Wheel Events - wheel: { - EventType: 'WheelEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - // Media Events - abort: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - canPlay: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - canPlayThrough: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - durationChange: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - emptied: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - encrypted: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - ended: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - loadedData: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - loadedMetadata: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - loadStart: { - EventType: 'ProgressEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - pause: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - play: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - playing: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - progress: { - EventType: 'ProgressEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - rateChange: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - seeked: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - seeking: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - stalled: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - suspend: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - timeUpdate: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - volumeChange: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - waiting: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - // Image Events - load: { - EventType: 'UIEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - error: { - EventType: 'Event', - defaultInit: {bubbles: false, cancelable: false}, - }, - // Animation Events - animationStart: { - EventType: 'AnimationEvent', - defaultInit: {bubbles: true, cancelable: false}, - }, - animationEnd: { - EventType: 'AnimationEvent', - defaultInit: {bubbles: true, cancelable: false}, - }, - animationIteration: { - EventType: 'AnimationEvent', - defaultInit: {bubbles: true, cancelable: false}, - }, - // Transition Events - transitionEnd: { - EventType: 'TransitionEvent', - defaultInit: {bubbles: true, cancelable: true}, - }, - // pointer events - pointerOver: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - pointerEnter: { - EventType: 'PointerEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - pointerDown: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - pointerMove: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - pointerUp: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - pointerCancel: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: false, composed: true}, - }, - pointerOut: { - EventType: 'PointerEvent', - defaultInit: {bubbles: true, cancelable: true, composed: true}, - }, - pointerLeave: { - EventType: 'PointerEvent', - defaultInit: {bubbles: false, cancelable: false}, - }, - gotPointerCapture: { - EventType: 'PointerEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - lostPointerCapture: { - EventType: 'PointerEvent', - defaultInit: {bubbles: false, cancelable: false, composed: true}, - }, - // history events - popState: { - EventType: 'PopStateEvent', - defaultInit: {bubbles: true, cancelable: false}, - }, - } - - export const eventAliasMap = { - doubleClick: 'dblClick', - } diff --git a/src/event-map.ts b/src/event-map.ts new file mode 100644 index 00000000..29348eb1 --- /dev/null +++ b/src/event-map.ts @@ -0,0 +1,460 @@ +export type EventMapKey = + | 'copy' + | 'cut' + | 'paste' + | 'compositionEnd' + | 'compositionStart' + | 'compositionUpdate' + | 'keyDown' + | 'keyPress' + | 'keyUp' + | 'focus' + | 'blur' + | 'focusIn' + | 'focusOut' + | 'change' + | 'input' + | 'invalid' + | 'submit' + | 'reset' + | 'click' + | 'contextMenu' + | 'dblClick' + | 'drag' + | 'dragEnd' + | 'dragEnter' + | 'dragExit' + | 'dragLeave' + | 'dragOver' + | 'dragStart' + | 'drop' + | 'mouseDown' + | 'mouseEnter' + | 'mouseLeave' + | 'mouseMove' + | 'mouseOut' + | 'mouseOver' + | 'mouseUp' + | 'popState' + | 'select' + | 'touchCancel' + | 'touchEnd' + | 'touchMove' + | 'touchStart' + | 'scroll' + | 'wheel' + | 'abort' + | 'canPlay' + | 'canPlayThrough' + | 'durationChange' + | 'emptied' + | 'encrypted' + | 'ended' + | 'loadedData' + | 'loadedMetadata' + | 'loadStart' + | 'pause' + | 'play' + | 'playing' + | 'progress' + | 'rateChange' + | 'seeked' + | 'seeking' + | 'stalled' + | 'suspend' + | 'timeUpdate' + | 'volumeChange' + | 'waiting' + | 'load' + | 'error' + | 'animationStart' + | 'animationEnd' + | 'animationIteration' + | 'transitionEnd' + | 'pointerOver' + | 'pointerEnter' + | 'pointerDown' + | 'pointerMove' + | 'pointerUp' + | 'pointerCancel' + | 'pointerOut' + | 'pointerLeave' + | 'gotPointerCapture' + | 'lostPointerCapture' + +type LegacyKeyboardEventInit = { + // lib.dom.ts marks 'charCode' as deprecated in KeyboardEvent and does not include it in KeyboardEventInit + charCode?: number +} & KeyboardEventInit + +type EventMapValue = + | {EventType: 'AnimationEvent'; defaultInit: AnimationEventInit} + | {EventType: 'ClipboardEvent'; defaultInit: ClipboardEventInit} + | {EventType: 'CompositionEvent'; defaultInit: CompositionEventInit} + | {EventType: 'DragEvent'; defaultInit: DragEventInit} + | {EventType: 'Event'; defaultInit: EventInit} + | {EventType: 'FocusEvent'; defaultInit: FocusEventInit} + | {EventType: 'KeyboardEvent'; defaultInit: LegacyKeyboardEventInit} + | {EventType: 'InputEvent'; defaultInit: InputEventInit} + | {EventType: 'MouseEvent'; defaultInit: MouseEventInit} + | {EventType: 'PointerEvent'; defaultInit: PointerEventInit} + | {EventType: 'PopStateEvent'; defaultInit: PopStateEventInit} + | {EventType: 'ProgressEvent'; defaultInit: ProgressEventInit} + | {EventType: 'TouchEvent'; defaultInit: TouchEventInit} + | {EventType: 'TransitionEvent'; defaultInit: TransitionEventInit} + | {EventType: 'UIEvent'; defaultInit: UIEventInit} + | {EventType: 'WheelEvent'; defaultInit: WheelEventInit} + +export const eventMap: Record = { + // Clipboard Events + copy: { + EventType: 'ClipboardEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + cut: { + EventType: 'ClipboardEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + paste: { + EventType: 'ClipboardEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + // Composition Events + compositionEnd: { + EventType: 'CompositionEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + compositionStart: { + EventType: 'CompositionEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + compositionUpdate: { + EventType: 'CompositionEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + // Keyboard Events + keyDown: { + EventType: 'KeyboardEvent', + defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, + }, + keyPress: { + EventType: 'KeyboardEvent', + defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, + }, + keyUp: { + EventType: 'KeyboardEvent', + defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true}, + }, + // Focus Events + focus: { + EventType: 'FocusEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + blur: { + EventType: 'FocusEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + focusIn: { + EventType: 'FocusEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + focusOut: { + EventType: 'FocusEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + // Form Events + change: { + EventType: 'Event', + defaultInit: {bubbles: true, cancelable: false}, + }, + input: { + EventType: 'InputEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + invalid: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: true}, + }, + submit: { + EventType: 'Event', + defaultInit: {bubbles: true, cancelable: true}, + }, + reset: { + EventType: 'Event', + defaultInit: {bubbles: true, cancelable: true}, + }, + // Mouse Events + click: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true}, + }, + contextMenu: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + dblClick: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + drag: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + dragEnd: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + dragEnter: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + dragExit: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + dragLeave: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + dragOver: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + dragStart: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + drop: { + EventType: 'DragEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + mouseDown: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + mouseEnter: { + EventType: 'MouseEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + mouseLeave: { + EventType: 'MouseEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + mouseMove: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + mouseOut: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + mouseOver: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + mouseUp: { + EventType: 'MouseEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + // Selection Events + select: { + EventType: 'Event', + defaultInit: {bubbles: true, cancelable: false}, + }, + // Touch Events + touchCancel: { + EventType: 'TouchEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + touchEnd: { + EventType: 'TouchEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + touchMove: { + EventType: 'TouchEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + touchStart: { + EventType: 'TouchEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + // UI Events + scroll: { + EventType: 'UIEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + // Wheel Events + wheel: { + EventType: 'WheelEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + // Media Events + abort: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + canPlay: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + canPlayThrough: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + durationChange: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + emptied: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + encrypted: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + ended: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + loadedData: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + loadedMetadata: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + loadStart: { + EventType: 'ProgressEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + pause: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + play: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + playing: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + progress: { + EventType: 'ProgressEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + rateChange: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + seeked: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + seeking: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + stalled: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + suspend: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + timeUpdate: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + volumeChange: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + waiting: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + // Image Events + load: { + EventType: 'UIEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + error: { + EventType: 'Event', + defaultInit: {bubbles: false, cancelable: false}, + }, + // Animation Events + animationStart: { + EventType: 'AnimationEvent', + defaultInit: {bubbles: true, cancelable: false}, + }, + animationEnd: { + EventType: 'AnimationEvent', + defaultInit: {bubbles: true, cancelable: false}, + }, + animationIteration: { + EventType: 'AnimationEvent', + defaultInit: {bubbles: true, cancelable: false}, + }, + // Transition Events + transitionEnd: { + EventType: 'TransitionEvent', + defaultInit: {bubbles: true, cancelable: true}, + }, + // pointer events + pointerOver: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + pointerEnter: { + EventType: 'PointerEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + pointerDown: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + pointerMove: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + pointerUp: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + pointerCancel: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: false, composed: true}, + }, + pointerOut: { + EventType: 'PointerEvent', + defaultInit: {bubbles: true, cancelable: true, composed: true}, + }, + pointerLeave: { + EventType: 'PointerEvent', + defaultInit: {bubbles: false, cancelable: false}, + }, + gotPointerCapture: { + EventType: 'PointerEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + lostPointerCapture: { + EventType: 'PointerEvent', + defaultInit: {bubbles: false, cancelable: false, composed: true}, + }, + // history events + popState: { + EventType: 'PopStateEvent', + defaultInit: {bubbles: true, cancelable: false}, + }, +} + +type EventAliasMapKey = 'doubleClick' +export const eventAliasMap: Record = { + doubleClick: 'dblClick', +} + +export type EventType = EventMapKey | EventAliasMapKey diff --git a/src/events.js b/src/events.ts similarity index 66% rename from src/events.js rename to src/events.ts index 6d19c1fa..d50fd7ab 100644 --- a/src/events.js +++ b/src/events.ts @@ -1,8 +1,16 @@ import {getConfig} from './config' import {getWindowFromNode} from './helpers' -import {eventMap, eventAliasMap} from './event-map' +import {eventMap, EventMapKey, eventAliasMap, EventType} from './event-map' -function fireEvent(element, event) { +declare global { + // FIXME we should not augment the interface here + interface Window { + DataTransfer: () => void + Event: () => void + } +} + +function internalfireEvent(element: EventTarget, event: Event) { return getConfig().eventWrapper(() => { if (!event) { throw new Error( @@ -18,9 +26,26 @@ function fireEvent(element, event) { }) } -const createEvent = {} +function isDragEventInit(eventInit: unknown): eventInit is DragEventInit { + return typeof (eventInit as DragEventInit).dataTransfer === 'object' +} + +type EventTargetWithFiles = HTMLInputElement +type EventTargetWithValue = + | HTMLInputElement + | HTMLButtonElement + | HTMLOutputElement + +type CreateObject = { + [K in EventType]: ( + //element: Document | Element | Window | Node, + element: EventTarget, + options?: {}, + ) => Event +} +const createEvent = {} as CreateObject -Object.keys(eventMap).forEach(key => { +Object.keys(eventMap).forEach((key: EventMapKey) => { const {EventType, defaultInit} = eventMap[key] const eventName = key.toLowerCase() @@ -30,8 +55,11 @@ Object.keys(eventMap).forEach(key => { `Unable to fire a "${key}" event - please provide a DOM element.`, ) } - const eventInit = {...defaultInit, ...init} - const {target: {value, files, ...targetProperties} = {}} = eventInit + const eventInit: EventInit = {...defaultInit, ...init} + const {target = {}} = eventInit as Event + const {value, files, ...targetProperties} = target as Partial< + EventTargetWithValue & EventTargetWithFiles + > if (value !== undefined) { setNativeValue(node, value) } @@ -56,15 +84,17 @@ Object.keys(eventMap).forEach(key => { } else { // IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill event = window.document.createEvent(EventType) - const {bubbles, cancelable, detail, ...otherInit} = eventInit + const {bubbles, cancelable, detail, ...otherInit} = eventInit as Partial< + UIEvent + > event.initEvent(eventName, bubbles, cancelable, detail) Object.keys(otherInit).forEach(eventKey => { event[eventKey] = otherInit[eventKey] }) } - const {dataTransfer} = eventInit - if (typeof dataTransfer === 'object') { + if (isDragEventInit(eventInit)) { + const {dataTransfer} = eventInit // DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568 /* istanbul ignore if */ if (typeof window.DataTransfer === 'function') { @@ -80,7 +110,8 @@ Object.keys(eventMap).forEach(key => { return event } - fireEvent[key] = (node, init) => fireEvent(node, createEvent[key](node, init)) + internalfireEvent[key] = (node: EventTarget, init) => + internalfireEvent(node, createEvent[key](node, init)) }) // function written after some investigation here: @@ -102,9 +133,17 @@ function setNativeValue(element, value) { Object.keys(eventAliasMap).forEach(aliasKey => { const key = eventAliasMap[aliasKey] - fireEvent[aliasKey] = (...args) => fireEvent[key](...args) + internalfireEvent[aliasKey] = (...args) => fireEvent[key](...args) }) +type FireEventAsFunction = (element: EventTarget, event: Event) => boolean +type FireEventAsHelper = Record< + EventType, + (element: EventTarget, init?: unknown) => boolean +> +type FireEvent = FireEventAsFunction & FireEventAsHelper + +const fireEvent = internalfireEvent as FireEvent export {fireEvent, createEvent} /* eslint complexity:["error", 9] */ diff --git a/src/get-node-text.js b/src/get-node-text.ts similarity index 81% rename from src/get-node-text.js rename to src/get-node-text.ts index 28b7d3d8..be7f7ef8 100644 --- a/src/get-node-text.js +++ b/src/get-node-text.ts @@ -2,9 +2,9 @@ // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants const TEXT_NODE = 3 -function getNodeText(node) { +function getNodeText(node: HTMLElement): string { if (node.matches('input[type=submit], input[type=button]')) { - return node.value + return (node as HTMLInputElement).value } return Array.from(node.childNodes) diff --git a/src/get-queries-for-element.js b/src/get-queries-for-element.js deleted file mode 100644 index cc81578b..00000000 --- a/src/get-queries-for-element.js +++ /dev/null @@ -1,25 +0,0 @@ -import * as defaultQueries from './queries' - -/** - * @typedef {{[key: string]: Function}} FuncMap - */ - -/** - * @param {HTMLElement} element container - * @param {FuncMap} queries object of functions - * @param {Object} initialValue for reducer - * @returns {FuncMap} returns object of functions bound to container - */ -function getQueriesForElement( - element, - queries = defaultQueries, - initialValue = {}, -) { - return Object.keys(queries).reduce((helpers, key) => { - const fn = queries[key] - helpers[key] = fn.bind(null, element) - return helpers - }, initialValue) -} - -export {getQueriesForElement} diff --git a/src/get-queries-for-element.ts b/src/get-queries-for-element.ts new file mode 100644 index 00000000..10e8a3a6 --- /dev/null +++ b/src/get-queries-for-element.ts @@ -0,0 +1,55 @@ +import * as defaultQueries from './queries' + +export type BoundFunction = T extends ( + attribute: string, + element: HTMLElement, + text: infer P, + options: infer Q, +) => infer R + ? (text: P, options?: Q) => R + : T extends ( + a1: any, + text: infer P, + options: infer Q, + waitForElementOptions: infer W, + ) => infer R + ? (text: P, options?: Q, waitForElementOptions?: W) => R + : T extends (a1: any, text: infer P, options: infer Q) => infer R + ? (text: P, options?: Q) => R + : never +export type BoundFunctions = {[P in keyof T]: BoundFunction} + +export type Query = ( + container: HTMLElement, + ...args: any[] +) => + | Error + | Promise + | Promise + | HTMLElement[] + | HTMLElement + | null + +export interface Queries { + [T: string]: Query +} + +/** + * @param {HTMLElement} element container + * @param {FuncMap} queries object of functions + * @param {Object} initialValue for reducer + * @returns {FuncMap} returns object of functions bound to container + */ +function getQueriesForElement( + element: HTMLElement, + queries: Queries = defaultQueries, + initialValue = {}, +): BoundFunctions { + return Object.keys(queries).reduce((helpers, key) => { + const fn: Query = queries[key] + helpers[key] = fn.bind(null, element) + return helpers + }, initialValue) as BoundFunctions +} + +export {getQueriesForElement} diff --git a/src/helpers.js b/src/helpers.ts similarity index 96% rename from src/helpers.js rename to src/helpers.ts index 64031562..7237084d 100644 --- a/src/helpers.js +++ b/src/helpers.ts @@ -4,7 +4,7 @@ const globalObj = typeof window === 'undefined' ? global : window function runWithRealTimers(callback) { const usingJestFakeTimers = globalObj.setTimeout && - globalObj.setTimeout._isMockFunction && + (globalObj.setTimeout as any)._isMockFunction && typeof jest !== 'undefined' if (usingJestFakeTimers) { @@ -46,7 +46,7 @@ function getDocument() { } return window.document } -function getWindowFromNode(node) { +function getWindowFromNode(node): Window { // istanbul ignore next I'm not sure what could cause the final else so we'll leave it uncovered. if (node.defaultView) { // node is document diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/matches.js b/src/matches.ts similarity index 67% rename from src/matches.js rename to src/matches.ts index 3241e680..30ddaea1 100644 --- a/src/matches.js +++ b/src/matches.ts @@ -1,4 +1,26 @@ -function fuzzyMatches(textToMatch, node, matcher, normalizer) { +export type MatcherFunction = (content: string, element: HTMLElement) => boolean + +export type Matcher = string | RegExp | MatcherFunction + +export type NormalizerFn = (text: string) => string + +export interface MatcherOptions { + exact?: boolean + /** Use normalizer with getDefaultNormalizer instead */ + trim?: boolean + /** Use normalizer with getDefaultNormalizer instead */ + collapseWhitespace?: boolean + normalizer?: NormalizerFn + /** suppress suggestions for a specific query */ + suggest?: boolean +} + +function fuzzyMatches( + textToMatch: string, + node: HTMLElement | null, + matcher: Matcher, + normalizer: NormalizerFn, +) { if (typeof textToMatch !== 'string') { return false } @@ -13,7 +35,12 @@ function fuzzyMatches(textToMatch, node, matcher, normalizer) { } } -function matches(textToMatch, node, matcher, normalizer) { +function matches( + textToMatch: string | undefined, + node: HTMLElement | null, + matcher: Matcher, + normalizer: NormalizerFn, +) { if (typeof textToMatch !== 'string') { return false } @@ -28,7 +55,15 @@ function matches(textToMatch, node, matcher, normalizer) { } } -function getDefaultNormalizer({trim = true, collapseWhitespace = true} = {}) { +export interface DefaultNormalizerOptions { + trim?: boolean + collapseWhitespace?: boolean +} + +function getDefaultNormalizer({ + trim = true, + collapseWhitespace = true, +}: DefaultNormalizerOptions = {}): NormalizerFn { return text => { let normalizedText = text normalizedText = trim ? normalizedText.trim() : normalizedText @@ -48,7 +83,7 @@ function getDefaultNormalizer({trim = true, collapseWhitespace = true} = {}) { * @param {Function|undefined} normalizer The user-specified normalizer * @returns {Function} A normalizer */ -function makeNormalizer({trim, collapseWhitespace, normalizer}) { +function makeNormalizer({trim, collapseWhitespace, normalizer}): NormalizerFn { if (normalizer) { // User has specified a custom normalizer if ( diff --git a/src/pretty-dom.js b/src/pretty-dom.ts similarity index 61% rename from src/pretty-dom.js rename to src/pretty-dom.ts index 56e8e90d..ee264eef 100644 --- a/src/pretty-dom.js +++ b/src/pretty-dom.ts @@ -1,6 +1,15 @@ -import prettyFormat from 'pretty-format' +import prettyFormat, {OptionsReceived} from 'pretty-format' import {getDocument} from './helpers' +// Declare the potential Cypress variable +declare global { + namespace NodeJS { + interface Global { + Cypress?: any + } + } +} + function inCypress(dom) { const window = (dom.ownerDocument && dom.ownerDocument.defaultView) || undefined @@ -15,14 +24,24 @@ const inNode = () => process.versions !== undefined && process.versions.node !== undefined -const getMaxLength = dom => +const getMaxLength = (dom): number => inCypress(dom) ? 0 - : (typeof process !== 'undefined' && process.env.DEBUG_PRINT_LIMIT) || 7000 + : (typeof process !== 'undefined' && + parseInt(process.env.DEBUG_PRINT_LIMIT, 10)) || + 7000 const {DOMElement, DOMCollection} = prettyFormat.plugins -function prettyDOM(dom, maxLength, options) { +function isDocument(dom?: Element | Document): dom is Document { + return (dom as Document).documentElement !== undefined +} + +function prettyDOM( + dom?: Element | Document, + maxLength?: number, + options?: OptionsReceived, +) { if (!dom) { dom = getDocument().body } @@ -33,16 +52,16 @@ function prettyDOM(dom, maxLength, options) { if (maxLength === 0) { return '' } - if (dom.documentElement) { + if (isDocument(dom)) { dom = dom.documentElement } - let domTypeName = typeof dom + let domTypeName: string = typeof dom if (domTypeName === 'object') { domTypeName = dom.constructor.name } else { // To don't fall with `in` operator - dom = {} + dom = {} as Element } if (!('outerHTML' in dom)) { throw new TypeError( @@ -61,6 +80,10 @@ function prettyDOM(dom, maxLength, options) { : debugContent } -const logDOM = (...args) => console.log(prettyDOM(...args)) +const logDOM = ( + dom?: Element | HTMLDocument, + maxLength?: number, + options?: OptionsReceived, +) => console.log(prettyDOM(dom, maxLength, options)) export {prettyDOM, logDOM} diff --git a/src/queries/all-utils.js b/src/queries/all-utils.ts similarity index 100% rename from src/queries/all-utils.js rename to src/queries/all-utils.ts diff --git a/src/queries/alt-text.js b/src/queries/alt-text.ts similarity index 73% rename from src/queries/alt-text.js rename to src/queries/alt-text.ts index a31c5164..c4e9a3e4 100644 --- a/src/queries/alt-text.js +++ b/src/queries/alt-text.ts @@ -1,18 +1,27 @@ import {wrapAllByQueryWithSuggestion} from '../query-helpers' import {checkContainerType} from '../helpers' -import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils' +import { + matches, + fuzzyMatches, + makeNormalizer, + buildQueries, + MatcherOptions, + Matcher, +} from './all-utils' function queryAllByAltText( - container, - alt, - {exact = true, collapseWhitespace, trim, normalizer} = {}, + container: HTMLElement, + alt: Matcher, + {exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {}, ) { checkContainerType(container) const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) - return Array.from(container.querySelectorAll('img,input,area')).filter(node => + return Array.from( + container.querySelectorAll('img,input,area'), + ).filter((node: HTMLElement) => matcher(node.getAttribute('alt'), node, alt, matchNormalizer), - ) + ) as HTMLElement[] } const getMultipleError = (c, alt) => diff --git a/src/queries/display-value.js b/src/queries/display-value.ts similarity index 74% rename from src/queries/display-value.js rename to src/queries/display-value.ts index 75ab083f..190db702 100644 --- a/src/queries/display-value.js +++ b/src/queries/display-value.ts @@ -6,30 +6,37 @@ import { fuzzyMatches, makeNormalizer, buildQueries, + Matcher, + MatcherOptions, } from './all-utils' +function getElementValue(element: Element): string | undefined { + return (element as any).value +} + function queryAllByDisplayValue( - container, - value, - {exact = true, collapseWhitespace, trim, normalizer} = {}, + container: HTMLElement, + value: Matcher, + {exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {}, ) { checkContainerType(container) const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) return Array.from(container.querySelectorAll(`input,textarea,select`)).filter( - node => { + (node: HTMLElement) => { if (node.tagName === 'SELECT') { - const selectedOptions = Array.from(node.options).filter( + const selectElement = node as HTMLSelectElement + const selectedOptions = Array.from(selectElement.options).filter( option => option.selected, ) return selectedOptions.some(optionNode => matcher(getNodeText(optionNode), optionNode, value, matchNormalizer), ) } else { - return matcher(node.value, node, value, matchNormalizer) + return matcher(getElementValue(node), node, value, matchNormalizer) } }, - ) + ) as HTMLElement[] } const getMultipleError = (c, value) => diff --git a/src/queries/index.js b/src/queries/index.ts similarity index 100% rename from src/queries/index.js rename to src/queries/index.ts diff --git a/src/queries/label-text.js b/src/queries/label-text.ts similarity index 92% rename from src/queries/label-text.js rename to src/queries/label-text.ts index e7eda117..d571d18c 100644 --- a/src/queries/label-text.js +++ b/src/queries/label-text.ts @@ -9,13 +9,16 @@ import { makeSingleQuery, wrapAllByQueryWithSuggestion, wrapSingleQueryWithSuggestion, + Matcher, + MatcherOptions, + SelectorMatcherOptions, } from './all-utils' import {queryAllByText} from './text' function queryAllLabelsByText( - container, - text, - {exact = true, trim, collapseWhitespace, normalizer} = {}, + container: HTMLElement, + text: Matcher, + {exact = true, trim, collapseWhitespace, normalizer}: MatcherOptions = {}, ) { const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) @@ -35,13 +38,19 @@ function queryAllLabelsByText( }) return matcher(textToMatch, label, text, matchNormalizer) - }) + }) as HTMLElement[] } function queryAllByLabelText( - container, - text, - {selector = '*', exact = true, collapseWhitespace, trim, normalizer} = {}, + container: HTMLElement, + text: Matcher, + { + selector = '*', + exact = true, + collapseWhitespace, + trim, + normalizer, + }: SelectorMatcherOptions = {}, ) { checkContainerType(container) @@ -51,7 +60,7 @@ function queryAllByLabelText( normalizer: matchNormalizer, }) const labelledElements = labels - .reduce((matchedElements, label) => { + .reduce((matchedElements, label: HTMLLabelElement) => { const elementsForLabel = [] if (label.control) { elementsForLabel.push(label.control) @@ -112,7 +121,7 @@ function queryAllByLabelText( return Array.from( new Set([...labelledElements, ...ariaLabelledElements]), - ).filter(element => element.matches(selector)) + ).filter(element => element.matches(selector)) as HTMLElement[] } // the getAll* query would normally look like this: diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.ts similarity index 73% rename from src/queries/placeholder-text.js rename to src/queries/placeholder-text.ts index bdea5945..7fc9a6d9 100644 --- a/src/queries/placeholder-text.js +++ b/src/queries/placeholder-text.ts @@ -1,10 +1,19 @@ import {wrapAllByQueryWithSuggestion} from '../query-helpers' import {checkContainerType} from '../helpers' -import {queryAllByAttribute, buildQueries} from './all-utils' +import { + queryAllByAttribute, + buildQueries, + Matcher, + MatcherOptions, +} from './all-utils' -function queryAllByPlaceholderText(...args) { - checkContainerType(...args) - return queryAllByAttribute('placeholder', ...args) +function queryAllByPlaceholderText( + container: HTMLElement, + placeholder: Matcher, + options?: MatcherOptions, +) { + checkContainerType(container) + return queryAllByAttribute('placeholder', container, placeholder, options) } const getMultipleError = (c, text) => `Found multiple elements with the placeholder text of: ${text}` diff --git a/src/queries/role.js b/src/queries/role.ts similarity index 78% rename from src/queries/role.js rename to src/queries/role.ts index 07fe0771..ab77e82d 100644 --- a/src/queries/role.js +++ b/src/queries/role.ts @@ -14,12 +14,40 @@ import { fuzzyMatches, getConfig, makeNormalizer, + Matcher, + MatcherOptions, matches, } from './all-utils' +export interface ByRoleOptions extends MatcherOptions { + /** + * If true includes elements in the query set that are usually excluded from + * the accessibility tree. `role="none"` or `role="presentation"` are included + * in either case. + */ + hidden?: boolean + /** + * If true only includes elements in the query set that are marked as + * selected in the accessibility tree, i.e., `aria-selected="true"` + */ + selected?: boolean + /** + * Includes every role used in the `role` attribute + * For example *ByRole('progressbar', {queryFallbacks: true})` will find
    `. + */ + queryFallbacks?: boolean + /** + * Only considers elements with the specified accessible name. + */ + name?: + | string + | RegExp + | ((accessibleName: string, element: Element) => boolean) +} + function queryAllByRole( - container, - role, + container: HTMLElement, + role: Matcher, { exact = true, collapseWhitespace, @@ -29,7 +57,7 @@ function queryAllByRole( normalizer, queryFallbacks = false, selected, - } = {}, + }: ByRoleOptions = {}, ) { checkContainerType(container) const matcher = exact ? matches : fuzzyMatches @@ -52,7 +80,7 @@ function queryAllByRole( } return Array.from(container.querySelectorAll('*')) - .filter(node => { + .filter((node: HTMLElement) => { const isRoleSpecifiedExplicitly = node.hasAttribute('role') if (isRoleSpecifiedExplicitly) { @@ -85,14 +113,14 @@ function queryAllByRole( // don't care if aria attributes are unspecified return true }) - .filter(element => { + .filter((element: HTMLElement) => { return hidden === false ? isInaccessible(element, { isSubtreeInaccessible: cachedIsSubtreeInaccessible, }) === false : true }) - .filter(element => { + .filter((element: HTMLElement) => { if (name === undefined) { // Don't care return true @@ -104,7 +132,7 @@ function queryAllByRole( name, text => text, ) - }) + }) as HTMLElement[] } const getMultipleError = (c, role) => @@ -113,16 +141,17 @@ const getMultipleError = (c, role) => const getMissingError = ( container, role, - {hidden = getConfig().defaultHidden, name} = {}, + {hidden = getConfig().defaultHidden, name}: ByRoleOptions = {}, ) => { if (getConfig()._disableExpensiveErrorDiagnostics) { return `Unable to find role="${role}"` } let roles = '' - Array.from(container.children).forEach(childElement => { + Array.from(container.children).forEach((childElement: HTMLElement) => { roles += prettyRoles(childElement, { hidden, + // @ts-ignore FIXME remove this code? prettyRoles does not seem handle 'includeName' includeName: name !== undefined, }) }) diff --git a/src/queries/test-id.js b/src/queries/test-id.ts similarity index 71% rename from src/queries/test-id.js rename to src/queries/test-id.ts index f2ef5a9d..2ab618d5 100644 --- a/src/queries/test-id.js +++ b/src/queries/test-id.ts @@ -1,12 +1,22 @@ import {checkContainerType} from '../helpers' import {wrapAllByQueryWithSuggestion} from '../query-helpers' -import {queryAllByAttribute, getConfig, buildQueries} from './all-utils' +import { + queryAllByAttribute, + getConfig, + buildQueries, + Matcher, + MatcherOptions, +} from './all-utils' const getTestIdAttribute = () => getConfig().testIdAttribute -function queryAllByTestId(...args) { - checkContainerType(...args) - return queryAllByAttribute(getTestIdAttribute(), ...args) +function queryAllByTestId( + container: HTMLElement, + testId: Matcher, + options?: MatcherOptions, +) { + checkContainerType(container) + return queryAllByAttribute(getTestIdAttribute(), container, testId, options) } const getMultipleError = (c, id) => diff --git a/src/queries/text.js b/src/queries/text.ts similarity index 80% rename from src/queries/text.js rename to src/queries/text.ts index 903bba25..5cbdfe46 100644 --- a/src/queries/text.js +++ b/src/queries/text.ts @@ -1,4 +1,7 @@ -import {wrapAllByQueryWithSuggestion} from '../query-helpers' +import { + SelectorMatcherOptions, + wrapAllByQueryWithSuggestion, +} from '../query-helpers' import {checkContainerType} from '../helpers' import {DEFAULT_IGNORE_TAGS} from '../config' import { @@ -7,11 +10,16 @@ import { makeNormalizer, getNodeText, buildQueries, + Matcher, } from './all-utils' +interface ByTextSelectorMatcherOptions extends SelectorMatcherOptions { + ignore?: string +} + function queryAllByText( - container, - text, + container: HTMLElement, + text: Matcher, { selector = '*', exact = true, @@ -19,7 +27,7 @@ function queryAllByText( trim, ignore = DEFAULT_IGNORE_TAGS, normalizer, - } = {}, + }: ByTextSelectorMatcherOptions = {}, ) { checkContainerType(container) const matcher = exact ? matches : fuzzyMatches @@ -30,7 +38,9 @@ function queryAllByText( } return [...baseArray, ...Array.from(container.querySelectorAll(selector))] .filter(node => !ignore || !node.matches(ignore)) - .filter(node => matcher(getNodeText(node), node, text, matchNormalizer)) + .filter(node => + matcher(getNodeText(node), node, text, matchNormalizer), + ) as HTMLElement[] } const getMultipleError = (c, text) => diff --git a/src/queries/title.js b/src/queries/title.ts similarity index 86% rename from src/queries/title.js rename to src/queries/title.ts index ee304466..f10e740f 100644 --- a/src/queries/title.js +++ b/src/queries/title.ts @@ -6,21 +6,23 @@ import { makeNormalizer, getNodeText, buildQueries, + Matcher, + MatcherOptions, } from './all-utils' function queryAllByTitle( - container, - text, - {exact = true, collapseWhitespace, trim, normalizer} = {}, + container: HTMLElement, + text: Matcher, + {exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {}, ) { checkContainerType(container) const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) return Array.from(container.querySelectorAll('[title], svg > title')).filter( - node => + (node: HTMLElement) => matcher(node.getAttribute('title'), node, text, matchNormalizer) || matcher(getNodeText(node), node, text, matchNormalizer), - ) + ) as HTMLElement[] } const getMultipleError = (c, title) => diff --git a/src/query-helpers.js b/src/query-helpers.ts similarity index 67% rename from src/query-helpers.js rename to src/query-helpers.ts index cd4c3481..8a090385 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.ts @@ -1,13 +1,27 @@ import {getSuggestedQuery} from './suggestions' -import {fuzzyMatches, matches, makeNormalizer} from './matches' -import {waitFor} from './wait-for' +import { + fuzzyMatches, + matches, + makeNormalizer, + Matcher, + MatcherOptions, +} from './matches' +import {waitFor, WaitForOptions} from './wait-for' import {getConfig} from './config' -function getElementError(message, container) { +export interface SelectorMatcherOptions extends MatcherOptions { + ignore?: string + selector?: string +} + +function getElementError(message: string, container: HTMLElement): Error { return getConfig().getElementError(message, container) } -function getMultipleElementsFoundError(message, container) { +function getMultipleElementsFoundError( + message: string, + container: HTMLElement, +): Error { return getElementError( `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`, container, @@ -15,20 +29,30 @@ function getMultipleElementsFoundError(message, container) { } function queryAllByAttribute( - attribute, - container, - text, - {exact = true, collapseWhitespace, trim, normalizer} = {}, -) { + attribute: string, + container: HTMLElement, + text: Matcher, + {exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {}, +): HTMLElement[] { const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node => - matcher(node.getAttribute(attribute), node, text, matchNormalizer), - ) + matcher( + node.getAttribute(attribute), + node as HTMLElement, + text, + matchNormalizer, + ), + ) as HTMLElement[] } -function queryByAttribute(attribute, container, text, ...args) { - const els = queryAllByAttribute(attribute, container, text, ...args) +function queryByAttribute( + attribute: string, + container: HTMLElement, + text: Matcher, + options?: MatcherOptions, +): HTMLElement | null { + const els = queryAllByAttribute(attribute, container, text, options) if (els.length > 1) { throw getMultipleElementsFoundError( `Found multiple elements by [${attribute}=${text}]`, @@ -115,6 +139,7 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ + // @ts-ignore FIXME with the right tsconfig settings ...new Set( els.map(element => getSuggestedQuery(element, variant)?.toString()), ), @@ -132,7 +157,43 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( return els } -function buildQueries(queryAllBy, getMultipleError, getMissingError) { +/** + * query methods have a common call signature. Only the return type differs. + */ +export type QueryMethod = ( + container: HTMLElement, + ...args: Arguments +) => Return +export type QueryBy = QueryMethod< + Arguments, + HTMLElement | null +> +export type GetAllBy = QueryMethod< + Arguments, + HTMLElement[] +> +export type FindAllBy = QueryMethod< + [Arguments[0], Arguments[1], WaitForOptions], + Promise +> +export type GetBy = QueryMethod +export type FindBy = QueryMethod< + [Arguments[0], Arguments[1], WaitForOptions], + Promise +> +export type BuiltQueryMethods = [ + QueryBy, + GetAllBy, + GetBy, + FindAllBy, + FindBy, +] + +function buildQueries( + queryAllBy: GetAllBy, + getMultipleError: (container: HTMLElement, ...args: Arguments) => string, + getMissingError: (container: HTMLElement, ...args: Arguments) => string, +): BuiltQueryMethods { const queryBy = wrapSingleQueryWithSuggestion( makeSingleQuery(queryAllBy, getMultipleError), queryAllBy.name, diff --git a/src/role-helpers.js b/src/role-helpers.ts similarity index 89% rename from src/role-helpers.js rename to src/role-helpers.ts index f42e3e14..4992a21a 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.ts @@ -8,7 +8,7 @@ const elementRoleList = buildElementRoleList(elementRoles) * @param {Element} element - * @returns {boolean} - `true` if `element` and its subtree are inaccessible */ -function isSubtreeInaccessible(element) { +function isSubtreeInaccessible(element: HTMLElement): boolean { if (element.hidden === true) { return true } @@ -25,6 +25,9 @@ function isSubtreeInaccessible(element) { return false } +interface IsInaccessibleOptions { + isSubtreeInaccessible?: typeof isSubtreeInaccessible +} /** * Partial implementation https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion * which should only be used for elements with a non-presentational role i.e. @@ -39,7 +42,10 @@ function isSubtreeInaccessible(element) { * can be used to return cached results from previous isSubtreeInaccessible calls * @returns {boolean} true if excluded, otherwise false */ -function isInaccessible(element, options = {}) { +function isInaccessible( + element: HTMLElement, + options: IsInaccessibleOptions = {}, +): boolean { const { isSubtreeInaccessible: isSubtreeInaccessibleImpl = isSubtreeInaccessible, } = options @@ -118,8 +124,8 @@ function buildElementRoleList(elementRolesMap) { return result.sort(bySelectorSpecificity) } -function getRoles(container, {hidden = false} = {}) { - function flattenDOM(node) { +function getRoles(container, {hidden = false} = {}): Record { + function flattenDOM(node: ParentNode) { return [ node, ...Array.from(node.children).reduce( @@ -133,8 +139,8 @@ function getRoles(container, {hidden = false} = {}) { .filter(element => { return hidden === false ? isInaccessible(element) === false : true }) - .reduce((acc, node) => { - let roles = [] + .reduce((acc, node: Element) => { + let roles: Array = [] // TODO: This violates html-aria which does not allow any role on every element if (node.hasAttribute('role')) { roles = node.getAttribute('role').split(' ').slice(0, 1) @@ -152,7 +158,7 @@ function getRoles(container, {hidden = false} = {}) { }, {}) } -function prettyRoles(dom, {hidden}) { +function prettyRoles(dom: HTMLElement, {hidden}) { const roles = getRoles(dom, {hidden}) return Object.entries(roles) @@ -161,7 +167,7 @@ function prettyRoles(dom, {hidden}) { const elementsString = elements .map(el => { const nameString = `Name "${computeAccessibleName(el)}":\n` - const domString = prettyDOM(el.cloneNode(false)) + const domString = prettyDOM(el.cloneNode(false) as Element) return `${nameString}${domString}` }) .join('\n\n') @@ -171,7 +177,7 @@ function prettyRoles(dom, {hidden}) { .join('\n') } -const logRoles = (dom, {hidden = false} = {}) => +const logRoles = (dom: HTMLElement, {hidden = false} = {}) => console.log(prettyRoles(dom, {hidden})) /** diff --git a/src/screen.js b/src/screen.ts similarity index 100% rename from src/screen.js rename to src/screen.ts diff --git a/src/suggestions.js b/src/suggestions.ts similarity index 69% rename from src/suggestions.js rename to src/suggestions.ts index ff960d68..88e7098b 100644 --- a/src/suggestions.js +++ b/src/suggestions.ts @@ -6,10 +6,19 @@ import {getImplicitAriaRoles} from './role-helpers' const normalize = getDefaultNormalizer() -function getLabelTextFor(element) { - let label = - element.labels && - Array.from(element.labels).find(el => Boolean(normalize(el.textContent))) +function isElementWithLabels( + element: HTMLElement, +): element is HTMLInputElement { + // We cast with HTMLInputElement, but it could be a HTMLSelectElement + return (element as HTMLInputElement).labels !== undefined +} + +function getLabelTextFor(element: HTMLElement): string | undefined { + let label: HTMLLabelElement | undefined = + isElementWithLabels(element) && + Array.from(element.labels).find((el: HTMLLabelElement) => + Boolean(normalize(el.textContent)), + ) // non form elements that are using aria-labelledby won't be included in `element.labels` if (!label) { @@ -30,7 +39,16 @@ function escapeRegExp(string) { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string } -function makeSuggestion(queryName, content, {variant = 'get', name}) { +interface MakeSuggestionOptions { + variant?: string + name?: string +} + +function makeSuggestion( + queryName, + content, + {variant = 'get', name}: MakeSuggestionOptions, +) { const queryArgs = [content] if (name) { @@ -55,7 +73,20 @@ function makeSuggestion(queryName, content, {variant = 'get', name}) { } } -export function getSuggestedQuery(element, variant) { +function isElementWithValue(element: HTMLElement): element is HTMLInputElement { + // We cast with HTMLInputElement, but it could be a HTMLSelectElement + return Boolean((element as HTMLInputElement).value) +} + +export interface Suggestion { + queryName: string + toString(): string +} + +export function getSuggestedQuery( + element: HTMLElement, + variant?: string, +): Suggestion | undefined { const role = element.getAttribute('role') ?? getImplicitAriaRoles(element)?.[0] if (role) { @@ -80,7 +111,7 @@ export function getSuggestedQuery(element, variant) { return makeSuggestion('Text', textContent, {variant}) } - if (element.value) { + if (isElementWithValue(element)) { return makeSuggestion('DisplayValue', normalize(element.value), {variant}) } diff --git a/src/wait-for-dom-change.js b/src/wait-for-dom-change.ts similarity index 85% rename from src/wait-for-dom-change.js rename to src/wait-for-dom-change.ts index 1344db9d..97076388 100644 --- a/src/wait-for-dom-change.js +++ b/src/wait-for-dom-change.ts @@ -7,6 +7,7 @@ import { runWithRealTimers, } from './helpers' import {getConfig} from './config' +import {WaitForOptions} from './wait-for' let hasWarned = false @@ -22,7 +23,7 @@ function waitForDomChange({ attributes: true, characterData: true, }, -} = {}) { +}: WaitForOptions = {}) { if (!hasWarned) { hasWarned = true console.warn( @@ -31,7 +32,9 @@ function waitForDomChange({ } return new Promise((resolve, reject) => { const timer = setTimeout(onTimeout, timeout) - const {MutationObserver} = getWindowFromNode(container) + const {MutationObserver} = getWindowFromNode(container) as Window & { + MutationObserver: (callback: MutationCallback) => void + } const observer = new MutationObserver(onMutation) runWithRealTimers(() => observer.observe(container, mutationObserverOptions), @@ -47,7 +50,7 @@ function waitForDomChange({ } } - function onMutation(mutationsList) { + function onMutation(mutationsList: MutationRecord[]) { onDone(null, mutationsList) } diff --git a/src/wait-for-element-to-be-removed.js b/src/wait-for-element-to-be-removed.ts similarity index 75% rename from src/wait-for-element-to-be-removed.js rename to src/wait-for-element-to-be-removed.ts index 0703b575..4a2a38b8 100644 --- a/src/wait-for-element-to-be-removed.js +++ b/src/wait-for-element-to-be-removed.ts @@ -1,4 +1,4 @@ -import {waitFor} from './wait-for' +import {waitFor, WaitForOptions} from './wait-for' const isRemoved = result => !result || (Array.isArray(result) && !result.length) @@ -12,26 +12,32 @@ function initialCheck(elements) { } } -async function waitForElementToBeRemoved(callback, options) { +async function waitForElementToBeRemoved( + callback: (() => T | T[]) | T | T[], + options?: WaitForOptions, +) { // created here so we get a nice stacktrace const timeoutError = new Error('Timed out in waitForElementToBeRemoved.') + let cb if (typeof callback !== 'function') { initialCheck(callback) - const elements = Array.isArray(callback) ? callback : [callback] + const elements: Array = Array.isArray(callback) ? callback : [callback] const getRemainingElements = elements.map(element => { let parent = element.parentElement while (parent.parentElement) parent = parent.parentElement return () => (parent.contains(element) ? element : null) }) - callback = () => getRemainingElements.map(c => c()).filter(Boolean) + cb = () => getRemainingElements.map(c => c()).filter(Boolean) + } else { + cb = callback } - initialCheck(callback()) + initialCheck(cb()) return waitFor(() => { let result try { - result = callback() + result = cb() } catch (error) { if (error.name === 'TestingLibraryElementError') { return true diff --git a/src/wait-for-element.js b/src/wait-for-element.ts similarity index 82% rename from src/wait-for-element.js rename to src/wait-for-element.ts index 060f17be..8bc585d9 100644 --- a/src/wait-for-element.js +++ b/src/wait-for-element.ts @@ -1,11 +1,14 @@ -import {waitFor} from './wait-for' +import {waitFor, WaitForOptions} from './wait-for' let hasWarned = false // deprecated... TODO: remove this method. People should use a find* query or // wait instead the reasoning is that this doesn't really do anything useful // that you can't get from using find* or wait. -async function waitForElement(callback, options) { +async function waitForElement( + callback: () => T extends Promise ? never : T, + options?: WaitForOptions, +): Promise { if (!hasWarned) { hasWarned = true console.warn( diff --git a/src/wait-for.js b/src/wait-for.ts similarity index 80% rename from src/wait-for.js rename to src/wait-for.ts index b271a00d..c77a54c2 100644 --- a/src/wait-for.js +++ b/src/wait-for.ts @@ -17,7 +17,7 @@ function copyStackTrace(target, source) { function waitFor( callback, { - container = getDocument(), + container = getDocument() as Node, timeout = getConfig().asyncUtilTimeout, showOriginalStackTrace = getConfig().showOriginalStackTrace, stackTraceError, @@ -27,7 +27,7 @@ function waitFor( childList: true, attributes: true, characterData: true, - }, + } as MutationObserverInit, }, ) { if (typeof callback !== 'function') { @@ -40,7 +40,9 @@ function waitFor( const overallTimeoutTimer = setTimeout(onTimeout, timeout) const intervalId = setInterval(checkCallback, interval) - const {MutationObserver} = getWindowFromNode(container) + const {MutationObserver} = (getWindowFromNode(container) as unknown) as { + MutationObserver: (callback: MutationCallback) => void + } const observer = new MutationObserver(checkCallback) runWithRealTimers(() => observer.observe(container, mutationObserverOptions), @@ -90,7 +92,18 @@ function waitFor( }) } -function waitForWrapper(callback, options) { +export interface WaitForOptions { + container?: Node + timeout?: number + interval?: number + mutationObserverOptions?: MutationObserverInit + showOriginalStackTrace?: boolean +} + +function waitForWrapper( + callback: () => T extends Promise ? never : T, + options?: WaitForOptions, +): Promise { // create the error here so its stack trace is as close to the // calling code as possible const stackTraceError = new Error('STACK_TRACE_MESSAGE') @@ -103,16 +116,21 @@ let hasWarned = false // deprecated... TODO: remove this method. We renamed this to `waitFor` so the // code people write reads more clearly. -function wait(...args) { +interface WaitOptions { + container?: Node + timeout?: number + interval?: number + mutationObserverOptions?: MutationObserverInit +} +function wait(first: () => void, options?: WaitOptions): Promise { // istanbul ignore next - const [first = () => {}, ...rest] = args if (!hasWarned) { hasWarned = true console.warn( `\`wait\` has been deprecated and replaced by \`waitFor\` instead. In most cases you should be able to find/replace \`wait\` with \`waitFor\`. Learn more: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.`, ) } - return waitForWrapper(first, ...rest) + return waitForWrapper(first, options) } export {waitForWrapper as waitFor, wait} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..3325a603 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "emitDeclarationOnly": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..406ef8e6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "./types/", + "allowJs": true, + "declaration": true, + "noEmit": true, + "allowSyntheticDefaultImports": true + }, + "include": ["./src/**/*.ts"] +} diff --git a/types/config.d.ts b/types/config.d.ts deleted file mode 100644 index a1fa9fe1..00000000 --- a/types/config.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface Config { - testIdAttribute: string; - asyncWrapper(cb: (...args: any[]) => any): Promise; - eventWrapper(cb: (...args: any[]) => any): void; - asyncUtilTimeout: number; - defaultHidden: boolean; - throwSuggestions: boolean; -} - -export interface ConfigFn { - (existingConfig: Config): Partial; -} - -export function configure(configDelta: Partial | ConfigFn): void; diff --git a/types/events.d.ts b/types/events.d.ts deleted file mode 100644 index d9c50cb7..00000000 --- a/types/events.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -export type EventType = - | 'copy' - | 'cut' - | 'paste' - | 'compositionEnd' - | 'compositionStart' - | 'compositionUpdate' - | 'keyDown' - | 'keyPress' - | 'keyUp' - | 'focus' - | 'blur' - | 'focusIn' - | 'focusOut' - | 'change' - | 'input' - | 'invalid' - | 'submit' - | 'reset' - | 'click' - | 'contextMenu' - | 'dblClick' - | 'drag' - | 'dragEnd' - | 'dragEnter' - | 'dragExit' - | 'dragLeave' - | 'dragOver' - | 'dragStart' - | 'drop' - | 'mouseDown' - | 'mouseEnter' - | 'mouseLeave' - | 'mouseMove' - | 'mouseOut' - | 'mouseOver' - | 'mouseUp' - | 'popState' - | 'select' - | 'touchCancel' - | 'touchEnd' - | 'touchMove' - | 'touchStart' - | 'scroll' - | 'wheel' - | 'abort' - | 'canPlay' - | 'canPlayThrough' - | 'durationChange' - | 'emptied' - | 'encrypted' - | 'ended' - | 'loadedData' - | 'loadedMetadata' - | 'loadStart' - | 'pause' - | 'play' - | 'playing' - | 'progress' - | 'rateChange' - | 'seeked' - | 'seeking' - | 'stalled' - | 'suspend' - | 'timeUpdate' - | 'volumeChange' - | 'waiting' - | 'load' - | 'error' - | 'animationStart' - | 'animationEnd' - | 'animationIteration' - | 'transitionEnd' - | 'doubleClick' - | 'pointerOver' - | 'pointerEnter' - | 'pointerDown' - | 'pointerMove' - | 'pointerUp' - | 'pointerCancel' - | 'pointerOut' - | 'pointerLeave' - | 'gotPointerCapture' - | 'lostPointerCapture'; - -export type FireFunction = (element: Document | Element | Window | Node, event: Event) => boolean; -export type FireObject = { - [K in EventType]: (element: Document | Element | Window | Node, options?: {}) => boolean; -}; -export type CreateObject = { - [K in EventType]: (element: Document | Element | Window | Node, options?: {}) => Event; -}; - -export const createEvent: CreateObject; -export const fireEvent: FireFunction & FireObject; diff --git a/types/get-node-text.d.ts b/types/get-node-text.d.ts deleted file mode 100644 index 5c5654b5..00000000 --- a/types/get-node-text.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function getNodeText(node: HTMLElement): string; diff --git a/types/get-queries-for-element.d.ts b/types/get-queries-for-element.d.ts deleted file mode 100644 index b93adfe1..00000000 --- a/types/get-queries-for-element.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as queries from './queries'; - -export type BoundFunction = T extends ( - attribute: string, - element: HTMLElement, - text: infer P, - options: infer Q, -) => infer R - ? (text: P, options?: Q) => R - : T extends (a1: any, text: infer P, options: infer Q, waitForElementOptions: infer W) => infer R - ? (text: P, options?: Q, waitForElementOptions?: W) => R - : T extends (a1: any, text: infer P, options: infer Q) => infer R - ? (text: P, options?: Q) => R - : never; -export type BoundFunctions = { [P in keyof T]: BoundFunction }; - -export type Query = ( - container: HTMLElement, - ...args: any[] -) => Error | Promise | Promise | HTMLElement[] | HTMLElement | null; - -export interface Queries { - [T: string]: Query; -} - -export function getQueriesForElement( - element: HTMLElement, - queriesToBind?: T, -): BoundFunctions; diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 5b199dcf..00000000 --- a/types/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -// TypeScript Version: 3.8 - -import { getQueriesForElement } from './get-queries-for-element'; -import * as queries from './queries'; -import * as queryHelpers from './query-helpers'; - -declare const within: typeof getQueriesForElement; -export { queries, queryHelpers, within }; - -export * from './queries'; -export * from './query-helpers'; -export * from './screen'; -export * from './wait'; -export * from './wait-for'; -export * from './wait-for-dom-change'; -export * from './wait-for-element'; -export * from './wait-for-element-to-be-removed'; -export * from './matches'; -export * from './get-node-text'; -export * from './events'; -export * from './get-queries-for-element'; -export * from './pretty-dom'; -export * from './role-helpers'; -export * from './config'; -export * from './suggestions'; diff --git a/types/matches.d.ts b/types/matches.d.ts deleted file mode 100644 index 6454c86a..00000000 --- a/types/matches.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type MatcherFunction = (content: string, element: HTMLElement) => boolean -export type Matcher = string | RegExp | MatcherFunction - -export type NormalizerFn = (text: string) => string - -export interface MatcherOptions { - exact?: boolean - /** Use normalizer with getDefaultNormalizer instead */ - trim?: boolean - /** Use normalizer with getDefaultNormalizer instead */ - collapseWhitespace?: boolean - normalizer?: NormalizerFn - /** suppress suggestions for a specific query */ - suggest?: boolean -} - -export type Match = ( - textToMatch: string, - node: HTMLElement | null, - matcher: Matcher, - options?: MatcherOptions, -) => boolean - -export interface DefaultNormalizerOptions { - trim?: boolean - collapseWhitespace?: boolean -} - -export function getDefaultNormalizer( - options?: DefaultNormalizerOptions, -): NormalizerFn - -// N.B. Don't expose fuzzyMatches + matches here: they're not public API diff --git a/types/pretty-dom.d.ts b/types/pretty-dom.d.ts deleted file mode 100644 index bca6afb4..00000000 --- a/types/pretty-dom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { OptionsReceived } from 'pretty-format'; - -export function prettyDOM(dom?: Element | HTMLDocument, maxLength?: number, options?: OptionsReceived): string | false; -export function logDOM(dom?: Element | HTMLDocument, maxLength?: number, options?: OptionsReceived): void; diff --git a/types/queries.d.ts b/types/queries.d.ts deleted file mode 100644 index 92c1b946..00000000 --- a/types/queries.d.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Matcher, MatcherOptions } from './matches'; -import { SelectorMatcherOptions } from './query-helpers'; -import { waitForOptions } from './wait-for'; - -export type QueryByBoundAttribute = ( - container: HTMLElement, - id: Matcher, - options?: MatcherOptions, -) => HTMLElement | null; - -export type AllByBoundAttribute = (container: HTMLElement, id: Matcher, options?: MatcherOptions) => HTMLElement[]; - -export type FindAllByBoundAttribute = ( - container: HTMLElement, - id: Matcher, - options?: MatcherOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export type GetByBoundAttribute = (container: HTMLElement, id: Matcher, options?: MatcherOptions) => HTMLElement; - -export type FindByBoundAttribute = ( - container: HTMLElement, - id: Matcher, - options?: MatcherOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export type QueryByText = (container: HTMLElement, id: Matcher, options?: SelectorMatcherOptions) => HTMLElement | null; - -export type AllByText = (container: HTMLElement, id: Matcher, options?: SelectorMatcherOptions) => HTMLElement[]; - -export type FindAllByText = ( - container: HTMLElement, - id: Matcher, - options?: SelectorMatcherOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export type GetByText = (container: HTMLElement, id: Matcher, options?: SelectorMatcherOptions) => HTMLElement; - -export type FindByText = ( - container: HTMLElement, - id: Matcher, - options?: SelectorMatcherOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export interface ByRoleOptions extends MatcherOptions { - /** - * If true includes elements in the query set that are usually excluded from - * the accessibility tree. `role="none"` or `role="presentation"` are included - * in either case. - */ - hidden?: boolean; - /** - * If true only includes elements in the query set that are marked as - * selected in the accessibility tree, i.e., `aria-selected="true"` - */ - selected?: boolean; - /** - * Includes every role used in the `role` attribute - * For example *ByRole('progressbar', {queryFallbacks: true})` will find
    `. - */ - queryFallbacks?: boolean; - /** - * Only considers elements with the specified accessible name. - */ - name?: string | RegExp | ((accessibleName: string, element: Element) => boolean); -} - -export type AllByRole = (container: HTMLElement, role: Matcher, options?: ByRoleOptions) => HTMLElement[]; - -export type GetByRole = (container: HTMLElement, role: Matcher, options?: ByRoleOptions) => HTMLElement; - -export type QueryByRole = (container: HTMLElement, role: Matcher, options?: ByRoleOptions) => HTMLElement | null; - -export type FindByRole = ( - container: HTMLElement, - role: Matcher, - options?: ByRoleOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export type FindAllByRole = ( - container: HTMLElement, - role: Matcher, - options?: ByRoleOptions, - waitForElementOptions?: waitForOptions, -) => Promise; - -export const getByLabelText: GetByText; -export const getAllByLabelText: AllByText; -export const queryByLabelText: QueryByText; -export const queryAllByLabelText: AllByText; -export const findByLabelText: FindByText; -export const findAllByLabelText: FindAllByText; -export const getByPlaceholderText: GetByBoundAttribute; -export const getAllByPlaceholderText: AllByBoundAttribute; -export const queryByPlaceholderText: QueryByBoundAttribute; -export const queryAllByPlaceholderText: AllByBoundAttribute; -export const findByPlaceholderText: FindByBoundAttribute; -export const findAllByPlaceholderText: FindAllByBoundAttribute; -export const getByText: GetByText; -export const getAllByText: AllByText; -export const queryByText: QueryByText; -export const queryAllByText: AllByText; -export const findByText: FindByText; -export const findAllByText: FindAllByText; -export const getByAltText: GetByBoundAttribute; -export const getAllByAltText: AllByBoundAttribute; -export const queryByAltText: QueryByBoundAttribute; -export const queryAllByAltText: AllByBoundAttribute; -export const findByAltText: FindByBoundAttribute; -export const findAllByAltText: FindAllByBoundAttribute; -export const getByTitle: GetByBoundAttribute; -export const getAllByTitle: AllByBoundAttribute; -export const queryByTitle: QueryByBoundAttribute; -export const queryAllByTitle: AllByBoundAttribute; -export const findByTitle: FindByBoundAttribute; -export const findAllByTitle: FindAllByBoundAttribute; -export const getByDisplayValue: GetByBoundAttribute; -export const getAllByDisplayValue: AllByBoundAttribute; -export const queryByDisplayValue: QueryByBoundAttribute; -export const queryAllByDisplayValue: AllByBoundAttribute; -export const findByDisplayValue: FindByBoundAttribute; -export const findAllByDisplayValue: FindAllByBoundAttribute; -export const getByRole: GetByRole; -export const getAllByRole: AllByRole; -export const queryByRole: QueryByRole; -export const queryAllByRole: AllByRole; -export const findByRole: FindByRole; -export const findAllByRole: FindAllByRole; -export const getByTestId: GetByBoundAttribute; -export const getAllByTestId: AllByBoundAttribute; -export const queryByTestId: QueryByBoundAttribute; -export const queryAllByTestId: AllByBoundAttribute; -export const findByTestId: FindByBoundAttribute; -export const findAllByTestId: FindAllByBoundAttribute; diff --git a/types/query-helpers.d.ts b/types/query-helpers.d.ts deleted file mode 100644 index de50a2d3..00000000 --- a/types/query-helpers.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Matcher, MatcherOptions } from './matches' -import { waitForOptions } from './wait-for' - -export interface SelectorMatcherOptions extends MatcherOptions { - selector?: string -} - -export type QueryByAttribute = ( - attribute: string, - container: HTMLElement, - id: Matcher, - options?: MatcherOptions, -) => HTMLElement | null - -export type AllByAttribute = ( - attribute: string, - container: HTMLElement, - id: Matcher, - options?: MatcherOptions, -) => HTMLElement[] - -export const queryByAttribute: QueryByAttribute -export const queryAllByAttribute: AllByAttribute -export function getElementError(message: string, container: HTMLElement): Error - -/** - * query methods have a common call signature. Only the return type differs. - */ -export type QueryMethod = ( - container: HTMLElement, - ...args: Arguments -) => Return -export type QueryBy = QueryMethod< - Arguments, - HTMLElement | null -> -export type GetAllBy = QueryMethod< - Arguments, - HTMLElement[] -> -export type FindAllBy = QueryMethod< - [Arguments[0], Arguments[1], waitForOptions], - Promise -> -export type GetBy = QueryMethod -export type FindBy = QueryMethod< - [Arguments[0], Arguments[1], waitForOptions], - Promise -> - -export type BuiltQueryMethods = [ - QueryBy, - GetAllBy, - GetBy, - FindAllBy, - FindBy, -] -export function buildQueries( - queryByAll: GetAllBy, - getMultipleError: (container: HTMLElement, ...args: Arguments) => string, - getMissingError: (container: HTMLElement, ...args: Arguments) => string, -): BuiltQueryMethods diff --git a/types/role-helpers.d.ts b/types/role-helpers.d.ts deleted file mode 100644 index 3dd35b78..00000000 --- a/types/role-helpers.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function logRoles(container: HTMLElement): string; -export function getRoles(container: HTMLElement): { [index: string]: HTMLElement[] }; -/** - * https://testing-library.com/docs/dom-testing-library/api-helpers#isinaccessible - */ -export function isInaccessible(element: Element): boolean; diff --git a/types/screen.d.ts b/types/screen.d.ts deleted file mode 100644 index 2594f5be..00000000 --- a/types/screen.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BoundFunctions, Queries } from './get-queries-for-element'; -import * as queries from './queries'; -import { OptionsReceived } from 'pretty-format'; - -export type Screen = BoundFunctions & { - /** - * Convenience function for `pretty-dom` which also allows an array - * of elements - */ - debug: ( - element?: Element | HTMLDocument | Array, - maxLength?: number, - options?: OptionsReceived, - ) => void; -}; - -export const screen: Screen; diff --git a/types/suggestions.d.ts b/types/suggestions.d.ts deleted file mode 100644 index f574f344..00000000 --- a/types/suggestions.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Suggestion { - queryName: string - toString(): string -} - -export function getSuggestedQuery(element: HTMLElement): Suggestion | undefined diff --git a/types/tslint.json b/types/tslint.json index 5d45232f..a439c551 100644 --- a/types/tslint.json +++ b/types/tslint.json @@ -1,8 +1,14 @@ { "extends": ["dtslint/dtslint.json"], "rules": { + "one-variable-per-declaration": false, + "max-line-length": false, + "no-redundant-jsdoc": false, "no-useless-files": false, "no-relative-import-in-test": false, - "semicolon": false + "prefer-declare-function": false, + "semicolon": false, + "strict-export-declare-modifiers": false, + "whitespace": false } } diff --git a/types/wait-for-dom-change.d.ts b/types/wait-for-dom-change.d.ts deleted file mode 100644 index 813437f4..00000000 --- a/types/wait-for-dom-change.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { waitForOptions } from "./wait-for"; - -export function waitForDomChange(options?: waitForOptions): Promise; diff --git a/types/wait-for-element-to-be-removed.d.ts b/types/wait-for-element-to-be-removed.d.ts deleted file mode 100644 index b6b1e1bb..00000000 --- a/types/wait-for-element-to-be-removed.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { waitForOptions } from "./wait-for"; - -export function waitForElementToBeRemoved( - callback: (() => T) | T, - options?: waitForOptions, -): Promise; diff --git a/types/wait-for-element.d.ts b/types/wait-for-element.d.ts deleted file mode 100644 index 4fe1de0e..00000000 --- a/types/wait-for-element.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { waitForOptions } from "./wait-for"; - -export function waitForElement(callback: () => T, options?: waitForOptions): Promise; diff --git a/types/wait-for.d.ts b/types/wait-for.d.ts deleted file mode 100644 index 0fd0e56a..00000000 --- a/types/wait-for.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface waitForOptions { - container?: HTMLElement - timeout?: number - interval?: number - mutationObserverOptions?: MutationObserverInit -} - -export function waitFor( - callback: () => T extends Promise ? never : T, - options?: waitForOptions, -): Promise diff --git a/types/wait.d.ts b/types/wait.d.ts deleted file mode 100644 index 3763e7bd..00000000 --- a/types/wait.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function wait( - callback?: () => void, - options?: { - timeout?: number; - interval?: number; - }, -): Promise;