diff --git a/addon-test-support/@ember/test-helpers/-filter-keys-by-type.ts b/addon-test-support/@ember/test-helpers/-filter-keys-by-type.ts new file mode 100644 index 000000000..6f7025f55 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-filter-keys-by-type.ts @@ -0,0 +1,52 @@ +/** + * Given an object that looks like: + * ```ts + * interface Person { + * firstName: string; + * lastName: string; + * age: number; + * admin: boolean; + * } + * ``` + * + * To filter for all the attributes with a given type, + * ```ts + * type KeysOfPersonThatAreStrings = FilterKeysByType; // => 'firstName' | 'lastName' + * ``` + * + * Implementation: + * + * ```ts + * type KeysOfPerson = keyof Person; // => 'firstName' | 'lastName' | 'age' | 'admin'; + * + * type ValuesOfPerson = Person[KeysOfPerson]; // == Person['firstName' | 'lastName' | 'age'] + * // == Person['firstName'] | Person['lastName'] | Person['age'] | Person['admin'] + * // == string | string | number | boolean + * // => string | number | boolean + * + * // Note that TS collapses unions to a minimum, removing any redudant types, e.g. + * type A = string | 'foo'; // => string (because 'foo' is already covered by string) + * type B = number | 1; // => number (likewise) + * type C = string | 'foo' | 'bar' | number | 1 | 2; // => string | number + * type D = string | number | never; // => string | number (`never` in unions are like `cond || false` in JS, it's a no-op) + * + * // Putting all of this together, we first convert `Person` to an intermediate form like this: + * + * interface IntermediatePersonThingy { + * firstName: 'firstName'; + * lastName: 'lastName'; + * age: never; + * admin: never; + * } + * + * // For all the fields that have the type we are looking for, we replaced their types with the key. + * // Otherwise, we replace their types with `never`. + * // Knowing that we can get a union of all the values of this with `IntermediatePersonThingy[keyof Person]`, + * // AND that TS collapses away any `never`s in an union, we can get our final result with: + * + * type KeysOfPersonThatAreStrings = IntermediatePersonThingy[keyof Person]; + * ``` + */ +export type FilterKeysByType = { + [K in keyof T]: T[K] extends V ? K : never; +}[keyof T]; diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.ts b/addon-test-support/@ember/test-helpers/dom/fire-event.ts index 1fca4495c..ad013b06f 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.ts @@ -1,4 +1,5 @@ -import { isDocument, isElement } from './-target'; +import { FilterKeysByType } from '../-filter-keys-by-type'; +import { isDocument, isElement, isWindow } from './-target'; import tuple from '../-tuple'; import Target from './-target'; import { log } from '@ember/test-helpers/dom/-logging'; @@ -24,9 +25,12 @@ export type KeyboardEventType = typeof KEYBOARD_EVENT_TYPES[number]; // eslint-disable-next-line require-jsdoc export function isKeyboardEventType( - eventType: any + eventType: unknown ): eventType is KeyboardEventType { - return KEYBOARD_EVENT_TYPES.indexOf(eventType) > -1; + return ( + typeof eventType === 'string' && + KEYBOARD_EVENT_TYPES.includes(eventType as KeyboardEventType) + ); } const MOUSE_EVENT_TYPES = tuple( @@ -43,8 +47,13 @@ const MOUSE_EVENT_TYPES = tuple( export type MouseEventType = typeof MOUSE_EVENT_TYPES[number]; // eslint-disable-next-line require-jsdoc -export function isMouseEventType(eventType: any): eventType is MouseEventType { - return MOUSE_EVENT_TYPES.indexOf(eventType) > -1; +export function isMouseEventType( + eventType: unknown +): eventType is MouseEventType { + return ( + typeof eventType === 'string' && + MOUSE_EVENT_TYPES.includes(eventType as MouseEventType) + ); } const FILE_SELECTION_EVENT_TYPES = tuple('change'); @@ -52,34 +61,214 @@ export type FileSelectionEventType = typeof FILE_SELECTION_EVENT_TYPES[number]; // eslint-disable-next-line require-jsdoc export function isFileSelectionEventType( - eventType: any + eventType: unknown ): eventType is FileSelectionEventType { - return FILE_SELECTION_EVENT_TYPES.indexOf(eventType) > -1; + return ( + typeof eventType === 'string' && + FILE_SELECTION_EVENT_TYPES.includes(eventType as FileSelectionEventType) + ); +} + +export interface HTMLFileInputElement extends HTMLInputElement { + files: FileList; } // eslint-disable-next-line require-jsdoc export function isFileSelectionInput( - element: any -): element is HTMLInputElement { - return element.files; + element: unknown +): element is HTMLFileInputElement { + return element instanceof HTMLInputElement && element.files !== null; } +export interface FileSelectionEventOptions extends EventInit { + files?: File[] | null | undefined; +} + +// Global Event overloads function fireEvent( element: Element | Document | Window, - eventType: KeyboardEventType, - options?: any + eventType: FilterKeysByType, + options?: EventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: AnimationEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: FocusEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: CompositionEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: DragEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: ErrorEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: FocusEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: FormDataEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: InputEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: KeyboardEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: MouseEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: PointerEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: ProgressEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType< + GlobalEventHandlersEventMap, + SecurityPolicyViolationEvent + >, + options?: SecurityPolicyViolationEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: SubmitEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: TouchEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: TransitionEventInit +): Promise; +function fireEvent( + element: Element | Document | Window, + eventType: FilterKeysByType, + options?: UIEventInit ): Promise; - function fireEvent( element: Element | Document | Window, - eventType: MouseEventType, - options?: any -): Promise; + eventType: FilterKeysByType, + options?: WheelEventInit +): Promise; +// Window Event overloads +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: EventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: GamepadEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: HashChangeEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: MessageEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: PageTransitionEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: PopStateEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType< + WindowEventHandlersEventMap, + PromiseRejectionEvent + >, + options?: PromiseRejectionEventInit +): Promise; +function fireEvent( + element: Window, + eventType: FilterKeysByType, + options?: StorageEventInit +): Promise; + +// Document or Element overloads +function fireEvent( + element: Document | Element, + eventType: FilterKeysByType< + DocumentAndElementEventHandlersEventMap, + ClipboardEvent + >, + options?: ClipboardEventInit +): Promise; + +// Document event overloads +function fireEvent( + element: Document, + eventType: FilterKeysByType, + options?: EventInit +): Promise; + +// Element event overloads +function fireEvent( + element: Element, + eventType: FilterKeysByType, + options?: EventInit +): Promise; + +// Special casing for File Input change events +function fireEvent( + element: HTMLFileInputElement, + eventType: FileSelectionEventType, + options?: FileSelectionEventOptions +): Promise; + +// Custom event overloads function fireEvent( element: Element | Document | Window, eventType: string, - options?: any + options?: CustomEventInit ): Promise; /** @@ -94,8 +283,8 @@ function fireEvent( function fireEvent( element: Element | Document | Window, eventType: string, - options = {} -): Promise { + options: EventInit = {} +): Promise { return Promise.resolve() .then(() => runHooks('fireEvent', 'start', element)) .then(() => runHooks(`fireEvent:${eventType}`, 'start', element)) @@ -104,24 +293,24 @@ function fireEvent( throw new Error('Must pass an element to `fireEvent`'); } - let event; + let event: Event; if (isKeyboardEventType(eventType)) { event = _buildKeyboardEvent(eventType, options); } else if (isMouseEventType(eventType)) { let rect; - if (element instanceof Window && element.document.documentElement) { + if (isWindow(element) && element.document.documentElement) { rect = element.document.documentElement.getBoundingClientRect(); } else if (isDocument(element)) { - rect = element.documentElement!.getBoundingClientRect(); + rect = element.documentElement.getBoundingClientRect(); } else if (isElement(element)) { rect = element.getBoundingClientRect(); } else { - return; + throw new Error('Could not determine coordinates for MouseEventInit'); } let x = rect.left + 1; let y = rect.top + 1; - let simulatedCoordinates = { + let simulatedCoordinates: MouseEventInit = { screenX: x + 5, // Those numbers don't really mean anything. screenY: y + 95, // They're just to make the screenX/Y be different of clientX/Y.. clientX: x, @@ -151,28 +340,40 @@ function fireEvent( export default fireEvent; // eslint-disable-next-line require-jsdoc -function buildBasicEvent(type: string, options: any = {}): Event { - let event = document.createEvent('Events'); +function buildBasicEvent(type: string, _options: EventInit = {}): Event { + let options: EventInit = { + ...DEFAULT_EVENT_OPTIONS, + ..._options, + }; + + try { + return new Event(type, options); + } catch { + let event = document.createEvent('Events'); - let bubbles = options.bubbles !== undefined ? options.bubbles : true; - let cancelable = options.cancelable !== undefined ? options.cancelable : true; + let { bubbles, cancelable } = options; - delete options.bubbles; - delete options.cancelable; + delete options.bubbles; + delete options.cancelable; - // bubbles and cancelable are readonly, so they can be - // set when initializing event - event.initEvent(type, bubbles, cancelable); - for (let prop in options) { - (event as any)[prop] = options[prop]; + // bubbles and cancelable are readonly, so they can be + // set when initializing event + event.initEvent(type, bubbles, cancelable); + for (let prop in options) { + (event as any)[prop] = (options as Record)[prop]; + } + return event; } - return event; } // eslint-disable-next-line require-jsdoc -function buildMouseEvent(type: MouseEventType, options: any = {}) { +function buildMouseEvent(type: MouseEventType, options: MouseEventInit = {}) { let event; - let eventOpts: any = { view: window, ...DEFAULT_EVENT_OPTIONS, ...options }; + let eventOpts = { + view: window, + ...DEFAULT_EVENT_OPTIONS, + ...options, + }; if (MOUSE_EVENT_CONSTRUCTOR) { event = new MouseEvent(type, eventOpts); } else { @@ -182,18 +383,18 @@ function buildMouseEvent(type: MouseEventType, options: any = {}) { type, eventOpts.bubbles, eventOpts.cancelable, - window, - eventOpts.detail, - eventOpts.screenX, - eventOpts.screenY, - eventOpts.clientX, - eventOpts.clientY, - eventOpts.ctrlKey, - eventOpts.altKey, - eventOpts.shiftKey, - eventOpts.metaKey, - eventOpts.button, - eventOpts.relatedTarget + eventOpts.view ?? window, + eventOpts.detail ?? 0, + eventOpts.screenX ?? 0, + eventOpts.screenY ?? 0, + eventOpts.clientX ?? 0, + eventOpts.clientY ?? 0, + eventOpts.ctrlKey ?? false, + eventOpts.altKey ?? false, + eventOpts.shiftKey ?? false, + eventOpts.metaKey ?? false, + eventOpts.button ?? 0, + eventOpts.relatedTarget ?? null ); } catch (e) { event = buildBasicEvent(type, options); @@ -207,32 +408,26 @@ function buildMouseEvent(type: MouseEventType, options: any = {}) { // eslint-disable-next-line require-jsdoc export function _buildKeyboardEvent( type: KeyboardEventType, - options: any = {} + options: KeyboardEventInit = {} ) { - let eventOpts: any = { ...DEFAULT_EVENT_OPTIONS, ...options }; - let event: Event | undefined; - let eventMethodName: 'initKeyboardEvent' | 'initKeyEvent' | undefined; + let eventOpts: KeyboardEventInit = { + ...DEFAULT_EVENT_OPTIONS, + ...options, + }; + let event: KeyboardEvent | Event | undefined; try { event = new KeyboardEvent(type, eventOpts); - // Property definitions are required for B/C for keyboard event usage - // If this properties are not defined, when listening for key events - // keyCode/which will be 0. Also, keyCode and which now are string - // and if app compare it with === with integer key definitions, - // there will be a fail. - // - // https://w3c.github.io/uievents/#interface-keyboardevent - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent Object.defineProperty(event, 'keyCode', { get() { - return parseInt(eventOpts.keyCode); + return extractKeyInfo('keyCode', eventOpts); }, }); Object.defineProperty(event, 'which', { get() { - return parseInt(eventOpts.which); + return extractKeyInfo('which', eventOpts); }, }); @@ -243,7 +438,19 @@ export function _buildKeyboardEvent( try { event = document.createEvent('KeyboardEvents'); - eventMethodName = 'initKeyboardEvent'; + (event as KeyboardEvent).initKeyboardEvent( + type, + eventOpts.bubbles, + eventOpts.cancelable, + window, + eventOpts.key, + eventOpts.location, + eventOpts.ctrlKey, + eventOpts.altKey, + eventOpts.shiftKey, + eventOpts.metaKey + ); + return event; } catch (e) { // left intentionally blank } @@ -251,37 +458,65 @@ export function _buildKeyboardEvent( if (!event) { try { event = document.createEvent('KeyEvents'); - eventMethodName = 'initKeyEvent'; + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyEvent + // @ts-expect-error This method is so long deprecated that TS doesn't know about it + (event as KeyboardEvent).initKeyEvent( + type, + eventOpts.bubbles, + eventOpts.cancelable, + window, + eventOpts.ctrlKey, + eventOpts.altKey, + eventOpts.shiftKey, + eventOpts.metaKey, + eventOpts.keyCode + ); + return event; } catch (e) { // left intentionally blank } } - if (event && eventMethodName) { - (event as any)[eventMethodName]( - type, - eventOpts.bubbles, - eventOpts.cancelable, - window, - eventOpts.ctrlKey, - eventOpts.altKey, - eventOpts.shiftKey, - eventOpts.metaKey, - eventOpts.keyCode, - eventOpts.charCode - ); - } else { + if (!event) { event = buildBasicEvent(type, options); } return event; } +// Property definitions are required for B/C for keyboard event usage +// If this properties are not defined, when listening for key events +// keyCode/which will be 0. Also, keyCode and which now are string +// and if app compare it with === with integer key definitions, +// there will be a fail. +// +// https://w3c.github.io/uievents/#interface-keyboardevent +// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent +// eslint-disable-next-line require-jsdoc +function extractKeyInfo(prop: 'keyCode' | 'which', options: KeyboardEventInit) { + let value = options[prop]; + if (typeof value === 'number') { + return value; + } else if (value === null || value === undefined) { + return undefined; + } else if (typeof value === 'string') { + let int = parseInt(value); + if (Number.isNaN(int)) { + throw new Error(`event.${prop} parsed to NaN`); + } + return int; + } else { + throw new Error( + `event.${prop} type not supported, value was ${options[prop]}` + ); + } +} + // eslint-disable-next-line require-jsdoc function buildFileEvent( type: FileSelectionEventType, - element: HTMLInputElement, - options: any = {} + element: HTMLFileInputElement, + options: FileSelectionEventOptions = {} ): Event { let event = buildBasicEvent(type); let files = options.files; @@ -304,7 +539,7 @@ function buildFileEvent( configurable: true, }); - let elementProto = Object.getPrototypeOf(element); + let elementProto: unknown = Object.getPrototypeOf(element); let valueProp = Object.getOwnPropertyDescriptor(elementProto, 'value'); Object.defineProperty(element, 'value', { configurable: true, diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.ts b/addon-test-support/@ember/test-helpers/dom/trigger-event.ts index c19bb6ff8..9d659e23d 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.ts @@ -1,20 +1,212 @@ import { getWindowOrElement } from './-get-window-or-element'; -import fireEvent from './fire-event'; +import fireEvent, { + FileSelectionEventOptions, + FileSelectionEventType, + HTMLFileInputElement, +} from './fire-event'; import settled from '../settled'; import Target from './-target'; import { log } from '@ember/test-helpers/dom/-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../-internal/helper-hooks'; +import { FilterKeysByType } from '@ember/test-helpers/-filter-keys-by-type'; registerHook('triggerEvent', 'start', (target: Target, eventType: string) => { log('triggerEvent', target, eventType); }); +// Global Event overloads +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: EventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: AnimationEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: FocusEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: CompositionEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: DragEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: ErrorEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: FocusEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: FormDataEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: InputEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: KeyboardEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: MouseEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: PointerEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: ProgressEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType< + GlobalEventHandlersEventMap, + SecurityPolicyViolationEvent + >, + options?: SecurityPolicyViolationEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: SubmitEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: TouchEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: TransitionEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: UIEventInit +): Promise; +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: FilterKeysByType, + options?: WheelEventInit +): Promise; + +// Window Event overloads +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: EventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: GamepadEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: HashChangeEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: MessageEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: PageTransitionEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: PopStateEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType< + WindowEventHandlersEventMap, + PromiseRejectionEvent + >, + options?: PromiseRejectionEventInit +): Promise; +export default function triggerEvent( + target: string | Window, + eventType: FilterKeysByType, + options?: StorageEventInit +): Promise; + +// Document or Element overloads +export default function triggerEvent( + target: string | Element | Document, + eventType: FilterKeysByType< + DocumentAndElementEventHandlersEventMap, + ClipboardEvent + >, + options?: ClipboardEventInit +): Promise; + +// Document event overloads +export default function triggerEvent( + target: string | Document, + eventType: FilterKeysByType, + options?: EventInit +): Promise; + +// Element event overloads +export default function triggerEvent( + target: string | Element, + eventType: FilterKeysByType, + options?: EventInit +): Promise; + +// Special casing for File Input change events +export default function triggerEvent( + target: string | HTMLFileInputElement, + eventType: FileSelectionEventType, + options?: FileSelectionEventOptions +): Promise; + +// Custom event overloads +export default function triggerEvent( + target: string | Element | Document | Window, + eventType: string, + options?: CustomEventInit +): Promise; + /** * Triggers an event on the specified target. * * @public - * @param {string|Element} target the element or selector to trigger the event on + * @param {Target} target the element or selector to trigger the event on * @param {string} eventType the type of event to trigger * @param {Object} options additional properties to be set on the event * @return {Promise} resolves when the application is settled @@ -56,7 +248,7 @@ registerHook('triggerEvent', 'start', (target: Target, eventType: string) => { export default function triggerEvent( target: Target, eventType: string, - options?: Record + options?: EventInit ): Promise { return Promise.resolve() .then(() => { diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts index 0da4b9cf3..bdbe68bb2 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts @@ -133,7 +133,7 @@ const keyFromKeyCodeWithShift: { [key: number]: string } = { function keyFromKeyCodeAndModifiers( keycode: number, modifiers: KeyModifiers -): string | void { +): string | undefined { if (keycode > 64 && keycode < 91) { if (modifiers.shiftKey) { return String.fromCharCode(keycode); @@ -166,20 +166,20 @@ function keyCodeFromKey(key: string) { /** @private - @param {Element | Document} element the element to trigger the key event on + @param {Element | Document | Window} element the element to trigger the key event on @param {'keydown' | 'keyup' | 'keypress'} eventType the type of event to trigger @param {number|string} key the `keyCode`(number) or `key`(string) of the event being triggered @param {Object} [modifiers] the state of various modifier keys @return {Promise} resolves when settled */ export function __triggerKeyEvent__( - element: Element | Document, - eventType: KeyboardEventType, + element: Element | Document | Window, + eventType: KeyboardEventType, // NOTE: This is not exhaustive of all KeyboardEventTypes key: number | string, modifiers: KeyModifiers = DEFAULT_MODIFIERS ): Promise { return Promise.resolve().then(() => { - let props; + let props: KeyboardEventInit; if (typeof key === 'number') { props = { keyCode: key, @@ -220,7 +220,7 @@ export function __triggerKeyEvent__( Optionally the user can also provide a POJO with extra modifiers for the event. @public - @param {string|Element} target the element or selector to trigger the event on + @param {Target} target the element or selector to trigger the event on @param {'keydown' | 'keyup' | 'keypress'} eventType the type of event to trigger @param {number|string} key the `keyCode`(number) or `key`(string) of the event being triggered @param {Object} [modifiers] the state of various modifier keys diff --git a/type-tests/api.ts b/type-tests/api.ts index 4e3e0a0bb..0ec6a210a 100644 --- a/type-tests/api.ts +++ b/type-tests/api.ts @@ -74,6 +74,7 @@ import { DebugInfo as BackburnerDebugInfo } from '@ember/runloop/-private/backbu import type { Resolver as EmberResolver } from '@ember/owner'; import Application from '@ember/application'; import { TemplateFactory } from 'ember-cli-htmlbars'; +import { Input } from '@ember/component'; // DOM Interaction Helpers expectTypeOf(blur).toEqualTypeOf<(target?: Target) => Promise>(); @@ -109,13 +110,22 @@ expectTypeOf(tab).toEqualTypeOf< expectTypeOf(tap).toEqualTypeOf< (target: Target, options?: TouchEventInit) => Promise >(); + expectTypeOf(triggerEvent).toEqualTypeOf< - ( - target: Target, - eventType: string, - options?: Record - ) => Promise + (target: Target, eventType: string, options?: EventInit) => Promise >(); +// `toBeCallableWith` doesn't seem to be working properly. +triggerEvent('#my-element', 'mousedown', { clientX: 1 }); +// @ts-expect-error 'clientX' does not exist in type 'CustomEventInit' +triggerEvent('#my-element', 'focus', { clientX: 1 }); +triggerEvent('#my-element', 'keydown', { + key: 'ArrowDown', +}); +// Uses custom FileSelectionEventOptions +triggerEvent('#my-element', 'change', { + files: [], +}); + expectTypeOf(triggerKeyEvent).toEqualTypeOf< ( target: Target,