Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a basic failing test for settled #458

Merged
merged 4 commits into from
Nov 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ branches:

addons:
chrome: stable
firefox: latest-esr

cache:
yarn: true
Expand Down
84 changes: 80 additions & 4 deletions addon-test-support/@ember/test-helpers/-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
/* globals Promise */
import { Promise as RSVPPromise } from 'rsvp';

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

export class _Promise extends RSVP.Promise<void> {}

const ORIGINAL_RSVP_ASYNC: Function = RSVP.configure('async');

/*
Long ago in a galaxy far far away, Ember forced RSVP.Promise to "resolve" on the Ember.run loop.
At the time, this was meant to help ease pain with folks receiving the dreaded "auto-run" assertion
during their tests, and to help ensure that promise resolution was coelesced to avoid "thrashing"
of the DOM. Unfortunately, the result of this configuration is that code like the following behaves
differently if using native `Promise` vs `RSVP.Promise`:

```js
console.log('first');
Ember.run(() => Promise.resolve().then(() => console.log('second')));
console.log('third');
```

When `Promise` is the native promise that will log `'first', 'third', 'second'`, but when `Promise`
is an `RSVP.Promise` that will log `'first', 'second', 'third'`. The fact that `RSVP.Promise`s can
be **forced** to flush synchronously is very scary!

Now, lets talk about why we are configuring `RSVP`'s `async` below...

---

The following _should_ always be guaranteed:

```js
await settled();

isSettled() === true
```

Unfortunately, without the custom `RSVP` `async` configuration we cannot ensure that `isSettled()` will
be truthy. This is due to the fact that Ember has configured `RSVP` to resolve all promises in the run
loop. What that means practically is this:

1. all checks within `waitUntil` (used by `settled()` internally) are completed and we are "settled"
2. `waitUntil` resolves the promise that it returned (to signify that the world is "settled")
3. resolving the promise (since it is an `RSVP.Promise` and Ember has configured RSVP.Promise) creates
a new Ember.run loop in order to resolve
4. the presence of that new run loop means that we are no longer "settled"
5. `isSettled()` returns false 😭😭😭😭😭😭😭😭😭

This custom `RSVP.configure('async`, ...)` below provides a way to prevent the promises that are returned
from `settled` from causing this "loop" and instead "just use normal Promise semantics".

😩😫🙀
*/
RSVP.configure('async', (callback, promise) => {
if (promise instanceof _Promise) {
// @ts-ignore - avoid erroring about useless `Promise !== RSVP.Promise` comparison
// (this handles when folks have polyfilled via Promise = Ember.RSVP.Promise)
if (typeof Promise !== 'undefined' && Promise !== RSVP.Promise) {
// use real native promise semantics whenever possible
Promise.resolve().then(() => callback(promise));
} else {
// fallback to using RSVP's natural `asap` (**not** the fake
// one configured by Ember...)
RSVP.asap(callback, promise);
}
} else {
// fall back to the normal Ember behavior
ORIGINAL_RSVP_ASYNC(callback, promise);
}
});
rwjblue marked this conversation as resolved.
Show resolved Hide resolved

export const nextTick: Function =
typeof Promise === 'undefined' ? setTimeout : cb => Promise.resolve().then(cb);
Expand All @@ -10,9 +79,16 @@ export const futureTick = setTimeout;
@returns {Promise<void>} Promise which can not be forced to be ran synchronously
*/
export function nextTickPromise() {
return new RSVPPromise(resolve => {
nextTick(resolve);
});
// Ember 3.4 removed the auto-run assertion, in 3.4+ we can (and should) avoid the "psuedo promisey" run loop configuration
// for our `nextTickPromise` implementation. This allows us to have real microtask based next tick timing...
if (hasEmberVersion(3,4)) {
return _Promise.resolve();
} else {
// on older Ember's fallback to RSVP.Promise + a setTimeout
return new RSVP.Promise(resolve => {
nextTick(resolve);
});
}
}

/**
Expand Down
3 changes: 1 addition & 2 deletions addon-test-support/@ember/test-helpers/wait-until.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Promise } from 'rsvp';
import { futureTick } from './-utils';
import { futureTick, _Promise as Promise } from './-utils';

const TIMEOUTS = [0, 1, 2, 5, 7];
const MAX_TIMEOUT = 10;
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/settled-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
teardownContext,
teardownRenderingContext,
click,
isSettled,
getSettledState,
} from '@ember/test-helpers';
import hasEmberVersion from '@ember/test-helpers/has-ember-version';
import { module, test } from 'qunit';
Expand Down Expand Up @@ -144,6 +146,15 @@ module('settled real-world scenarios', function(hooks) {
await teardownContext(this);
});

test('basic behavior', async function(assert) {
await settled();

assert.ok(
isSettled(),
`should be settled after awaiting: ${JSON.stringify(getSettledState())}`
);
});

test('it works when async exists in `init`', async function(assert) {
this.owner.register('component:x-test-1', TestComponent1);

Expand Down