From f5396faeca0938b7b64dbc1484e2b59631a0a542 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 5 Dec 2017 18:12:13 -0500 Subject: [PATCH 01/61] Add `waitUntil` helper. Implementation from https://github.com/cibernox/ember-native-dom-helpers/blob/v0.5.8/addon-test-support/wait-until.js. --- .../@ember/test-helpers/helpers/wait-until.js | 22 ++++++++++ .../@ember/test-helpers/index.js | 1 + tests/unit/helpers/wait-until-test.js | 40 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 addon-test-support/@ember/test-helpers/helpers/wait-until.js create mode 100644 tests/unit/helpers/wait-until-test.js diff --git a/addon-test-support/@ember/test-helpers/helpers/wait-until.js b/addon-test-support/@ember/test-helpers/helpers/wait-until.js new file mode 100644 index 000000000..5f03f9bb6 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/helpers/wait-until.js @@ -0,0 +1,22 @@ +import { Promise } from 'rsvp'; + +export default function(callback, options = {}) { + let timeout = 'timeout' in options ? options.timeout : 1000; + let waitUntilTimedOut = new Error('waitUntil timed out'); + + return new Promise(function(resolve, reject) { + let time = 0; + function tick() { + time += 10; + let value = callback(); + if (value) { + resolve(value); + } else if (time < timeout) { + setTimeout(tick, 10); + } else { + reject(waitUntilTimedOut); + } + } + setTimeout(tick, 10); + }); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index f543a20aa..edee1df6c 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -13,3 +13,4 @@ export { default as setupRenderingContext, render, clearRender } from './setup-r export { default as teardownRenderingContext } from './teardown-rendering-context'; export { default as settled, isSettled, getState as getSettledState } from './settled'; export { default as validateErrorHandler } from './validate-error-handler'; +export { default as waitUntil } from './helpers/wait-until'; diff --git a/tests/unit/helpers/wait-until-test.js b/tests/unit/helpers/wait-until-test.js new file mode 100644 index 000000000..9d01cff2a --- /dev/null +++ b/tests/unit/helpers/wait-until-test.js @@ -0,0 +1,40 @@ +import { module, test } from 'qunit'; +import { waitUntil } from '@ember/test-helpers'; + +module('DOM helper: waitUntil', function() { + test('waits until the provided function returns true', async function(assert) { + let current = false; + assert.step('before invocation'); + let waiter = waitUntil(() => current).then(() => { + assert.step('after resolved'); + + assert.verifySteps(['before invocation', 'after invocation', 'after resolved']); + }); + assert.step('after invocation'); + + setTimeout(() => (current = true)); + + return waiter; + }); + + test('waits until timeout expires', function(assert) { + assert.step('before invocation'); + let waiter = waitUntil(() => {}, { timeout: 20 }); + assert.step('after invocation'); + + setTimeout(() => assert.step('waiting'), 10); + + return waiter + .catch(reason => { + assert.step(`catch handler: ${reason.message}`); + }) + .finally(() => { + assert.verifySteps([ + 'before invocation', + 'after invocation', + 'waiting', + 'catch handler: waitUntil timed out', + ]); + }); + }); +}); From 657e8d82778a3d98899a2fe34f6893a3ad303e55 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 5 Dec 2017 21:29:27 -0500 Subject: [PATCH 02/61] Add initial implementations for `click` and `focus`. Original implementations are from https://github.com/cibernox/ember-native-dom-helpers and then modified: * Prefer default exports when reasonable * Remove manual run-wrapping when firing events (event listeners should already be run-wrapped). * Allow `focusin` to bubble in `focus` * Remove `context` arguments from helper methods. --- .../@ember/test-helpers/dom/-get-element.js | 16 ++ .../@ember/test-helpers/dom/blur.js | 0 .../@ember/test-helpers/dom/click.js | 21 ++ .../@ember/test-helpers/dom/fire-event.js | 225 ++++++++++++++++++ .../@ember/test-helpers/dom/focus.js | 55 +++++ .../@ember/test-helpers/index.js | 6 +- .../test-helpers/{helpers => }/wait-until.js | 0 7 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 addon-test-support/@ember/test-helpers/dom/-get-element.js create mode 100644 addon-test-support/@ember/test-helpers/dom/blur.js create mode 100644 addon-test-support/@ember/test-helpers/dom/click.js create mode 100644 addon-test-support/@ember/test-helpers/dom/fire-event.js create mode 100644 addon-test-support/@ember/test-helpers/dom/focus.js rename addon-test-support/@ember/test-helpers/{helpers => }/wait-until.js (100%) diff --git a/addon-test-support/@ember/test-helpers/dom/-get-element.js b/addon-test-support/@ember/test-helpers/dom/-get-element.js new file mode 100644 index 000000000..f392b2ac6 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/-get-element.js @@ -0,0 +1,16 @@ +import { getContext } from '../setup-context'; + +export default function getElement(selectorOrElement) { + if ( + selectorOrElement instanceof Window || + selectorOrElement instanceof Document || + selectorOrElement instanceof HTMLElement || + selectorOrElement instanceof SVGElement + ) { + return selectorOrElement; + } + + let rootElement = getContext().element; + + return rootElement.querySelector(selectorOrElement); +} diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js new file mode 100644 index 000000000..e69de29bb diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js new file mode 100644 index 000000000..bd46c7a74 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -0,0 +1,21 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import focus from './focus'; +import settled from '../settled'; + +/* + @method click + @param {String|HTMLElement} selector + @return {RSVP.Promise} + @public +*/ +export default function click(selector) { + let element = getElement(selector); + + fireEvent(element, 'mousedown'); + focus(element); + fireEvent(element, 'mouseup'); + fireEvent(element, 'click'); + + return settled(); +} diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js new file mode 100644 index 000000000..51919e4c5 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -0,0 +1,225 @@ +import { merge } from '@ember/polyfills'; + +const DEFAULT_EVENT_OPTIONS = { bubbles: true, cancelable: true }; +const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; +const MOUSE_EVENT_TYPES = [ + 'click', + 'mousedown', + 'mouseup', + 'dblclick', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', +]; +const FILE_SELECTION_EVENT_TYPES = ['change']; + +/* + @method fireEvent + @param {HTMLElement} element + @param {String} type + @param {Object} (optional) options + @return {Event} The dispatched event + @private +*/ +export default function fireEvent(element, type, options = {}) { + if (!element) { + throw new Error('Must pass an element to `fireEvent`'); + } + + let event; + if (KEYBOARD_EVENT_TYPES.indexOf(type) > -1) { + event = buildKeyboardEvent(type, options); + } else if (MOUSE_EVENT_TYPES.indexOf(type) > -1) { + let rect; + if (element instanceof Window) { + rect = element.document.documentElement.getBoundingClientRect(); + } else if (element instanceof Document) { + rect = element.documentElement.getBoundingClientRect(); + } else if (element instanceof HTMLElement || element instanceof SVGElement) { + 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, + }; + + event = buildMouseEvent(type, merge(simulatedCoordinates, options)); + } else if (FILE_SELECTION_EVENT_TYPES.indexOf(type) > -1 && element.files) { + event = buildFileEvent(type, element, options); + } else { + event = buildBasicEvent(type, options); + } + + element.dispatchEvent(event); +} + +/* + @method buildBasicEvent + @param {String} type + @param {Object} (optional) options + @return {Event} + @private +*/ +function buildBasicEvent(type, options = {}) { + let event = document.createEvent('Events'); + + let bubbles = options.bubbles !== undefined ? options.bubbles : true; + let cancelable = options.cancelable !== undefined ? options.cancelable : true; + + delete options.bubbles; + delete options.cancelable; + + // bubbles and cancelable are readonly, so they can be + // set when initializing event + event.initEvent(type, bubbles, cancelable); + merge(event, options); + return event; +} + +/* + @method buildMouseEvent + @param {String} type + @param {Object} (optional) options + @return {Event} + @private +*/ +function buildMouseEvent(type, options = {}) { + let event; + try { + event = document.createEvent('MouseEvents'); + let eventOpts = merge(merge({}, DEFAULT_EVENT_OPTIONS), options); + event.initMouseEvent( + type, + eventOpts.bubbles, + eventOpts.cancelable, + window, + eventOpts.detail, + eventOpts.screenX, + eventOpts.screenY, + eventOpts.clientX, + eventOpts.clientY, + eventOpts.ctrlKey, + eventOpts.altKey, + eventOpts.shiftKey, + eventOpts.metaKey, + eventOpts.button, + eventOpts.relatedTarget + ); + } catch (e) { + event = buildBasicEvent(type, options); + } + return event; +} + +/* + @method buildKeyboardEvent + @param {String} type + @param {Object} (optional) options + @return {Event} + @private +*/ +function buildKeyboardEvent(type, options = {}) { + let eventOpts = merge(merge({}, DEFAULT_EVENT_OPTIONS), options); + let event, eventMethodName; + + try { + event = new KeyboardEvent(type, eventOpts); + + // Property definitions are required for B/C for keyboard event usage + // If this properties are not defined, when listening for key events + // keyCode/which will be 0. Also, keyCode and which now are string + // and if app compare it with === with integer key definitions, + // there will be a fail. + // + // https://w3c.github.io/uievents/#interface-keyboardevent + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + Object.defineProperty(event, 'keyCode', { + get() { + return parseInt(this.key); + }, + }); + + Object.defineProperty(event, 'which', { + get() { + return parseInt(this.key); + }, + }); + + return event; + } catch (e) { + // left intentionally blank + } + + try { + event = document.createEvent('KeyboardEvents'); + eventMethodName = 'initKeyboardEvent'; + } catch (e) { + // left intentionally blank + } + + if (!event) { + try { + event = document.createEvent('KeyEvents'); + eventMethodName = 'initKeyEvent'; + } catch (e) { + // left intentionally blank + } + } + + if (event) { + event[eventMethodName]( + type, + eventOpts.bubbles, + eventOpts.cancelable, + window, + eventOpts.ctrlKey, + eventOpts.altKey, + eventOpts.shiftKey, + eventOpts.metaKey, + eventOpts.keyCode, + eventOpts.charCode + ); + } else { + event = buildBasicEvent(type, options); + } + + return event; +} + +/* + @method buildFileEvent + @param {String} type + @param {Array} array of flies + @param {HTMLElement} element + @return {Event} + @private +*/ +function buildFileEvent(type, element, files = []) { + let event = buildBasicEvent(type); + + if (files.length > 0) { + Object.defineProperty(files, 'item', { + value(index) { + return typeof index === 'number' ? this[index] : null; + }, + }); + Object.defineProperty(element, 'files', { + value: files, + }); + } + + Object.defineProperty(event, 'target', { + value: element, + }); + + return event; +} diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js new file mode 100644 index 000000000..b064f5dba --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -0,0 +1,55 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import settled from '../settled'; + +const FOCUSABLE_TAGS = ['INPUT', 'BUTTON', 'LINK', 'SELECT', 'A', 'TEXTAREA']; +function isFocusable(el) { + let { tagName, type } = el; + + if (type === 'hidden') { + return false; + } + + if (FOCUSABLE_TAGS.indexOf(tagName) > -1 || el.contentEditable === 'true') { + return true; + } + + return el.hasAttribute('tabindex'); +} + +/* + @method focus + @param {String|HTMLElement} selector + @return {RSVP.Promise} + @public +*/ +export default function focus(selectorOrElement) { + if (!selectorOrElement) { + throw new Error('Must pass an element or selector to `focus`.'); + } + + let element = getElement(selectorOrElement); + + if (isFocusable(element)) { + let browserIsNotFocused = document.hasFocus && !document.hasFocus(); + + // 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) { + fireEvent(element, 'focusin'); + } + + // makes `document.activeElement` be `el`. If the browser is focused, it also fires a focus event + element.focus(); + + // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it + if (browserIsNotFocused) { + fireEvent(element, 'focus', { + bubbles: false, + }); + } + } + + return settled(); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index edee1df6c..7b22fc013 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -12,5 +12,9 @@ export { default as teardownContext } from './teardown-context'; export { default as setupRenderingContext, render, clearRender } from './setup-rendering-context'; export { default as teardownRenderingContext } from './teardown-rendering-context'; export { default as settled, isSettled, getState as getSettledState } from './settled'; +export { default as waitUntil } from './wait-until'; export { default as validateErrorHandler } from './validate-error-handler'; -export { default as waitUntil } from './helpers/wait-until'; + +// DOM Helpers +export { default as click } from './dom/click'; +export { default as focus } from './dom/focus'; diff --git a/addon-test-support/@ember/test-helpers/helpers/wait-until.js b/addon-test-support/@ember/test-helpers/wait-until.js similarity index 100% rename from addon-test-support/@ember/test-helpers/helpers/wait-until.js rename to addon-test-support/@ember/test-helpers/wait-until.js From 31e66a861b033889465ab2758637b6cb4fbf576b Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 5 Dec 2017 21:58:41 -0500 Subject: [PATCH 03/61] Bring over `blur` implementation. Original implementations are from https://github.com/cibernox/ember-native-dom-helpers and then modified: * Added `fireEvent(element, 'focusout')` * Remove run wrapping --- .../@ember/test-helpers/dom/-is-focusable.js | 14 +++++++ .../@ember/test-helpers/dom/blur.js | 37 +++++++++++++++++++ .../@ember/test-helpers/dom/focus.js | 16 +------- .../@ember/test-helpers/index.js | 1 + 4 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 addon-test-support/@ember/test-helpers/dom/-is-focusable.js diff --git a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js new file mode 100644 index 000000000..57d325924 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js @@ -0,0 +1,14 @@ +const FOCUSABLE_TAGS = ['INPUT', 'BUTTON', 'LINK', 'SELECT', 'A', 'TEXTAREA']; +export default function isFocusable(el) { + let { tagName, type } = el; + + if (type === 'hidden') { + return false; + } + + if (FOCUSABLE_TAGS.indexOf(tagName) > -1 || el.contentEditable === 'true') { + return true; + } + + return el.hasAttribute('tabindex'); +} diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index e69de29bb..c820cb0ec 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -0,0 +1,37 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import settled from '../settled'; +import isFocusable from './-is-focusable'; + +/* + @method blur + @param {String|HTMLElement} selector + @return {RSVP.Promise} + @public +*/ +export default function blur(selectorOrElement) { + if (!selectorOrElement) { + throw new Error('Must pass an element or selector to `blur`.'); + } + + let element = getElement(selectorOrElement); + + if (isFocusable(element)) { + let browserIsNotFocused = document.hasFocus && !document.hasFocus(); + + fireEvent(element, 'focusout'); + + // makes `document.activeElement` be `body`. + // If the browser is focused, it also fires a blur event + element.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) { + fireEvent(element, 'blur', { bubbles: false }); + } + } + + return settled(); +} diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index b064f5dba..ec6f780c9 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -1,21 +1,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; - -const FOCUSABLE_TAGS = ['INPUT', 'BUTTON', 'LINK', 'SELECT', 'A', 'TEXTAREA']; -function isFocusable(el) { - let { tagName, type } = el; - - if (type === 'hidden') { - return false; - } - - if (FOCUSABLE_TAGS.indexOf(tagName) > -1 || el.contentEditable === 'true') { - return true; - } - - return el.hasAttribute('tabindex'); -} +import isFocusable from './-is-focusable'; /* @method focus diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 7b22fc013..9f7a39705 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -18,3 +18,4 @@ export { default as validateErrorHandler } from './validate-error-handler'; // DOM Helpers export { default as click } from './dom/click'; export { default as focus } from './dom/focus'; +export { default as blur } from './dom/blur'; From 4b91a8c5ba7b8ec10881a11e19be19d8cfdf31f7 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 5 Dec 2017 22:07:47 -0500 Subject: [PATCH 04/61] Add triggerEvent and triggerKeyEvent. Original implementations are from https://github.com/cibernox/ember-native-dom-helpers and then modified: * Remove run wrapping --- .../@ember/test-helpers/dom/trigger-event.js | 17 ++++++++++++++ .../test-helpers/dom/trigger-key-event.js | 23 +++++++++++++++++++ .../@ember/test-helpers/index.js | 2 ++ 3 files changed, 42 insertions(+) create mode 100644 addon-test-support/@ember/test-helpers/dom/trigger-event.js create mode 100644 addon-test-support/@ember/test-helpers/dom/trigger-key-event.js diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js new file mode 100644 index 000000000..6e3ff5d5e --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -0,0 +1,17 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import settled from '../settled'; + +/* + @method triggerEvent + @param {String|HTMLElement} selector + @param {String} type + @param {Object} options + @return {RSVP.Promise} + @public +*/ +export default function triggerEvent(selectorOrElement, type, options) { + let element = getElement(selectorOrElement); + fireEvent(element, type, options); + return settled(); +} diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js new file mode 100644 index 000000000..5d447684f --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -0,0 +1,23 @@ +import { merge } from '@ember/polyfills'; +import triggerEvent from './trigger-event'; + +/** + * @public + * @param selector + * @param type + * @param keyCode + * @param modifiers + * @return {*} + */ +export function keyEvent( + selectorOrElement, + type, + keyCode, + modifiers = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false } +) { + return triggerEvent( + selectorOrElement, + type, + merge({ keyCode, which: keyCode, key: keyCode }, modifiers) + ); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 9f7a39705..140387c36 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -19,3 +19,5 @@ export { default as validateErrorHandler } from './validate-error-handler'; export { default as click } from './dom/click'; export { default as focus } from './dom/focus'; export { default as blur } from './dom/blur'; +export { default as triggerEvent } from './dom/blur'; +export { default as triggerKeyEvent } from './dom/blur'; From 534e3306115b95f8ac3fcc1c8f5f7a0058a1f06a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 5 Dec 2017 22:25:02 -0500 Subject: [PATCH 05/61] Refactor tests to use public API helpers. --- tests/helpers/events.js | 179 ------------------ tests/integration/settled-test.js | 18 +- .../test-module-for-component-test.js | 4 +- tests/unit/legacy-0-6-x/wait-test.js | 2 +- tests/unit/setup-rendering-context-test.js | 4 +- 5 files changed, 11 insertions(+), 196 deletions(-) delete mode 100644 tests/helpers/events.js diff --git a/tests/helpers/events.js b/tests/helpers/events.js deleted file mode 100644 index 2bb19e2ee..000000000 --- a/tests/helpers/events.js +++ /dev/null @@ -1,179 +0,0 @@ -import { run } from '@ember/runloop'; -import { merge } from '@ember/polyfills'; - -const DEFAULT_EVENT_OPTIONS = { canBubble: true, cancelable: true }; -const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; -const MOUSE_EVENT_TYPES = [ - 'click', - 'mousedown', - 'mouseup', - 'dblclick', - 'mouseenter', - 'mouseleave', - 'mousemove', - 'mouseout', - 'mouseover', -]; - -export const elMatches = - typeof Element !== 'undefined' && - (Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector); - -export function matches(el, selector) { - return elMatches.call(el, selector); -} - -function isFocusable(el) { - let focusableTags = ['INPUT', 'BUTTON', 'LINK', 'SELECT', 'A', 'TEXTAREA']; - let { tagName, type } = el; - - if (type === 'hidden') { - return false; - } - - return focusableTags.indexOf(tagName) > -1 || el.contentEditable === 'true'; -} - -export function click(el, options = {}) { - run(() => fireEvent(el, 'mousedown', options)); - focus(el); - run(() => fireEvent(el, 'mouseup', options)); - run(() => fireEvent(el, 'click', options)); -} - -export function focus(el) { - if (!el) { - return; - } - if (isFocusable(el)) { - run(null, function() { - let browserIsNotFocused = document.hasFocus && !document.hasFocus(); - - // Firefox does not trigger the `focusin` event if the window - // does not have focus. If the document doesn't have focus just - // use trigger('focusin') instead. - if (browserIsNotFocused) { - fireEvent(el, 'focusin'); - } - - // makes `document.activeElement` be `el`. If the browser is focused, it also fires a focus event - el.focus(); - - // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it - if (browserIsNotFocused) { - fireEvent(el, 'focus'); - } - }); - } -} - -export function blur(el) { - if (isFocusable(el)) { - run(null, function() { - let browserIsNotFocused = document.hasFocus && !document.hasFocus(); - - fireEvent(el, 'focusout'); - - // makes `document.activeElement` be `body`. - // If the browser is focused, it also fires a blur event - el.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) { - fireEvent(el, 'blur'); - } - }); - } -} - -export function fireEvent(element, type, options = {}) { - if (!element) { - return; - } - let event; - if (KEYBOARD_EVENT_TYPES.indexOf(type) > -1) { - event = buildKeyboardEvent(type, options); - } else if (MOUSE_EVENT_TYPES.indexOf(type) > -1) { - let rect = element.getBoundingClientRect(); - let x = rect.left + 1; - let y = rect.top + 1; - let simulatedCoordinates = { - screenX: x + 5, - screenY: y + 95, - clientX: x, - clientY: y, - }; - event = buildMouseEvent(type, merge(simulatedCoordinates, options)); - } else { - event = buildBasicEvent(type, options); - } - element.dispatchEvent(event); -} - -function buildBasicEvent(type, options = {}) { - let event = document.createEvent('Events'); - event.initEvent(type, true, true); - merge(event, options); - return event; -} - -function buildMouseEvent(type, options = {}) { - let event; - try { - event = document.createEvent('MouseEvents'); - let eventOpts = merge({}, DEFAULT_EVENT_OPTIONS); - merge(eventOpts, options); - - event.initMouseEvent( - type, - eventOpts.canBubble, - eventOpts.cancelable, - window, - eventOpts.detail, - eventOpts.screenX, - eventOpts.screenY, - eventOpts.clientX, - eventOpts.clientY, - eventOpts.ctrlKey, - eventOpts.altKey, - eventOpts.shiftKey, - eventOpts.metaKey, - eventOpts.button, - eventOpts.relatedTarget - ); - } catch (e) { - event = buildBasicEvent(type, options); - } - return event; -} - -function buildKeyboardEvent(type, options = {}) { - let event; - try { - event = document.createEvent('KeyEvents'); - let eventOpts = merge({}, DEFAULT_EVENT_OPTIONS); - merge(eventOpts, options); - event.initKeyEvent( - type, - eventOpts.canBubble, - eventOpts.cancelable, - window, - eventOpts.ctrlKey, - eventOpts.altKey, - eventOpts.shiftKey, - eventOpts.metaKey, - eventOpts.keyCode, - eventOpts.charCode - ); - } catch (e) { - event = buildBasicEvent(type, options); - } - return event; -} diff --git a/tests/integration/settled-test.js b/tests/integration/settled-test.js index c214d7e4f..e84fe1546 100644 --- a/tests/integration/settled-test.js +++ b/tests/integration/settled-test.js @@ -12,7 +12,7 @@ import hasEmberVersion from 'ember-test-helpers/has-ember-version'; import { module, test, skip } from 'qunit'; import hbs from 'htmlbars-inline-precompile'; import Pretender from 'pretender'; -import { fireEvent } from '../helpers/events'; +import { click } from '@ember/test-helpers'; import hasjQuery from '../helpers/has-jquery'; import ajax from '../helpers/ajax'; @@ -162,9 +162,7 @@ module('settled real-world scenarios', function(hooks) { assert.equal(this.element.textContent, 'initial value'); - fireEvent(this.element.querySelector('div'), 'click'); - - await settled(); + await click('div'); assert.equal(this.element.textContent, 'async value'); }); @@ -174,9 +172,7 @@ module('settled real-world scenarios', function(hooks) { await this.render(hbs`{{x-test-3}}`); - fireEvent(this.element.querySelector('div'), 'click'); - - await settled(); + await click('div'); assert.equal(this.element.textContent, 'Remote Data!'); }); @@ -186,9 +182,7 @@ module('settled real-world scenarios', function(hooks) { await this.render(hbs`{{x-test-4}}`); - fireEvent(this.element.querySelector('div'), 'click'); - - await settled(); + await click('div'); assert.equal(this.element.textContent, 'Local Data!Remote Data!Remote Data!'); }); @@ -198,7 +192,7 @@ module('settled real-world scenarios', function(hooks) { await this.render(hbs`{{x-test-4}}`); - fireEvent(this.element.querySelector('div'), 'click'); + click('div'); await settled({ waitForTimers: false }); @@ -215,7 +209,7 @@ module('settled real-world scenarios', function(hooks) { await this.render(hbs`{{x-test-4}}`); - fireEvent(this.element.querySelector('div'), 'click'); + click('div'); await settled({ waitForAJAX: false }); diff --git a/tests/unit/legacy-0-6-x/test-module-for-component-test.js b/tests/unit/legacy-0-6-x/test-module-for-component-test.js index 4644c8016..6392062ca 100644 --- a/tests/unit/legacy-0-6-x/test-module-for-component-test.js +++ b/tests/unit/legacy-0-6-x/test-module-for-component-test.js @@ -16,7 +16,7 @@ import wait from 'ember-test-helpers/wait'; import qunitModuleFor from '../../helpers/qunit-module-for'; import hasjQuery from '../../helpers/has-jquery'; import hbs from 'htmlbars-inline-precompile'; -import { fireEvent, focus, blur } from '../../helpers/events'; +import { triggerEvent, focus, blur } from '@ember/test-helpers'; import { htmlSafe } from '@ember/string'; var Service = EmberService || EmberObject; @@ -475,7 +475,7 @@ test('it supports updating an input', function(assert) { let input = this._element.querySelector('input'); input.value = '1'; - fireEvent(input, 'change'); + triggerEvent(input, 'change'); assert.equal(this.get('value'), '1'); }); diff --git a/tests/unit/legacy-0-6-x/wait-test.js b/tests/unit/legacy-0-6-x/wait-test.js index f654798d8..64d032a99 100644 --- a/tests/unit/legacy-0-6-x/wait-test.js +++ b/tests/unit/legacy-0-6-x/wait-test.js @@ -7,7 +7,7 @@ import wait from 'ember-test-helpers/wait'; import { module, test } from 'qunit'; import hbs from 'htmlbars-inline-precompile'; import Pretender from 'pretender'; -import { fireEvent } from '../../helpers/events'; +import fireEvent from '@ember/test-helpers/dom/fire-event'; import hasjQuery from '../../helpers/has-jquery'; import require from 'require'; diff --git a/tests/unit/setup-rendering-context-test.js b/tests/unit/setup-rendering-context-test.js index 71b7cf18f..715a32bb3 100644 --- a/tests/unit/setup-rendering-context-test.js +++ b/tests/unit/setup-rendering-context-test.js @@ -16,7 +16,7 @@ import { import hasEmberVersion from 'ember-test-helpers/has-ember-version'; import hasjQuery from '../helpers/has-jquery'; import { setResolverRegistry, application, resolver } from '../helpers/resolver'; -import { focus, blur, fireEvent, click } from '../helpers/events'; +import { triggerEvent, focus, blur, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; module('setupRenderingContext', function(hooks) { @@ -226,7 +226,7 @@ module('setupRenderingContext', function(hooks) { // trigger the change input.value = '1'; - fireEvent(input, 'change'); + triggerEvent(input, 'change'); assert.equal(this.get('value'), '1'); }); From 56c6026974f291e462e35b2d95f2531a15ce51ee Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:18:04 -0500 Subject: [PATCH 06/61] Guard for strings in -get-element. --- .../@ember/test-helpers/dom/-get-element.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-get-element.js b/addon-test-support/@ember/test-helpers/dom/-get-element.js index f392b2ac6..34134dcf1 100644 --- a/addon-test-support/@ember/test-helpers/dom/-get-element.js +++ b/addon-test-support/@ember/test-helpers/dom/-get-element.js @@ -8,9 +8,12 @@ export default function getElement(selectorOrElement) { selectorOrElement instanceof SVGElement ) { return selectorOrElement; - } + } else if (typeof selectorOrElement === 'string') { + let rootElement = getContext().element; - let rootElement = getContext().element; + return rootElement.querySelector(selectorOrElement); + } else { + throw new Error('Must use an element or a selector string'); + } - return rootElement.querySelector(selectorOrElement); } From 1220ed40ca85b48ba7a47199c9e15109e9459981 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:26:37 -0500 Subject: [PATCH 07/61] Fix documentation blocks for helpers. --- .../@ember/test-helpers/dom/-get-element.js | 1 - .../@ember/test-helpers/dom/blur.js | 2 +- .../@ember/test-helpers/dom/click.js | 2 +- .../@ember/test-helpers/dom/fire-event.js | 2 +- .../@ember/test-helpers/dom/focus.js | 2 +- .../@ember/test-helpers/dom/trigger-event.js | 2 +- .../@ember/test-helpers/dom/trigger-key-event.js | 16 ++++++++-------- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-get-element.js b/addon-test-support/@ember/test-helpers/dom/-get-element.js index 34134dcf1..62d6673b2 100644 --- a/addon-test-support/@ember/test-helpers/dom/-get-element.js +++ b/addon-test-support/@ember/test-helpers/dom/-get-element.js @@ -15,5 +15,4 @@ export default function getElement(selectorOrElement) { } else { throw new Error('Must use an element or a selector string'); } - } diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index c820cb0ec..19ee3a296 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -3,7 +3,7 @@ import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -/* +/** @method blur @param {String|HTMLElement} selector @return {RSVP.Promise} diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index bd46c7a74..f673dc606 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -3,7 +3,7 @@ import fireEvent from './fire-event'; import focus from './focus'; import settled from '../settled'; -/* +/** @method click @param {String|HTMLElement} selector @return {RSVP.Promise} diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index 51919e4c5..ca55203ad 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -15,7 +15,7 @@ const MOUSE_EVENT_TYPES = [ ]; const FILE_SELECTION_EVENT_TYPES = ['change']; -/* +/** @method fireEvent @param {HTMLElement} element @param {String} type diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index ec6f780c9..24c3cebaa 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -3,7 +3,7 @@ import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -/* +/** @method focus @param {String|HTMLElement} selector @return {RSVP.Promise} diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index 6e3ff5d5e..8165aa55c 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -2,7 +2,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; -/* +/** @method triggerEvent @param {String|HTMLElement} selector @param {String} type diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 5d447684f..8169f0915 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -2,14 +2,14 @@ import { merge } from '@ember/polyfills'; import triggerEvent from './trigger-event'; /** - * @public - * @param selector - * @param type - * @param keyCode - * @param modifiers - * @return {*} - */ -export function keyEvent( + @public + @param selector + @param type + @param keyCode + @param modifiers + @return {*} +*/ +export default function keyEvent( selectorOrElement, type, keyCode, From 59596f8073c770fd70424af59c8371aef473a053 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:29:07 -0500 Subject: [PATCH 08/61] Defaults `blur`'s element to document.activeElement. --- addon-test-support/@ember/test-helpers/dom/blur.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 19ee3a296..7d512e51f 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -9,11 +9,7 @@ import isFocusable from './-is-focusable'; @return {RSVP.Promise} @public */ -export default function blur(selectorOrElement) { - if (!selectorOrElement) { - throw new Error('Must pass an element or selector to `blur`.'); - } - +export default function blur(selectorOrElement = document.activeElement) { let element = getElement(selectorOrElement); if (isFocusable(element)) { From e5f84cda78b96bd3c2ac4581ea345aab05b4e563 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:42:47 -0500 Subject: [PATCH 09/61] Ensure `focus` throws if invoked with unfocusable selector. Extract `_focus` helper method that can be used from `click` to avoid the assertion... --- .../@ember/test-helpers/dom/click.js | 4 +- .../@ember/test-helpers/dom/focus.js | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index f673dc606..3d413953e 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -1,6 +1,6 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; -import focus from './focus'; +import { _focus } from './focus'; import settled from '../settled'; /** @@ -13,7 +13,7 @@ export default function click(selector) { let element = getElement(selector); fireEvent(element, 'mousedown'); - focus(element); + _focus(element); fireEvent(element, 'mouseup'); fireEvent(element, 'click'); diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 24c3cebaa..2b8ccf107 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -3,19 +3,11 @@ import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -/** - @method focus - @param {String|HTMLElement} selector - @return {RSVP.Promise} - @public -*/ -export default function focus(selectorOrElement) { - if (!selectorOrElement) { - throw new Error('Must pass an element or selector to `focus`.'); - } - - let element = getElement(selectorOrElement); +// Use Symbol here #someday... +const FOCUS_SUCCESSFUL = Object.freeze({}); +const FOCUS_UNSUCCESSFUL = Object.freeze({}); +export function _focus(element) { if (isFocusable(element)) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); @@ -35,6 +27,29 @@ export default function focus(selectorOrElement) { bubbles: false, }); } + + return FOCUS_SUCCESSFUL; + } + + return FOCUS_UNSUCCESSFUL; +} + +/** + @method focus + @param {String|HTMLElement} selector + @return {RSVP.Promise} + @public +*/ +export default function focus(selectorOrElement) { + if (!selectorOrElement) { + throw new Error('Must pass an element or selector to `focus`.'); + } + + let element = getElement(selectorOrElement); + let focusResult = _focus(element); + + if (focusResult !== FOCUS_SUCCESSFUL) { + throw new Error(`${selectorOrElement} is not focusable`); } return settled(); From 51a5da8bc77077c43c03a28378d4593988568b43 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:57:50 -0500 Subject: [PATCH 10/61] Refactor inline docs to be consistent. --- addon-test-support/@ember/test-helpers/dom/blur.js | 2 +- addon-test-support/@ember/test-helpers/dom/click.js | 2 +- addon-test-support/@ember/test-helpers/dom/fire-event.js | 2 +- addon-test-support/@ember/test-helpers/dom/focus.js | 2 +- addon-test-support/@ember/test-helpers/dom/trigger-event.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 7d512e51f..7d0e7bfe7 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -6,7 +6,7 @@ import isFocusable from './-is-focusable'; /** @method blur @param {String|HTMLElement} selector - @return {RSVP.Promise} + @return {Promise} @public */ export default function blur(selectorOrElement = document.activeElement) { diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 3d413953e..739b65755 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -6,7 +6,7 @@ import settled from '../settled'; /** @method click @param {String|HTMLElement} selector - @return {RSVP.Promise} + @return {Promise} @public */ export default function click(selector) { diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index ca55203ad..76a665466 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -20,7 +20,7 @@ const FILE_SELECTION_EVENT_TYPES = ['change']; @param {HTMLElement} element @param {String} type @param {Object} (optional) options - @return {Event} The dispatched event + @private */ export default function fireEvent(element, type, options = {}) { diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 2b8ccf107..77fabd3e1 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -37,7 +37,7 @@ export function _focus(element) { /** @method focus @param {String|HTMLElement} selector - @return {RSVP.Promise} + @return {Promise} @public */ export default function focus(selectorOrElement) { diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index 8165aa55c..fcd128f53 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -7,7 +7,7 @@ import settled from '../settled'; @param {String|HTMLElement} selector @param {String} type @param {Object} options - @return {RSVP.Promise} + @return {Promise} @public */ export default function triggerEvent(selectorOrElement, type, options) { From 457333e5e3a84905a22e4a79ea5866003e664e0d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 14:58:03 -0500 Subject: [PATCH 11/61] Refactor triggerKeyEvent to be more readable. --- .../test-helpers/dom/trigger-key-event.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 8169f0915..27152a250 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -1,23 +1,27 @@ import { merge } from '@ember/polyfills'; import triggerEvent from './trigger-event'; +const DEFAULT_MODIFIERS = Object.freeze({ + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, +}); + /** @public - @param selector - @param type - @param keyCode - @param modifiers - @return {*} + @param {String|HTMLElement} selector + @param {'keydown' | 'keyup' | 'keypress'} type + @param {String} keyCode + @param {Object} modifiers + @param {Boolean} modifiers.ctrlKey + @param {Boolean} modifiers.altKey + @param {Boolean} modifiers.shiftKey + @param {Boolean} modifiers.metaKey + @return {Promise} */ -export default function keyEvent( - selectorOrElement, - type, - keyCode, - modifiers = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false } -) { - return triggerEvent( - selectorOrElement, - type, - merge({ keyCode, which: keyCode, key: keyCode }, modifiers) - ); +export default function keyEvent(selectorOrElement, type, keyCode, modifiers = DEFAULT_MODIFIERS) { + let options = merge({ keyCode, which: keyCode, key: keyCode }, modifiers); + + return triggerEvent(selectorOrElement, type, options); } From 2e0cc6c2082ff79137da07c9d198d84ddb2eaa8e Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 15:08:54 -0500 Subject: [PATCH 12/61] Move wait-until-test to correct location. --- tests/unit/{helpers => }/wait-until-test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{helpers => }/wait-until-test.js (100%) diff --git a/tests/unit/helpers/wait-until-test.js b/tests/unit/wait-until-test.js similarity index 100% rename from tests/unit/helpers/wait-until-test.js rename to tests/unit/wait-until-test.js From 622ce97c793055e77764096ab5a9b0568cd51dd7 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 16:55:48 -0500 Subject: [PATCH 13/61] Reorder events in `focus`. Even though https://w3c.github.io/uievents/#events-focusevent-event-order suggests that `focus` is always fired after `focusin`, after testing on Firefox, Chrome, and Safari (snippet below) this is not true. ```js let element = document.createElement('input'); ['mousedown', 'mouseup', 'click', 'focus', 'focusin'].forEach(type => { element.addEventListener(type, () => { console.log('event:', type); }); }); document.body.appendChild(element); ``` --- .../@ember/test-helpers/dom/focus.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 77fabd3e1..527ba17f1 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -11,21 +11,19 @@ export function _focus(element) { if (isFocusable(element)) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); + // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event + element.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. if (browserIsNotFocused) { - fireEvent(element, 'focusin'); - } - - // makes `document.activeElement` be `el`. If the browser is focused, it also fires a focus event - element.focus(); - - // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it - if (browserIsNotFocused) { + // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it fireEvent(element, 'focus', { bubbles: false, }); + + fireEvent(element, 'focusin'); } return FOCUS_SUCCESSFUL; From da7d8e55ae7ab561ca0b4e8c49607f60999c1d08 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 17:09:39 -0500 Subject: [PATCH 14/61] Add unit tests for `click` helper. --- tests/helpers/events.js | 181 +++++++++++++++++++++++++++++++++++ tests/unit/dom/click-test.js | 100 +++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 tests/helpers/events.js create mode 100644 tests/unit/dom/click-test.js diff --git a/tests/helpers/events.js b/tests/helpers/events.js new file mode 100644 index 000000000..33fb0442d --- /dev/null +++ b/tests/helpers/events.js @@ -0,0 +1,181 @@ +// from https://mdn.mozilla.org/en-US/docs/Web/Events +export const KNOWN_EVENTS = Object.freeze([ + 'abort', + 'afterprint', + 'animationend', + 'animationiteration', + 'animationstart', + 'appinstalled', + 'audioprocess', + 'audioend ', + 'audiostart ', + 'beforeprint', + 'beforeunload', + 'beginEvent', + 'blocked ', + 'blur', + 'boundary ', + 'cached', + 'canplay', + 'canplaythrough', + 'change', + 'chargingchange', + 'chargingtimechange', + 'checking', + 'click', + 'close', + 'complete', + 'compositionend', + 'compositionstart', + 'compositionupdate', + 'contextmenu', + 'copy', + 'cut', + 'dblclick', + 'devicechange', + 'devicelight', + 'devicemotion', + 'deviceorientation', + 'deviceproximity', + 'dischargingtimechange', + 'downloading', + 'drag', + 'dragend', + 'dragenter', + 'dragleave', + 'dragover', + 'dragstart', + 'drop', + 'durationchange', + 'emptied', + 'end', + 'ended', + 'endEvent', + 'error', + 'focus', + 'focusin', + 'focusout', + 'fullscreenchange', + 'fullscreenerror', + 'gamepadconnected', + 'gamepaddisconnected', + 'gotpointercapture', + 'hashchange', + 'lostpointercapture', + 'input', + 'invalid', + 'keydown', + 'keypress', + 'keyup', + 'languagechange ', + 'levelchange', + 'load', + 'loadeddata', + 'loadedmetadata', + 'loadend', + 'loadstart', + 'mark ', + 'message', + 'messageerror', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'nomatch', + 'notificationclick', + 'noupdate', + 'obsolete', + 'offline', + 'online', + 'open', + 'orientationchange', + 'pagehide', + 'pageshow', + 'paste', + 'pause', + 'pointercancel', + 'pointerdown', + 'pointerenter', + 'pointerleave', + 'pointerlockchange', + 'pointerlockerror', + 'pointermove', + 'pointerout', + 'pointerover', + 'pointerup', + 'play', + 'playing', + 'popstate', + 'progress', + 'push', + 'pushsubscriptionchange', + 'ratechange', + 'readystatechange', + 'repeatEvent', + 'reset', + 'resize', + 'resourcetimingbufferfull', + 'result ', + 'resume ', + 'scroll', + 'seeked', + 'seeking', + 'select', + 'selectstart', + 'selectionchange', + 'show', + 'soundend ', + 'soundstart ', + 'speechend', + 'speechstart', + 'stalled', + 'start', + 'storage', + 'submit', + 'success', + 'suspend', + 'SVGAbort', + 'SVGError', + 'SVGLoad', + 'SVGResize', + 'SVGScroll', + 'SVGUnload', + 'SVGZoom', + 'timeout', + 'timeupdate', + 'touchcancel', + 'touchend', + 'touchmove', + 'touchstart', + 'transitionend', + 'unload', + 'updateready', + 'upgradeneeded ', + 'userproximity', + 'voiceschanged', + 'versionchange', + 'visibilitychange', + 'volumechange', + 'waiting', + 'wheel', +]); + +let uuid = 0; +export function buildInstrumentedElement(elementType) { + let element = document.createElement(elementType); + element.setAttribute('id', `fixture-${uuid++}`); + + KNOWN_EVENTS.forEach(type => { + element.addEventListener(type, () => { + QUnit.config.current.assert.step(type); + }); + }); + + let fixture = document.querySelector('#qunit-fixture'); + fixture.appendChild(element); + + return element; +} diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js new file mode 100644 index 000000000..ce2ede050 --- /dev/null +++ b/tests/unit/dom/click-test.js @@ -0,0 +1,100 @@ +import { module, test } from 'qunit'; +import { click, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: click', function(hooks) { + let element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + this.element = document.querySelector('#qunit-fixture'); + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + module('non-focusable element types', function() { + test('clicking a div via selector with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await click(`#${element.id}`); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + + test('clicking a div via element with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await click(element); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + + test('clicking a div via element without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await click(element); + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('div'); + + let promise = click(element); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['mousedown', 'mouseup', 'click']); + }); + + test('throws an error if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + click(`#foo-bar-baz-not-here-ever-bye-bye`); + }, /Element not found when calling `click\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); + }); + }); + + module('focusable element types', function() { + test('clicking a input via selector with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(this); + await click(`#${element.id}`); + + assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('clicking a input via element with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(this); + await click(element); + + assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('clicking a input via element without context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await click(element); + + assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + }); +}); From fa9f7c1e7bb80cdc8180b670127e5974fd2262fd Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 17:46:18 -0500 Subject: [PATCH 15/61] Add helpful (eager) error conditions for `click` and `getElement`. --- addon-test-support/@ember/test-helpers/dom/-get-element.js | 6 +++++- addon-test-support/@ember/test-helpers/dom/click.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-get-element.js b/addon-test-support/@ember/test-helpers/dom/-get-element.js index 62d6673b2..078d63c3b 100644 --- a/addon-test-support/@ember/test-helpers/dom/-get-element.js +++ b/addon-test-support/@ember/test-helpers/dom/-get-element.js @@ -9,7 +9,11 @@ export default function getElement(selectorOrElement) { ) { return selectorOrElement; } else if (typeof selectorOrElement === 'string') { - let rootElement = getContext().element; + let context = getContext(); + let rootElement = context && context.element; + if (!rootElement) { + throw new Error(`Must setup rendering context before attempting to interact with elements.`); + } return rootElement.querySelector(selectorOrElement); } else { diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 739b65755..95b579815 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -11,6 +11,9 @@ import settled from '../settled'; */ export default function click(selector) { let element = getElement(selector); + if (!element) { + throw new Error(`Element not found when calling \`click('${selector}')\`.`); + } fireEvent(element, 'mousedown'); _focus(element); From 05d1f005dc3bbdca3f89318c7c1222b6e99639df Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 17:46:53 -0500 Subject: [PATCH 16/61] Ensure `click` is _always_ async. Never fire events synchronously. --- addon-test-support/@ember/test-helpers/dom/click.js | 13 +++++++++---- tests/unit/setup-rendering-context-test.js | 10 +++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 95b579815..12a719af4 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -2,6 +2,9 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import { _focus } from './focus'; import settled from '../settled'; +import isFocusable from './-is-focusable'; + +const nextTick = setTimeout; /** @method click @@ -15,10 +18,12 @@ export default function click(selector) { throw new Error(`Element not found when calling \`click('${selector}')\`.`); } - fireEvent(element, 'mousedown'); - _focus(element); - fireEvent(element, 'mouseup'); - fireEvent(element, 'click'); + nextTick(() => { + fireEvent(element, 'mousedown'); + _focus(element); + fireEvent(element, 'mouseup'); + fireEvent(element, 'click'); + }); return settled(); } diff --git a/tests/unit/setup-rendering-context-test.js b/tests/unit/setup-rendering-context-test.js index 715a32bb3..d156016d3 100644 --- a/tests/unit/setup-rendering-context-test.js +++ b/tests/unit/setup-rendering-context-test.js @@ -172,7 +172,7 @@ module('setupRenderingContext', function(hooks) { await this.render(hbs`{{x-foo}}`); assert.equal(this.element.textContent, 'Click me!', 'precond - component was rendered'); - click(this.element.querySelector('button')); + await click(this.element.querySelector('button')); }); test('can use action based event handling', async function(assert) { @@ -196,7 +196,7 @@ module('setupRenderingContext', function(hooks) { await this.render(hbs`{{x-foo}}`); assert.equal(this.element.textContent, 'Click me!', 'precond - component was rendered'); - click(this.element.querySelector('button')); + await click(this.element.querySelector('button')); }); test('can pass function to be used as a "closure action"', async function(assert) { @@ -211,7 +211,7 @@ module('setupRenderingContext', function(hooks) { await this.render(hbs`{{x-foo clicked=clicked}}`); assert.equal(this.element.textContent, 'Click me!', 'precond - component was rendered'); - click(this.element.querySelector('button')); + await click(this.element.querySelector('button')); }); test('can update a passed in argument with an ', async function(assert) { @@ -280,7 +280,7 @@ module('setupRenderingContext', function(hooks) { await this.render(hbs`{{my-component foo=foo}}`); assert.equal(this.element.textContent, 'original', 'value after initial render'); - click(this.element.querySelector('button')); + await click(this.element.querySelector('button')); assert.equal(this.element.textContent, 'updated!', 'value after updating'); assert.equal(this.get('foo'), 'updated!'); }); @@ -306,7 +306,7 @@ module('setupRenderingContext', function(hooks) { // works both for things rendered in the component's layout // and those only used in the components JS file await this.render(hbs`{{my-component foo=foo bar=bar}}`); - click(this.element.querySelector('button')); + await click(this.element.querySelector('button')); await this.clearRender(); From dbbb7a3e7ba894c3920006d53f90348a9cf2f5c0 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 18:01:30 -0500 Subject: [PATCH 17/61] Refactor `focus` to be always async. --- .../@ember/test-helpers/dom/click.js | 4 +- .../@ember/test-helpers/dom/focus.js | 42 +++++++++---------- .../test-module-for-component-test.js | 13 +++--- tests/unit/setup-rendering-context-test.js | 2 +- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 12a719af4..9067cc197 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -20,7 +20,9 @@ export default function click(selector) { nextTick(() => { fireEvent(element, 'mousedown'); - _focus(element); + if (isFocusable(element)) { + _focus(element); + } fireEvent(element, 'mouseup'); fireEvent(element, 'click'); }); diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 527ba17f1..e1b6f9053 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -3,33 +3,25 @@ import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -// Use Symbol here #someday... -const FOCUS_SUCCESSFUL = Object.freeze({}); -const FOCUS_UNSUCCESSFUL = Object.freeze({}); +const nextTick = setTimeout; export function _focus(element) { - if (isFocusable(element)) { - let browserIsNotFocused = document.hasFocus && !document.hasFocus(); + let browserIsNotFocused = document.hasFocus && !document.hasFocus(); - // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event - element.focus(); + // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event + element.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. - if (browserIsNotFocused) { - // if the browser is not focused the previous `el.focus()` didn't fire an event, so we simulate it - fireEvent(element, 'focus', { - bubbles: false, - }); + // 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(element, 'focus', { + bubbles: false, + }); - fireEvent(element, 'focusin'); - } - - return FOCUS_SUCCESSFUL; + fireEvent(element, 'focusin'); } - - return FOCUS_UNSUCCESSFUL; } /** @@ -44,11 +36,15 @@ export default function focus(selectorOrElement) { } let element = getElement(selectorOrElement); - let focusResult = _focus(element); + if (!element) { + throw new Error(`Element not found when calling \`focus('${selectorOrElement}')\`.`); + } - if (focusResult !== FOCUS_SUCCESSFUL) { + if (!isFocusable(element)) { throw new Error(`${selectorOrElement} is not focusable`); } + nextTick(() => _focus(element)); + return settled(); } diff --git a/tests/unit/legacy-0-6-x/test-module-for-component-test.js b/tests/unit/legacy-0-6-x/test-module-for-component-test.js index 6392062ca..59edc0f5d 100644 --- a/tests/unit/legacy-0-6-x/test-module-for-component-test.js +++ b/tests/unit/legacy-0-6-x/test-module-for-component-test.js @@ -498,11 +498,14 @@ test('it supports dom triggered focus events', function(assert) { let input = this._element.querySelector('input'); assert.equal(input.value, 'init'); - focus(input); - assert.equal(input.value, 'focusin'); - - blur(input); - assert.equal(input.value, 'focusout'); + return focus(input) + .then(() => { + assert.equal(input.value, 'focusin'); + return blur(input); + }) + .then(() => { + assert.equal(input.value, 'focusout'); + }); }); moduleForComponent('Component Integration Tests: render during setup', { diff --git a/tests/unit/setup-rendering-context-test.js b/tests/unit/setup-rendering-context-test.js index d156016d3..b1ce3a6c4 100644 --- a/tests/unit/setup-rendering-context-test.js +++ b/tests/unit/setup-rendering-context-test.js @@ -253,7 +253,7 @@ module('setupRenderingContext', function(hooks) { let input = this.element.querySelector('input'); assert.equal(input.value, 'init'); - focus(input); + await focus(input); assert.equal(input.value, 'focusin'); blur(input); From 8f55c947c17d77cc31f3abec30635921a65a9f78 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 18:01:48 -0500 Subject: [PATCH 18/61] Add unit tests for `focus`. --- tests/unit/dom/focus-test.js | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/unit/dom/focus-test.js diff --git a/tests/unit/dom/focus-test.js b/tests/unit/dom/focus-test.js new file mode 100644 index 000000000..bea23c9ee --- /dev/null +++ b/tests/unit/dom/focus-test.js @@ -0,0 +1,88 @@ +import { module, test } from 'qunit'; +import { focus, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: focus', function(hooks) { + let element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + this.element = document.querySelector('#qunit-fixture'); + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + test('focusing a div via selector with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + assert.throws(() => { + focus(`#${element.id}`); + }, /is not focusable/); + }); + + test('focusing a div via element with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + assert.throws(() => { + focus(element); + }, /is not focusable/); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('input'); + + let promise = focus(element); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['focus', 'focusin']); + }); + + test('throws an error if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + focus(`#foo-bar-baz-not-here-ever-bye-bye`); + }, /Element not found when calling `focus\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); + }); + + test('focusing a input via selector with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(this); + await focus(`#${element.id}`); + + assert.verifySteps(['focus', 'focusin']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('focusing a input via element with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(this); + await focus(element); + + assert.verifySteps(['focus', 'focusin']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('focusing a input via element without context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await focus(element); + + assert.verifySteps(['focus', 'focusin']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); +}); From 88beb6b5bfcd5befd58f3a3f1317579b60516d99 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 18:11:12 -0500 Subject: [PATCH 19/61] Create private `nextTick` utility. In order to ensure "time traveling" (sinon's fake timers, timecop, etc) we need to capture our `setTimeout` early during evaluation. --- addon-test-support/@ember/test-helpers/-utils.js | 1 + addon-test-support/@ember/test-helpers/dom/click.js | 3 +-- addon-test-support/@ember/test-helpers/dom/focus.js | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 addon-test-support/@ember/test-helpers/-utils.js diff --git a/addon-test-support/@ember/test-helpers/-utils.js b/addon-test-support/@ember/test-helpers/-utils.js new file mode 100644 index 000000000..912da8fca --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-utils.js @@ -0,0 +1 @@ +export const nextTick = setTimeout; diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 9067cc197..2f54269ad 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -3,8 +3,7 @@ import fireEvent from './fire-event'; import { _focus } from './focus'; import settled from '../settled'; import isFocusable from './-is-focusable'; - -const nextTick = setTimeout; +import { nextTick } from '../-utils'; /** @method click diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index e1b6f9053..024549c8a 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -2,8 +2,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; - -const nextTick = setTimeout; +import { nextTick } from '../-utils'; export function _focus(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); From 043c4bc9fc6fa76983bd74a760047171933d10f7 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 18:23:55 -0500 Subject: [PATCH 20/61] Use `nextTick` in `waitUntil`. --- addon-test-support/@ember/test-helpers/wait-until.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/wait-until.js b/addon-test-support/@ember/test-helpers/wait-until.js index 5f03f9bb6..7225cd95a 100644 --- a/addon-test-support/@ember/test-helpers/wait-until.js +++ b/addon-test-support/@ember/test-helpers/wait-until.js @@ -1,22 +1,29 @@ import { Promise } from 'rsvp'; +import { nextTick } from './-utils'; + export default function(callback, options = {}) { let timeout = 'timeout' in options ? options.timeout : 1000; let waitUntilTimedOut = new Error('waitUntil timed out'); return new Promise(function(resolve, reject) { - let time = 0; + // starting at -10 because the first invocation happens on 0 + // but still increments the time... + let time = -10; function tick() { time += 10; let value = callback(); if (value) { resolve(value); } else if (time < timeout) { + // using `setTimeout` directly to allow fake timers + // to intercept setTimeout(tick, 10); } else { reject(waitUntilTimedOut); } } - setTimeout(tick, 10); + + nextTick(tick); }); } From cf98536b310711637aa9835b893c051f21e719ac Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:03:19 -0500 Subject: [PATCH 21/61] Add monkey patch for QUnit until qunit > 2.4.1 is released. Brings in the changes from https://github.com/qunitjs/qunit/commit/6a7910a995bbe901c0b2e70cdcf86cb7572c583f. --- tests/test-helper.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test-helper.js b/tests/test-helper.js index 105917a1d..ccb785849 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -100,3 +100,11 @@ QUnit.assert.deprecationsInclude = function(expected) { message: `expected to find \`${expected}\` deprecation`, }); }; + +// Monkey patch in the fix from https://github.com/qunitjs/qunit/pull/1233 +// This can be removed after qunit > 2.4.1 has been released +const ASSERT_VERIFY_STEPS = QUnit.assert.verifySteps; +QUnit.assert.verifySteps = function() { + ASSERT_VERIFY_STEPS.apply(this, arguments); + this.test.steps.length = 0; +}; From c919c77c4faa44ad07434d3c97a6d58bd09e71e8 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:04:27 -0500 Subject: [PATCH 22/61] Add blur unit tests. --- tests/unit/dom/blur-test.js | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/unit/dom/blur-test.js diff --git a/tests/unit/dom/blur-test.js b/tests/unit/dom/blur-test.js new file mode 100644 index 000000000..a2b2a45c8 --- /dev/null +++ b/tests/unit/dom/blur-test.js @@ -0,0 +1,68 @@ +import { module, test } from 'qunit'; +import { focus, blur, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: blur', function(hooks) { + let element; + + hooks.beforeEach(async function(assert) { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + this.element = document.querySelector('#qunit-fixture'); + + // create the element and focus in preparation for blur testing + element = buildInstrumentedElement('input'); + await focus(element); + + // verify that focus was ran, and reset steps + assert.verifySteps(['focus', 'focusin']); + assert.equal(document.activeElement, element, 'activeElement updated'); + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + test('does not run sync', async function(assert) { + let promise = blur(element); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['blur', 'focusout']); + }); + + test('throws an error if selector is not found', async function(assert) { + setContext(this); + assert.throws(() => { + blur(`#foo-bar-baz-not-here-ever-bye-bye`); + }, /Element not found when calling `blur\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); + }); + + test('bluring via selector with context set', async function(assert) { + setContext(this); + await blur(`#${element.id}`); + + assert.verifySteps(['blur', 'focusout']); + assert.notEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('bluring via element with context set', async function(assert) { + setContext(this); + await blur(element); + + assert.verifySteps(['blur', 'focusout']); + assert.notEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('bluring via element without context set', async function(assert) { + await blur(element); + + assert.verifySteps(['blur', 'focusout']); + assert.notEqual(document.activeElement, element, 'activeElement updated'); + }); +}); From 58441b06a0218d49e29ddb716c90d8154fe440ac Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:04:38 -0500 Subject: [PATCH 23/61] Refactor `blur` * ensure always async * fire correct events (in order) To ensure correct event order, used the following snippet: ```js let element = document.createElement('input'); ['blur', 'focusout'].forEach(type => { element.addEventListener(type, () => { console.log('event:', type); }); }); document.body.appendChild(element); ``` --- .../@ember/test-helpers/dom/blur.js | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 7d0e7bfe7..f683675df 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -2,6 +2,23 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; +import { nextTick } from '../-utils'; + +export function _blur(element) { + let browserIsNotFocused = document.hasFocus && !document.hasFocus(); + + // makes `document.activeElement` be `body`. + // If the browser is focused, it also fires a blur event + element.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) { + fireEvent(element, 'blur', { bubbles: false }); + fireEvent(element, 'focusout'); + } +} /** @method blur @@ -11,23 +28,15 @@ import isFocusable from './-is-focusable'; */ export default function blur(selectorOrElement = document.activeElement) { let element = getElement(selectorOrElement); + if (!element) { + throw new Error(`Element not found when calling \`blur('${selectorOrElement}')\`.`); + } - if (isFocusable(element)) { - let browserIsNotFocused = document.hasFocus && !document.hasFocus(); - - fireEvent(element, 'focusout'); - - // makes `document.activeElement` be `body`. - // If the browser is focused, it also fires a blur event - element.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) { - fireEvent(element, 'blur', { bubbles: false }); - } + if (!isFocusable(element)) { + throw new Error(`${selectorOrElement} is not focusable`); } + nextTick(() => _blur(element)); + return settled(); } From 1f101411ccd0daf2400629e9f279d1cc9e229c23 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:47:07 -0500 Subject: [PATCH 24/61] Fix stupid mistake in reexports. --- addon-test-support/@ember/test-helpers/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 140387c36..bfcfae983 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -19,5 +19,5 @@ export { default as validateErrorHandler } from './validate-error-handler'; export { default as click } from './dom/click'; export { default as focus } from './dom/focus'; export { default as blur } from './dom/blur'; -export { default as triggerEvent } from './dom/blur'; -export { default as triggerKeyEvent } from './dom/blur'; +export { default as triggerEvent } from './dom/trigger-event'; +export { default as triggerKeyEvent } from './dom/trigger-key-event'; From 88c88edbfca82ef8b29c836c61e2323fe98c8bf1 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:47:46 -0500 Subject: [PATCH 25/61] Update tests to deal with async dom helpers. --- tests/unit/legacy-0-6-x/test-module-for-component-test.js | 5 +++-- tests/unit/setup-rendering-context-test.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/legacy-0-6-x/test-module-for-component-test.js b/tests/unit/legacy-0-6-x/test-module-for-component-test.js index 59edc0f5d..c75c71cd5 100644 --- a/tests/unit/legacy-0-6-x/test-module-for-component-test.js +++ b/tests/unit/legacy-0-6-x/test-module-for-component-test.js @@ -475,8 +475,9 @@ test('it supports updating an input', function(assert) { let input = this._element.querySelector('input'); input.value = '1'; - triggerEvent(input, 'change'); - assert.equal(this.get('value'), '1'); + return triggerEvent(input, 'change').then(() => { + assert.equal(this.get('value'), '1'); + }); }); test('it supports dom triggered focus events', function(assert) { diff --git a/tests/unit/setup-rendering-context-test.js b/tests/unit/setup-rendering-context-test.js index b1ce3a6c4..b817c1c44 100644 --- a/tests/unit/setup-rendering-context-test.js +++ b/tests/unit/setup-rendering-context-test.js @@ -226,7 +226,7 @@ module('setupRenderingContext', function(hooks) { // trigger the change input.value = '1'; - triggerEvent(input, 'change'); + await triggerEvent(input, 'change'); assert.equal(this.get('value'), '1'); }); @@ -256,7 +256,7 @@ module('setupRenderingContext', function(hooks) { await focus(input); assert.equal(input.value, 'focusin'); - blur(input); + await blur(input); assert.equal(input.value, 'focusout'); }); From e523951d0b04f75e1281c281e7f7f2d272818882 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 6 Dec 2017 23:48:10 -0500 Subject: [PATCH 26/61] Add and utilize `nextTickPromise` helper method. Prior to this change the `nextTick` would run and _might_ not be properly waited on by the returned `settle()` promise (though in practice `nextTick` did finish first). This updates to ensure that we only return `settled()` after the actual events have been fired. --- addon-test-support/@ember/test-helpers/-utils.js | 8 ++++++++ addon-test-support/@ember/test-helpers/dom/blur.js | 8 +++++--- addon-test-support/@ember/test-helpers/dom/click.js | 10 ++++++---- addon-test-support/@ember/test-helpers/dom/focus.js | 8 +++++--- .../@ember/test-helpers/dom/trigger-event.js | 9 +++++++-- .../@ember/test-helpers/dom/trigger-key-event.js | 4 ++-- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/-utils.js b/addon-test-support/@ember/test-helpers/-utils.js index 912da8fca..110b7eb8f 100644 --- a/addon-test-support/@ember/test-helpers/-utils.js +++ b/addon-test-support/@ember/test-helpers/-utils.js @@ -1 +1,9 @@ +import { Promise } from 'rsvp'; + export const nextTick = setTimeout; + +export function nextTickPromise() { + return new Promise(resolve => { + nextTick(resolve); + }); +} diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index f683675df..1b8eb12dd 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -2,7 +2,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -import { nextTick } from '../-utils'; +import { nextTickPromise } from '../-utils'; export function _blur(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); @@ -36,7 +36,9 @@ export default function blur(selectorOrElement = document.activeElement) { throw new Error(`${selectorOrElement} is not focusable`); } - nextTick(() => _blur(element)); + return nextTickPromise().then(() => { + _blur(element); - return settled(); + return settled(); + }); } diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 2f54269ad..f218ca6c0 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -3,7 +3,7 @@ import fireEvent from './fire-event'; import { _focus } from './focus'; import settled from '../settled'; import isFocusable from './-is-focusable'; -import { nextTick } from '../-utils'; +import { nextTickPromise } from '../-utils'; /** @method click @@ -17,14 +17,16 @@ export default function click(selector) { throw new Error(`Element not found when calling \`click('${selector}')\`.`); } - nextTick(() => { + return nextTickPromise().then(() => { fireEvent(element, 'mousedown'); + if (isFocusable(element)) { _focus(element); } + fireEvent(element, 'mouseup'); fireEvent(element, 'click'); - }); - return settled(); + return settled(); + }); } diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 024549c8a..8f0db4f98 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -2,7 +2,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; import isFocusable from './-is-focusable'; -import { nextTick } from '../-utils'; +import { nextTickPromise } from '../-utils'; export function _focus(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); @@ -43,7 +43,9 @@ export default function focus(selectorOrElement) { throw new Error(`${selectorOrElement} is not focusable`); } - nextTick(() => _focus(element)); + return nextTickPromise().then(() => { + _focus(element); - return settled(); + return settled(); + }); } diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index fcd128f53..0dc05929a 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -1,6 +1,7 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; import settled from '../settled'; +import { nextTickPromise } from '../-utils'; /** @method triggerEvent @@ -12,6 +13,10 @@ import settled from '../settled'; */ export default function triggerEvent(selectorOrElement, type, options) { let element = getElement(selectorOrElement); - fireEvent(element, type, options); - return settled(); + + return nextTickPromise().then(() => { + fireEvent(element, type, options); + + return settled(); + }); } diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 27152a250..6465f345c 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -20,8 +20,8 @@ const DEFAULT_MODIFIERS = Object.freeze({ @param {Boolean} modifiers.metaKey @return {Promise} */ -export default function keyEvent(selectorOrElement, type, keyCode, modifiers = DEFAULT_MODIFIERS) { +export default function triggerKeyEvent(selector, type, keyCode, modifiers = DEFAULT_MODIFIERS) { let options = merge({ keyCode, which: keyCode, key: keyCode }, modifiers); - return triggerEvent(selectorOrElement, type, options); + return triggerEvent(selector, type, options); } From b751706aa7ad689a262239d11aee083abce74d5b Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 10:07:58 -0500 Subject: [PATCH 27/61] Ensure events fired receive native event. --- tests/helpers/events.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/helpers/events.js b/tests/helpers/events.js index 33fb0442d..68fb9a4b1 100644 --- a/tests/helpers/events.js +++ b/tests/helpers/events.js @@ -165,12 +165,15 @@ export const KNOWN_EVENTS = Object.freeze([ let uuid = 0; export function buildInstrumentedElement(elementType) { + let assert = QUnit.config.current.assert; + let element = document.createElement(elementType); element.setAttribute('id', `fixture-${uuid++}`); KNOWN_EVENTS.forEach(type => { - element.addEventListener(type, () => { - QUnit.config.current.assert.step(type); + element.addEventListener(type, e => { + assert.step(type); + assert.ok(e instanceof Event, `${type} listener should receive a native event`); }); }); From d21bf56a321bcdfc723acd321d19daebc8eaf170 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 10:27:03 -0500 Subject: [PATCH 28/61] Add unit tests for `triggerEvent`. --- tests/unit/dom/trigger-event-test.js | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/unit/dom/trigger-event-test.js diff --git a/tests/unit/dom/trigger-event-test.js b/tests/unit/dom/trigger-event-test.js new file mode 100644 index 000000000..59ebeaada --- /dev/null +++ b/tests/unit/dom/trigger-event-test.js @@ -0,0 +1,114 @@ +import { module, test } from 'qunit'; +import { triggerEvent, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: triggerEvent', function(hooks) { + let element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + this.element = document.querySelector('#qunit-fixture'); + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + test('can trigger arbitrary event types', async function(assert) { + element = buildInstrumentedElement('div'); + element.addEventListener('fliberty', e => { + assert.step('fliberty'); + assert.ok(e instanceof Event, `fliberty listener receives a native event`); + }); + + setContext(this); + await triggerEvent(`#${element.id}`, 'fliberty'); + + assert.verifySteps(['fliberty']); + }); + + test('triggering event via selector with context set fires the given event type', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await triggerEvent(`#${element.id}`, 'mouseenter'); + + assert.verifySteps(['mouseenter']); + }); + + test('triggering event via element with context set fires the given event type', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await triggerEvent(element, 'mouseenter'); + + assert.verifySteps(['mouseenter']); + }); + + test('triggering event via element without context set fires the given event type', async function(assert) { + element = buildInstrumentedElement('div'); + + await triggerEvent(element, 'mouseenter'); + + assert.verifySteps(['mouseenter']); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('div'); + + let promise = triggerEvent(element, 'mouseenter'); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['mouseenter']); + }); + + test('throws an error if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + triggerEvent(`#foo-bar-baz-not-here-ever-bye-bye`, 'mouseenter'); + }, /Element not found when calling `triggerEvent\('#foo-bar-baz-not-here-ever-bye-bye'/); + }); + + test('throws an error if event type is not passed', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + triggerEvent(element); + }, /Must provide an `eventType` to `triggerEvent`/); + }); + + test('events properly bubble upwards', async function(assert) { + setContext(this); + element = buildInstrumentedElement('div'); + element.innerHTML = ` +
+
+
+ `; + + let outer = element.querySelector('#outer'); + let inner = element.querySelector('#inner'); + + outer.addEventListener('mouseenter', () => { + assert.step('outer: mouseenter'); + }); + + inner.addEventListener('mouseenter', () => { + assert.step('inner: mouseenter'); + }); + + await triggerEvent('#inner', 'mouseenter'); + + assert.verifySteps(['inner: mouseenter', 'outer: mouseenter', 'mouseenter']); + }); +}); From fbd035380c882acb0f855e1f9c609c141a40b12a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 10:27:26 -0500 Subject: [PATCH 29/61] Flesh out `triggerEvent` input validation. --- .../@ember/test-helpers/dom/trigger-event.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index 0dc05929a..22c64ea03 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -6,13 +6,26 @@ import { nextTickPromise } from '../-utils'; /** @method triggerEvent @param {String|HTMLElement} selector - @param {String} type + @param {String} eventType @param {Object} options @return {Promise} @public */ export default function triggerEvent(selectorOrElement, type, options) { + if (!selectorOrElement) { + throw new Error('Must pass an element or selector to `triggerEvent`.'); + } + let element = getElement(selectorOrElement); + if (!element) { + throw new Error( + `Element not found when calling \`triggerEvent('${selectorOrElement}', ...)\`.` + ); + } + + if (!type) { + throw new Error(`Must provide an \`eventType\` to \`triggerEvent\``); + } return nextTickPromise().then(() => { fireEvent(element, type, options); From 6d17cc9df91d2b3fb30fdbfa3b510d16c50a1ebc Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 11:05:50 -0500 Subject: [PATCH 30/61] Add unit tests for triggerKeyEvent. --- tests/unit/dom/trigger-key-event-test.js | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/unit/dom/trigger-key-event-test.js diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js new file mode 100644 index 000000000..9eb503355 --- /dev/null +++ b/tests/unit/dom/trigger-key-event-test.js @@ -0,0 +1,100 @@ +import { module, test } from 'qunit'; +import { triggerKeyEvent, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: triggerKeyEvent', function(hooks) { + let element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + this.element = document.querySelector('#qunit-fixture'); + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + test('throws an error if event type is missing', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + triggerKeyEvent(element); + }, /Must provide an `eventType` to `triggerKeyEvent`/); + }); + + test('throws an error if event type is invalid', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + triggerKeyEvent(element, 'mouseenter'); + }, /Must provide an `eventType` of keydown, keypress, keyup to `triggerKeyEvent` but you passed `mouseenter`./); + }); + + test('throws an error if key code is missing', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(this); + triggerKeyEvent(element, 'keypress'); + }, /Must provide a `keyCode` to `triggerKeyEvent`/); + }); + + test('triggering via selector with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await triggerKeyEvent(`#${element.id}`, 'keydown', 13); + + assert.verifySteps(['keydown']); + }); + + test('triggering via element with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(this); + await triggerKeyEvent(element, 'keydown', 13); + + assert.verifySteps(['keydown']); + }); + + test('triggering via element without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await triggerKeyEvent(element, 'keydown', 13); + + assert.verifySteps(['keydown']); + }); + + ['ctrl', 'shift', 'alt', 'meta'].forEach(function(modifierType) { + test(`triggering passing with ${modifierType} pressed`, async function(assert) { + element = buildInstrumentedElement('div'); + element.addEventListener('keypress', e => { + assert.ok(e[`${modifierType}Key`], `has ${modifierType} indicated`); + }); + + setContext(this); + await triggerKeyEvent(element, 'keypress', 13, { [`${modifierType}Key`]: true }); + + assert.verifySteps(['keypress']); + }); + }); + + test(`can combine modifier keys`, async function(assert) { + element = buildInstrumentedElement('div'); + element.addEventListener('keypress', e => { + assert.ok(e.ctrlKey, `has ctrlKey indicated`); + assert.ok(e.altKey, `has altKey indicated`); + }); + + setContext(this); + await triggerKeyEvent(element, 'keypress', 13, { altKey: true, ctrlKey: true }); + + assert.verifySteps(['keypress']); + }); +}); From 48ab3106793e228135831ee43f9a57a25332d7e9 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 11:06:06 -0500 Subject: [PATCH 31/61] Add input validation to triggerKeyEvent. --- .../@ember/test-helpers/dom/fire-event.js | 2 +- .../test-helpers/dom/trigger-key-event.js | 35 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index 76a665466..d6109cfa2 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -1,7 +1,7 @@ import { merge } from '@ember/polyfills'; const DEFAULT_EVENT_OPTIONS = { bubbles: true, cancelable: true }; -const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; +export const KEYBOARD_EVENT_TYPES = Object.freeze(['keydown', 'keypress', 'keyup']); const MOUSE_EVENT_TYPES = [ 'click', 'mousedown', diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 6465f345c..073534205 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -1,5 +1,7 @@ import { merge } from '@ember/polyfills'; +import getElement from './-get-element'; import triggerEvent from './trigger-event'; +import { KEYBOARD_EVENT_TYPES } from './fire-event'; const DEFAULT_MODIFIERS = Object.freeze({ ctrlKey: false, @@ -10,8 +12,8 @@ const DEFAULT_MODIFIERS = Object.freeze({ /** @public - @param {String|HTMLElement} selector - @param {'keydown' | 'keyup' | 'keypress'} type + @param {String|HTMLElement} target + @param {'keydown' | 'keyup' | 'keypress'} eventType @param {String} keyCode @param {Object} modifiers @param {Boolean} modifiers.ctrlKey @@ -20,8 +22,33 @@ const DEFAULT_MODIFIERS = Object.freeze({ @param {Boolean} modifiers.metaKey @return {Promise} */ -export default function triggerKeyEvent(selector, type, keyCode, modifiers = DEFAULT_MODIFIERS) { +export default function triggerKeyEvent(target, eventType, keyCode, modifiers = DEFAULT_MODIFIERS) { + if (!target) { + throw new Error('Must pass an element or selector to `triggerKeyEvent`.'); + } + + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`triggerKeyEvent('${target}', ...)\`.`); + } + + if (!eventType) { + throw new Error(`Must provide an \`eventType\` to \`triggerKeyEvent\``); + } + + if (KEYBOARD_EVENT_TYPES.indexOf(eventType) === -1) { + throw new Error( + `Must provide an \`eventType\` of ${KEYBOARD_EVENT_TYPES.join( + ', ' + )} to \`triggerKeyEvent\` but you passed \`${eventType}\`.` + ); + } + + if (!keyCode) { + throw new Error(`Must provide a \`keyCode\` to \`triggerKeyEvent\``); + } + let options = merge({ keyCode, which: keyCode, key: keyCode }, modifiers); - return triggerEvent(selector, type, options); + return triggerEvent(element, eventType, options); } From 2221b42f47655b721e0701e34b123d97574ca996 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 11:22:43 -0500 Subject: [PATCH 32/61] Incorporate changes from cibernox/ember-native-dom-helpers#104. --- .../test-helpers/dom/-is-content-editable.js | 3 +++ .../@ember/test-helpers/dom/-is-focusable.js | 19 ++++++++++--------- .../test-helpers/dom/-is-form-control.js | 11 +++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 addon-test-support/@ember/test-helpers/dom/-is-content-editable.js create mode 100644 addon-test-support/@ember/test-helpers/dom/-is-form-control.js diff --git a/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js b/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js new file mode 100644 index 000000000..aece30637 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js @@ -0,0 +1,3 @@ +export default function isContentEditable(el) { + return el.contentEditable === 'true'; +} diff --git a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js index 57d325924..f3f06d45b 100644 --- a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js +++ b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js @@ -1,14 +1,15 @@ -const FOCUSABLE_TAGS = ['INPUT', 'BUTTON', 'LINK', 'SELECT', 'A', 'TEXTAREA']; -export default function isFocusable(el) { - let { tagName, type } = el; +import isFormControl from './-is-form-control'; +import isContentEditable from './-is-content-editable'; - if (type === 'hidden') { - return false; - } - - if (FOCUSABLE_TAGS.indexOf(tagName) > -1 || el.contentEditable === 'true') { +const FOCUSABLE_TAGS = ['LINK', 'A']; +export default function isFocusable(element) { + if ( + isFormControl(element) || + isContentEditable(element) || + FOCUSABLE_TAGS.indexOf(element.tagName) > -1 + ) { return true; } - return el.hasAttribute('tabindex'); + return element.hasAttribute('tabindex'); } diff --git a/addon-test-support/@ember/test-helpers/dom/-is-form-control.js b/addon-test-support/@ember/test-helpers/dom/-is-form-control.js new file mode 100644 index 000000000..79f42cb0d --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/-is-form-control.js @@ -0,0 +1,11 @@ +const FORM_CONTROL_TAGS = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA']; + +export default function isFormControl(el) { + let { tagName, type } = el; + + if (type === 'hidden') { + return false; + } + + return FORM_CONTROL_TAGS.indexOf(tagName) > -1; +} From 83444ea1623190a91aa44e08d892e0796184a0a1 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 7 Dec 2017 11:31:01 -0500 Subject: [PATCH 33/61] Initial implementation of `fillIn`. Intial implementation from https://github.com/cibernox/ember-native-dom-helpers, but modified in a few ways: * Ensure validations are ran sync * Ensure events are triggered async * Remove run loop wrapping * Tweak error message and validation. --- .../@ember/test-helpers/dom/fill-in.js | 40 +++++++++++++++++++ .../@ember/test-helpers/index.js | 1 + 2 files changed, 41 insertions(+) create mode 100644 addon-test-support/@ember/test-helpers/dom/fill-in.js diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js new file mode 100644 index 000000000..59cd58270 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -0,0 +1,40 @@ +import getElement from './-get-element'; +import isFormControl from './-is-form-control'; +import isContentEditable from './-is-content-editable'; +import { _focus } from './focus'; +import settled from '../settled'; +import { fireEvent } from './fire-event'; +import { nextTickPromise } from '../-utils'; + +/* + @method fillIn + @param {String|HTMLElement} target + @param {String} text + @return {Promise} + @public +*/ +export function fillIn(target, text) { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); + } + + if (!isFormControl(element) && !isContentEditable(element)) { + throw new Error('`fillIn` is only usable on form controls or contenteditable elements.'); + } + + return nextTickPromise().then(() => { + _focus(element); + + if (isContentEditable(element)) { + element.innerHTML = text; + } else { + element.value = text; + } + + fireEvent(element, 'input'); + fireEvent(element, 'change'); + + return settled(); + }); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index bfcfae983..44f068832 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -21,3 +21,4 @@ export { default as focus } from './dom/focus'; export { default as blur } from './dom/blur'; export { default as triggerEvent } from './dom/trigger-event'; export { default as triggerKeyEvent } from './dom/trigger-key-event'; +export { default as fillIn } from './dom/fill-in'; From 59886742be4b75f70cd23a53f90b5ce04d77289f Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 21:41:04 -0500 Subject: [PATCH 34/61] Mark internal helper functions as private. Also add extra underscores :P --- addon-test-support/@ember/test-helpers/dom/blur.js | 9 +++++++-- addon-test-support/@ember/test-helpers/dom/click.js | 4 ++-- addon-test-support/@ember/test-helpers/dom/fill-in.js | 4 ++-- addon-test-support/@ember/test-helpers/dom/focus.js | 9 +++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 1b8eb12dd..0d37d1154 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -4,7 +4,12 @@ import settled from '../settled'; import isFocusable from './-is-focusable'; import { nextTickPromise } from '../-utils'; -export function _blur(element) { +/** + @private + @method __blur__ + @param {HTMLElement} element +*/ +export function __blur__(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); // makes `document.activeElement` be `body`. @@ -37,7 +42,7 @@ export default function blur(selectorOrElement = document.activeElement) { } return nextTickPromise().then(() => { - _blur(element); + __blur__(element); return settled(); }); diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index f218ca6c0..c2c3554a4 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -1,6 +1,6 @@ import getElement from './-get-element'; import fireEvent from './fire-event'; -import { _focus } from './focus'; +import { __focus__ } from './focus'; import settled from '../settled'; import isFocusable from './-is-focusable'; import { nextTickPromise } from '../-utils'; @@ -21,7 +21,7 @@ export default function click(selector) { fireEvent(element, 'mousedown'); if (isFocusable(element)) { - _focus(element); + __focus__(element); } fireEvent(element, 'mouseup'); diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index 59cd58270..ba6f7039e 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -1,7 +1,7 @@ import getElement from './-get-element'; import isFormControl from './-is-form-control'; import isContentEditable from './-is-content-editable'; -import { _focus } from './focus'; +import { __focus__ } from './focus'; import settled from '../settled'; import { fireEvent } from './fire-event'; import { nextTickPromise } from '../-utils'; @@ -24,7 +24,7 @@ export function fillIn(target, text) { } return nextTickPromise().then(() => { - _focus(element); + __focus__(element); if (isContentEditable(element)) { element.innerHTML = text; diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 8f0db4f98..c5fc25596 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -4,7 +4,12 @@ import settled from '../settled'; import isFocusable from './-is-focusable'; import { nextTickPromise } from '../-utils'; -export function _focus(element) { +/** + @private + @method __focus__ + @param {HTMLElement} element +*/ +export function __focus__(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); // makes `document.activeElement` be `element`. If the browser is focused, it also fires a focus event @@ -44,7 +49,7 @@ export default function focus(selectorOrElement) { } return nextTickPromise().then(() => { - _focus(element); + __focus__(element); return settled(); }); From 5b7da2262bd1422f0bd949085e2c45353c5fc5ff Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 21:48:21 -0500 Subject: [PATCH 35/61] Fix more API doc comment blocks. --- addon-test-support/@ember/test-helpers/dom/fire-event.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index d6109cfa2..9b0758afc 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -62,7 +62,7 @@ export default function fireEvent(element, type, options = {}) { element.dispatchEvent(event); } -/* +/** @method buildBasicEvent @param {String} type @param {Object} (optional) options @@ -85,7 +85,7 @@ function buildBasicEvent(type, options = {}) { return event; } -/* +/** @method buildMouseEvent @param {String} type @param {Object} (optional) options @@ -120,7 +120,7 @@ function buildMouseEvent(type, options = {}) { return event; } -/* +/** @method buildKeyboardEvent @param {String} type @param {Object} (optional) options @@ -195,7 +195,7 @@ function buildKeyboardEvent(type, options = {}) { return event; } -/* +/** @method buildFileEvent @param {String} type @param {Array} array of flies From ca88ee26cb664204546f5ddbf3bb7c8a9f9b011d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 21:55:34 -0500 Subject: [PATCH 36/61] Tweak more documentation. * Use `target` to mean `selectorOrElement` * Properly indicate optional params * Show default values --- addon-test-support/@ember/test-helpers/dom/blur.js | 10 +++++----- addon-test-support/@ember/test-helpers/dom/click.js | 8 ++++---- .../@ember/test-helpers/dom/fire-event.js | 8 ++++---- addon-test-support/@ember/test-helpers/dom/focus.js | 12 ++++++------ .../@ember/test-helpers/dom/trigger-event.js | 12 +++++------- .../@ember/test-helpers/dom/trigger-key-event.js | 10 +++++----- 6 files changed, 29 insertions(+), 31 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 0d37d1154..d68064ca5 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -27,18 +27,18 @@ export function __blur__(element) { /** @method blur - @param {String|HTMLElement} selector + @param {String|HTMLElement} [target=document.activeElement] the element to blur @return {Promise} @public */ -export default function blur(selectorOrElement = document.activeElement) { - let element = getElement(selectorOrElement); +export default function blur(target = document.activeElement) { + let element = getElement(target); if (!element) { - throw new Error(`Element not found when calling \`blur('${selectorOrElement}')\`.`); + throw new Error(`Element not found when calling \`blur('${target}')\`.`); } if (!isFocusable(element)) { - throw new Error(`${selectorOrElement} is not focusable`); + throw new Error(`${target} is not focusable`); } return nextTickPromise().then(() => { diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index c2c3554a4..f65c2f7c2 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -7,14 +7,14 @@ import { nextTickPromise } from '../-utils'; /** @method click - @param {String|HTMLElement} selector + @param {String|HTMLElement} target @return {Promise} @public */ -export default function click(selector) { - let element = getElement(selector); +export default function click(target) { + let element = getElement(target); if (!element) { - throw new Error(`Element not found when calling \`click('${selector}')\`.`); + throw new Error(`Element not found when calling \`click('${target}')\`.`); } return nextTickPromise().then(() => { diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index 9b0758afc..0a651f42a 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -19,7 +19,7 @@ const FILE_SELECTION_EVENT_TYPES = ['change']; @method fireEvent @param {HTMLElement} element @param {String} type - @param {Object} (optional) options + @param {Object} [options] @private */ @@ -65,7 +65,7 @@ export default function fireEvent(element, type, options = {}) { /** @method buildBasicEvent @param {String} type - @param {Object} (optional) options + @param {Object} [options] @return {Event} @private */ @@ -88,7 +88,7 @@ function buildBasicEvent(type, options = {}) { /** @method buildMouseEvent @param {String} type - @param {Object} (optional) options + @param {Object} [options] @return {Event} @private */ @@ -198,8 +198,8 @@ function buildKeyboardEvent(type, options = {}) { /** @method buildFileEvent @param {String} type - @param {Array} array of flies @param {HTMLElement} element + @param {Array} [files] array of files @return {Event} @private */ diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index c5fc25596..1250fd199 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -30,22 +30,22 @@ export function __focus__(element) { /** @method focus - @param {String|HTMLElement} selector + @param {String|HTMLElement} target @return {Promise} @public */ -export default function focus(selectorOrElement) { - if (!selectorOrElement) { +export default function focus(target) { + if (!target) { throw new Error('Must pass an element or selector to `focus`.'); } - let element = getElement(selectorOrElement); + let element = getElement(target); if (!element) { - throw new Error(`Element not found when calling \`focus('${selectorOrElement}')\`.`); + throw new Error(`Element not found when calling \`focus('${target}')\`.`); } if (!isFocusable(element)) { - throw new Error(`${selectorOrElement} is not focusable`); + throw new Error(`${target} is not focusable`); } return nextTickPromise().then(() => { diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index 22c64ea03..7606978f2 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -5,22 +5,20 @@ import { nextTickPromise } from '../-utils'; /** @method triggerEvent - @param {String|HTMLElement} selector + @param {String|HTMLElement} target @param {String} eventType @param {Object} options @return {Promise} @public */ -export default function triggerEvent(selectorOrElement, type, options) { - if (!selectorOrElement) { +export default function triggerEvent(target, type, options) { + if (!target) { throw new Error('Must pass an element or selector to `triggerEvent`.'); } - let element = getElement(selectorOrElement); + let element = getElement(target); if (!element) { - throw new Error( - `Element not found when calling \`triggerEvent('${selectorOrElement}', ...)\`.` - ); + throw new Error(`Element not found when calling \`triggerEvent('${target}', ...)\`.`); } if (!type) { diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 073534205..196471b06 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -15,11 +15,11 @@ const DEFAULT_MODIFIERS = Object.freeze({ @param {String|HTMLElement} target @param {'keydown' | 'keyup' | 'keypress'} eventType @param {String} keyCode - @param {Object} modifiers - @param {Boolean} modifiers.ctrlKey - @param {Boolean} modifiers.altKey - @param {Boolean} modifiers.shiftKey - @param {Boolean} modifiers.metaKey + @param {Object} [modifiers] + @param {Boolean} [modifiers.ctrlKey=false] + @param {Boolean} [modifiers.altKey=false] + @param {Boolean} [modifiers.shiftKey=false] + @param {Boolean} [modifiers.metaKey=false] @return {Promise} */ export default function triggerKeyEvent(target, eventType, keyCode, modifiers = DEFAULT_MODIFIERS) { From 1ffe33c909363df9d7a8b147e3ad2c6ef6b198dd Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 22:08:03 -0500 Subject: [PATCH 37/61] Add explicit `assert.expect`. --- tests/unit/legacy-0-6-x/test-module-for-component-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/legacy-0-6-x/test-module-for-component-test.js b/tests/unit/legacy-0-6-x/test-module-for-component-test.js index c75c71cd5..571987e18 100644 --- a/tests/unit/legacy-0-6-x/test-module-for-component-test.js +++ b/tests/unit/legacy-0-6-x/test-module-for-component-test.js @@ -466,6 +466,8 @@ test('it supports DOM events', function(assert) { }); test('it supports updating an input', function(assert) { + assert.expect(1); + setResolverRegistry({ 'component:my-input': TextField.extend({ value: null, From d91821c7c655d1f0d8b13917320ee18206bb0f44 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 22:08:15 -0500 Subject: [PATCH 38/61] Use `nextTick` to avoid time shifting issues. --- addon-test-support/@ember/test-helpers/wait-until.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/wait-until.js b/addon-test-support/@ember/test-helpers/wait-until.js index 7225cd95a..75bcf515b 100644 --- a/addon-test-support/@ember/test-helpers/wait-until.js +++ b/addon-test-support/@ember/test-helpers/wait-until.js @@ -18,7 +18,7 @@ export default function(callback, options = {}) { } else if (time < timeout) { // using `setTimeout` directly to allow fake timers // to intercept - setTimeout(tick, 10); + nextTick(tick, 10); } else { reject(waitUntilTimedOut); } From 2f34b26f055a7cc484fde531295bea738762e115 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 22:30:46 -0500 Subject: [PATCH 39/61] Add missing test case from DOM helpers. --- tests/unit/dom/blur-test.js | 16 +++++++++++ tests/unit/dom/click-test.js | 34 ++++++++++++++++++++++++ tests/unit/dom/focus-test.js | 17 ++++++++++++ tests/unit/dom/trigger-event-test.js | 18 +++++++++++++ tests/unit/dom/trigger-key-event-test.js | 18 +++++++++++++ 5 files changed, 103 insertions(+) diff --git a/tests/unit/dom/blur-test.js b/tests/unit/dom/blur-test.js index a2b2a45c8..3cb1255e7 100644 --- a/tests/unit/dom/blur-test.js +++ b/tests/unit/dom/blur-test.js @@ -51,6 +51,22 @@ module('DOM Helper: blur', function(hooks) { assert.notEqual(document.activeElement, element, 'activeElement updated'); }); + test('bluring via selector without context set', async function(assert) { + let errorThrown; + + try { + await blur(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + test('bluring via element with context set', async function(assert) { setContext(this); await blur(element); diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index ce2ede050..5c120f71a 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -65,6 +65,23 @@ module('DOM Helper: click', function(hooks) { click(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `click\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); + + test('clicking a div via selector without context set', async function(assert) { + element = buildInstrumentedElement('div'); + let errorThrown; + + try { + await click(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); }); module('focusable element types', function() { @@ -96,5 +113,22 @@ module('DOM Helper: click', function(hooks) { assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); assert.strictEqual(document.activeElement, element, 'activeElement updated'); }); + + test('clicking a input via selector without context set', async function(assert) { + element = buildInstrumentedElement('input'); + let errorThrown; + + try { + await click(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); }); }); diff --git a/tests/unit/dom/focus-test.js b/tests/unit/dom/focus-test.js index bea23c9ee..3394dcb89 100644 --- a/tests/unit/dom/focus-test.js +++ b/tests/unit/dom/focus-test.js @@ -85,4 +85,21 @@ module('DOM Helper: focus', function(hooks) { assert.verifySteps(['focus', 'focusin']); assert.strictEqual(document.activeElement, element, 'activeElement updated'); }); + + test('focusing a input via selector without context set', async function(assert) { + element = buildInstrumentedElement('input'); + let errorThrown; + + try { + await focus(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); }); diff --git a/tests/unit/dom/trigger-event-test.js b/tests/unit/dom/trigger-event-test.js index 59ebeaada..3b65a7702 100644 --- a/tests/unit/dom/trigger-event-test.js +++ b/tests/unit/dom/trigger-event-test.js @@ -57,6 +57,24 @@ module('DOM Helper: triggerEvent', function(hooks) { assert.verifySteps(['mouseenter']); }); + test('triggering event via selector without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + let errorThrown; + + try { + await triggerEvent(`#${element.id}`, 'mouseenter'); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + test('does not run sync', async function(assert) { element = buildInstrumentedElement('div'); diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js index 9eb503355..30f69d616 100644 --- a/tests/unit/dom/trigger-key-event-test.js +++ b/tests/unit/dom/trigger-key-event-test.js @@ -71,6 +71,24 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { assert.verifySteps(['keydown']); }); + test('triggering via selector without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + let errorThrown; + + try { + await triggerKeyEvent(`#${element.id}`, 'keydown', 13); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + ['ctrl', 'shift', 'alt', 'meta'].forEach(function(modifierType) { test(`triggering passing with ${modifierType} pressed`, async function(assert) { element = buildInstrumentedElement('div'); From 61222a542f4fea26f089b50b87b4e0d15f2e50e8 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 9 Dec 2017 22:49:56 -0500 Subject: [PATCH 40/61] Refactor DOM helper tests. Avoid `setContext(this)` (use `setContext(localVariable)`) to make it much clearer what is going on. --- tests/unit/dom/blur-test.js | 38 +++++++++++++----------- tests/unit/dom/click-test.js | 16 +++++----- tests/unit/dom/focus-test.js | 16 +++++----- tests/unit/dom/trigger-event-test.js | 18 ++++++----- tests/unit/dom/trigger-key-event-test.js | 20 +++++++------ 5 files changed, 59 insertions(+), 49 deletions(-) diff --git a/tests/unit/dom/blur-test.js b/tests/unit/dom/blur-test.js index 3cb1255e7..45b38c1bf 100644 --- a/tests/unit/dom/blur-test.js +++ b/tests/unit/dom/blur-test.js @@ -3,31 +3,33 @@ import { focus, blur, setContext, unsetContext } from '@ember/test-helpers'; import { buildInstrumentedElement } from '../../helpers/events'; module('DOM Helper: blur', function(hooks) { - let element; + let context, elementWithFocus; hooks.beforeEach(async function(assert) { // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) // set context.element to the rootElement - this.element = document.querySelector('#qunit-fixture'); + context = { + element: document.querySelector('#qunit-fixture'), + }; // create the element and focus in preparation for blur testing - element = buildInstrumentedElement('input'); - await focus(element); + elementWithFocus = buildInstrumentedElement('input'); + await focus(elementWithFocus); // verify that focus was ran, and reset steps assert.verifySteps(['focus', 'focusin']); - assert.equal(document.activeElement, element, 'activeElement updated'); + assert.equal(document.activeElement, elementWithFocus, 'activeElement updated'); }); hooks.afterEach(function() { - if (element) { - element.parentNode.removeChild(element); + if (elementWithFocus) { + elementWithFocus.parentNode.removeChild(elementWithFocus); } unsetContext(); }); test('does not run sync', async function(assert) { - let promise = blur(element); + let promise = blur(elementWithFocus); assert.verifySteps([]); @@ -37,25 +39,25 @@ module('DOM Helper: blur', function(hooks) { }); test('throws an error if selector is not found', async function(assert) { - setContext(this); + setContext(context); assert.throws(() => { blur(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `blur\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); test('bluring via selector with context set', async function(assert) { - setContext(this); - await blur(`#${element.id}`); + setContext(context); + await blur(`#${elementWithFocus.id}`); assert.verifySteps(['blur', 'focusout']); - assert.notEqual(document.activeElement, element, 'activeElement updated'); + assert.notEqual(document.activeElement, elementWithFocus, 'activeElement updated'); }); test('bluring via selector without context set', async function(assert) { let errorThrown; try { - await blur(`#${element.id}`); + await blur(`#${elementWithFocus.id}`); } catch (error) { errorThrown = error; } @@ -68,17 +70,17 @@ module('DOM Helper: blur', function(hooks) { }); test('bluring via element with context set', async function(assert) { - setContext(this); - await blur(element); + setContext(context); + await blur(elementWithFocus); assert.verifySteps(['blur', 'focusout']); - assert.notEqual(document.activeElement, element, 'activeElement updated'); + assert.notEqual(document.activeElement, elementWithFocus, 'activeElement updated'); }); test('bluring via element without context set', async function(assert) { - await blur(element); + await blur(elementWithFocus); assert.verifySteps(['blur', 'focusout']); - assert.notEqual(document.activeElement, element, 'activeElement updated'); + assert.notEqual(document.activeElement, elementWithFocus, 'activeElement updated'); }); }); diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index 5c120f71a..844b86b3d 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -3,12 +3,14 @@ import { click, setContext, unsetContext } from '@ember/test-helpers'; import { buildInstrumentedElement } from '../../helpers/events'; module('DOM Helper: click', function(hooks) { - let element; + let context, element; hooks.beforeEach(function() { // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) // set context.element to the rootElement - this.element = document.querySelector('#qunit-fixture'); + context = { + element: document.querySelector('#qunit-fixture'), + }; }); hooks.afterEach(function() { @@ -22,7 +24,7 @@ module('DOM Helper: click', function(hooks) { test('clicking a div via selector with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await click(`#${element.id}`); assert.verifySteps(['mousedown', 'mouseup', 'click']); @@ -31,7 +33,7 @@ module('DOM Helper: click', function(hooks) { test('clicking a div via element with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await click(element); assert.verifySteps(['mousedown', 'mouseup', 'click']); @@ -61,7 +63,7 @@ module('DOM Helper: click', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); click(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `click\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); @@ -88,7 +90,7 @@ module('DOM Helper: click', function(hooks) { test('clicking a input via selector with context set', async function(assert) { element = buildInstrumentedElement('input'); - setContext(this); + setContext(context); await click(`#${element.id}`); assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); @@ -98,7 +100,7 @@ module('DOM Helper: click', function(hooks) { test('clicking a input via element with context set', async function(assert) { element = buildInstrumentedElement('input'); - setContext(this); + setContext(context); await click(element); assert.verifySteps(['mousedown', 'focus', 'focusin', 'mouseup', 'click']); diff --git a/tests/unit/dom/focus-test.js b/tests/unit/dom/focus-test.js index 3394dcb89..dbed0c686 100644 --- a/tests/unit/dom/focus-test.js +++ b/tests/unit/dom/focus-test.js @@ -3,12 +3,14 @@ import { focus, setContext, unsetContext } from '@ember/test-helpers'; import { buildInstrumentedElement } from '../../helpers/events'; module('DOM Helper: focus', function(hooks) { - let element; + let context, element; hooks.beforeEach(function() { // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) // set context.element to the rootElement - this.element = document.querySelector('#qunit-fixture'); + context = { + element: document.querySelector('#qunit-fixture'), + }; }); hooks.afterEach(function() { @@ -21,7 +23,7 @@ module('DOM Helper: focus', function(hooks) { test('focusing a div via selector with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); assert.throws(() => { focus(`#${element.id}`); }, /is not focusable/); @@ -30,7 +32,7 @@ module('DOM Helper: focus', function(hooks) { test('focusing a div via element with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); assert.throws(() => { focus(element); }, /is not focusable/); @@ -52,7 +54,7 @@ module('DOM Helper: focus', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); focus(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `focus\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); @@ -60,7 +62,7 @@ module('DOM Helper: focus', function(hooks) { test('focusing a input via selector with context set', async function(assert) { element = buildInstrumentedElement('input'); - setContext(this); + setContext(context); await focus(`#${element.id}`); assert.verifySteps(['focus', 'focusin']); @@ -70,7 +72,7 @@ module('DOM Helper: focus', function(hooks) { test('focusing a input via element with context set', async function(assert) { element = buildInstrumentedElement('input'); - setContext(this); + setContext(context); await focus(element); assert.verifySteps(['focus', 'focusin']); diff --git a/tests/unit/dom/trigger-event-test.js b/tests/unit/dom/trigger-event-test.js index 3b65a7702..cb01a7eb5 100644 --- a/tests/unit/dom/trigger-event-test.js +++ b/tests/unit/dom/trigger-event-test.js @@ -3,12 +3,14 @@ import { triggerEvent, setContext, unsetContext } from '@ember/test-helpers'; import { buildInstrumentedElement } from '../../helpers/events'; module('DOM Helper: triggerEvent', function(hooks) { - let element; + let context, element; hooks.beforeEach(function() { // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) // set context.element to the rootElement - this.element = document.querySelector('#qunit-fixture'); + context = { + element: document.querySelector('#qunit-fixture'), + }; }); hooks.afterEach(function() { @@ -25,7 +27,7 @@ module('DOM Helper: triggerEvent', function(hooks) { assert.ok(e instanceof Event, `fliberty listener receives a native event`); }); - setContext(this); + setContext(context); await triggerEvent(`#${element.id}`, 'fliberty'); assert.verifySteps(['fliberty']); @@ -34,7 +36,7 @@ module('DOM Helper: triggerEvent', function(hooks) { test('triggering event via selector with context set fires the given event type', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await triggerEvent(`#${element.id}`, 'mouseenter'); assert.verifySteps(['mouseenter']); @@ -43,7 +45,7 @@ module('DOM Helper: triggerEvent', function(hooks) { test('triggering event via element with context set fires the given event type', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await triggerEvent(element, 'mouseenter'); assert.verifySteps(['mouseenter']); @@ -91,7 +93,7 @@ module('DOM Helper: triggerEvent', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); triggerEvent(`#foo-bar-baz-not-here-ever-bye-bye`, 'mouseenter'); }, /Element not found when calling `triggerEvent\('#foo-bar-baz-not-here-ever-bye-bye'/); }); @@ -100,13 +102,13 @@ module('DOM Helper: triggerEvent', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); triggerEvent(element); }, /Must provide an `eventType` to `triggerEvent`/); }); test('events properly bubble upwards', async function(assert) { - setContext(this); + setContext(context); element = buildInstrumentedElement('div'); element.innerHTML = `
diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js index 30f69d616..772aa59c6 100644 --- a/tests/unit/dom/trigger-key-event-test.js +++ b/tests/unit/dom/trigger-key-event-test.js @@ -3,12 +3,14 @@ import { triggerKeyEvent, setContext, unsetContext } from '@ember/test-helpers'; import { buildInstrumentedElement } from '../../helpers/events'; module('DOM Helper: triggerKeyEvent', function(hooks) { - let element; + let context, element; hooks.beforeEach(function() { // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) // set context.element to the rootElement - this.element = document.querySelector('#qunit-fixture'); + context = { + element: document.querySelector('#qunit-fixture'), + }; }); hooks.afterEach(function() { @@ -22,7 +24,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); triggerKeyEvent(element); }, /Must provide an `eventType` to `triggerKeyEvent`/); }); @@ -31,7 +33,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); triggerKeyEvent(element, 'mouseenter'); }, /Must provide an `eventType` of keydown, keypress, keyup to `triggerKeyEvent` but you passed `mouseenter`./); }); @@ -40,7 +42,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { element = buildInstrumentedElement('div'); assert.throws(() => { - setContext(this); + setContext(context); triggerKeyEvent(element, 'keypress'); }, /Must provide a `keyCode` to `triggerKeyEvent`/); }); @@ -48,7 +50,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { test('triggering via selector with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await triggerKeyEvent(`#${element.id}`, 'keydown', 13); assert.verifySteps(['keydown']); @@ -57,7 +59,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { test('triggering via element with context set', async function(assert) { element = buildInstrumentedElement('div'); - setContext(this); + setContext(context); await triggerKeyEvent(element, 'keydown', 13); assert.verifySteps(['keydown']); @@ -96,7 +98,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { assert.ok(e[`${modifierType}Key`], `has ${modifierType} indicated`); }); - setContext(this); + setContext(context); await triggerKeyEvent(element, 'keypress', 13, { [`${modifierType}Key`]: true }); assert.verifySteps(['keypress']); @@ -110,7 +112,7 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { assert.ok(e.altKey, `has altKey indicated`); }); - setContext(this); + setContext(context); await triggerKeyEvent(element, 'keypress', 13, { altKey: true, ctrlKey: true }); assert.verifySteps(['keypress']); From b6073e4e7ff8950c12e3bd5a0b8c2ffd54257187 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 10:43:13 -0500 Subject: [PATCH 41/61] Add tests for `fillIn`. --- .../@ember/test-helpers/dom/fill-in.js | 8 +- tests/unit/dom/fill-in-test.js | 145 ++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/unit/dom/fill-in-test.js diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index ba6f7039e..c69651ff6 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -3,7 +3,7 @@ import isFormControl from './-is-form-control'; import isContentEditable from './-is-content-editable'; import { __focus__ } from './focus'; import settled from '../settled'; -import { fireEvent } from './fire-event'; +import fireEvent from './fire-event'; import { nextTickPromise } from '../-utils'; /* @@ -13,7 +13,7 @@ import { nextTickPromise } from '../-utils'; @return {Promise} @public */ -export function fillIn(target, text) { +export default function fillIn(target, text) { let element = getElement(target); if (!element) { throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); @@ -23,6 +23,10 @@ export function fillIn(target, text) { throw new Error('`fillIn` is only usable on form controls or contenteditable elements.'); } + if (!text) { + throw new Error('Must provide `text` when calling `fillIn`.'); + } + return nextTickPromise().then(() => { __focus__(element); diff --git a/tests/unit/dom/fill-in-test.js b/tests/unit/dom/fill-in-test.js new file mode 100644 index 000000000..a6de4da0c --- /dev/null +++ b/tests/unit/dom/fill-in-test.js @@ -0,0 +1,145 @@ +import { module, test } from 'qunit'; +import { fillIn, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: fillIn', function(hooks) { + let context, element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + context = { + element: document.querySelector('#qunit-fixture'), + }; + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + test('filling in a non-fillable element', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(context); + assert.throws(() => { + fillIn(`#${element.id}`, 'foo'); + }, /`fillIn` is only usable on form controls or contenteditable elements/); + }); + + test('throws an error if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(context); + fillIn(`#foo-bar-baz-not-here-ever-bye-bye`, 'foo'); + }, /Element not found when calling `fillIn\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); + }); + + test('throws an error if text to fill in is not provided', async function(assert) { + element = buildInstrumentedElement('input'); + + assert.throws(() => { + fillIn(element); + }, /Must provide `text` when calling `fillIn`/); + }); + + test('filling an input via selector without context set', async function(assert) { + element = buildInstrumentedElement('input'); + let errorThrown; + + try { + await fillIn(`#${element.id}`, 'foo'); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('input'); + + let promise = fillIn(element, 'foo'); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + assert.equal(element.value, 'foo'); + }); + + test('filling a textarea via selector with context set', async function(assert) { + element = buildInstrumentedElement('textarea'); + + setContext(context); + await fillIn(`#${element.id}`, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + assert.equal(element.value, 'foo'); + }); + + test('filling an input via element with context set', async function(assert) { + element = buildInstrumentedElement('textarea'); + + setContext(context); + await fillIn(element, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + 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'); + + setContext(context); + await fillIn(`#${element.id}`, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + assert.equal(element.value, 'foo'); + }); + + test('filling an input via element with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(context); + await fillIn(element, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + 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', ''); + + setContext(context); + await fillIn(element, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + 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'); + + await fillIn(element, 'foo'); + + assert.verifySteps(['focus', 'focusin', 'input', 'change']); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + assert.equal(element.value, 'foo'); + }); +}); From ead022af49d5ede969b253087784b14ffc1204b5 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 15:54:21 -0500 Subject: [PATCH 42/61] Leverage element.isContentEditable instead of local util. --- .../@ember/test-helpers/dom/-is-content-editable.js | 3 --- addon-test-support/@ember/test-helpers/dom/-is-focusable.js | 3 +-- addon-test-support/@ember/test-helpers/dom/fill-in.js | 5 ++--- 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 addon-test-support/@ember/test-helpers/dom/-is-content-editable.js diff --git a/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js b/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js deleted file mode 100644 index aece30637..000000000 --- a/addon-test-support/@ember/test-helpers/dom/-is-content-editable.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function isContentEditable(el) { - return el.contentEditable === 'true'; -} diff --git a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js index f3f06d45b..857923dca 100644 --- a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js +++ b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js @@ -1,11 +1,10 @@ import isFormControl from './-is-form-control'; -import isContentEditable from './-is-content-editable'; const FOCUSABLE_TAGS = ['LINK', 'A']; export default function isFocusable(element) { if ( isFormControl(element) || - isContentEditable(element) || + element.isContentEditable || FOCUSABLE_TAGS.indexOf(element.tagName) > -1 ) { return true; diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index c69651ff6..8760f18e6 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -1,6 +1,5 @@ import getElement from './-get-element'; import isFormControl from './-is-form-control'; -import isContentEditable from './-is-content-editable'; import { __focus__ } from './focus'; import settled from '../settled'; import fireEvent from './fire-event'; @@ -19,7 +18,7 @@ export default function fillIn(target, text) { throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); } - if (!isFormControl(element) && !isContentEditable(element)) { + if (!isFormControl(element) && !element.isContentEditable) { throw new Error('`fillIn` is only usable on form controls or contenteditable elements.'); } @@ -30,7 +29,7 @@ export default function fillIn(target, text) { return nextTickPromise().then(() => { __focus__(element); - if (isContentEditable(element)) { + if (element.isContentEditable) { element.innerHTML = text; } else { element.value = text; From 62e22c7db744fb8c5b5cab7754abaa9d002b563a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 15:55:53 -0500 Subject: [PATCH 43/61] Remove LINK from `FOCUSABLE_TAGS`. --- addon-test-support/@ember/test-helpers/dom/-is-focusable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js index 857923dca..a00abd843 100644 --- a/addon-test-support/@ember/test-helpers/dom/-is-focusable.js +++ b/addon-test-support/@ember/test-helpers/dom/-is-focusable.js @@ -1,6 +1,6 @@ import isFormControl from './-is-form-control'; -const FOCUSABLE_TAGS = ['LINK', 'A']; +const FOCUSABLE_TAGS = ['A']; export default function isFocusable(element) { if ( isFormControl(element) || From 612c81582e096c2dbedc6c6569f9b50fa06f4a19 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 16:02:09 -0500 Subject: [PATCH 44/61] Replace HTMLElement || SVGElement with Element. --- addon-test-support/@ember/test-helpers/dom/-get-element.js | 3 +-- addon-test-support/@ember/test-helpers/dom/blur.js | 4 ++-- addon-test-support/@ember/test-helpers/dom/click.js | 2 +- addon-test-support/@ember/test-helpers/dom/fill-in.js | 2 +- addon-test-support/@ember/test-helpers/dom/fire-event.js | 6 +++--- addon-test-support/@ember/test-helpers/dom/focus.js | 4 ++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-get-element.js b/addon-test-support/@ember/test-helpers/dom/-get-element.js index 078d63c3b..10eb30cf2 100644 --- a/addon-test-support/@ember/test-helpers/dom/-get-element.js +++ b/addon-test-support/@ember/test-helpers/dom/-get-element.js @@ -4,8 +4,7 @@ export default function getElement(selectorOrElement) { if ( selectorOrElement instanceof Window || selectorOrElement instanceof Document || - selectorOrElement instanceof HTMLElement || - selectorOrElement instanceof SVGElement + selectorOrElement instanceof Element ) { return selectorOrElement; } else if (typeof selectorOrElement === 'string') { diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index d68064ca5..59611291e 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -7,7 +7,7 @@ import { nextTickPromise } from '../-utils'; /** @private @method __blur__ - @param {HTMLElement} element + @param {Element} element */ export function __blur__(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); @@ -27,7 +27,7 @@ export function __blur__(element) { /** @method blur - @param {String|HTMLElement} [target=document.activeElement] the element to blur + @param {String|Element} [target=document.activeElement] the element to blur @return {Promise} @public */ diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index f65c2f7c2..0bdd3cfbd 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -7,7 +7,7 @@ import { nextTickPromise } from '../-utils'; /** @method click - @param {String|HTMLElement} target + @param {String|Element} target @return {Promise} @public */ diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index 8760f18e6..c62776322 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -7,7 +7,7 @@ import { nextTickPromise } from '../-utils'; /* @method fillIn - @param {String|HTMLElement} target + @param {String|Element} target @param {String} text @return {Promise} @public diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index 0a651f42a..822d34d74 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -17,7 +17,7 @@ const FILE_SELECTION_EVENT_TYPES = ['change']; /** @method fireEvent - @param {HTMLElement} element + @param {Element} element @param {String} type @param {Object} [options] @@ -37,7 +37,7 @@ export default function fireEvent(element, type, options = {}) { rect = element.document.documentElement.getBoundingClientRect(); } else if (element instanceof Document) { rect = element.documentElement.getBoundingClientRect(); - } else if (element instanceof HTMLElement || element instanceof SVGElement) { + } else if (element instanceof Element) { rect = element.getBoundingClientRect(); } else { return; @@ -198,7 +198,7 @@ function buildKeyboardEvent(type, options = {}) { /** @method buildFileEvent @param {String} type - @param {HTMLElement} element + @param {Element} element @param {Array} [files] array of files @return {Event} @private diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 1250fd199..9bca2c8eb 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -7,7 +7,7 @@ import { nextTickPromise } from '../-utils'; /** @private @method __focus__ - @param {HTMLElement} element + @param {Element} element */ export function __focus__(element) { let browserIsNotFocused = document.hasFocus && !document.hasFocus(); @@ -30,7 +30,7 @@ export function __focus__(element) { /** @method focus - @param {String|HTMLElement} target + @param {String|Element} target @return {Promise} @public */ From f2b3c35665bbd34b78141b74c177fe7a2ac3c9f5 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 16:09:51 -0500 Subject: [PATCH 45/61] Split "click even sequence" from `click`. --- .../@ember/test-helpers/dom/click.js | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 0bdd3cfbd..4dcf3b874 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -5,6 +5,22 @@ import settled from '../settled'; import isFocusable from './-is-focusable'; import { nextTickPromise } from '../-utils'; +/** + @private + @method __click__ + @param {Element} element +*/ +export function __click__(element) { + fireEvent(element, 'mousedown'); + + if (isFocusable(element)) { + __focus__(element); + } + + fireEvent(element, 'mouseup'); + fireEvent(element, 'click'); +} + /** @method click @param {String|Element} target @@ -18,15 +34,7 @@ export default function click(target) { } return nextTickPromise().then(() => { - fireEvent(element, 'mousedown'); - - if (isFocusable(element)) { - __focus__(element); - } - - fireEvent(element, 'mouseup'); - fireEvent(element, 'click'); - + __click__(element); return settled(); }); } From 2cc394e7ccf3fcdba93dcdb4f4c709f2cd92585d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 16:10:14 -0500 Subject: [PATCH 46/61] Add `tap` implementation. --- .../@ember/test-helpers/dom/fire-event.js | 1 + .../@ember/test-helpers/dom/tap.js | 30 +++++++++++++++++++ .../@ember/test-helpers/index.js | 1 + 3 files changed, 32 insertions(+) create mode 100644 addon-test-support/@ember/test-helpers/dom/tap.js diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index 822d34d74..adb67e57b 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -60,6 +60,7 @@ export default function fireEvent(element, type, options = {}) { } element.dispatchEvent(event); + return event; } /** diff --git a/addon-test-support/@ember/test-helpers/dom/tap.js b/addon-test-support/@ember/test-helpers/dom/tap.js new file mode 100644 index 000000000..29115e05e --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/tap.js @@ -0,0 +1,30 @@ +import getElement from './-get-element'; +import fireEvent from './fire-event'; +import { __click__ } from './click'; +import settled from '../settled'; +import { nextTickPromise } from '../-utils'; + +/* + @method tap + @param {String|Element} target + @param {Object} options + @return {Promise} + @public +*/ +export default function tap(target, options = {}) { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`tap('${target}')\`.`); + } + + return nextTickPromise().then(() => { + let touchstartEv = fireEvent(element, 'touchstart', options); + let touchendEv = fireEvent(element, 'touchend', options); + + if (!touchstartEv.defaultPrevented && !touchendEv.defaultPrevented) { + __click__(element); + } + + return settled(); + }); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 44f068832..386c080a1 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -17,6 +17,7 @@ export { default as validateErrorHandler } from './validate-error-handler'; // DOM Helpers export { default as click } from './dom/click'; +export { default as tap } from './dom/tap'; export { default as focus } from './dom/focus'; export { default as blur } from './dom/blur'; export { default as triggerEvent } from './dom/trigger-event'; From f03eead6d558ceba762842aa7bb5a99244000f9a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 16:24:21 -0500 Subject: [PATCH 47/61] Add unit tests for tap. --- tests/unit/dom/tap-test.js | 160 +++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/unit/dom/tap-test.js diff --git a/tests/unit/dom/tap-test.js b/tests/unit/dom/tap-test.js new file mode 100644 index 000000000..05cb75d85 --- /dev/null +++ b/tests/unit/dom/tap-test.js @@ -0,0 +1,160 @@ +import { module, test } from 'qunit'; +import { tap, setContext, unsetContext } from '@ember/test-helpers'; +import { buildInstrumentedElement } from '../../helpers/events'; + +module('DOM Helper: tap', function(hooks) { + let context, element; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + context = { + element: document.querySelector('#qunit-fixture'), + }; + }); + + hooks.afterEach(function() { + if (element) { + element.parentNode.removeChild(element); + } + unsetContext(); + }); + + module('non-focusable element types', function() { + test('taping a div via selector with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(context); + await tap(`#${element.id}`); + + assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); + }); + + test('tapping a div via element with context set', async function(assert) { + element = buildInstrumentedElement('div'); + + setContext(context); + await tap(element); + + assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); + }); + + test('tapping a div via element without context set', async function(assert) { + element = buildInstrumentedElement('div'); + + await tap(element); + + assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); + }); + + test('does not run sync', async function(assert) { + element = buildInstrumentedElement('div'); + + let promise = tap(element); + + assert.verifySteps([]); + + await promise; + + assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); + }); + + test('throws an error if selector is not found', async function(assert) { + element = buildInstrumentedElement('div'); + + assert.throws(() => { + setContext(context); + tap(`#foo-bar-baz-not-here-ever-bye-bye`); + }, /Element not found when calling `tap\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); + }); + + test('tapping a div via selector without context set', async function(assert) { + element = buildInstrumentedElement('div'); + let errorThrown; + + try { + await tap(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + }); + + module('focusable element types', function() { + test('tapping a input via selector with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(context); + await tap(`#${element.id}`); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + ]); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('tapping a input via element with context set', async function(assert) { + element = buildInstrumentedElement('input'); + + setContext(context); + await tap(element); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + ]); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('tapping a input via element without context set', async function(assert) { + element = buildInstrumentedElement('input'); + + await tap(element); + + assert.verifySteps([ + 'touchstart', + 'touchend', + 'mousedown', + 'focus', + 'focusin', + 'mouseup', + 'click', + ]); + assert.strictEqual(document.activeElement, element, 'activeElement updated'); + }); + + test('tapping a input via selector without context set', async function(assert) { + element = buildInstrumentedElement('input'); + let errorThrown; + + try { + await tap(`#${element.id}`); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + }); +}); From 5c23057a29d2d6ee57bd964a3b92d52f7a7fcda1 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 10 Dec 2017 16:35:19 -0500 Subject: [PATCH 48/61] Add basic `waitFor` implementation. --- .../@ember/test-helpers/dom/wait-for.js | 27 +++++++++++++++++++ .../@ember/test-helpers/index.js | 1 + 2 files changed, 28 insertions(+) create mode 100644 addon-test-support/@ember/test-helpers/dom/wait-for.js diff --git a/addon-test-support/@ember/test-helpers/dom/wait-for.js b/addon-test-support/@ember/test-helpers/dom/wait-for.js new file mode 100644 index 000000000..3bac0a016 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/wait-for.js @@ -0,0 +1,27 @@ +import waitUntil from '../wait-until'; +import { getContext } from '../setup-context'; +import getElement from './-get-element'; + +/** + @method waitFor + @param {string|Element} target + @param {Object} [options] + @param {number} [options.timeout=1000] + @param {number} [options.count=1] +*/ +export default function waitFor(selector, { timeout = 1000, count = null } = {}) { + let callback; + if (count !== null) { + callback = () => { + let context = getContext(); + let rootElement = context && context.element; + let elements = rootElement.querySelectorAll(selector); + if (elements.length === count) { + return elements; + } + }; + } else { + callback = () => getElement(selector); + } + return waitUntil(callback, { timeout }); +} diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js index 386c080a1..36f840124 100644 --- a/addon-test-support/@ember/test-helpers/index.js +++ b/addon-test-support/@ember/test-helpers/index.js @@ -23,3 +23,4 @@ export { default as blur } from './dom/blur'; export { default as triggerEvent } from './dom/trigger-event'; export { default as triggerKeyEvent } from './dom/trigger-key-event'; export { default as fillIn } from './dom/fill-in'; +export { default as waitFor } from './dom/wait-for'; From 2a8b97d87b7fef01b00e12bf3a2d2b1a787214a0 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Tue, 12 Dec 2017 09:28:32 -0500 Subject: [PATCH 49/61] More API documentation tweaks. --- addon-test-support/@ember/test-helpers/dom/fire-event.js | 1 + addon-test-support/@ember/test-helpers/dom/trigger-event.js | 2 +- addon-test-support/@ember/test-helpers/dom/trigger-key-event.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.js b/addon-test-support/@ember/test-helpers/dom/fire-event.js index adb67e57b..1679e4c10 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.js +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.js @@ -20,6 +20,7 @@ const FILE_SELECTION_EVENT_TYPES = ['change']; @param {Element} element @param {String} type @param {Object} [options] + @returns {Event} @private */ diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index 7606978f2..cd942b11b 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -5,7 +5,7 @@ import { nextTickPromise } from '../-utils'; /** @method triggerEvent - @param {String|HTMLElement} target + @param {String|Element} target @param {String} eventType @param {Object} options @return {Promise} diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index 196471b06..afd6f78ad 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -12,7 +12,7 @@ const DEFAULT_MODIFIERS = Object.freeze({ /** @public - @param {String|HTMLElement} target + @param {String|Element} target @param {'keydown' | 'keyup' | 'keypress'} eventType @param {String} keyCode @param {Object} [modifiers] From caf3e13fbfde0418669da7f859c3e689aee7a753 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 13 Dec 2017 23:10:12 -0500 Subject: [PATCH 50/61] Ensure that `waitUntil` properly handles callbacks with errors. --- addon-test-support/@ember/test-helpers/wait-until.js | 9 ++++++++- tests/unit/wait-until-test.js | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/wait-until.js b/addon-test-support/@ember/test-helpers/wait-until.js index 75bcf515b..d9ad68a18 100644 --- a/addon-test-support/@ember/test-helpers/wait-until.js +++ b/addon-test-support/@ember/test-helpers/wait-until.js @@ -12,7 +12,14 @@ export default function(callback, options = {}) { let time = -10; function tick() { time += 10; - let value = callback(); + + let value; + try { + value = callback(); + } catch (error) { + reject(error); + } + if (value) { resolve(value); } else if (time < timeout) { diff --git a/tests/unit/wait-until-test.js b/tests/unit/wait-until-test.js index 9d01cff2a..26776a22a 100644 --- a/tests/unit/wait-until-test.js +++ b/tests/unit/wait-until-test.js @@ -37,4 +37,12 @@ module('DOM helper: waitUntil', function() { ]); }); }); + + test('rejects when callback throws', function(assert) { + return waitUntil(() => { + throw new Error('error goes here'); + }).catch(reason => { + assert.equal(reason.message, 'error goes here', 'valid error was thrown'); + }); + }); }); From 3b3d7c1b87fff6ca8a9fc47b8ce2ed0d777efe12 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 13 Dec 2017 23:10:36 -0500 Subject: [PATCH 51/61] Return an array of elements for `waitFor` with `count`. --- .../@ember/test-helpers/dom/wait-for.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/dom/wait-for.js b/addon-test-support/@ember/test-helpers/dom/wait-for.js index 3bac0a016..7eb7c651a 100644 --- a/addon-test-support/@ember/test-helpers/dom/wait-for.js +++ b/addon-test-support/@ember/test-helpers/dom/wait-for.js @@ -2,12 +2,22 @@ import waitUntil from '../wait-until'; import { getContext } from '../setup-context'; import getElement from './-get-element'; +function toArray(nodelist) { + let array = new Array(nodelist.length); + for (let i = 0; i < nodelist.length; i++) { + array[i] = nodelist[i]; + } + + return array; +} + /** @method waitFor @param {string|Element} target @param {Object} [options] @param {number} [options.timeout=1000] @param {number} [options.count=1] + @returns {Element|Array} */ export default function waitFor(selector, { timeout = 1000, count = null } = {}) { let callback; @@ -17,7 +27,7 @@ export default function waitFor(selector, { timeout = 1000, count = null } = {}) let rootElement = context && context.element; let elements = rootElement.querySelectorAll(selector); if (elements.length === count) { - return elements; + return toArray(elements); } }; } else { From 326b968779df44752ac70773baedb6e39eec74bb Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 13 Dec 2017 23:38:03 -0500 Subject: [PATCH 52/61] Add unit tests for `waitFor`. --- tests/unit/dom/wait-for-test.js | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/unit/dom/wait-for-test.js diff --git a/tests/unit/dom/wait-for-test.js b/tests/unit/dom/wait-for-test.js new file mode 100644 index 000000000..cef1ee11a --- /dev/null +++ b/tests/unit/dom/wait-for-test.js @@ -0,0 +1,84 @@ +import { module, test } from 'qunit'; +import { waitFor, setContext, unsetContext } from '@ember/test-helpers'; + +module('DOM Helper: waitFor', function(hooks) { + let context, rootElement; + + hooks.beforeEach(function() { + // used to simulate how `setupRenderingTest` (and soon `setupApplicationTest`) + // set context.element to the rootElement + rootElement = document.querySelector('#qunit-fixture'); + context = { + element: rootElement, + }; + }); + + hooks.afterEach(function() { + unsetContext(); + }); + + test('wait for selector without context set', async function(assert) { + let errorThrown; + try { + await waitFor('.something'); + } catch (error) { + errorThrown = error; + } + + assert.equal( + errorThrown.message, + 'Must setup rendering context before attempting to interact with elements.', + 'valid error was thrown' + ); + }); + + test('wait for selector', async function(assert) { + setContext(context); + + let waitPromise = waitFor('.something'); + + setTimeout(() => { + rootElement.innerHTML = `
Hi!
`; + }, 10); + + let element = await waitPromise; + + assert.equal(element.textContent, 'Hi!'); + }); + + test('wait for count of selector', async function(assert) { + setContext(context); + + let waitPromise = waitFor('.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); + + setContext(context); + + let start = Date.now(); + try { + await waitFor('.something', { timeout: 100 }); + } catch (error) { + let end = Date.now(); + assert.ok(end - start >= 100, 'timed out after correct time'); + assert.equal(error.message, 'waitUntil timed out'); + } + }); +}); From fdefbc9cb4bd5bd3772ee27af25ddc5f208698f9 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 10:17:48 -0500 Subject: [PATCH 53/61] Remove tests requiring `click` only wait for subset of settledness. These tests previously used a sync version of click that allowed `settled` to be tested with _only_ waiting for some portions of settledness. When the tests were converted to use the new public version of `click` the test began failing (because `click` doesn't kick off until the next tick of the event loop, so `settled` resolves immediately). --- tests/integration/settled-test.js | 32 +------------------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/tests/integration/settled-test.js b/tests/integration/settled-test.js index e84fe1546..70c5e6ec1 100644 --- a/tests/integration/settled-test.js +++ b/tests/integration/settled-test.js @@ -9,11 +9,10 @@ import { teardownRenderingContext, } from 'ember-test-helpers'; import hasEmberVersion from 'ember-test-helpers/has-ember-version'; -import { module, test, skip } from 'qunit'; +import { module, test } from 'qunit'; import hbs from 'htmlbars-inline-precompile'; import Pretender from 'pretender'; import { click } from '@ember/test-helpers'; -import hasjQuery from '../helpers/has-jquery'; import ajax from '../helpers/ajax'; const TestComponent1 = Component.extend({ @@ -187,35 +186,6 @@ module('settled real-world scenarios', function(hooks) { assert.equal(this.element.textContent, 'Local Data!Remote Data!Remote Data!'); }); - test('it can wait only for AJAX', async function(assert) { - this.owner.register('component:x-test-4', TestComponent4); - - await this.render(hbs`{{x-test-4}}`); - - click('div'); - - await settled({ waitForTimers: false }); - - assert.equal(this.element.textContent, 'Local Data!Remote Data!'); - }); - - // in the wait utility we specific listen for artificial jQuery events - // to start/stop waiting, but when using ember-fetch those events are not - // emitted and instead test waiters are used - // - // therefore, this test is only valid when using jQuery.ajax - (hasjQuery() ? test : skip)('it can wait only for timers', async function(assert) { - this.owner.register('component:x-test-4', TestComponent4); - - await this.render(hbs`{{x-test-4}}`); - - click('div'); - - await settled({ waitForAJAX: false }); - - assert.equal(this.element.textContent, 'Local Data!'); - }); - test('it waits for Ember test waiters', async function(assert) { this.owner.register('component:x-test-5', TestComponent5); From 95b34ee7e8ccfbdeea2bee5be080e43356514ed2 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 10:45:36 -0500 Subject: [PATCH 54/61] Add basic `assert.rejects` infrastructure. --- tests/test-helper.js | 81 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/test-helper.js b/tests/test-helper.js index ccb785849..b79701d46 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -2,6 +2,7 @@ import QUnit from 'qunit'; import { registerDeprecationHandler } from '@ember/debug'; import AbstractTestLoader from 'ember-cli-test-loader/test-support/index'; import Ember from 'ember'; +import { Promise } from 'rsvp'; if (QUnit.config.seed) { QUnit.config.reorder = false; @@ -108,3 +109,83 @@ QUnit.assert.verifySteps = function() { ASSERT_VERIFY_STEPS.apply(this, arguments); this.test.steps.length = 0; }; + +QUnit.assert.rejects = function(callback, expected, message) { + this.test.ignoreGlobalErrors = true; + + let actual; + let result = false; + + let done = this.async(); + + return Promise.resolve() + .then(callback) + .then(null, reason => { + actual = reason; + }) + .finally(() => { + if (actual) { + const expectedType = typeof expected; + + // We don't want to validate thrown error + if (!expected) { + result = true; + expected = null; + + // Expected is a regexp + } else if (expected instanceof RegExp) { + result = expected.test(errorString(actual)); + + // Expected is a constructor, maybe an Error constructor + } else if (expectedType === 'function' && actual instanceof expected) { + result = true; + + // Expected is an Error object + } else if (expectedType === 'object') { + result = + actual instanceof expected.constructor && + actual.name === expected.name && + actual.message === expected.message; + + // Expected is a validation function which returns true if validation passed + } else if (expectedType === 'function' && expected.call(null, actual) === true) { + expected = null; + result = true; + } + } + + this.pushResult({ + result, + actual, + expected, + message, + }); + + this.test.ignoreGlobalErrors = false; + }) + .finally(() => { + this.test.ignoreGlobalErrors = false; + done(); + }); +}; + +function errorString(error) { + const resultErrorString = error.toString(); + + if (resultErrorString.substring(0, 7) === '[object') { + const name = error.name ? error.name.toString() : 'Error'; + const message = error.message ? error.message.toString() : ''; + + if (name && message) { + return `${name}: ${message}`; + } else if (name) { + return name; + } else if (message) { + return message; + } else { + return 'Error'; + } + } else { + return resultErrorString; + } +} From adb006b2017aac4eb0f5d64373bab30c74c6bdbf Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 10:46:00 -0500 Subject: [PATCH 55/61] Make argument assertions into rejections. --- .../@ember/test-helpers/dom/blur.js | 16 +++++++------- .../@ember/test-helpers/dom/click.js | 10 ++++----- .../@ember/test-helpers/dom/fill-in.js | 22 +++++++++---------- .../@ember/test-helpers/dom/focus.js | 22 +++++++++---------- .../@ember/test-helpers/dom/tap.js | 10 ++++----- .../@ember/test-helpers/dom/trigger-event.js | 22 +++++++++---------- tests/unit/dom/blur-test.js | 7 +++--- tests/unit/dom/click-test.js | 6 ++--- tests/unit/dom/fill-in-test.js | 16 +++++++------- tests/unit/dom/focus-test.js | 14 ++++++------ tests/unit/dom/tap-test.js | 6 ++--- tests/unit/dom/trigger-event-test.js | 12 +++++----- tests/unit/dom/trigger-key-event-test.js | 14 ++++++------ 13 files changed, 89 insertions(+), 88 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/blur.js b/addon-test-support/@ember/test-helpers/dom/blur.js index 59611291e..4e734c175 100644 --- a/addon-test-support/@ember/test-helpers/dom/blur.js +++ b/addon-test-support/@ember/test-helpers/dom/blur.js @@ -32,16 +32,16 @@ export function __blur__(element) { @public */ export default function blur(target = document.activeElement) { - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`blur('${target}')\`.`); - } + return nextTickPromise().then(() => { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`blur('${target}')\`.`); + } - if (!isFocusable(element)) { - throw new Error(`${target} is not focusable`); - } + if (!isFocusable(element)) { + throw new Error(`${target} is not focusable`); + } - return nextTickPromise().then(() => { __blur__(element); return settled(); diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index 4dcf3b874..def0830ee 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -28,12 +28,12 @@ export function __click__(element) { @public */ export default function click(target) { - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`click('${target}')\`.`); - } - return nextTickPromise().then(() => { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`click('${target}')\`.`); + } + __click__(element); return settled(); }); diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index c62776322..e1414e48c 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -13,20 +13,20 @@ import { nextTickPromise } from '../-utils'; @public */ export default function fillIn(target, text) { - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); - } + return nextTickPromise().then(() => { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); + } - if (!isFormControl(element) && !element.isContentEditable) { - throw new Error('`fillIn` is only usable on form controls or contenteditable elements.'); - } + if (!isFormControl(element) && !element.isContentEditable) { + throw new Error('`fillIn` is only usable on form controls or contenteditable elements.'); + } - if (!text) { - throw new Error('Must provide `text` when calling `fillIn`.'); - } + if (!text) { + throw new Error('Must provide `text` when calling `fillIn`.'); + } - return nextTickPromise().then(() => { __focus__(element); if (element.isContentEditable) { diff --git a/addon-test-support/@ember/test-helpers/dom/focus.js b/addon-test-support/@ember/test-helpers/dom/focus.js index 9bca2c8eb..b6ec24cc9 100644 --- a/addon-test-support/@ember/test-helpers/dom/focus.js +++ b/addon-test-support/@ember/test-helpers/dom/focus.js @@ -35,20 +35,20 @@ export function __focus__(element) { @public */ export default function focus(target) { - if (!target) { - throw new Error('Must pass an element or selector to `focus`.'); - } + return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `focus`.'); + } - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`focus('${target}')\`.`); - } + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`focus('${target}')\`.`); + } - if (!isFocusable(element)) { - throw new Error(`${target} is not focusable`); - } + if (!isFocusable(element)) { + throw new Error(`${target} is not focusable`); + } - return nextTickPromise().then(() => { __focus__(element); return settled(); diff --git a/addon-test-support/@ember/test-helpers/dom/tap.js b/addon-test-support/@ember/test-helpers/dom/tap.js index 29115e05e..d92974cdc 100644 --- a/addon-test-support/@ember/test-helpers/dom/tap.js +++ b/addon-test-support/@ember/test-helpers/dom/tap.js @@ -12,12 +12,12 @@ import { nextTickPromise } from '../-utils'; @public */ export default function tap(target, options = {}) { - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`tap('${target}')\`.`); - } - return nextTickPromise().then(() => { + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`tap('${target}')\`.`); + } + let touchstartEv = fireEvent(element, 'touchstart', options); let touchendEv = fireEvent(element, 'touchend', options); diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-event.js index cd942b11b..dce53075c 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-event.js @@ -12,20 +12,20 @@ import { nextTickPromise } from '../-utils'; @public */ export default function triggerEvent(target, type, options) { - if (!target) { - throw new Error('Must pass an element or selector to `triggerEvent`.'); - } + return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `triggerEvent`.'); + } - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`triggerEvent('${target}', ...)\`.`); - } + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`triggerEvent('${target}', ...)\`.`); + } - if (!type) { - throw new Error(`Must provide an \`eventType\` to \`triggerEvent\``); - } + if (!type) { + throw new Error(`Must provide an \`eventType\` to \`triggerEvent\``); + } - return nextTickPromise().then(() => { fireEvent(element, type, options); return settled(); diff --git a/tests/unit/dom/blur-test.js b/tests/unit/dom/blur-test.js index 45b38c1bf..f129e2a4b 100644 --- a/tests/unit/dom/blur-test.js +++ b/tests/unit/dom/blur-test.js @@ -38,10 +38,11 @@ module('DOM Helper: blur', function(hooks) { assert.verifySteps(['blur', 'focusout']); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { setContext(context); - assert.throws(() => { - blur(`#foo-bar-baz-not-here-ever-bye-bye`); + + assert.rejects(() => { + return blur(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `blur\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index 844b86b3d..62085cc4b 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -59,12 +59,12 @@ module('DOM Helper: click', function(hooks) { assert.verifySteps(['mousedown', 'mouseup', 'click']); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - click(`#foo-bar-baz-not-here-ever-bye-bye`); + return click(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `click\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); diff --git a/tests/unit/dom/fill-in-test.js b/tests/unit/dom/fill-in-test.js index a6de4da0c..2b3ebb41a 100644 --- a/tests/unit/dom/fill-in-test.js +++ b/tests/unit/dom/fill-in-test.js @@ -24,25 +24,25 @@ module('DOM Helper: fillIn', function(hooks) { element = buildInstrumentedElement('div'); setContext(context); - assert.throws(() => { - fillIn(`#${element.id}`, 'foo'); + assert.rejects(() => { + return fillIn(`#${element.id}`, 'foo'); }, /`fillIn` is only usable on form controls or contenteditable elements/); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - fillIn(`#foo-bar-baz-not-here-ever-bye-bye`, 'foo'); + return fillIn(`#foo-bar-baz-not-here-ever-bye-bye`, 'foo'); }, /Element not found when calling `fillIn\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); - test('throws an error if text to fill in is not provided', async function(assert) { + test('rejects if text to fill in is not provided', async function(assert) { element = buildInstrumentedElement('input'); - assert.throws(() => { - fillIn(element); + assert.rejects(() => { + return fillIn(element); }, /Must provide `text` when calling `fillIn`/); }); diff --git a/tests/unit/dom/focus-test.js b/tests/unit/dom/focus-test.js index dbed0c686..a3dc9a3d8 100644 --- a/tests/unit/dom/focus-test.js +++ b/tests/unit/dom/focus-test.js @@ -24,8 +24,8 @@ module('DOM Helper: focus', function(hooks) { element = buildInstrumentedElement('div'); setContext(context); - assert.throws(() => { - focus(`#${element.id}`); + assert.rejects(() => { + return focus(`#${element.id}`); }, /is not focusable/); }); @@ -33,8 +33,8 @@ module('DOM Helper: focus', function(hooks) { element = buildInstrumentedElement('div'); setContext(context); - assert.throws(() => { - focus(element); + assert.rejects(() => { + return focus(element); }, /is not focusable/); }); @@ -50,12 +50,12 @@ module('DOM Helper: focus', function(hooks) { assert.verifySteps(['focus', 'focusin']); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - focus(`#foo-bar-baz-not-here-ever-bye-bye`); + return focus(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `focus\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); diff --git a/tests/unit/dom/tap-test.js b/tests/unit/dom/tap-test.js index 05cb75d85..de539a6ec 100644 --- a/tests/unit/dom/tap-test.js +++ b/tests/unit/dom/tap-test.js @@ -59,12 +59,12 @@ module('DOM Helper: tap', function(hooks) { assert.verifySteps(['touchstart', 'touchend', 'mousedown', 'mouseup', 'click']); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - tap(`#foo-bar-baz-not-here-ever-bye-bye`); + return tap(`#foo-bar-baz-not-here-ever-bye-bye`); }, /Element not found when calling `tap\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); diff --git a/tests/unit/dom/trigger-event-test.js b/tests/unit/dom/trigger-event-test.js index cb01a7eb5..35f2613a9 100644 --- a/tests/unit/dom/trigger-event-test.js +++ b/tests/unit/dom/trigger-event-test.js @@ -89,21 +89,21 @@ module('DOM Helper: triggerEvent', function(hooks) { assert.verifySteps(['mouseenter']); }); - test('throws an error if selector is not found', async function(assert) { + test('rejects if selector is not found', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - triggerEvent(`#foo-bar-baz-not-here-ever-bye-bye`, 'mouseenter'); + return triggerEvent(`#foo-bar-baz-not-here-ever-bye-bye`, 'mouseenter'); }, /Element not found when calling `triggerEvent\('#foo-bar-baz-not-here-ever-bye-bye'/); }); - test('throws an error if event type is not passed', async function(assert) { + test('rejects if event type is not passed', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - triggerEvent(element); + return triggerEvent(element); }, /Must provide an `eventType` to `triggerEvent`/); }); diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js index 772aa59c6..a61bc2653 100644 --- a/tests/unit/dom/trigger-key-event-test.js +++ b/tests/unit/dom/trigger-key-event-test.js @@ -20,28 +20,28 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { unsetContext(); }); - test('throws an error if event type is missing', async function(assert) { + test('rejects if event type is missing', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); triggerKeyEvent(element); }, /Must provide an `eventType` to `triggerKeyEvent`/); }); - test('throws an error if event type is invalid', async function(assert) { + test('rejects if event type is invalid', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); - triggerKeyEvent(element, 'mouseenter'); + return triggerKeyEvent(element, 'mouseenter'); }, /Must provide an `eventType` of keydown, keypress, keyup to `triggerKeyEvent` but you passed `mouseenter`./); }); - test('throws an error if key code is missing', async function(assert) { + test('rejects if key code is missing', async function(assert) { element = buildInstrumentedElement('div'); - assert.throws(() => { + assert.rejects(() => { setContext(context); triggerKeyEvent(element, 'keypress'); }, /Must provide a `keyCode` to `triggerKeyEvent`/); From 7ee206abaf60692ae8000f746c6ae2187485645e Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 11:00:45 -0500 Subject: [PATCH 56/61] Refactor manual rejection assertions to assert.rejects. --- tests/unit/dom/blur-test.js | 18 +++-------- tests/unit/dom/click-test.js | 38 +++++++----------------- tests/unit/dom/fill-in-test.js | 15 ++-------- tests/unit/dom/focus-test.js | 15 ++-------- tests/unit/dom/tap-test.js | 28 ++++------------- tests/unit/dom/trigger-event-test.js | 18 +++-------- tests/unit/dom/trigger-key-event-test.js | 18 +++-------- tests/unit/dom/wait-for-test.js | 15 ++-------- 8 files changed, 36 insertions(+), 129 deletions(-) diff --git a/tests/unit/dom/blur-test.js b/tests/unit/dom/blur-test.js index f129e2a4b..6caadcf26 100644 --- a/tests/unit/dom/blur-test.js +++ b/tests/unit/dom/blur-test.js @@ -54,20 +54,10 @@ module('DOM Helper: blur', function(hooks) { assert.notEqual(document.activeElement, elementWithFocus, 'activeElement updated'); }); - test('bluring via selector without context set', async function(assert) { - let errorThrown; - - try { - await blur(`#${elementWithFocus.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + test('bluring via selector without context set', function(assert) { + assert.rejects(() => { + return blur(`#${elementWithFocus.id}`); + }, /Must setup rendering context before attempting to interact with elements/); }); test('bluring via element with context set', async function(assert) { diff --git a/tests/unit/dom/click-test.js b/tests/unit/dom/click-test.js index 62085cc4b..2cb1f1f39 100644 --- a/tests/unit/dom/click-test.js +++ b/tests/unit/dom/click-test.js @@ -68,21 +68,12 @@ module('DOM Helper: click', function(hooks) { }, /Element not found when calling `click\('#foo-bar-baz-not-here-ever-bye-bye'\)`/); }); - test('clicking a div via selector without context set', async function(assert) { + test('clicking a div via selector without context set', function(assert) { element = buildInstrumentedElement('div'); - let errorThrown; - - try { - await click(`#${element.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + + assert.rejects(() => { + return click(`#${element.id}`); + }, /Must setup rendering context before attempting to interact with elements/); }); }); @@ -116,21 +107,12 @@ module('DOM Helper: click', function(hooks) { assert.strictEqual(document.activeElement, element, 'activeElement updated'); }); - test('clicking a input via selector without context set', async function(assert) { + test('clicking a input via selector without context set', function(assert) { element = buildInstrumentedElement('input'); - let errorThrown; - - try { - await click(`#${element.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + + assert.rejects(() => { + return click(`#${element.id}`); + }, /Must setup rendering context before attempting to interact with elements/); }); }); }); diff --git a/tests/unit/dom/fill-in-test.js b/tests/unit/dom/fill-in-test.js index 2b3ebb41a..add29c82a 100644 --- a/tests/unit/dom/fill-in-test.js +++ b/tests/unit/dom/fill-in-test.js @@ -48,19 +48,10 @@ module('DOM Helper: fillIn', function(hooks) { test('filling an input via selector without context set', async function(assert) { element = buildInstrumentedElement('input'); - let errorThrown; - try { - await fillIn(`#${element.id}`, 'foo'); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + assert.rejects(() => { + return fillIn(`#${element.id}`, 'foo'); + }, /Must setup rendering context before attempting to interact with elements/); }); test('does not run sync', async function(assert) { diff --git a/tests/unit/dom/focus-test.js b/tests/unit/dom/focus-test.js index a3dc9a3d8..05fecc480 100644 --- a/tests/unit/dom/focus-test.js +++ b/tests/unit/dom/focus-test.js @@ -90,18 +90,9 @@ module('DOM Helper: focus', function(hooks) { test('focusing a input via selector without context set', async function(assert) { element = buildInstrumentedElement('input'); - let errorThrown; - try { - await focus(`#${element.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + assert.rejects(() => { + return focus(`#${element.id}`); + }, /Must setup rendering context before attempting to interact with elements/); }); }); diff --git a/tests/unit/dom/tap-test.js b/tests/unit/dom/tap-test.js index de539a6ec..cb6eab5dd 100644 --- a/tests/unit/dom/tap-test.js +++ b/tests/unit/dom/tap-test.js @@ -70,19 +70,10 @@ module('DOM Helper: tap', function(hooks) { test('tapping a div via selector without context set', async function(assert) { element = buildInstrumentedElement('div'); - let errorThrown; - try { + assert.rejects(async () => { await tap(`#${element.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + }, /Must setup rendering context before attempting to interact with elements/); }); }); @@ -140,21 +131,12 @@ module('DOM Helper: tap', function(hooks) { assert.strictEqual(document.activeElement, element, 'activeElement updated'); }); - test('tapping a input via selector without context set', async function(assert) { + test('tapping a input via selector without context set', function(assert) { element = buildInstrumentedElement('input'); - let errorThrown; - try { + assert.rejects(async () => { await tap(`#${element.id}`); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + }, /Must setup rendering context before attempting to interact with elements/); }); }); }); diff --git a/tests/unit/dom/trigger-event-test.js b/tests/unit/dom/trigger-event-test.js index 35f2613a9..874294634 100644 --- a/tests/unit/dom/trigger-event-test.js +++ b/tests/unit/dom/trigger-event-test.js @@ -59,22 +59,12 @@ module('DOM Helper: triggerEvent', function(hooks) { assert.verifySteps(['mouseenter']); }); - test('triggering event via selector without context set', async function(assert) { + test('triggering event via selector without context set', function(assert) { element = buildInstrumentedElement('div'); - let errorThrown; - - try { - await triggerEvent(`#${element.id}`, 'mouseenter'); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + assert.rejects(() => { + return triggerEvent(`#${element.id}`, 'mouseenter'); + }, /Must setup rendering context before attempting to interact with elements/); }); test('does not run sync', async function(assert) { diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js index a61bc2653..e6886e1ed 100644 --- a/tests/unit/dom/trigger-key-event-test.js +++ b/tests/unit/dom/trigger-key-event-test.js @@ -73,22 +73,12 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { assert.verifySteps(['keydown']); }); - test('triggering via selector without context set', async function(assert) { + test('triggering via selector without context set', function(assert) { element = buildInstrumentedElement('div'); - let errorThrown; - - try { - await triggerKeyEvent(`#${element.id}`, 'keydown', 13); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + assert.rejects(() => { + return triggerKeyEvent(`#${element.id}`, 'keydown', 13); + }, /Must setup rendering context before attempting to interact with elements/); }); ['ctrl', 'shift', 'alt', 'meta'].forEach(function(modifierType) { diff --git a/tests/unit/dom/wait-for-test.js b/tests/unit/dom/wait-for-test.js index cef1ee11a..bed4e08ec 100644 --- a/tests/unit/dom/wait-for-test.js +++ b/tests/unit/dom/wait-for-test.js @@ -18,18 +18,9 @@ module('DOM Helper: waitFor', function(hooks) { }); test('wait for selector without context set', async function(assert) { - let errorThrown; - try { - await waitFor('.something'); - } catch (error) { - errorThrown = error; - } - - assert.equal( - errorThrown.message, - 'Must setup rendering context before attempting to interact with elements.', - 'valid error was thrown' - ); + assert.rejects(() => { + return waitFor('.something'); + }, /Must setup rendering context before attempting to interact with elements/); }); test('wait for selector', async function(assert) { From 7e41f7f50671261cb278c9158117474f9b080cb2 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 19:05:14 -0500 Subject: [PATCH 57/61] Update `moduleForAcceptance` interop test to use `click`. --- tests/integration/module-for-acceptance-interop-test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/integration/module-for-acceptance-interop-test.js b/tests/integration/module-for-acceptance-interop-test.js index cdfa6616d..805b046ae 100644 --- a/tests/integration/module-for-acceptance-interop-test.js +++ b/tests/integration/module-for-acceptance-interop-test.js @@ -2,12 +2,11 @@ import { test } from 'qunit'; import { run } from '@ember/runloop'; import EmberRouter from '@ember/routing/router'; import Component from '@ember/component'; -import { settled } from '@ember/test-helpers'; +import { click } from '@ember/test-helpers'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; import hbs from 'htmlbars-inline-precompile'; import ajax from '../helpers/ajax'; -import { fireEvent } from '../helpers/events'; import Pretender from 'pretender'; import moduleForAcceptance from '../helpers/module-for-acceptance'; @@ -63,8 +62,10 @@ if (hasEmberVersion(2, 8)) { return this.application.testHelpers .visit('/ajax-request') .then(() => { - fireEvent(document.querySelector('.special-thing'), 'click'); - return settled(); + // returning `click` here is going to trigger the `click` event + // then `return settled()` (which is how we are testing the underlying + // settled interop). + return click(document.querySelector('.special-thing')); }) .then(() => { let testingElement = document.getElementById('ember-testing'); From e6f8668d50fbeca6b23f209ede00af52a60dd5a6 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 19:08:27 -0500 Subject: [PATCH 58/61] Add comment RE: eager error creation without throwing. --- addon-test-support/@ember/test-helpers/wait-until.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addon-test-support/@ember/test-helpers/wait-until.js b/addon-test-support/@ember/test-helpers/wait-until.js index d9ad68a18..58485cb72 100644 --- a/addon-test-support/@ember/test-helpers/wait-until.js +++ b/addon-test-support/@ember/test-helpers/wait-until.js @@ -4,6 +4,8 @@ import { nextTick } from './-utils'; export default function(callback, options = {}) { let timeout = 'timeout' in options ? options.timeout : 1000; + + // creating this error eagerly so it has the proper invocation stack let waitUntilTimedOut = new Error('waitUntil timed out'); return new Promise(function(resolve, reject) { From 0412bbf19b82d8e175baa7863048630da10a4a5a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 19:18:12 -0500 Subject: [PATCH 59/61] Add helpful errors to helpers for `selector|element` arg. --- addon-test-support/@ember/test-helpers/dom/click.js | 4 ++++ addon-test-support/@ember/test-helpers/dom/fill-in.js | 4 ++++ addon-test-support/@ember/test-helpers/dom/tap.js | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/addon-test-support/@ember/test-helpers/dom/click.js b/addon-test-support/@ember/test-helpers/dom/click.js index def0830ee..eab9c0e60 100644 --- a/addon-test-support/@ember/test-helpers/dom/click.js +++ b/addon-test-support/@ember/test-helpers/dom/click.js @@ -29,6 +29,10 @@ export function __click__(element) { */ export default function click(target) { return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `click`.'); + } + let element = getElement(target); if (!element) { throw new Error(`Element not found when calling \`click('${target}')\`.`); diff --git a/addon-test-support/@ember/test-helpers/dom/fill-in.js b/addon-test-support/@ember/test-helpers/dom/fill-in.js index e1414e48c..45ddedbeb 100644 --- a/addon-test-support/@ember/test-helpers/dom/fill-in.js +++ b/addon-test-support/@ember/test-helpers/dom/fill-in.js @@ -14,6 +14,10 @@ import { nextTickPromise } from '../-utils'; */ export default function fillIn(target, text) { return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `fillIn`.'); + } + let element = getElement(target); if (!element) { throw new Error(`Element not found when calling \`fillIn('${target}')\`.`); diff --git a/addon-test-support/@ember/test-helpers/dom/tap.js b/addon-test-support/@ember/test-helpers/dom/tap.js index d92974cdc..57e1d430d 100644 --- a/addon-test-support/@ember/test-helpers/dom/tap.js +++ b/addon-test-support/@ember/test-helpers/dom/tap.js @@ -13,6 +13,10 @@ import { nextTickPromise } from '../-utils'; */ export default function tap(target, options = {}) { return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `tap`.'); + } + let element = getElement(target); if (!element) { throw new Error(`Element not found when calling \`tap('${target}')\`.`); From d07492b543365f3b547a01472ac45f2a0679f6b2 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 19:19:04 -0500 Subject: [PATCH 60/61] Ensure `triggerKeyEvent` rejects (and doesn't throw eagerly). --- .../test-helpers/dom/trigger-key-event.js | 63 ++++++++++--------- tests/unit/dom/trigger-key-event-test.js | 10 +-- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js index afd6f78ad..dbfd0db38 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.js @@ -1,7 +1,9 @@ import { merge } from '@ember/polyfills'; import getElement from './-get-element'; -import triggerEvent from './trigger-event'; +import fireEvent from './fire-event'; +import settled from '../settled'; import { KEYBOARD_EVENT_TYPES } from './fire-event'; +import { nextTickPromise } from '../-utils'; const DEFAULT_MODIFIERS = Object.freeze({ ctrlKey: false, @@ -23,32 +25,35 @@ const DEFAULT_MODIFIERS = Object.freeze({ @return {Promise} */ export default function triggerKeyEvent(target, eventType, keyCode, modifiers = DEFAULT_MODIFIERS) { - if (!target) { - throw new Error('Must pass an element or selector to `triggerKeyEvent`.'); - } - - let element = getElement(target); - if (!element) { - throw new Error(`Element not found when calling \`triggerKeyEvent('${target}', ...)\`.`); - } - - if (!eventType) { - throw new Error(`Must provide an \`eventType\` to \`triggerKeyEvent\``); - } - - if (KEYBOARD_EVENT_TYPES.indexOf(eventType) === -1) { - throw new Error( - `Must provide an \`eventType\` of ${KEYBOARD_EVENT_TYPES.join( - ', ' - )} to \`triggerKeyEvent\` but you passed \`${eventType}\`.` - ); - } - - if (!keyCode) { - throw new Error(`Must provide a \`keyCode\` to \`triggerKeyEvent\``); - } - - let options = merge({ keyCode, which: keyCode, key: keyCode }, modifiers); - - return triggerEvent(element, eventType, options); + return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `triggerKeyEvent`.'); + } + + let element = getElement(target); + if (!element) { + throw new Error(`Element not found when calling \`triggerKeyEvent('${target}', ...)\`.`); + } + + if (!eventType) { + throw new Error(`Must provide an \`eventType\` to \`triggerKeyEvent\``); + } + + if (KEYBOARD_EVENT_TYPES.indexOf(eventType) === -1) { + let validEventTypes = KEYBOARD_EVENT_TYPES.join(', '); + throw new Error( + `Must provide an \`eventType\` of ${validEventTypes} to \`triggerKeyEvent\` but you passed \`${eventType}\`.` + ); + } + + if (!keyCode) { + throw new Error(`Must provide a \`keyCode\` to \`triggerKeyEvent\``); + } + + let options = merge({ keyCode, which: keyCode, key: keyCode }, modifiers); + + fireEvent(element, eventType, options); + + return settled(); + }); } diff --git a/tests/unit/dom/trigger-key-event-test.js b/tests/unit/dom/trigger-key-event-test.js index e6886e1ed..a305135c1 100644 --- a/tests/unit/dom/trigger-key-event-test.js +++ b/tests/unit/dom/trigger-key-event-test.js @@ -20,16 +20,16 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { unsetContext(); }); - test('rejects if event type is missing', async function(assert) { + test('rejects if event type is missing', function(assert) { element = buildInstrumentedElement('div'); assert.rejects(() => { setContext(context); - triggerKeyEvent(element); + return triggerKeyEvent(element); }, /Must provide an `eventType` to `triggerKeyEvent`/); }); - test('rejects if event type is invalid', async function(assert) { + test('rejects if event type is invalid', function(assert) { element = buildInstrumentedElement('div'); assert.rejects(() => { @@ -38,12 +38,12 @@ module('DOM Helper: triggerKeyEvent', function(hooks) { }, /Must provide an `eventType` of keydown, keypress, keyup to `triggerKeyEvent` but you passed `mouseenter`./); }); - test('rejects if key code is missing', async function(assert) { + test('rejects if key code is missing', function(assert) { element = buildInstrumentedElement('div'); assert.rejects(() => { setContext(context); - triggerKeyEvent(element, 'keypress'); + return triggerKeyEvent(element, 'keypress'); }, /Must provide a `keyCode` to `triggerKeyEvent`/); }); From 1848f83f748c1503332e4047f7a372a998de8d8a Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 14 Dec 2017 19:26:45 -0500 Subject: [PATCH 61/61] Validate `target` in `waitFor`. --- .../@ember/test-helpers/dom/wait-for.js | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/wait-for.js b/addon-test-support/@ember/test-helpers/dom/wait-for.js index 7eb7c651a..1364596c1 100644 --- a/addon-test-support/@ember/test-helpers/dom/wait-for.js +++ b/addon-test-support/@ember/test-helpers/dom/wait-for.js @@ -1,6 +1,7 @@ import waitUntil from '../wait-until'; import { getContext } from '../setup-context'; import getElement from './-get-element'; +import { nextTickPromise } from '../-utils'; function toArray(nodelist) { let array = new Array(nodelist.length); @@ -19,19 +20,25 @@ function toArray(nodelist) { @param {number} [options.count=1] @returns {Element|Array} */ -export default function waitFor(selector, { timeout = 1000, count = null } = {}) { - let callback; - if (count !== null) { - callback = () => { - let context = getContext(); - let rootElement = context && context.element; - let elements = rootElement.querySelectorAll(selector); - if (elements.length === count) { - return toArray(elements); - } - }; - } else { - callback = () => getElement(selector); - } - return waitUntil(callback, { timeout }); +export default function waitFor(target, { timeout = 1000, count = null } = {}) { + return nextTickPromise().then(() => { + if (!target) { + throw new Error('Must pass an element or selector to `waitFor`.'); + } + + let callback; + if (count !== null) { + callback = () => { + let context = getContext(); + let rootElement = context && context.element; + let elements = rootElement.querySelectorAll(target); + if (elements.length === count) { + return toArray(elements); + } + }; + } else { + callback = () => getElement(target); + } + return waitUntil(callback, { timeout }); + }); }