From aa465cc8dc7b785f8fc0fc1d328dc130c3d69ce7 Mon Sep 17 00:00:00 2001 From: Ben Demboski Date: Mon, 30 Aug 2021 13:24:01 -0700 Subject: [PATCH] Support passing IDOMElementDescriptors as targets --- API.md | 28 ++++---- .../test-helpers/dom/-get-description.ts | 19 +++++ .../@ember/test-helpers/dom/-get-element.ts | 16 ++++- .../@ember/test-helpers/dom/-get-elements.ts | 21 +++++- .../@ember/test-helpers/dom/-target.ts | 4 +- .../@ember/test-helpers/dom/blur.ts | 6 +- .../@ember/test-helpers/dom/click.ts | 10 ++- .../@ember/test-helpers/dom/double-click.ts | 10 ++- .../@ember/test-helpers/dom/fill-in.ts | 18 +++-- .../@ember/test-helpers/dom/focus.ts | 10 ++- .../@ember/test-helpers/dom/scroll-to.ts | 41 ++++++++--- .../@ember/test-helpers/dom/select.ts | 28 +++++--- .../@ember/test-helpers/dom/tap.ts | 12 +++- .../@ember/test-helpers/dom/trigger-event.ts | 10 ++- .../test-helpers/dom/trigger-key-event.ts | 14 +++- .../@ember/test-helpers/dom/type-in.ts | 18 +++-- .../@ember/test-helpers/dom/wait-for.ts | 24 ++++--- addon/package.json | 1 + addon/tests/integration/dom/scroll-to-test.js | 2 +- addon/tests/unit/dom/blur-test.js | 25 +++++++ addon/tests/unit/dom/click-test.js | 45 ++++++++++++ addon/tests/unit/dom/double-click-test.js | 61 ++++++++++++++++ addon/tests/unit/dom/fill-in-test.js | 63 ++++++++++++++++- addon/tests/unit/dom/focus-test.js | 35 ++++++++++ addon/tests/unit/dom/select-test.js | 25 ++++++- addon/tests/unit/dom/tap-test.js | 57 +++++++++++++++ addon/tests/unit/dom/trigger-event-test.js | 18 +++++ .../tests/unit/dom/trigger-key-event-test.js | 18 +++++ addon/tests/unit/dom/type-in-test.js | 14 ++++ addon/tests/unit/dom/wait-for-test.js | 69 +++++++++++++++++++ addon/type-tests/api.ts | 9 ++- yarn.lock | 5 ++ 32 files changed, 656 insertions(+), 80 deletions(-) create mode 100644 addon/addon-test-support/@ember/test-helpers/dom/-get-description.ts diff --git a/API.md b/API.md index a6fc2d11f..e13aaa859 100644 --- a/API.md +++ b/API.md @@ -87,7 +87,7 @@ to continue to emulate how actual browsers handle unfocusing a given element. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to unfocus (optional, default `document.activeElement`) +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to unfocus (optional, default `document.activeElement`) #### Examples @@ -129,7 +129,7 @@ You can use this to specifiy modifier keys as well. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to click on +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to click on * `_options` **MouseEventInit** the options to be merged into the mouse events. (optional, default `{}`) #### Examples @@ -185,7 +185,7 @@ Use the `options` hash to change the parameters of the [MouseEvents][67]. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to double-click on +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to double-click on * `_options` **MouseEventInit** the options to be merged into the mouse events (optional, default `{}`) #### Examples @@ -212,7 +212,7 @@ events on the specified target. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to enter text into +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to enter text into * `text` **[string][64]** the text to fill into the target element #### Examples @@ -242,7 +242,7 @@ to continue to emulate how actual browsers handle focusing a given element. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to focus +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to focus #### Examples @@ -256,11 +256,11 @@ Returns **[Promise][66]\** resolves when the application is settled ### scrollTo -Scrolls DOM element or selector to the given coordinates. +Scrolls DOM element, selector, or descriptor to the given coordinates. #### Parameters -* `target` **([string][64] | [HTMLElement][68])** the element or selector to trigger scroll on +* `target` **([string][64] | [HTMLElement][68] | IDOMElementDescriptor)** the element, selector, or descriptor to trigger scroll on * `x` **[Number][69]** x-coordinate * `y` **[Number][69]** y-coordinate @@ -284,7 +284,7 @@ multiple attribute is set true on the HTMLSelectElement) then trigger #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector for the select element +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor for the select element * `options` **([string][64] | [Array][70]<[string][64]>)** the value/values of the items to select * `keepPreviouslySelected` **[boolean][71]** a flag keep any existing selections (optional, default `false`) @@ -365,7 +365,7 @@ Use the `options` hash to change the parameters of the tap events. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to tap on +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to tap on * `options` **[Object][72]** the options to be merged into the touch events (optional, default `{}`) #### Examples @@ -384,7 +384,7 @@ Triggers an event on the specified target. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to trigger the event on +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to trigger the event on * `eventType` **[string][64]** the type of event to trigger * `options` **[Object][72]** additional properties to be set on the event @@ -432,7 +432,7 @@ Optionally the user can also provide a POJO with extra modifiers for the event. #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to trigger the event on +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to trigger the event on * `eventType` **(`"keydown"` | `"keyup"` | `"keypress"`)** the type of event to trigger * `key` **([number][69] | [string][64])** the `keyCode`(number) or `key`(string) of the event being triggered * `modifiers` **[Object][72]?** the state of various modifier keys (optional, default `DEFAULT_MODIFIERS`) @@ -466,7 +466,7 @@ per character of the passed text (this may vary on some browsers). #### Parameters -* `target` **([string][64] | [Element][65])** the element or selector to enter text into +* `target` **([string][64] | [Element][65] | IDOMElementDescriptor)** the element, selector, or descriptor to enter text into * `text` **[string][64]** the test to fill the element with * `options` **[Object][72]** {delay: x} (default 50) number of milliseconds to wait per keypress (optional, default `{}`) @@ -618,7 +618,7 @@ interim DOM states (e.g. loading states, pending promises, etc). #### Parameters -* `selector` **[string][64]** the selector to wait for +* `target` **([string][64] | IDOMElementDescriptor)** the selector or DOM element descriptor to wait for * `options` **[Object][72]?** the options to be used (optional, default `{}`) * `options.timeout` **[number][69]** the time to wait (in ms) for a match (optional, default `1000`) @@ -876,7 +876,7 @@ Responsible for: #### Parameters * `context` **[Object][72]** the context to setup -* `options` **[Object][72]?** options used to override defaults +* `options` **[Object][72]?** options used to override defaults (optional, default `{}`) * `options.waitForSettled` **[boolean][71]** should the teardown wait for `settled()`ness (optional, default `true`) diff --git a/addon/addon-test-support/@ember/test-helpers/dom/-get-description.ts b/addon/addon-test-support/@ember/test-helpers/dom/-get-description.ts new file mode 100644 index 000000000..26172c3b6 --- /dev/null +++ b/addon/addon-test-support/@ember/test-helpers/dom/-get-description.ts @@ -0,0 +1,19 @@ +import { isDescriptor, lookupDescriptorData } from 'dom-element-descriptors'; +import type Target from './-target'; + +/** + Used internally by the DOM interaction helpers to get a description of a + target for debug/error messaging. + + @private + @param {Target} target the target + @returns {string} a description of the target +*/ +export default function getDescription(target: Target): string { + let data = isDescriptor(target) ? lookupDescriptorData(target) : null; + if (data) { + return data.description || ''; + } else { + return `${target}`; + } +} diff --git a/addon/addon-test-support/@ember/test-helpers/dom/-get-element.ts b/addon/addon-test-support/@ember/test-helpers/dom/-get-element.ts index 5f152620b..801c674d6 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/-get-element.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/-get-element.ts @@ -1,5 +1,10 @@ import getRootElement from './get-root-element'; import Target, { isDocument, isElement } from './-target'; +import { + type IDOMElementDescriptor, + lookupDescriptorData, + resolveDOMElement, +} from 'dom-element-descriptors'; function getElement< K extends keyof (HTMLElementTagNameMap | SVGElementTagNameMap) @@ -12,8 +17,10 @@ function getElement( ): SVGElementTagNameMap[K] | null; function getElement(target: string): Element | null; function getElement(target: Element): Element; +function getElement(target: IDOMElementDescriptor): Element | null; function getElement(target: Document): Document; function getElement(target: Window): Document; +function getElement(target: string | IDOMElementDescriptor): Element | null; function getElement(target: Target): Element | Document | null; /** Used internally by the DOM interaction helpers to find one element. @@ -32,7 +39,14 @@ function getElement(target: Target): Element | Document | null { } else if (target instanceof Window) { return target.document; } else { - throw new Error('Must use an element or a selector string'); + let descriptorData = lookupDescriptorData(target); + if (descriptorData) { + return resolveDOMElement(descriptorData); + } else { + throw new Error( + 'Must use an element, selector string, or DOM element descriptor' + ); + } } } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/-get-elements.ts b/addon/addon-test-support/@ember/test-helpers/dom/-get-elements.ts index 696a415c7..c92662789 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/-get-elements.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/-get-elements.ts @@ -1,5 +1,13 @@ +import { + type IDOMElementDescriptor, + lookupDescriptorData, + resolveDOMElements, +} from 'dom-element-descriptors'; import getRootElement from './get-root-element'; +function getElements(target: string): NodeListOf; +function getElements(target: IDOMElementDescriptor): Iterable; +function getElements(target: string | IDOMElementDescriptor): Iterable; /** Used internally by the DOM interaction helpers to find multiple elements. @@ -7,12 +15,21 @@ import getRootElement from './get-root-element'; @param {string} target the selector to retrieve @returns {NodeList} the matched elements */ -export default function getElements(target: string): NodeListOf { +function getElements( + target: string | IDOMElementDescriptor +): NodeListOf | Iterable { if (typeof target === 'string') { let rootElement = getRootElement(); return rootElement.querySelectorAll(target); } else { - throw new Error('Must use a selector string'); + let descriptorData = lookupDescriptorData(target); + if (descriptorData) { + return resolveDOMElements(descriptorData); + } else { + throw new Error('Must use a selector string or DOM element descriptor'); + } } } + +export default getElements; diff --git a/addon/addon-test-support/@ember/test-helpers/dom/-target.ts b/addon/addon-test-support/@ember/test-helpers/dom/-target.ts index 9683664fe..27d27aaa1 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/-target.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/-target.ts @@ -1,4 +1,6 @@ -type Target = string | Element | Document | Window; +import type { IDOMElementDescriptor } from 'dom-element-descriptors'; + +type Target = string | Element | IDOMElementDescriptor | Document | Window; export default Target; diff --git a/addon/addon-test-support/@ember/test-helpers/dom/blur.ts b/addon/addon-test-support/@ember/test-helpers/dom/blur.ts index 11facf939..87c49c4d3 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/blur.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/blur.ts @@ -5,6 +5,7 @@ import Target from './-target'; import { log } from './-logging'; import isFocusable from './-is-focusable'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook('blur', 'start', (target: Target) => { log('blur', target); @@ -59,7 +60,7 @@ export function __blur__( to continue to emulate how actual browsers handle unfocusing a given element. @public - @param {string|Element} [target=document.activeElement] the element or selector to unfocus + @param {string|Element|IDOMElementDescriptor} [target=document.activeElement] the element, selector, or descriptor to unfocus @return {Promise} resolves when settled @example @@ -77,8 +78,9 @@ export default function blur( .then(() => { let element = getElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`blur('${target}')\`.` + `Element not found when calling \`blur('${description}')\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/click.ts b/addon/addon-test-support/@ember/test-helpers/dom/click.ts index fa4d277f9..ef12bbfa9 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/click.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/click.ts @@ -6,6 +6,7 @@ import isFormControl from './-is-form-control'; import Target, { isWindow } from './-target'; import { log } from './-logging'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; const PRIMARY_BUTTON = 1; const MAIN_BUTTON_PRESSED = 0; @@ -72,7 +73,7 @@ export function __click__( You can use this to specify modifier keys as well. @public - @param {string|Element} target the element or selector to click on + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to click on @param {MouseEventInit} _options the options to be merged into the mouse events. @return {Promise} resolves when settled @@ -99,13 +100,16 @@ export default function click( .then(() => runHooks('click', 'start', target, _options)) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `click`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `click`.' + ); } let element = getWindowOrElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`click('${target}')\`.` + `Element not found when calling \`click('${description}')\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/double-click.ts b/addon/addon-test-support/@ember/test-helpers/dom/double-click.ts index b1fbd9246..20d40ea48 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/double-click.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/double-click.ts @@ -7,6 +7,7 @@ import Target, { isWindow } from './-target'; import { log } from './-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook('doubleClick', 'start', (target: Target) => { log('doubleClick', target); @@ -72,7 +73,7 @@ export function __doubleClick__( Use the `options` hash to change the parameters of the [MouseEvents](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent). @public - @param {string|Element} target the element or selector to double-click on + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to double-click on @param {MouseEventInit} _options the options to be merged into the mouse events @return {Promise} resolves when settled @@ -100,13 +101,16 @@ export default function doubleClick( .then(() => runHooks('doubleClick', 'start', target, _options)) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `doubleClick`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `doubleClick`.' + ); } let element = getWindowOrElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`doubleClick('${target}')\`.` + `Element not found when calling \`doubleClick('${description}')\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/fill-in.ts b/addon/addon-test-support/@ember/test-helpers/dom/fill-in.ts index cffb3f5d5..1bf6ddee4 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/fill-in.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/fill-in.ts @@ -7,6 +7,7 @@ import fireEvent from './fire-event'; import Target, { isContentEditable } from './-target'; import { log } from './-logging'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook('fillIn', 'start', (target: Target, text: string) => { log('fillIn', target, text); @@ -18,7 +19,7 @@ registerHook('fillIn', 'start', (target: Target, text: string) => { events on the specified target. @public - @param {string|Element} target the element or selector to enter text into + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to enter text into @param {string} text the text to fill into the target element @return {Promise} resolves when the application is settled @@ -34,13 +35,16 @@ export default function fillIn(target: Target, text: string): Promise { .then(() => runHooks('fillIn', 'start', target, text)) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `fillIn`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `fillIn`.' + ); } let element = getElement(target) as Element | HTMLElement; if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`fillIn('${target}')\`.` + `Element not found when calling \`fillIn('${description}')\`.` ); } @@ -50,11 +54,15 @@ export default function fillIn(target: Target, text: string): Promise { if (isFormControl(element)) { if (element.disabled) { - throw new Error(`Can not \`fillIn\` disabled '${target}'.`); + throw new Error( + `Can not \`fillIn\` disabled '${getDescription(target)}'.` + ); } if ('readOnly' in element && element.readOnly) { - throw new Error(`Can not \`fillIn\` readonly '${target}'.`); + throw new Error( + `Can not \`fillIn\` readonly '${getDescription(target)}'.` + ); } guardForMaxlength(element, text, 'fillIn'); diff --git a/addon/addon-test-support/@ember/test-helpers/dom/focus.ts b/addon/addon-test-support/@ember/test-helpers/dom/focus.ts index a99634cc5..5ad6d65de 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/focus.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/focus.ts @@ -6,6 +6,7 @@ import Target, { isDocument } from './-target'; import { log } from './-logging'; import { runHooks, registerHook } from '../helper-hooks'; import { __blur__ } from './blur'; +import getDescription from './-get-description'; registerHook('focus', 'start', (target: Target) => { log('focus', target); @@ -122,7 +123,7 @@ export function __focus__( to continue to emulate how actual browsers handle focusing a given element. @public - @param {string|Element} target the element or selector to focus + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to focus @return {Promise} resolves when the application is settled @example @@ -137,13 +138,16 @@ export default function focus(target: Target): Promise { .then(() => runHooks('focus', 'start', target)) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `focus`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `focus`.' + ); } let element = getElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`focus('${target}')\`.` + `Element not found when calling \`focus('${description}')\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/scroll-to.ts b/addon/addon-test-support/@ember/test-helpers/dom/scroll-to.ts index 08f58f6bf..41c201086 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/scroll-to.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/scroll-to.ts @@ -1,13 +1,21 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; -import { isElement } from './-target'; +import Target, { isDocument, isElement } from './-target'; import { runHooks } from '../helper-hooks'; +import type { IDOMElementDescriptor } from 'dom-element-descriptors'; +import getDescription from './-get-description'; + +// eslint-disable-next-line require-jsdoc +function errorMessage(message: string, target: Target) { + let description = getDescription(target); + return `${message} when calling \`scrollTo('${description}')\`.`; +} /** - Scrolls DOM element or selector to the given coordinates. + Scrolls DOM element, selector, or descriptor to the given coordinates. @public - @param {string|HTMLElement} target the element or selector to trigger scroll on + @param {string|HTMLElement|IDOMElementDescriptor} target the element, selector, or descriptor to trigger scroll on @param {Number} x x-coordinate @param {Number} y y-coordinate @return {Promise} resolves when settled @@ -21,7 +29,7 @@ import { runHooks } from '../helper-hooks'; scrollTo('#my-long-div', 0, 100); // scroll down */ export default function scrollTo( - target: string | HTMLElement, + target: string | HTMLElement | IDOMElementDescriptor, x: number, y: number ): Promise { @@ -29,7 +37,9 @@ export default function scrollTo( .then(() => runHooks('scrollTo', 'start', target)) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `scrollTo`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `scrollTo`.' + ); } if (x === undefined || y === undefined) { @@ -38,14 +48,27 @@ export default function scrollTo( let element = getElement(target); if (!element) { - throw new Error( - `Element not found when calling \`scrollTo('${target}')\`.` - ); + throw new Error(errorMessage('Element not found', target)); } if (!isElement(element)) { + let nodeType: string; + if (isDocument(element)) { + nodeType = 'Document'; + } else { + // This is an error check for non-typescript callers passing in the + // wrong type for `target`, so we have to cast `element` (which is + // `never` inside this block) to something that will allow us to + // access `nodeType`. + let notElement = element as { nodeType: string }; + nodeType = notElement.nodeType; + } + throw new Error( - `"target" must be an element, but was a ${element.nodeType} when calling \`scrollTo('${target}')\`.` + errorMessage( + `"target" must be an element, but was a ${nodeType}`, + target + ) ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/select.ts b/addon/addon-test-support/@ember/test-helpers/dom/select.ts index 1da3b985d..eca24089e 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/select.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/select.ts @@ -5,6 +5,13 @@ import settled from '../settled'; import fireEvent from './fire-event'; import Target from './-target'; import { runHooks } from '../helper-hooks'; +import getDescription from './-get-description'; + +// eslint-disable-next-line require-jsdoc +function errorMessage(message: string, target: Target) { + let description = getDescription(target); + return `${message} when calling \`select('${description}')\`.`; +} /** Set the `selected` property true for the provided option the target is a @@ -13,7 +20,7 @@ import { runHooks } from '../helper-hooks'; `change` and `input` events on the specified target. @public - @param {string|Element} target the element or selector for the select element + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor for the select element @param {string|string[]} options the value/values of the items to select @param {boolean} keepPreviouslySelected a flag keep any existing selections @return {Promise} resolves when the application is settled @@ -40,7 +47,9 @@ export default function select( ) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `select`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `select`.' + ); } if (typeof options === 'undefined' || options === null) { @@ -51,28 +60,27 @@ export default function select( const element = getElement(target); if (!element) { - throw new Error( - `Element not found when calling \`select('${target}')\`.` - ); + throw new Error(errorMessage('Element not found', target)); } if (!isSelectElement(element)) { throw new Error( - `Element is not a HTMLSelectElement when calling \`select('${target}')\`.` + errorMessage('Element is not a HTMLSelectElement', target) ); } if (element.disabled) { - throw new Error( - `Element is disabled when calling \`select('${target}')\`.` - ); + throw new Error(errorMessage('Element is disabled', target)); } options = Array.isArray(options) ? options : [options]; if (!element.multiple && options.length > 1) { throw new Error( - `HTMLSelectElement \`multiple\` attribute is set to \`false\` but multiple options were passed when calling \`select('${target}')\`.` + errorMessage( + 'HTMLSelectElement `multiple` attribute is set to `false` but multiple options were passed', + target + ) ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/tap.ts b/addon/addon-test-support/@ember/test-helpers/dom/tap.ts index 89eec13b6..f73bce36b 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/tap.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/tap.ts @@ -6,6 +6,7 @@ import Target from './-target'; import { log } from './-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook('tap', 'start', (target: Target) => { log('tap', target); @@ -42,7 +43,7 @@ registerHook('tap', 'start', (target: Target) => { Use the `options` hash to change the parameters of the tap events. @public - @param {string|Element} target the element or selector to tap on + @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to tap on @param {Object} options the options to be merged into the touch events @return {Promise} resolves when settled @@ -63,12 +64,17 @@ export default function tap( }) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `tap`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `tap`.' + ); } let element = getElement(target); if (!element) { - throw new Error(`Element not found when calling \`tap('${target}')\`.`); + let description = getDescription(target); + throw new Error( + `Element not found when calling \`tap('${description}')\`.` + ); } if (isFormControl(element) && element.disabled) { diff --git a/addon/addon-test-support/@ember/test-helpers/dom/trigger-event.ts b/addon/addon-test-support/@ember/test-helpers/dom/trigger-event.ts index 1a16cc5fa..766aece51 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/trigger-event.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/trigger-event.ts @@ -5,6 +5,7 @@ import Target from './-target'; import { log } from './-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook('triggerEvent', 'start', (target: Target, eventType: string) => { log('triggerEvent', target, eventType); @@ -14,7 +15,7 @@ registerHook('triggerEvent', 'start', (target: Target, eventType: string) => { * Triggers an event on the specified target. * * @public - * @param {string|Element} target the element or selector to trigger the event on + * @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor 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 @@ -64,7 +65,9 @@ export default function triggerEvent( }) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `triggerEvent`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `triggerEvent`.' + ); } if (!eventType) { @@ -73,8 +76,9 @@ export default function triggerEvent( let element = getWindowOrElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`triggerEvent('${target}', ...)\`.` + `Element not found when calling \`triggerEvent('${description}', ...)\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts b/addon/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts index 20c746e43..de2b51403 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts @@ -11,6 +11,7 @@ import Target from './-target'; import { log } from './-logging'; import isFormControl from './-is-form-control'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; registerHook( 'triggerKeyEvent', @@ -213,6 +214,12 @@ export function __triggerKeyEvent__( }); } +// eslint-disable-next-line require-jsdoc +function errorMessage(message: string, target: Target) { + let description = getDescription(target); + return `${message} when calling \`triggerKeyEvent('${description}')\`.`; +} + /** Triggers a keyboard event of given type in the target element. It also requires the developer to provide either a string with the [`key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) @@ -220,7 +227,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 {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor 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 @@ -249,14 +256,15 @@ export default function triggerKeyEvent( .then(() => { if (!target) { throw new Error( - 'Must pass an element or selector to `triggerKeyEvent`.' + 'Must pass an element, selector, or descriptor to `triggerKeyEvent`.' ); } let element = getElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`triggerKeyEvent('${target}', ...)\`.` + `Element not found when calling \`triggerKeyEvent('${description}')\`.` ); } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/type-in.ts b/addon/addon-test-support/@ember/test-helpers/dom/type-in.ts index 9d52d22a4..856cf1166 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/type-in.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/type-in.ts @@ -12,6 +12,7 @@ import Target, { import { __triggerKeyEvent__ } from './trigger-key-event'; import { log } from './-logging'; import { runHooks, registerHook } from '../helper-hooks'; +import getDescription from './-get-description'; export interface Options { delay?: number; @@ -33,7 +34,7 @@ registerHook('typeIn', 'start', (target: Target, text: string) => { * per character of the passed text (this may vary on some browsers). * * @public - * @param {string|Element} target the element or selector to enter text into + * @param {string|Element|IDOMElementDescriptor} target the element, selector, or descriptor to enter text into * @param {string} text the test to fill the element with * @param {Object} options {delay: x} (default 50) number of milliseconds to wait per keypress * @return {Promise} resolves when the application is settled @@ -56,14 +57,17 @@ export default function typeIn( }) .then(() => { if (!target) { - throw new Error('Must pass an element or selector to `typeIn`.'); + throw new Error( + 'Must pass an element, selector, or descriptor to `typeIn`.' + ); } const element = getElement(target); if (!element) { + let description = getDescription(target); throw new Error( - `Element not found when calling \`typeIn('${target}')\`` + `Element not found when calling \`typeIn('${description}')\`` ); } @@ -82,11 +86,15 @@ export default function typeIn( if (isFormControl(element)) { if (element.disabled) { - throw new Error(`Can not \`typeIn\` disabled '${target}'.`); + throw new Error( + `Can not \`typeIn\` disabled '${getDescription(target)}'.` + ); } if ('readOnly' in element && element.readOnly) { - throw new Error(`Can not \`typeIn\` readonly '${target}'.`); + throw new Error( + `Can not \`typeIn\` readonly '${getDescription(target)}'.` + ); } } diff --git a/addon/addon-test-support/@ember/test-helpers/dom/wait-for.ts b/addon/addon-test-support/@ember/test-helpers/dom/wait-for.ts index cc40e7a6b..0232f3fc9 100644 --- a/addon/addon-test-support/@ember/test-helpers/dom/wait-for.ts +++ b/addon/addon-test-support/@ember/test-helpers/dom/wait-for.ts @@ -1,6 +1,11 @@ import waitUntil from '../wait-until'; import getElement from './-get-element'; import getElements from './-get-elements'; +import { + type IDOMElementDescriptor, + lookupDescriptorData, +} from 'dom-element-descriptors'; +import getDescription from './-get-description'; export interface Options { timeout?: number; @@ -13,7 +18,7 @@ export interface Options { that it does not wait for general settledness, this is quite useful for testing interim DOM states (e.g. loading states, pending promises, etc). - @param {string} selector the selector to wait for + @param {string|IDOMElementDescriptor} target the selector or DOM element descriptor to wait for @param {Object} [options] the options to be used @param {number} [options.timeout=1000] the time to wait (in ms) for a match @param {number} [options.count=null] the number of elements that should match the provided selector (null means one or more) @@ -26,30 +31,33 @@ export interface Options { await waitFor('.my-selector', { timeout: 2000 }) */ export default function waitFor( - selector: string, + target: string | IDOMElementDescriptor, options: Options = {} ): Promise { return Promise.resolve().then(() => { - if (!selector) { - throw new Error('Must pass a selector to `waitFor`.'); + if (typeof target !== 'string' && !lookupDescriptorData(target)) { + throw new Error( + 'Must pass a selector or DOM element descriptor to `waitFor`.' + ); } let { timeout = 1000, count = null, timeoutMessage } = options; if (!timeoutMessage) { - timeoutMessage = `waitFor timed out waiting for selector "${selector}"`; + let description = getDescription(target); + timeoutMessage = `waitFor timed out waiting for selector "${description}"`; } let callback: () => Element | Element[] | void | null; if (count !== null) { callback = () => { - let elements = getElements(selector); + let elements = Array.from(getElements(target)); if (elements.length === count) { - return Array.from(elements); + return elements; } return; }; } else { - callback = () => getElement(selector); + callback = () => getElement(target); } return waitUntil(callback, { timeout, timeoutMessage }); }); diff --git a/addon/package.json b/addon/package.json index 3122c61fb..a2fbe0197 100644 --- a/addon/package.json +++ b/addon/package.json @@ -50,6 +50,7 @@ "@simple-dom/interface": "^1.4.0", "broccoli-debug": "^0.6.5", "broccoli-funnel": "^3.0.8", + "dom-element-descriptors": "^0.5.0", "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.2.0" diff --git a/addon/tests/integration/dom/scroll-to-test.js b/addon/tests/integration/dom/scroll-to-test.js index 0383dbb5b..2ec6bad03 100644 --- a/addon/tests/integration/dom/scroll-to-test.js +++ b/addon/tests/integration/dom/scroll-to-test.js @@ -121,7 +121,7 @@ module('DOM Helper: scroll-to', function (hooks) { test('It throws an error if a target is not supplied', async function (assert) { assert.rejects( scrollTo('', 0, 0), - new Error('Must pass an element or selector to `scrollTo`.') + new Error('Must pass an element, selector, or descriptor to `scrollTo`.') ); }); diff --git a/addon/tests/unit/dom/blur-test.js b/addon/tests/unit/dom/blur-test.js index acfe8a21f..7a80acfd3 100644 --- a/addon/tests/unit/dom/blur-test.js +++ b/addon/tests/unit/dom/blur-test.js @@ -9,6 +9,7 @@ import { import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import { isEdge } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { createDescriptor } from 'dom-element-descriptors'; let focusSteps = ['focus', 'focusin']; let blurSteps = ['blur', 'focusout']; @@ -140,4 +141,28 @@ module('DOM Helper: blur', function (hooks) { 'activeElement updated' ); }); + + test('bluring via descriptor with context set', async function (assert) { + await setupContext(context); + await blur(createDescriptor({ element: elementWithFocus })); + + assert.verifySteps(blurSteps); + assert.notEqual( + document.activeElement, + elementWithFocus, + 'activeElement updated' + ); + }); + + test('bluring via descriptor without context set', async function (assert) { + await blur(elementWithFocus); + await blur(createDescriptor({ element: elementWithFocus })); + + assert.verifySteps(blurSteps); + assert.notEqual( + document.activeElement, + elementWithFocus, + 'activeElement updated' + ); + }); }); diff --git a/addon/tests/unit/dom/click-test.js b/addon/tests/unit/dom/click-test.js index 8440eb8bc..88a5925db 100644 --- a/addon/tests/unit/dom/click-test.js +++ b/addon/tests/unit/dom/click-test.js @@ -11,6 +11,7 @@ import { unregisterHooks, buildExpectedSteps, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; module('DOM Helper: click', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -82,6 +83,23 @@ module('DOM Helper: click', function (hooks) { assert.verifySteps(['mousedown', 'mouseup', 'click']); }); + test('clicking a div via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await click(createDescriptor({ element })); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + + test('clicking a div via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await click(createDescriptor({ element })); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + test('does not run sync', async function (assert) { element = buildInstrumentedElement('div'); @@ -225,6 +243,33 @@ module('DOM Helper: click', function (hooks) { ); }); + test('clicking a input via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await click(createDescriptor({ element })); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + + test('clicking a input via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await click(createDescriptor({ element })); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('clicking a input via selector without context set', function (assert) { element = buildInstrumentedElement('input'); diff --git a/addon/tests/unit/dom/double-click-test.js b/addon/tests/unit/dom/double-click-test.js index ef658d42e..a9f64f33c 100644 --- a/addon/tests/unit/dom/double-click-test.js +++ b/addon/tests/unit/dom/double-click-test.js @@ -15,6 +15,7 @@ import { unregisterHooks, buildExpectedSteps, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; const expectedEvents = [ 'mousedown', @@ -122,6 +123,39 @@ module('DOM Helper: doubleClick', function (hooks) { ]); }); + test('double-clicking a div via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await doubleClick(createDescriptor({ element })); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + + test('double-clicking a div via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await doubleClick(createDescriptor({ element })); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + test('does not run sync', async function (assert) { element = buildInstrumentedElement('div'); @@ -265,6 +299,33 @@ module('DOM Helper: doubleClick', function (hooks) { ); }); + test('double-clicking a input via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await doubleClick(createDescriptor({ element })); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + + test('double-clicking a input via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await doubleClick(createDescriptor({ element })); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('double-clicking a input via selector without context set', function (assert) { element = buildInstrumentedElement('input'); diff --git a/addon/tests/unit/dom/fill-in-test.js b/addon/tests/unit/dom/fill-in-test.js index 5d44dec17..f765a3340 100644 --- a/addon/tests/unit/dom/fill-in-test.js +++ b/addon/tests/unit/dom/fill-in-test.js @@ -8,6 +8,7 @@ import { unregisterHooks, buildExpectedSteps, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; let clickSteps = ['focus', 'focusin', 'input', 'change']; @@ -170,7 +171,7 @@ module('DOM Helper: fillIn', function (hooks) { assert.equal(element.value, 'foo'); }); - test('filling an input via element with context set', async function (assert) { + test('filling a textarea via element with context set', async function (assert) { element = buildInstrumentedElement('textarea'); await setupContext(context); @@ -185,6 +186,21 @@ module('DOM Helper: fillIn', function (hooks) { assert.equal(element.value, 'foo'); }); + test('filling a textarea via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('textarea'); + + await setupContext(context); + await fillIn(createDescriptor({ element }), 'foo'); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + assert.equal(element.value, 'foo'); + }); + test('filling an input via selector with context set', async function (assert) { element = buildInstrumentedElement('input'); @@ -215,6 +231,21 @@ module('DOM Helper: fillIn', function (hooks) { assert.equal(element.value, 'foo'); }); + test('filling an input via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await fillIn(createDescriptor({ element }), 'foo'); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + assert.equal(element.value, 'foo'); + }); + test('filling a content editable div via element with context set', async function (assert) { element = buildInstrumentedElement('div'); element.setAttribute('contenteditable', ''); @@ -232,6 +263,22 @@ module('DOM Helper: fillIn', function (hooks) { assert.equal(element.innerHTML, 'foo'); }); + test('filling a content editable div via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + element.setAttribute('contenteditable', ''); + + await setupContext(context); + await fillIn(createDescriptor({ element }), 'foo'); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + assert.equal(element.innerHTML, 'foo'); + }); + test('filling an input via element without context set', async function (assert) { element = buildInstrumentedElement('input'); @@ -246,6 +293,20 @@ module('DOM Helper: fillIn', function (hooks) { assert.equal(element.value, 'foo'); }); + test('filling an input via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await fillIn(createDescriptor({ element }), 'foo'); + + assert.verifySteps(clickSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + assert.equal(element.value, 'foo'); + }); + test('filling an input via selector with empty string', async function (assert) { element = buildInstrumentedElement('input'); diff --git a/addon/tests/unit/dom/focus-test.js b/addon/tests/unit/dom/focus-test.js index b81148d63..49f298071 100644 --- a/addon/tests/unit/dom/focus-test.js +++ b/addon/tests/unit/dom/focus-test.js @@ -12,6 +12,7 @@ import { } from '../../helpers/events'; import { isEdge } from '../../helpers/browser-detect'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { createDescriptor } from 'dom-element-descriptors'; let focusSteps = ['focus', 'focusin']; let blurSteps = ['blur', 'focusout']; @@ -82,6 +83,13 @@ module('DOM Helper: focus', function (hooks) { assert.rejects(focus(element), /is not focusable/); }); + test('focusing a div via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + assert.rejects(focus(createDescriptor({ element })), /is not focusable/); + }); + test('focusing a disabled form control', async function (assert) { element = buildInstrumentedElement('input'); element.setAttribute('disabled', ''); @@ -181,6 +189,20 @@ module('DOM Helper: focus', function (hooks) { ); }); + test('focusing an input via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await focus(createDescriptor({ element })); + + assert.verifySteps(focusSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('focusing an input via element without context set', async function (assert) { element = buildInstrumentedElement('input'); @@ -194,6 +216,19 @@ module('DOM Helper: focus', function (hooks) { ); }); + test('focusing an input via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await focus(createDescriptor({ element })); + + assert.verifySteps(focusSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('focusing an input via selector without context set', async function (assert) { element = buildInstrumentedElement('input'); diff --git a/addon/tests/unit/dom/select-test.js b/addon/tests/unit/dom/select-test.js index 9c634a830..0f891c9f1 100644 --- a/addon/tests/unit/dom/select-test.js +++ b/addon/tests/unit/dom/select-test.js @@ -6,6 +6,7 @@ import { registerHooks, unregisterHooks, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; let selectSteps = ['focus', 'focusin', 'input', 'change']; let additionalSteps = ['input', 'change']; @@ -51,7 +52,10 @@ module('DOM Helper: select', function (hooks) { test('select without target', async function (assert) { await setupContext(context); - assert.rejects(select(), /Must pass an element or selector to `select`./); + assert.rejects( + select(), + /Must pass an element, selector, or descriptor to `select`./ + ); }); test('select without options', async function (assert) { @@ -228,4 +232,23 @@ module('DOM Helper: select', function (hooks) { assert.equal(element.selectedOptions[0].value, 'orange'); assert.equal(element.selectedOptions.length, 1); }); + + test('select with descriptor', async function (assert) { + const optionValues = ['apple', 'orange', 'pineapple', 'pear']; + element = buildInstrumentedElement('select'); + element.multiple = false; + + optionValues.forEach((optionValue) => { + const optionElement = buildInstrumentedElement('option'); + optionElement.value = optionValue; + element.appendChild(optionElement); + }); + + await setupContext(context); + + await select(createDescriptor({ element }), 'orange'); + + assert.verifySteps(selectSteps); + assert.equal(element.selectedIndex, 1); + }); }); diff --git a/addon/tests/unit/dom/tap-test.js b/addon/tests/unit/dom/tap-test.js index d1a2ac541..fbde8a5dc 100644 --- a/addon/tests/unit/dom/tap-test.js +++ b/addon/tests/unit/dom/tap-test.js @@ -7,6 +7,7 @@ import { unregisterHooks, buildExpectedSteps, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; module('DOM Helper: tap', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -93,6 +94,21 @@ module('DOM Helper: tap', function (hooks) { ]); }); + test('tapping a div via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await tap(createDescriptor({ element })); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'mouseup', + 'click', + ]); + }); + test('tapping a div via element without context set', async function (assert) { element = buildInstrumentedElement('div'); @@ -107,6 +123,20 @@ module('DOM Helper: tap', function (hooks) { ]); }); + test('tapping a div via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await tap(createDescriptor({ element })); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'mouseup', + 'click', + ]); + }); + test('tapping passes options through to mouse events', async function (assert) { element = buildInstrumentedElement('div', [ 'clientX', @@ -203,6 +233,20 @@ module('DOM Helper: tap', function (hooks) { ); }); + test('tapping a input via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await tap(createDescriptor({ element })); + + assert.verifySteps(tapSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('tapping a input via element without context set', async function (assert) { element = buildInstrumentedElement('input'); @@ -216,6 +260,19 @@ module('DOM Helper: tap', function (hooks) { ); }); + test('tapping a input via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('input'); + + await tap(createDescriptor({ element })); + + assert.verifySteps(tapSteps); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + }); + test('tapping a input via selector without context set', function (assert) { element = buildInstrumentedElement('input'); diff --git a/addon/tests/unit/dom/trigger-event-test.js b/addon/tests/unit/dom/trigger-event-test.js index 29940f190..50dfa1a65 100644 --- a/addon/tests/unit/dom/trigger-event-test.js +++ b/addon/tests/unit/dom/trigger-event-test.js @@ -7,6 +7,7 @@ import { } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { createDescriptor } from 'dom-element-descriptors'; module('DOM Helper: triggerEvent', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -95,6 +96,15 @@ module('DOM Helper: triggerEvent', function (hooks) { assert.verifySteps(['mouseenter']); }); + test('triggering event via descriptor with context set fires the given event type', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await triggerEvent(createDescriptor({ element }), 'mouseenter'); + + assert.verifySteps(['mouseenter']); + }); + test('triggering event via element without context set fires the given event type', async function (assert) { element = buildInstrumentedElement('div'); @@ -103,6 +113,14 @@ module('DOM Helper: triggerEvent', function (hooks) { assert.verifySteps(['mouseenter']); }); + test('triggering event via descriptor without context set fires the given event type', async function (assert) { + element = buildInstrumentedElement('div'); + + await triggerEvent(createDescriptor({ element }), 'mouseenter'); + + assert.verifySteps(['mouseenter']); + }); + test('triggering event via selector without context set', function (assert) { element = buildInstrumentedElement('div'); diff --git a/addon/tests/unit/dom/trigger-key-event-test.js b/addon/tests/unit/dom/trigger-key-event-test.js index f39359caf..8217f6668 100644 --- a/addon/tests/unit/dom/trigger-key-event-test.js +++ b/addon/tests/unit/dom/trigger-key-event-test.js @@ -7,6 +7,7 @@ import { } from '@ember/test-helpers'; import { buildInstrumentedElement, insertElement } from '../../helpers/events'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { createDescriptor } from 'dom-element-descriptors'; module('DOM Helper: triggerKeyEvent', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -158,6 +159,15 @@ module('DOM Helper: triggerKeyEvent', function (hooks) { assert.verifySteps(['keydown']); }); + test('triggering via descriptor with context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await triggerKeyEvent(createDescriptor({ element }), 'keydown', 13); + + assert.verifySteps(['keydown']); + }); + test('triggering via element without context set', async function (assert) { element = buildInstrumentedElement('div'); @@ -166,6 +176,14 @@ module('DOM Helper: triggerKeyEvent', function (hooks) { assert.verifySteps(['keydown']); }); + test('triggering via descriptor without context set', async function (assert) { + element = buildInstrumentedElement('div'); + + await triggerKeyEvent(createDescriptor({ element }), 'keydown', 13); + + assert.verifySteps(['keydown']); + }); + test('triggering via selector without context set', function (assert) { element = buildInstrumentedElement('div'); diff --git a/addon/tests/unit/dom/type-in-test.js b/addon/tests/unit/dom/type-in-test.js index ee7ff5342..2643ac675 100644 --- a/addon/tests/unit/dom/type-in-test.js +++ b/addon/tests/unit/dom/type-in-test.js @@ -10,6 +10,7 @@ import { unregisterHooks, buildExpectedSteps, } from '../../helpers/register-hooks'; +import { createDescriptor } from 'dom-element-descriptors'; /* * Event order based on https://jsbin.com/zitazuxabe/edit?html,js,console,output @@ -127,6 +128,19 @@ module('DOM Helper: typeIn', function (hooks) { assert.equal(element.value, 'foo'); }); + test('typing in an input via descriptor', async function (assert) { + element = buildInstrumentedElement('input'); + await typeIn(createDescriptor({ element }), 'foo'); + + assert.verifySteps(expectedEvents); + assert.strictEqual( + document.activeElement, + element, + 'activeElement updated' + ); + assert.equal(element.value, 'foo'); + }); + test('it triggers key events with correct arguments', async function (assert) { element = buildInstrumentedElement('input', ['key', 'shiftKey']); await typeIn(element, 'F o'); diff --git a/addon/tests/unit/dom/wait-for-test.js b/addon/tests/unit/dom/wait-for-test.js index cfaa989ce..0a3355a70 100644 --- a/addon/tests/unit/dom/wait-for-test.js +++ b/addon/tests/unit/dom/wait-for-test.js @@ -1,6 +1,7 @@ import { module, test } from 'qunit'; import { waitFor, setupContext, teardownContext } from '@ember/test-helpers'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; +import { registerDescriptorData } from 'dom-element-descriptors'; module('DOM Helper: waitFor', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -22,6 +23,22 @@ module('DOM Helper: waitFor', function (hooks) { document.getElementById('ember-testing').innerHTML = ''; }); + class SelectorData { + constructor(selector) { + this.selector = selector; + } + + get elements() { + return rootElement.querySelectorAll(this.selector); + } + } + + class SelectorDescriptor { + constructor(selector) { + registerDescriptorData(this, new SelectorData(selector)); + } + } + test('wait for selector without context set', async function (assert) { assert.rejects( waitFor('.something'), @@ -29,6 +46,18 @@ module('DOM Helper: waitFor', function (hooks) { ); }); + test('wait for descriptor without context set', async function (assert) { + let waitPromise = waitFor(new SelectorDescriptor('.something')); + + setTimeout(() => { + rootElement.innerHTML = `
Hi!
`; + }, 10); + + let element = await waitPromise; + + assert.equal(element.textContent, 'Hi!'); + }); + test('wait for selector', async function (assert) { await setupContext(context); @@ -43,6 +72,20 @@ module('DOM Helper: waitFor', function (hooks) { assert.equal(element.textContent, 'Hi!'); }); + test('wait for descriptor without context set', async function (assert) { + await setupContext(context); + + let waitPromise = waitFor(new SelectorDescriptor('.something')); + + setTimeout(() => { + rootElement.innerHTML = `
Hi!
`; + }, 10); + + let element = await waitPromise; + + assert.equal(element.textContent, 'Hi!'); + }); + test('wait for count of selector', async function (assert) { await setupContext(context); @@ -67,6 +110,32 @@ module('DOM Helper: waitFor', function (hooks) { ); }); + test('wait for count of descriptor', async function (assert) { + await setupContext(context); + + let waitPromise = waitFor(new SelectorDescriptor('.something'), { + count: 2, + }); + + setTimeout(() => { + rootElement.innerHTML = `
No!
`; + }, 10); + + setTimeout(() => { + rootElement.innerHTML = ` +
Hi!
+
Bye!
+ `; + }, 20); + + let elements = await waitPromise; + + assert.deepEqual( + elements.map((e) => e.textContent), + ['Hi!', 'Bye!'] + ); + }); + test('wait for selector with timeout', async function (assert) { assert.expect(2); diff --git a/addon/type-tests/api.ts b/addon/type-tests/api.ts index e0bbdde1d..778d3c413 100644 --- a/addon/type-tests/api.ts +++ b/addon/type-tests/api.ts @@ -80,6 +80,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 type { IDOMElementDescriptor } from 'dom-element-descriptors'; // DOM Interaction Helpers expectTypeOf(blur).toEqualTypeOf<(target?: Target) => Promise>(); @@ -94,7 +95,11 @@ expectTypeOf(fillIn).toEqualTypeOf< >(); expectTypeOf(focus).toEqualTypeOf<(target: Target) => Promise>(); expectTypeOf(scrollTo).toEqualTypeOf< - (target: string | HTMLElement, x: number, y: number) => Promise + ( + target: string | HTMLElement | IDOMElementDescriptor, + x: number, + y: number + ) => Promise >(); expectTypeOf(select).toEqualTypeOf< ( @@ -175,7 +180,7 @@ expectTypeOf(clearRender).toEqualTypeOf<() => Promise>(); // Wait Helpers expectTypeOf(waitFor).toEqualTypeOf< ( - selector: string, + selector: string | IDOMElementDescriptor, options?: { timeout?: number; count?: number | null; diff --git a/yarn.lock b/yarn.lock index d2558c1d8..0ac82b601 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5180,6 +5180,11 @@ documentation@^14.0.1: "@vue/compiler-sfc" "^3.2.37" vue-template-compiler "^2.7.8" +dom-element-descriptors@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/dom-element-descriptors/-/dom-element-descriptors-0.5.0.tgz#78c37d2a2d3566b1e2b83804ba04a21536a99ab8" + integrity sha512-CVzntLid1oFVHTKdTp/Qu7Kz+wSm8uO30TSQyAJ6n4Dz09yTzVQn3S1oRhVhUubxdMuKs1DjDqt88pubHagbPw== + dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"