From a3956351438603b0404bc391b639b02355316ba6 Mon Sep 17 00:00:00 2001 From: Shane Martin Date: Mon, 4 Apr 2022 16:12:11 -0400 Subject: [PATCH] feat: add `start` and `end` hooks to the `fireEvent` helper (#1185) Co-authored-by: Shane Martin --- .../@ember/test-helpers/dom/blur.ts | 18 +-- .../@ember/test-helpers/dom/click.ts | 24 ++-- .../@ember/test-helpers/dom/double-click.ts | 32 +++--- .../@ember/test-helpers/dom/fill-in.ts | 32 +++--- .../@ember/test-helpers/dom/fire-event.ts | 106 ++++++++++-------- .../@ember/test-helpers/dom/focus.ts | 106 +++++++++++------- .../@ember/test-helpers/dom/scroll-to.ts | 4 +- .../@ember/test-helpers/dom/select.ts | 12 +- .../@ember/test-helpers/dom/tab.ts | 23 ++-- .../@ember/test-helpers/dom/tap.ts | 24 ++-- .../@ember/test-helpers/dom/trigger-event.ts | 4 +- .../test-helpers/dom/trigger-key-event.ts | 67 +++++------ .../@ember/test-helpers/dom/type-in.ts | 11 +- tests/helpers/register-hooks.js | 102 +++++++++++++++++ tests/integration/dom/scroll-to-test.js | 28 ++--- tests/unit/dom/click-test.js | 28 ++--- tests/unit/dom/double-click-test.js | 33 ++++-- tests/unit/dom/fill-in-test.js | 28 ++--- tests/unit/dom/select-test.js | 28 ++--- tests/unit/dom/tap-test.js | 34 +++--- tests/unit/dom/type-in-test.js | 42 ++++--- 21 files changed, 472 insertions(+), 314 deletions(-) create mode 100644 tests/helpers/register-hooks.js diff --git a/addon-test-support/@ember/test-helpers/dom/blur.ts b/addon-test-support/@ember/test-helpers/dom/blur.ts index c85ae4dfd..f75aaac2e 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.ts +++ b/addon-test-support/@ember/test-helpers/dom/blur.ts @@ -15,11 +15,12 @@ registerHook('blur', 'start', (target: Target) => { @private @param {Element} element the element to trigger events on @param {Element} relatedTarget the element that is focused after blur + @return {Promise} resolves when settled */ export function __blur__( element: HTMLElement | Element | Document | SVGElement, relatedTarget: HTMLElement | Element | Document | SVGElement | null = null -): void { +): Promise { if (!isFocusable(element)) { throw new Error(`${element} is not focusable`); } @@ -36,11 +37,12 @@ export function __blur__( // Chrome/Firefox does not trigger the `blur` event if the window // does not have focus. If the document does not have focus then // fire `blur` event via native event. - if (browserIsNotFocused || needsCustomEventOptions) { - let options = { relatedTarget }; - fireEvent(element, 'blur', { bubbles: false, ...options }); - fireEvent(element, 'focusout', options); - } + let options = { relatedTarget }; + return browserIsNotFocused || needsCustomEventOptions + ? Promise.resolve() + .then(() => fireEvent(element, 'blur', { bubbles: false, ...options })) + .then(() => fireEvent(element, 'focusout', options)) + : Promise.resolve(); } /** @@ -81,9 +83,7 @@ export default function blur( ); } - __blur__(element); - - return settled(); + return __blur__(element).then(() => settled()); }) .then(() => runHooks('blur', 'end', target)); } diff --git a/addon-test-support/@ember/test-helpers/dom/click.ts b/addon-test-support/@ember/test-helpers/dom/click.ts index ba506e155..2fe64623e 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.ts +++ b/addon-test-support/@ember/test-helpers/dom/click.ts @@ -28,19 +28,21 @@ export const DEFAULT_CLICK_OPTIONS = { @private @param {Element} element the element to click on @param {MouseEventInit} options the options to be merged into the mouse events + @return {Promise} resolves when settled */ export function __click__( element: Element | Document | Window, options: MouseEventInit -): void { - let mouseDownEvent = fireEvent(element, 'mousedown', options); - - if (!isWindow(element) && !mouseDownEvent?.defaultPrevented) { - __focus__(element); - } - - fireEvent(element, 'mouseup', options); - fireEvent(element, 'click', options); +): Promise { + return Promise.resolve() + .then(() => fireEvent(element, 'mousedown', options)) + .then((mouseDownEvent) => + !isWindow(element) && !mouseDownEvent?.defaultPrevented + ? __focus__(element) + : Promise.resolve() + ) + .then(() => fireEvent(element, 'mouseup', options)) + .then(() => fireEvent(element, 'click', options)); } /** @@ -112,9 +114,7 @@ export default function click( throw new Error(`Can not \`click\` disabled ${element}`); } - __click__(element, options); - - return settled(); + return __click__(element, options).then(settled); }) .then(() => runHooks('click', 'end', target, _options)); } diff --git a/addon-test-support/@ember/test-helpers/dom/double-click.ts b/addon-test-support/@ember/test-helpers/dom/double-click.ts index 4a74bbefd..c0f044086 100644 --- a/addon-test-support/@ember/test-helpers/dom/double-click.ts +++ b/addon-test-support/@ember/test-helpers/dom/double-click.ts @@ -17,23 +17,25 @@ registerHook('doubleClick', 'start', (target: Target) => { @private @param {Element} element the element to double-click on @param {MouseEventInit} options the options to be merged into the mouse events + @returns {Promise} resolves when settled */ export function __doubleClick__( element: Element | Document | Window, options: MouseEventInit -): void { - let mouseDownEvent = fireEvent(element, 'mousedown', options); - - if (!isWindow(element) && !mouseDownEvent?.defaultPrevented) { - __focus__(element); - } - - fireEvent(element, 'mouseup', options); - fireEvent(element, 'click', options); - fireEvent(element, 'mousedown', options); - fireEvent(element, 'mouseup', options); - fireEvent(element, 'click', options); - fireEvent(element, 'dblclick', options); +): Promise { + return Promise.resolve() + .then(() => fireEvent(element, 'mousedown', options)) + .then((mouseDownEvent) => { + return !isWindow(element) && !mouseDownEvent?.defaultPrevented + ? __focus__(element) + : Promise.resolve(); + }) + .then(() => fireEvent(element, 'mouseup', options)) + .then(() => fireEvent(element, 'click', options)) + .then(() => fireEvent(element, 'mousedown', options)) + .then(() => fireEvent(element, 'mouseup', options)) + .then(() => fireEvent(element, 'click', options)) + .then(() => fireEvent(element, 'dblclick', options)); } /** @@ -113,9 +115,7 @@ export default function doubleClick( throw new Error(`Can not \`doubleClick\` disabled ${element}`); } - __doubleClick__(element, options); - - return settled(); + return __doubleClick__(element, options).then(settled); }) .then(() => runHooks('doubleClick', 'end', target, _options)); } diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.ts b/addon-test-support/@ember/test-helpers/dom/fill-in.ts index bab2f31d6..97a998cfd 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.ts +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.ts @@ -1,5 +1,5 @@ import getElement from './-get-element'; -import isFormControl from './-is-form-control'; +import isFormControl, { FormControl } from './-is-form-control'; import guardForMaxlength from './-guard-for-maxlength'; import { __focus__ } from './focus'; import settled from '../settled'; @@ -21,7 +21,7 @@ registerHook('fillIn', 'start', (target: Target, text: string) => { @public @param {string|Element} target the element or selector to enter text into @param {string} text the text to fill into the target element - @return {Promise} resolves when the application is settled + @return {Promise} resolves when the application is settled @example @@ -30,7 +30,10 @@ registerHook('fillIn', 'start', (target: Target, text: string) => { fillIn('input', 'hello world'); */ -export default function fillIn(target: Target, text: string): Promise { +export default function fillIn( + target: Target, + text: string +): Promise { return Promise.resolve() .then(() => runHooks('fillIn', 'start', target, text)) .then(() => { @@ -60,22 +63,25 @@ export default function fillIn(target: Target, text: string): Promise { guardForMaxlength(element, text, 'fillIn'); - __focus__(element); - - element.value = text; + return __focus__(element).then(() => { + (element as FormControl).value = text; + return element; + }); } else if (isContentEditable(element)) { - __focus__(element); - - element.innerHTML = text; + return __focus__(element).then(() => { + element.innerHTML = text; + return element; + }); } else { throw new Error( '`fillIn` is only usable on form controls or contenteditable elements.' ); } - fireEvent(element, 'input'); - fireEvent(element, 'change'); - - return settled(); }) + .then((element) => + fireEvent(element, 'input') + .then(() => fireEvent(element, 'change')) + .then(settled) + ) .then(() => runHooks('fillIn', 'end', target, text)); } 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 5728c9c8d..1fca4495c 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.ts @@ -1,5 +1,12 @@ import { isDocument, isElement } from './-target'; import tuple from '../-tuple'; +import Target from './-target'; +import { log } from '@ember/test-helpers/dom/-logging'; +import { runHooks, registerHook } from '../-internal/helper-hooks'; + +registerHook('fireEvent', 'start', (target: Target) => { + log('fireEvent', target); +}); // eslint-disable-next-line require-jsdoc const MOUSE_EVENT_CONSTRUCTOR = (() => { @@ -61,19 +68,19 @@ function fireEvent( element: Element | Document | Window, eventType: KeyboardEventType, options?: any -): Event; +): Promise; function fireEvent( element: Element | Document | Window, eventType: MouseEventType, options?: any -): Event | void; +): Promise; function fireEvent( element: Element | Document | Window, eventType: string, options?: any -): Event; +): Promise; /** Internal helper used to build and dispatch events throughout the other DOM helpers. @@ -88,48 +95,57 @@ function fireEvent( element: Element | Document | Window, eventType: string, options = {} -): Event | void { - if (!element) { - throw new Error('Must pass an element to `fireEvent`'); - } - - let event; - if (isKeyboardEventType(eventType)) { - event = _buildKeyboardEvent(eventType, options); - } else if (isMouseEventType(eventType)) { - let rect; - if (element instanceof Window && element.document.documentElement) { - rect = element.document.documentElement.getBoundingClientRect(); - } else if (isDocument(element)) { - rect = element.documentElement!.getBoundingClientRect(); - } else if (isElement(element)) { - rect = element.getBoundingClientRect(); - } else { - return; - } - - let x = rect.left + 1; - let y = rect.top + 1; - let simulatedCoordinates = { - 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, - clientY: y, - ...options, - }; - - event = buildMouseEvent(eventType, simulatedCoordinates); - } else if ( - isFileSelectionEventType(eventType) && - isFileSelectionInput(element) - ) { - event = buildFileEvent(eventType, element, options); - } else { - event = buildBasicEvent(eventType, options); - } - - element.dispatchEvent(event); - return event; +): Promise { + return Promise.resolve() + .then(() => runHooks('fireEvent', 'start', element)) + .then(() => runHooks(`fireEvent:${eventType}`, 'start', element)) + .then(() => { + if (!element) { + throw new Error('Must pass an element to `fireEvent`'); + } + + let event; + if (isKeyboardEventType(eventType)) { + event = _buildKeyboardEvent(eventType, options); + } else if (isMouseEventType(eventType)) { + let rect; + if (element instanceof Window && element.document.documentElement) { + rect = element.document.documentElement.getBoundingClientRect(); + } else if (isDocument(element)) { + rect = element.documentElement!.getBoundingClientRect(); + } else if (isElement(element)) { + rect = element.getBoundingClientRect(); + } else { + return; + } + + let x = rect.left + 1; + let y = rect.top + 1; + let simulatedCoordinates = { + 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, + clientY: y, + ...options, + }; + + event = buildMouseEvent(eventType, simulatedCoordinates); + } else if ( + isFileSelectionEventType(eventType) && + isFileSelectionInput(element) + ) { + event = buildFileEvent(eventType, element, options); + } else { + event = buildBasicEvent(eventType, options); + } + + element.dispatchEvent(event); + return event; + }) + .then((event) => + runHooks(`fireEvent:${eventType}`, 'end', element).then(() => event) + ) + .then((event) => runHooks('fireEvent', 'end', element).then(() => event)); } export default fireEvent; diff --git a/addon-test-support/@ember/test-helpers/dom/focus.ts b/addon-test-support/@ember/test-helpers/dom/focus.ts index ede77df07..7be0525fa 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.ts +++ b/addon-test-support/@ember/test-helpers/dom/focus.ts @@ -12,6 +12,11 @@ registerHook('focus', 'start', (target: Target) => { log('focus', target); }); +type FocusRecord = { + focusTarget: HTMLElement | SVGElement; + previousFocusedElement?: HTMLElement | SVGElement | null; +}; + /** Get the closest focusable ancestor of a given element (or the element itself if it's focusable) @@ -39,51 +44,68 @@ function getClosestFocusable( /** @private @param {Element} element the element to trigger events on + @return {Promise} resolves when settled */ export function __focus__( element: HTMLElement | Element | Document | SVGElement -): void { - let focusTarget = getClosestFocusable(element); - - const previousFocusedElement = - document.activeElement && - document.activeElement !== focusTarget && - isFocusable(document.activeElement) - ? document.activeElement - : null; - - // fire __blur__ manually with the null relatedTarget when the target is not focusable - // and there was a previously focused element - if (!focusTarget) { - if (previousFocusedElement) { - __blur__(previousFocusedElement, null); - } - - return; - } - - let browserIsNotFocused = document.hasFocus && !document.hasFocus(); - - // fire __blur__ manually with the correct relatedTarget when the browser is not - // already in focus and there was a previously focused element - if (previousFocusedElement && browserIsNotFocused) { - __blur__(previousFocusedElement, focusTarget); - } - - // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event - focusTarget.focus(); +): Promise { + return Promise.resolve() + .then(() => { + let focusTarget = getClosestFocusable(element); + + const previousFocusedElement = + document.activeElement && + document.activeElement !== focusTarget && + isFocusable(document.activeElement) + ? document.activeElement + : null; + + // fire __blur__ manually with the null relatedTarget when the target is not focusable + // and there was a previously focused element + return !focusTarget && previousFocusedElement + ? __blur__(previousFocusedElement, null).then(() => + Promise.resolve({ focusTarget, previousFocusedElement }) + ) + : Promise.resolve({ focusTarget, previousFocusedElement }); + }) + .then(({ focusTarget, previousFocusedElement }) => { + if (!focusTarget) { + throw new Error('There was a previously focused element'); + } - // Firefox does not trigger the `focusin` event if the window - // does not have focus. If the document does not have focus then - // fire `focusin` event as well. - if (browserIsNotFocused) { - // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it - fireEvent(focusTarget, 'focus', { - bubbles: false, - }); + let browserIsNotFocused = !document?.hasFocus(); - fireEvent(focusTarget, 'focusin'); - } + // fire __blur__ manually with the correct relatedTarget when the browser is not + // already in focus and there was a previously focused element + return previousFocusedElement && browserIsNotFocused + ? __blur__(previousFocusedElement, focusTarget).then(() => + Promise.resolve({ focusTarget }) + ) + : Promise.resolve({ focusTarget }); + }) + .then(({ focusTarget }) => { + // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event + focusTarget.focus(); + + // Firefox does not trigger the `focusin` event if the window + // does not have focus. If the document does not have focus then + // fire `focusin` event as well. + let browserIsFocused = document?.hasFocus(); + return browserIsFocused + ? Promise.resolve() + : // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it + Promise.resolve() + .then(() => + fireEvent(focusTarget as HTMLElement | SVGElement, 'focus', { + bubbles: false, + }) + ) + .then(() => + fireEvent(focusTarget as HTMLElement | SVGElement, 'focusin') + ) + .then(() => settled()); + }) + .catch(() => {}); } /** @@ -130,9 +152,7 @@ export default function focus(target: Target): Promise { throw new Error(`${element} is not focusable`); } - __focus__(element); - - return settled(); + return __focus__(element).then(settled); }) .then(() => runHooks('focus', 'end', target)); } diff --git a/addon-test-support/@ember/test-helpers/dom/scroll-to.ts b/addon-test-support/@ember/test-helpers/dom/scroll-to.ts index 8f44e70f3..467c168b2 100644 --- a/addon-test-support/@ember/test-helpers/dom/scroll-to.ts +++ b/addon-test-support/@ember/test-helpers/dom/scroll-to.ts @@ -53,9 +53,7 @@ export default function scrollTo( element.scrollTop = y; element.scrollLeft = x; - fireEvent(element, 'scroll'); - - return settled(); + return fireEvent(element, 'scroll').then(settled); }) .then(() => runHooks('scrollTo', 'end', target)); } diff --git a/addon-test-support/@ember/test-helpers/dom/select.ts b/addon-test-support/@ember/test-helpers/dom/select.ts index 7ad5721df..afb4927ce 100644 --- a/addon-test-support/@ember/test-helpers/dom/select.ts +++ b/addon-test-support/@ember/test-helpers/dom/select.ts @@ -77,8 +77,9 @@ export default function select( ); } - __focus__(element); - + return __focus__(element).then(() => element); + }) + .then((element) => { for (let i = 0; i < element.options.length; i++) { let elementOption = element.options.item(i); if (elementOption) { @@ -90,10 +91,9 @@ export default function select( } } - fireEvent(element, 'input'); - fireEvent(element, 'change'); - - return settled(); + return fireEvent(element, 'input') + .then(() => fireEvent(element, 'change')) + .then(settled); }) .then(() => runHooks('select', 'end', target, options, keepPreviouslySelected) diff --git a/addon-test-support/@ember/test-helpers/dom/tab.ts b/addon-test-support/@ember/test-helpers/dom/tab.ts index 5b0444aca..97231f138 100644 --- a/addon-test-support/@ember/test-helpers/dom/tab.ts +++ b/addon-test-support/@ember/test-helpers/dom/tab.ts @@ -227,11 +227,9 @@ function triggerResponderChange( return Promise.resolve() .then(() => runHooks('tab', 'start', debugData)) .then(() => getActiveElement(ownerDocument)) - .then((activeElement) => { - return runHooks('tab', 'targetFound', activeElement).then( - () => activeElement - ); - }) + .then((activeElement) => + runHooks('tab', 'targetFound', activeElement).then(() => activeElement) + ) .then((activeElement) => { let event = _buildKeyboardEvent('keydown', keyboardEventOptions); let defaultNotPrevented = activeElement.dispatchEvent(event); @@ -242,19 +240,24 @@ function triggerResponderChange( let target = findNextResponders(rootElement, activeElement); if (target) { if (backwards && target.previous) { - __focus__(target.previous); + return __focus__(target.previous); } else if (!backwards && target.next) { - __focus__(target.next); + return __focus__(target.next); } else { - __blur__(activeElement); + return __blur__(activeElement); } } } + + return Promise.resolve(); }) .then(() => { let activeElement = getActiveElement(ownerDocument); - fireEvent(activeElement, 'keyup', keyboardEventOptions); - + return fireEvent(activeElement, 'keyup', keyboardEventOptions).then( + () => activeElement + ); + }) + .then((activeElement) => { if (!unRestrainTabIndex && activeElement.tabIndex > 0) { throw new Error( `tabindex of greater than 0 is not allowed. Found tabindex=${activeElement.tabIndex}` diff --git a/addon-test-support/@ember/test-helpers/dom/tap.ts b/addon-test-support/@ember/test-helpers/dom/tap.ts index 4cedbcba5..e651e628f 100644 --- a/addon-test-support/@ember/test-helpers/dom/tap.ts +++ b/addon-test-support/@ember/test-helpers/dom/tap.ts @@ -45,7 +45,7 @@ registerHook('tap', 'start', (target: Target) => { @public @param {string|Element} target the element or selector to tap on @param {Object} options the options to be merged into the touch events - @return {Promise} resolves when settled + @return {Promise} resolves when settled @example @@ -57,7 +57,7 @@ registerHook('tap', 'start', (target: Target) => { export default function tap( target: Target, options: object = {} -): Promise { +): Promise { return Promise.resolve() .then(() => { return runHooks('tap', 'start', target, options); @@ -76,14 +76,18 @@ export default function tap( throw new Error(`Can not \`tap\` disabled ${element}`); } - let touchstartEv = fireEvent(element, 'touchstart', options); - let touchendEv = fireEvent(element, 'touchend', options); - - if (!touchstartEv.defaultPrevented && !touchendEv.defaultPrevented) { - __click__(element, options); - } - - return settled(); + return fireEvent(element, 'touchstart', options) + .then((touchstartEv) => + fireEvent(element as Element, 'touchend', options).then( + (touchendEv) => [touchstartEv, touchendEv] + ) + ) + .then(([touchstartEv, touchendEv]) => + !touchstartEv.defaultPrevented && !touchendEv.defaultPrevented + ? __click__(element as Element, options) + : Promise.resolve() + ) + .then(settled); }) .then(() => { return runHooks('tap', 'end', target, options); 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 25e590523..c38320239 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.ts @@ -83,9 +83,7 @@ export default function triggerEvent( throw new Error(`Can not \`triggerEvent\` on disabled ${element}`); } - fireEvent(element, eventType, options); - - return settled(); + return fireEvent(element, eventType, options).then(settled); }) .then(() => { return runHooks('triggerEvent', 'end', target, eventType, options); 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 8e1943916..56cc8eae5 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 @@ -140,44 +140,47 @@ function keyCodeFromKey(key: string) { @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, key: number | string, modifiers: KeyModifiers = DEFAULT_MODIFIERS -) { - let props; - if (typeof key === 'number') { - props = { - keyCode: key, - which: key, - key: keyFromKeyCodeAndModifiers(key, modifiers), - ...modifiers, - }; - } else if (typeof key === 'string' && key.length !== 0) { - let firstCharacter = key[0]; - if (firstCharacter !== firstCharacter.toUpperCase()) { - throw new Error( - `Must provide a \`key\` to \`triggerKeyEvent\` that starts with an uppercase character but you passed \`${key}\`.` - ); - } +): Promise { + return Promise.resolve().then(() => { + let props; + if (typeof key === 'number') { + props = { + keyCode: key, + which: key, + key: keyFromKeyCodeAndModifiers(key, modifiers), + ...modifiers, + }; + } else if (typeof key === 'string' && key.length !== 0) { + let firstCharacter = key[0]; + if (firstCharacter !== firstCharacter.toUpperCase()) { + throw new Error( + `Must provide a \`key\` to \`triggerKeyEvent\` that starts with an uppercase character but you passed \`${key}\`.` + ); + } - if (isNumeric(key) && key.length > 1) { + if (isNumeric(key) && key.length > 1) { + throw new Error( + `Must provide a numeric \`keyCode\` to \`triggerKeyEvent\` but you passed \`${key}\` as a string.` + ); + } + + let keyCode = keyCodeFromKey(key); + props = { keyCode, which: keyCode, key, ...modifiers }; + } else { throw new Error( - `Must provide a numeric \`keyCode\` to \`triggerKeyEvent\` but you passed \`${key}\` as a string.` + `Must provide a \`key\` or \`keyCode\` to \`triggerKeyEvent\`` ); } - let keyCode = keyCodeFromKey(key); - props = { keyCode, which: keyCode, key, ...modifiers }; - } else { - throw new Error( - `Must provide a \`key\` or \`keyCode\` to \`triggerKeyEvent\`` - ); - } - - fireEvent(element, eventType, props); + return fireEvent(element, eventType, props); + }); } /** @@ -242,11 +245,9 @@ export default function triggerKeyEvent( throw new Error(`Can not \`triggerKeyEvent\` on disabled ${element}`); } - __triggerKeyEvent__(element, eventType, key, modifiers); - - return settled(); + return __triggerKeyEvent__(element, eventType, key, modifiers).then( + settled + ); }) - .then(() => { - return runHooks('triggerKeyEvent', 'end', target, eventType, key); - }); + .then(() => runHooks('triggerKeyEvent', 'end', target, eventType, key)); } diff --git a/addon-test-support/@ember/test-helpers/dom/type-in.ts b/addon-test-support/@ember/test-helpers/dom/type-in.ts index 64eaa3410..26873436e 100644 --- a/addon-test-support/@ember/test-helpers/dom/type-in.ts +++ b/addon-test-support/@ember/test-helpers/dom/type-in.ts @@ -91,16 +91,13 @@ export default function typeIn( } } - __focus__(element); - let { delay = 50 } = options; - return fillOut(element, text, delay) + return __focus__(element) + .then(() => fillOut(element, text, delay)) .then(() => fireEvent(element, 'change')) .then(settled) - .then(() => { - return runHooks('typeIn', 'end', target, text, options); - }); + .then(() => runHooks('typeIn', 'end', target, text, options)); }); } @@ -147,7 +144,7 @@ function keyEntry( const newValue = element.innerHTML + character; element.innerHTML = newValue; } - fireEvent(element, 'input'); + return fireEvent(element, 'input'); }) .then(() => __triggerKeyEvent__(element, 'keyup', characterKey, options)); }; diff --git a/tests/helpers/register-hooks.js b/tests/helpers/register-hooks.js new file mode 100644 index 000000000..351864f36 --- /dev/null +++ b/tests/helpers/register-hooks.js @@ -0,0 +1,102 @@ +import { _registerHook } from '@ember/test-helpers'; + +/** + * Register mock hooks for a helper and optional list of expected events performed while the helper is executed. + * + * @param {Assert} assert Test assertion context + * @param {string} helperName Helper name + * @param {Object} [options] Options object + * @param {string[]} [options.expectedEvents] Expected events to register as `fireEvent` hooks. (NOTE: These are deduplicated to prevent registering duplicate step assertions.) + * @returns {HookUnregister[]} Unregisterable hooks + */ +export const registerHooks = (assert, helperName, { expectedEvents } = {}) => { + const mockHooks = [ + _registerHook(helperName, 'start', () => { + assert.step(`${helperName}:start`); + }), + _registerHook(helperName, 'end', () => { + assert.step(`${helperName}:end`); + }), + ]; + + if (Array.isArray(expectedEvents)) { + const fireEventHooks = registerFireEventHooks(assert, expectedEvents); + mockHooks.push(...fireEventHooks); + } + + return mockHooks; +}; + +/** + * Register mock `fireEvent` hooks for provided event types. + * + * @param {Assert} assert Test assertion context + * @param {string[]} [options.expectedEvents] Expected events to register as `fireEvent` hooks (NOTE: These are deduplicated to prevent registering duplicate step assertions.) + * @returns {HookUnregister[]} Unregisterable hooks + */ +export const registerFireEventHooks = (assert, expectedEvents) => { + const startHook = _registerHook('fireEvent', 'start', () => { + assert.step(`fireEvent:start`); + }); + const endHook = _registerHook('fireEvent', 'end', () => { + assert.step(`fireEvent:end`); + }); + + const eventTypes = [...new Set(expectedEvents)]; + const eventSpecificHooks = eventTypes.flatMap((eventType) => [ + _registerHook(`fireEvent:${eventType}`, 'start', () => { + assert.step(`fireEvent:${eventType}:start`); + }), + _registerHook(`fireEvent:${eventType}`, 'end', () => { + assert.step(`fireEvent:${eventType}:end`); + }), + ]); + + return [startHook, endHook, ...eventSpecificHooks]; +}; + +/** + * Unregister list of provided mock hooks + * + * @param {HookUnregister[]} hooks Unregister hook objects + */ +export const unregisterHooks = (hooks) => { + hooks.forEach((hook) => hook.unregister()); +}; + +/** + * Build expected `fireEvent` steps for verification. + * + * @param {string[]} expectedEvents Events expected to be executed + * @return {string[]} Expected executed `fireEvent` steps + */ +export const buildExpectedFireEventSteps = (expectedEvents) => + expectedEvents?.flatMap((event) => [ + 'fireEvent:start', + `fireEvent:${event}:start`, + `fireEvent:${event}:end`, + `fireEvent:end`, + ]); + +/** + * Build list of expected executed steps for verification. + * + * @param {string} helperName Helper name + * @param {Object} [options] Options object + * @param {string[]} [options.expectedEvents] Events expected to be executed + * @return {string[]} Expected executed steps + */ +export const buildExpectedSteps = (helperName, { expectedEvents } = {}) => + [ + `${helperName}:start`, + ...buildExpectedFireEventSteps(expectedEvents), + `${helperName}:end`, + ].filter(Boolean); + +export default { + registerHooks, + registerFireEventHooks, + buildExpectedSteps, + buildExpectedFireEventSteps, + unregisterHooks, +}; diff --git a/tests/integration/dom/scroll-to-test.js b/tests/integration/dom/scroll-to-test.js index 205aa2127..e24804a1c 100644 --- a/tests/integration/dom/scroll-to-test.js +++ b/tests/integration/dom/scroll-to-test.js @@ -6,9 +6,13 @@ import { setupContext, setupRenderingContext, teardownContext, - _registerHook, } from '@ember/test-helpers'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + buildExpectedSteps, + registerHooks, + unregisterHooks, +} from '../../helpers/register-hooks'; module('DOM Helper: scroll-to', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -25,14 +29,10 @@ module('DOM Helper: scroll-to', function (hooks) { }); test('it executes registered scrollTo hooks', async function (assert) { - assert.expect(3); + assert.expect(7); - let startHook = _registerHook('scrollTo', 'start', () => { - assert.step('scrollTo:start'); - }); - let endHook = _registerHook('scrollTo', 'end', () => { - assert.step('scrollTo:end'); - }); + const expectedEvents = ['scroll']; + const mockHooks = registerHooks(assert, 'scrollTo', { expectedEvents }); await render(hbs`
`); - await scrollTo('.container', 0, 50); - - assert.verifySteps(['scrollTo:start', 'scrollTo:end']); + try { + await scrollTo('.container', 0, 50); - startHook.unregister(); - endHook.unregister(); + const expectedSteps = buildExpectedSteps('scrollTo', { expectedEvents }); + assert.verifySteps(expectedSteps); + } finally { + unregisterHooks(mockHooks); + } }); test('Scroll in vertical direction', async function (assert) { diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index f72cd8922..55fb3dd2c 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -1,10 +1,5 @@ import { module, test } from 'qunit'; -import { - click, - setupContext, - teardownContext, - _registerHook, -} from '@ember/test-helpers'; +import { click, setupContext, teardownContext } from '@ember/test-helpers'; import { buildInstrumentedElement, instrumentElement, @@ -12,6 +7,11 @@ import { } from '../../helpers/events'; import { isIE11 } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + registerHooks, + unregisterHooks, + buildExpectedSteps, +} from '../../helpers/register-hooks'; module('DOM Helper: click', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -38,25 +38,21 @@ module('DOM Helper: click', function (hooks) { }); test('it executes registered click hooks', async function (assert) { - assert.expect(3); + assert.expect(15); element = document.createElement('div'); insertElement(element); - let startHook = _registerHook('click', 'start', () => { - assert.step('click:start'); - }); - let endHook = _registerHook('click', 'end', () => { - assert.step('click:end'); - }); + const expectedEvents = ['mousedown', 'mouseup', 'click']; + const mockHooks = registerHooks(assert, 'click', { expectedEvents }); try { await click(element); - assert.verifySteps(['click:start', 'click:end']); + const expectedSteps = buildExpectedSteps('click', { expectedEvents }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } }); diff --git a/tests/unit/dom/double-click-test.js b/tests/unit/dom/double-click-test.js index 36a84881c..5608c5a2c 100644 --- a/tests/unit/dom/double-click-test.js +++ b/tests/unit/dom/double-click-test.js @@ -3,7 +3,6 @@ import { doubleClick, setupContext, teardownContext, - _registerHook, } from '@ember/test-helpers'; import { buildInstrumentedElement, @@ -12,6 +11,21 @@ import { } from '../../helpers/events'; import { isIE11 } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + registerHooks, + unregisterHooks, + buildExpectedSteps, +} from '../../helpers/register-hooks'; + +const expectedEvents = [ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', +]; module('DOM Helper: doubleClick', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -39,25 +53,22 @@ module('DOM Helper: doubleClick', function (hooks) { }); test('it executes registered doubleClick hooks', async function (assert) { - assert.expect(3); + assert.expect(31); element = document.createElement('div'); insertElement(element); - let startHook = _registerHook('doubleClick', 'start', () => { - assert.step('doubleClick:start'); - }); - let endHook = _registerHook('doubleClick', 'end', () => { - assert.step('doubleClick:end'); - }); + const mockHooks = registerHooks(assert, 'doubleClick', { expectedEvents }); try { await doubleClick(element); - assert.verifySteps(['doubleClick:start', 'doubleClick:end']); + const expectedSteps = buildExpectedSteps('doubleClick', { + expectedEvents, + }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } }); diff --git a/tests/unit/dom/fill-in-test.js b/tests/unit/dom/fill-in-test.js index d25a69556..98560511d 100644 --- a/tests/unit/dom/fill-in-test.js +++ b/tests/unit/dom/fill-in-test.js @@ -1,13 +1,13 @@ import { module, test } from 'qunit'; -import { - fillIn, - setupContext, - teardownContext, - _registerHook, -} from '@ember/test-helpers'; +import { fillIn, setupContext, teardownContext } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import { isIE11, isFirefox } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + registerHooks, + unregisterHooks, + buildExpectedSteps, +} from '../../helpers/register-hooks'; let clickSteps = ['focus', 'focusin', 'input', 'change']; @@ -43,25 +43,21 @@ module('DOM Helper: fillIn', function (hooks) { }); test('it executes registered fillIn hooks', async function (assert) { - assert.expect(3); + assert.expect(11); element = document.createElement('input'); insertElement(element); - let startHook = _registerHook('fillIn', 'start', () => { - assert.step('fillIn:start'); - }); - let endHook = _registerHook('fillIn', 'end', () => { - assert.step('fillIn:end'); - }); + const expectedEvents = ['input', 'change']; + const mockHooks = registerHooks(assert, 'fillIn', { expectedEvents }); try { await fillIn(element, 'foo'); - assert.verifySteps(['fillIn:start', 'fillIn:end']); + const expectedSteps = buildExpectedSteps('fillIn', { expectedEvents }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } }); diff --git a/tests/unit/dom/select-test.js b/tests/unit/dom/select-test.js index a72437ff0..f87929b79 100644 --- a/tests/unit/dom/select-test.js +++ b/tests/unit/dom/select-test.js @@ -1,12 +1,12 @@ import { module, test } from 'qunit'; -import { - select, - setupContext, - teardownContext, - _registerHook, -} from '@ember/test-helpers'; +import { select, setupContext, teardownContext } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import { isIE11 } from '../../helpers/browser-detect'; +import { + buildExpectedSteps, + registerHooks, + unregisterHooks, +} from '../../helpers/register-hooks'; let selectSteps = ['focus', 'focusin', 'input', 'change']; let additionalSteps = ['input', 'change']; @@ -35,25 +35,21 @@ module('DOM Helper: select', function (hooks) { }); test('it executes registered select hooks', async function (assert) { - assert.expect(3); + assert.expect(11); element = document.createElement('select'); insertElement(element); - let startHook = _registerHook('select', 'start', () => { - assert.step('select:start'); - }); - let endHook = _registerHook('select', 'end', () => { - assert.step('select:end'); - }); + const expectedEvents = ['input', 'change']; + const mockHooks = registerHooks(assert, 'select', { expectedEvents }); try { await select(element, 'apple'); - assert.verifySteps(['select:start', 'select:end']); + const expectedSteps = buildExpectedSteps('select', { expectedEvents }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } }); diff --git a/tests/unit/dom/tap-test.js b/tests/unit/dom/tap-test.js index 1a812b186..4e9e672cb 100644 --- a/tests/unit/dom/tap-test.js +++ b/tests/unit/dom/tap-test.js @@ -1,13 +1,13 @@ import { module, test } from 'qunit'; -import { - tap, - setupContext, - teardownContext, - _registerHook, -} from '@ember/test-helpers'; +import { tap, setupContext, teardownContext } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import { isIE11 } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + registerHooks, + unregisterHooks, + buildExpectedSteps, +} from '../../helpers/register-hooks'; module('DOM Helper: tap', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -39,25 +39,27 @@ module('DOM Helper: tap', function (hooks) { }); test('it executes registered tap hooks', async function (assert) { - assert.expect(3); + assert.expect(23); element = document.createElement('div'); insertElement(element); - let startHook = _registerHook('tap', 'start', () => { - assert.step('tap:start'); - }); - let endHook = _registerHook('tap', 'end', () => { - assert.step('tap:end'); - }); + const expectedEvents = [ + 'touchstart', + 'touchend', + 'mousedown', + 'mouseup', + 'click', + ]; + const mockHooks = registerHooks(assert, 'tap', { expectedEvents }); try { await tap(element); - assert.verifySteps(['tap:start', 'tap:end']); + const expectedSteps = buildExpectedSteps('tap', { expectedEvents }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } }); diff --git a/tests/unit/dom/type-in-test.js b/tests/unit/dom/type-in-test.js index 776135c81..0d6bd3323 100644 --- a/tests/unit/dom/type-in-test.js +++ b/tests/unit/dom/type-in-test.js @@ -1,15 +1,15 @@ import { module, test } from 'qunit'; -import { - typeIn, - setupContext, - teardownContext, - _registerHook, -} from '@ember/test-helpers'; +import { typeIn, setupContext, teardownContext } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import { isIE11, isFirefox } from '../../helpers/browser-detect'; import { debounce } from '@ember/runloop'; import { Promise } from 'rsvp'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { + registerHooks, + unregisterHooks, + buildExpectedSteps, +} from '../../helpers/register-hooks'; /* * Event order based on https://jsbin.com/zitazuxabe/edit?html,js,console,output @@ -98,25 +98,35 @@ module('DOM Helper: typeIn', function (hooks) { }); test('it executes registered typeIn hooks', async function (assert) { - assert.expect(3); + assert.expect(55); element = document.createElement('input'); insertElement(element); - let startHook = _registerHook('typeIn', 'start', () => { - assert.step('typeIn:start'); - }); - let endHook = _registerHook('typeIn', 'end', () => { - assert.step('typeIn:end'); - }); + const expectedEvents = [ + 'keydown', + 'keypress', + 'input', + 'keyup', + 'keydown', + 'keypress', + 'input', + 'keyup', + 'keydown', + 'keypress', + 'input', + 'keyup', + 'change', + ]; + const mockHooks = registerHooks(assert, 'typeIn', { expectedEvents }); try { await typeIn(element, 'foo'); - assert.verifySteps(['typeIn:start', 'typeIn:end']); + const expectedSteps = buildExpectedSteps('typeIn', { expectedEvents }); + assert.verifySteps(expectedSteps); } finally { - startHook.unregister(); - endHook.unregister(); + unregisterHooks(mockHooks); } });