diff --git a/API.md b/API.md index 15885bf0a..c572b62cb 100644 --- a/API.md +++ b/API.md @@ -4,6 +4,7 @@ - [DOM Interaction Helpers](#dom-interaction-helpers) - [click](#click) + - [doubleClick](#doubleclick) - [tap](#tap) - [focus](#focus) - [blur](#blur) @@ -82,6 +83,45 @@ to continue to emulate how actual browsers handle clicking a given element. Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void>** resolves when settled +### doubleClick + +Double-clicks on the specified target. + +Sends a number of events intending to simulate a "real" user double-clicking on an +element. + +For non-focusable elements the following events are triggered (in order): + +- `mousedown` +- `mouseup` +- `click` +- `mousedown` +- `mouseup` +- `click` +- `dblclick` + +For focusable (e.g. form control) elements the following events are triggered +(in order): + +- `mousedown` +- `focus` +- `focusin` +- `mouseup` +- `click` +- `mousedown` +- `mouseup` +- `click` +- `dblclick` + +The exact listing of events that are triggered may change over time as needed +to continue to emulate how actual browsers handle double-clicking a given element. + +**Parameters** + +- `target` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Element](https://developer.mozilla.org/docs/Web/API/Element))** the element or selector to double-click on + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void>** resolves when settled + ### tap Taps on the specified target. diff --git a/addon-test-support/@ember/test-helpers/dom/double-click.js b/addon-test-support/@ember/test-helpers/dom/double-click.js new file mode 100644 index 000000000..dfab3082c --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/double-click.js @@ -0,0 +1,77 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import { __focus__ } from './focus'; +import settled from '../settled'; +import isFocusable from './-is-focusable'; +import { nextTickPromise } from '../-utils'; + +/** + @private + @param {Element} element the element to double-click on +*/ +export function __doubleClick__(element) { + fireEvent(element, 'mousedown'); + + if (isFocusable(element)) { + __focus__(element); + } + + fireEvent(element, 'mouseup'); + fireEvent(element, 'click'); + fireEvent(element, 'mousedown'); + fireEvent(element, 'mouseup'); + fireEvent(element, 'click'); + fireEvent(element, 'dblclick'); +} + +/** + Double-clicks on the specified target. + + Sends a number of events intending to simulate a "real" user clicking on an + element. + + For non-focusable elements the following events are triggered (in order): + + - `mousedown` + - `mouseup` + - `click` + - `mousedown` + - `mouseup` + - `click` + - `dblclick` + + For focusable (e.g. form control) elements the following events are triggered + (in order): + + - `mousedown` + - `focus` + - `focusin` + - `mouseup` + - `click` + - `mousedown` + - `mouseup` + - `click` + - `dblclick` + + The exact listing of events that are triggered may change over time as needed + to continue to emulate how actual browsers handle clicking a given element. + + @public + @param {string|Element} target the element or selector to double-click on + @return {Promise} resolves when settled +*/ +export default function doubleClick(target) { + return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `doubleClick`.'); + } + + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`doubleClick('${target}')\`.`); + } + + __doubleClick__(element); + return settled(); + }); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 008b6b865..6904f7219 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -24,6 +24,7 @@ export { default as validateErrorHandler } from './validate-error-handler'; // DOM Helpers export { default as click } from './dom/click'; +export { default as doubleClick } from './dom/double-click'; export { default as tap } from './dom/tap'; export { default as focus } from './dom/focus'; export { default as blur } from './dom/blur'; diff --git a/documentation.yml b/documentation.yml index 7ff018cd9..a1ca75dfe 100644 --- a/documentation.yml +++ b/documentation.yml @@ -2,6 +2,7 @@ toc: - name: DOM Interaction Helpers children: - click + - doubleClick - tap - focus - blur diff --git a/tests/unit/dom/double-click-test.js b/tests/unit/dom/double-click-test.js new file mode 100644 index 000000000..2a8658639 --- /dev/null +++ b/tests/unit/dom/double-click-test.js @@ -0,0 +1,213 @@ +import { module, test } from 'qunit'; +import { doubleClick, setupContext, teardownContext } from '@ember/test-helpers'; +import { buildInstrumentedElement, instrumentElement, insertElement } from '../../helpers/events'; +import { isIE11 } from '../../helpers/browser-detect'; +import hasEmberVersion from 'ember-test-helpers/has-ember-version'; + +module('DOM Helper: doubleClick', function(hooks) { + if (!hasEmberVersion(2, 4)) { + return; + } + + let context, element; + + hooks.beforeEach(function() { + context = {}; + }); + + hooks.afterEach(async function() { + element.setAttribute('data-skip-steps', true); + + if (element) { + element.parentNode.removeChild(element); + } + if (context.owner) { + await teardownContext(context); + } + + document.getElementById('ember-testing').innerHTML = ''; + }); + + module('non-focusable element types', function() { + test('double-clicking a div via selector with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await doubleClick(`#${element.id}`); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + + test('double-clicking a div via element with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + await doubleClick(element); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + + test('double-clicking a div via element without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await doubleClick(element); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('div'); + + let promise = doubleClick(element); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + + test('rejects if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + await setupContext(context); + + assert.rejects( + doubleClick(`#foo-bar-baz-not-here-ever-bye-bye`), + /Element not found when calling `doubleClick\('#foo-bar-baz-not-here-ever-bye-bye'\)`/ + ); + }); + + test('double-clicking a div via selector without context set', function(assert) { + element = buildInstrumentedElement('div'); + + assert.rejects( + doubleClick(`#${element.id}`), + /Must setup rendering context before attempting to interact with elements/ + ); + }); + }); + + module('focusable element types', function() { + let clickSteps = [ + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]; + + if (isIE11) { + clickSteps = [ + 'mousedown', + 'focusin', + 'mouseup', + 'click', + 'focus', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]; + } + + test('double-clicking a input via selector with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await doubleClick(`#${element.id}`); + + assert.verifySteps(clickSteps); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('double-clicking a input via element with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await setupContext(context); + await doubleClick(element); + + assert.verifySteps(clickSteps); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('double-clicking a input via element without context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await doubleClick(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'); + + assert.rejects( + doubleClick(`#${element.id}`), + /Must setup rendering context before attempting to interact with elements/ + ); + }); + }); + + module('elements in different realms', function() { + test('double-clicking an element in a different realm', async function(assert) { + element = document.createElement('iframe'); + + insertElement(element); + + let iframeDocument = element.contentDocument; + let iframeElement = iframeDocument.createElement('div'); + + instrumentElement(iframeElement); + + await doubleClick(iframeElement); + + assert.verifySteps([ + 'mousedown', + 'mouseup', + 'click', + 'mousedown', + 'mouseup', + 'click', + 'dblclick', + ]); + }); + }); +});