Skip to content

Commit 6e9988f

Browse files
authored
Merge pull request #958 from emberjs/avoid-rsvp-promises-internally
2 parents 400c0b2 + 8c33a27 commit 6e9988f

33 files changed

+2765
-231
lines changed

addon-test-support/@ember/test-helpers/-internal/helper-hooks.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { _Promise as Promise } from '../-utils';
1+
import { Promise } from '../-utils';
22

33
type Hook = (...args: any[]) => void | Promise<void>;
44
type HookLabel = 'start' | 'end' | string;
@@ -14,7 +14,7 @@ const registeredHooks = new Map<string, Set<Hook>>();
1414
* @param {string} label A label to help identify the hook.
1515
* @returns {string} The compound key for the helper.
1616
*/
17-
function getHelperKey(helperName: string, label: string) {
17+
function getHelperKey(helperName: string, label: string): string {
1818
return `${helperName}:${label}`;
1919
}
2020

@@ -59,7 +59,13 @@ export function registerHook(helperName: string, label: HookLabel, hook: Hook):
5959
*/
6060
export function runHooks(helperName: string, label: HookLabel, ...args: any[]): Promise<void> {
6161
let hooks = registeredHooks.get(getHelperKey(helperName, label)) || new Set<Hook>();
62-
let promises = [...hooks].map(hook => hook(...args));
62+
let promises: Array<void | Promise<void>> = [];
63+
64+
hooks.forEach(hook => {
65+
let hookResult = hook(...args);
66+
67+
promises.push(hookResult);
68+
});
6369

6470
return Promise.all(promises).then(() => {});
6571
}

addon-test-support/@ember/test-helpers/-utils.ts

+8-83
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,21 @@
11
/* globals Promise */
22

33
import RSVP from 'rsvp';
4-
import hasEmberVersion from './has-ember-version';
54

6-
export class _Promise<T> extends RSVP.Promise<T> {}
5+
const HAS_PROMISE =
6+
typeof Promise === 'function' &&
7+
// @ts-ignore this is checking if someone has explicitly done `window.Promise = window.Promise || Ember.RSVP.Promise
8+
Promise !== RSVP.Promise;
79

8-
const ORIGINAL_RSVP_ASYNC: Function = RSVP.configure('async');
10+
import { Promise as PromisePolyfill } from 'es6-promise';
911

10-
/*
11-
Long ago in a galaxy far far away, Ember forced RSVP.Promise to "resolve" on the Ember.run loop.
12-
At the time, this was meant to help ease pain with folks receiving the dreaded "auto-run" assertion
13-
during their tests, and to help ensure that promise resolution was coelesced to avoid "thrashing"
14-
of the DOM. Unfortunately, the result of this configuration is that code like the following behaves
15-
differently if using native `Promise` vs `RSVP.Promise`:
12+
const _Promise: typeof Promise = HAS_PROMISE ? Promise : (PromisePolyfill as typeof Promise);
1613

17-
```js
18-
console.log('first');
19-
Ember.run(() => Promise.resolve().then(() => console.log('second')));
20-
console.log('third');
21-
```
14+
export { _Promise as Promise };
2215

23-
When `Promise` is the native promise that will log `'first', 'third', 'second'`, but when `Promise`
24-
is an `RSVP.Promise` that will log `'first', 'second', 'third'`. The fact that `RSVP.Promise`s can
25-
be **forced** to flush synchronously is very scary!
26-
27-
Now, lets talk about why we are configuring `RSVP`'s `async` below...
28-
29-
---
30-
31-
The following _should_ always be guaranteed:
32-
33-
```js
34-
await settled();
35-
36-
isSettled() === true
37-
```
38-
39-
Unfortunately, without the custom `RSVP` `async` configuration we cannot ensure that `isSettled()` will
40-
be truthy. This is due to the fact that Ember has configured `RSVP` to resolve all promises in the run
41-
loop. What that means practically is this:
42-
43-
1. all checks within `waitUntil` (used by `settled()` internally) are completed and we are "settled"
44-
2. `waitUntil` resolves the promise that it returned (to signify that the world is "settled")
45-
3. resolving the promise (since it is an `RSVP.Promise` and Ember has configured RSVP.Promise) creates
46-
a new Ember.run loop in order to resolve
47-
4. the presence of that new run loop means that we are no longer "settled"
48-
5. `isSettled()` returns false 😭😭😭😭😭😭😭😭😭
49-
50-
This custom `RSVP.configure('async`, ...)` below provides a way to prevent the promises that are returned
51-
from `settled` from causing this "loop" and instead "just use normal Promise semantics".
52-
53-
😩😫🙀
54-
*/
55-
RSVP.configure('async', (callback: any, promise: any) => {
56-
if (promise instanceof _Promise) {
57-
// @ts-ignore - avoid erroring about useless `Promise !== RSVP.Promise` comparison
58-
// (this handles when folks have polyfilled via Promise = Ember.RSVP.Promise)
59-
if (typeof Promise !== 'undefined' && Promise !== RSVP.Promise) {
60-
// use real native promise semantics whenever possible
61-
Promise.resolve().then(() => callback(promise));
62-
} else {
63-
// fallback to using RSVP's natural `asap` (**not** the fake
64-
// one configured by Ember...)
65-
RSVP.asap(callback, promise);
66-
}
67-
} else {
68-
// fall back to the normal Ember behavior
69-
ORIGINAL_RSVP_ASYNC(callback, promise);
70-
}
71-
});
72-
73-
export const nextTick: Function =
74-
typeof Promise === 'undefined' ? setTimeout : (cb: () => void) => Promise.resolve().then(cb);
16+
export const nextTick = HAS_PROMISE ? (cb: () => void) => Promise.resolve().then(cb) : RSVP.asap;
7517
export const futureTick = setTimeout;
7618

77-
/**
78-
@private
79-
@returns {Promise<void>} Promise which can not be forced to be ran synchronously
80-
*/
81-
export function nextTickPromise(): RSVP.Promise<void> {
82-
// Ember 3.4 removed the auto-run assertion, in 3.4+ we can (and should) avoid the "psuedo promisey" run loop configuration
83-
// for our `nextTickPromise` implementation. This allows us to have real microtask based next tick timing...
84-
if (hasEmberVersion(3, 4)) {
85-
return _Promise.resolve();
86-
} else {
87-
// on older Ember's fallback to RSVP.Promise + a setTimeout
88-
return new RSVP.Promise(resolve => {
89-
nextTick(resolve);
90-
});
91-
}
92-
}
93-
9419
/**
9520
Retrieves an array of destroyables from the specified property on the object
9621
provided, iterates that array invoking each function, then deleting the

addon-test-support/@ember/test-helpers/build-owner.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Application from '@ember/application';
22
import Resolver from '@ember/application/resolver';
33

4-
import { Promise } from 'rsvp';
4+
import { Promise } from './-utils';
55

66
import legacyBuildRegistry from './-internal/build-registry';
77
import ContainerProxyMixin from '@ember/engine/-private/container-proxy-mixin';
@@ -52,5 +52,6 @@ export default function buildOwner(
5252
}
5353

5454
let { owner } = legacyBuildRegistry(resolver) as { owner: Owner };
55+
5556
return Promise.resolve(owner);
5657
}

addon-test-support/@ember/test-helpers/dom/blur.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import getElement from './-get-element';
22
import fireEvent from './fire-event';
33
import settled from '../settled';
4-
import { nextTickPromise } from '../-utils';
4+
import { Promise } from '../-utils';
55
import Target from './-target';
66
import { log } from '@ember/test-helpers/dom/-logging';
77
import isFocusable from './-is-focusable';
@@ -61,7 +61,7 @@ export function __blur__(element: HTMLElement | Element | Document | SVGElement)
6161
blur('input');
6262
*/
6363
export default function blur(target: Target = document.activeElement!): Promise<void> {
64-
return nextTickPromise()
64+
return Promise.resolve()
6565
.then(() => runHooks('blur', 'start', target))
6666
.then(() => {
6767
let element = getElement(target);

addon-test-support/@ember/test-helpers/dom/click.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fireEvent from './fire-event';
44
import { __focus__ } from './focus';
55
import settled from '../settled';
66
import isFocusable from './-is-focusable';
7-
import { nextTickPromise } from '../-utils';
7+
import { Promise } from '../-utils';
88
import isFormControl from './-is-form-control';
99
import Target from './-target';
1010
import { log } from '@ember/test-helpers/dom/-logging';
@@ -90,7 +90,7 @@ export function __click__(element: Element | Document | Window, options: MouseEv
9090
export default function click(target: Target, _options: MouseEventInit = {}): Promise<void> {
9191
let options = assign({}, DEFAULT_CLICK_OPTIONS, _options);
9292

93-
return nextTickPromise()
93+
return Promise.resolve()
9494
.then(() => runHooks('click', 'start', target, _options))
9595
.then(() => {
9696
if (!target) {

addon-test-support/@ember/test-helpers/dom/double-click.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fireEvent from './fire-event';
44
import { __focus__ } from './focus';
55
import settled from '../settled';
66
import isFocusable from './-is-focusable';
7-
import { nextTickPromise } from '../-utils';
7+
import { Promise } from '../-utils';
88
import { DEFAULT_CLICK_OPTIONS } from './click';
99
import Target from './-target';
1010
import { log } from '@ember/test-helpers/dom/-logging';
@@ -94,7 +94,7 @@ export function __doubleClick__(
9494
export default function doubleClick(target: Target, _options: MouseEventInit = {}): Promise<void> {
9595
let options = assign({}, DEFAULT_CLICK_OPTIONS, _options);
9696

97-
return nextTickPromise()
97+
return Promise.resolve()
9898
.then(() => runHooks('doubleClick', 'start', target, _options))
9999
.then(() => {
100100
if (!target) {

addon-test-support/@ember/test-helpers/dom/fill-in.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import guardForMaxlength from './-guard-for-maxlength';
44
import { __focus__ } from './focus';
55
import settled from '../settled';
66
import fireEvent from './fire-event';
7-
import { nextTickPromise } from '../-utils';
7+
import { Promise } from '../-utils';
88
import Target, { isContentEditable } from './-target';
99
import { log } from '@ember/test-helpers/dom/-logging';
1010
import { runHooks, registerHook } from '../-internal/helper-hooks';
@@ -31,7 +31,7 @@ registerHook('fillIn', 'start', (target: Target, text: string) => {
3131
fillIn('input', 'hello world');
3232
*/
3333
export default function fillIn(target: Target, text: string): Promise<void> {
34-
return nextTickPromise()
34+
return Promise.resolve()
3535
.then(() => runHooks('fillIn', 'start', target, text))
3636
.then(() => {
3737
if (!target) {

addon-test-support/@ember/test-helpers/dom/focus.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import getElement from './-get-element';
22
import fireEvent from './fire-event';
33
import settled from '../settled';
44
import isFocusable from './-is-focusable';
5-
import { nextTickPromise } from '../-utils';
5+
import { Promise } from '../-utils';
66
import Target from './-target';
77
import { log } from '@ember/test-helpers/dom/-logging';
88
import { runHooks, registerHook } from '../-internal/helper-hooks';
@@ -73,7 +73,7 @@ export function __focus__(element: HTMLElement | Element | Document | SVGElement
7373
focus('input');
7474
*/
7575
export default function focus(target: Target): Promise<void> {
76-
return nextTickPromise()
76+
return Promise.resolve()
7777
.then(() => runHooks('focus', 'start', target))
7878
.then(() => {
7979
if (!target) {

addon-test-support/@ember/test-helpers/dom/scroll-to.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import getElement from './-get-element';
22
import fireEvent from './fire-event';
33
import settled from '../settled';
4-
import { nextTickPromise } from '../-utils';
4+
import { Promise } from '../-utils';
55
import { isElement } from './-target';
66
import { runHooks } from '../-internal/helper-hooks';
77

@@ -26,7 +26,7 @@ export default function scrollTo(
2626
x: number,
2727
y: number
2828
): Promise<void> {
29-
return nextTickPromise()
29+
return Promise.resolve()
3030
.then(() => runHooks('scrollTo', 'start', target))
3131
.then(() => {
3232
if (!target) {

addon-test-support/@ember/test-helpers/dom/select.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import isSelectElement from './-is-select-element';
33
import { __focus__ } from './focus';
44
import settled from '../settled';
55
import fireEvent from './fire-event';
6-
import { nextTickPromise } from '../-utils';
6+
import { Promise } from '../-utils';
77
import Target from './-target';
88
import { runHooks } from '../-internal/helper-hooks';
99

@@ -35,7 +35,7 @@ export default function select(
3535
options: string | string[],
3636
keepPreviouslySelected = false
3737
): Promise<void> {
38-
return nextTickPromise()
38+
return Promise.resolve()
3939
.then(() => runHooks('select', 'start', target, options, keepPreviouslySelected))
4040
.then(() => {
4141
if (!target) {

addon-test-support/@ember/test-helpers/dom/tap.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import getElement from './-get-element';
22
import fireEvent from './fire-event';
33
import { __click__ } from './click';
44
import settled from '../settled';
5-
import { nextTickPromise } from '../-utils';
5+
import { Promise } from '../-utils';
66
import Target from './-target';
77
import { log } from '@ember/test-helpers/dom/-logging';
88
import isFormControl from './-is-form-control';
@@ -55,7 +55,7 @@ registerHook('tap', 'start', (target: Target) => {
5555
tap('button');
5656
*/
5757
export default function tap(target: Target, options: object = {}): Promise<void> {
58-
return nextTickPromise()
58+
return Promise.resolve()
5959
.then(() => {
6060
return runHooks('tap', 'start', target, options);
6161
})

addon-test-support/@ember/test-helpers/dom/trigger-event.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getWindowOrElement } from './-get-window-or-element';
22
import fireEvent from './fire-event';
33
import settled from '../settled';
4-
import { nextTickPromise } from '../-utils';
4+
import { Promise } from '../-utils';
55
import Target from './-target';
66
import { log } from '@ember/test-helpers/dom/-logging';
77
import isFormControl from './-is-form-control';
@@ -59,7 +59,7 @@ export default function triggerEvent(
5959
eventType: string,
6060
options?: object
6161
): Promise<void> {
62-
return nextTickPromise()
62+
return Promise.resolve()
6363
.then(() => {
6464
return runHooks('triggerEvent', 'start', target, eventType, options);
6565
})

addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import getElement from './-get-element';
33
import fireEvent from './fire-event';
44
import settled from '../settled';
55
import { KEYBOARD_EVENT_TYPES, KeyboardEventType, isKeyboardEventType } from './fire-event';
6-
import { nextTickPromise, isNumeric } from '../-utils';
6+
import { Promise, isNumeric } from '../-utils';
77
import Target from './-target';
88
import { log } from '@ember/test-helpers/dom/-logging';
99
import isFormControl from './-is-form-control';
@@ -194,7 +194,7 @@ export default function triggerKeyEvent(
194194
key: number | string,
195195
modifiers: KeyModifiers = DEFAULT_MODIFIERS
196196
): Promise<void> {
197-
return nextTickPromise()
197+
return Promise.resolve()
198198
.then(() => {
199199
return runHooks('triggerKeyEvent', 'start', target, eventType, key);
200200
})

addon-test-support/@ember/test-helpers/dom/type-in.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { nextTickPromise } from '../-utils';
1+
import { Promise } from '../-utils';
22
import settled from '../settled';
33
import getElement from './-get-element';
44
import isFormControl, { FormControl } from './-is-form-control';
55
import { __focus__ } from './focus';
6-
import { Promise } from 'rsvp';
76
import fireEvent from './fire-event';
87
import guardForMaxlength from './-guard-for-maxlength';
98
import Target, { isContentEditable, isDocument, HTMLElementContentEditable } from './-target';
@@ -44,7 +43,7 @@ registerHook('typeIn', 'start', (target: Target, text: string) => {
4443
* typeIn('input', 'hello world');
4544
*/
4645
export default function typeIn(target: Target, text: string, options: Options = {}): Promise<void> {
47-
return nextTickPromise()
46+
return Promise.resolve()
4847
.then(() => {
4948
return runHooks('typeIn', 'start', target, text, options);
5049
})
@@ -108,7 +107,7 @@ function keyEntry(
108107
let characterKey = character.toUpperCase();
109108

110109
return function () {
111-
return nextTickPromise()
110+
return Promise.resolve()
112111
.then(() => __triggerKeyEvent__(element, 'keydown', characterKey, options))
113112
.then(() => __triggerKeyEvent__(element, 'keypress', characterKey, options))
114113
.then(() => {

addon-test-support/@ember/test-helpers/dom/wait-for.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import waitUntil from '../wait-until';
22
import getElement from './-get-element';
33
import getElements from './-get-elements';
44
import toArray from './-to-array';
5-
import { nextTickPromise } from '../-utils';
5+
import { Promise } from '../-utils';
66

77
export interface Options {
88
timeout?: number;
@@ -31,7 +31,7 @@ export default function waitFor(
3131
selector: string,
3232
options: Options = {}
3333
): Promise<Element | Element[]> {
34-
return nextTickPromise().then(() => {
34+
return Promise.resolve().then(() => {
3535
if (!selector) {
3636
throw new Error('Must pass a selector to `waitFor`.');
3737
}

0 commit comments

Comments
 (0)