Skip to content

Commit

Permalink
[BUGFIX beta] Ensure App.visit resolves when rendering completed.
Browse files Browse the repository at this point in the history
Prior to this change, rendering was not guaranteed to have been
completed before the promise returned from the `visit` API had resolved.

With these changes no additional delay is added, but we have still
ensured that all rendering is completed.
  • Loading branch information
rwjblue committed Jan 8, 2018
1 parent cc9617c commit 65819a8
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 11 deletions.
14 changes: 4 additions & 10 deletions packages/ember-application/lib/system/application-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
*/

import { assign } from 'ember-utils';
import { get, set, run, computed } from 'ember-metal';
import { RSVP } from 'ember-runtime';
import { get, set, computed } from 'ember-metal';
import { environment } from 'ember-environment';
import { jQuery } from 'ember-views';
import EngineInstance from './engine-instance';
import { renderSettled } from 'ember-glimmer';

/**
The `ApplicationInstance` encapsulates all of the stateful aspects of a
Expand Down Expand Up @@ -243,14 +243,8 @@ const ApplicationInstance = EngineInstance.extend({
// No rendering is needed, and routing has completed, simply return.
return this;
} else {
return new RSVP.Promise((resolve) => {
// Resolve once rendering is completed. `router.handleURL` returns the transition (as a thennable)
// which resolves once the transition is completed, but the transition completion only queues up
// a scheduled revalidation (into the `render` queue) in the Renderer.
//
// This uses `run.schedule('afterRender', ....)` to resolve after that rendering has completed.
run.schedule('afterRender', null, resolve, this);
});
// Ensure that the visit promise resolves when all rendering has completed
return renderSettled().then(() => this);
}
};

Expand Down
1 change: 1 addition & 0 deletions packages/ember-glimmer/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export {
InertRenderer,
InteractiveRenderer,
_resetRenderers,
renderSettled,
} from './renderer';
export {
getTemplate,
Expand Down
35 changes: 35 additions & 0 deletions packages/ember-glimmer/lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { RootReference } from './utils/references';
import OutletView, { OutletState, RootOutletStateReference } from './views/outlet';

import { ComponentDefinition, NULL_REFERENCE, RenderResult } from '@glimmer/runtime';
import RSVP from 'rsvp';

const { backburner } = run;

Expand Down Expand Up @@ -181,6 +182,39 @@ function loopBegin(): void {

function K() { /* noop */ }

let renderSettledDeferred: RSVP.Deferred<void> | null = null;
/*
Returns a promise which will resolve when rendering has settled. Settled in
this context is defined as when all of the tags in use are "current" (e.g.
`renderers.every(r => r._isValid())`). When this is checked at the _end_ of
the run loop, this essentially guarantees that all rendering is completed.
@method renderSettled
@returns {Promise<void>} a promise which fulfills when rendering has settled
*/
export function renderSettled() {
if (renderSettledDeferred === null) {
renderSettledDeferred = RSVP.defer();
// if there is no current runloop, the promise created above will not have
// a chance to resolve (because its resolved in backburner's "end" event)
if (!run.currentRunLoop) {
// ensure a runloop has been kicked off
backburner.schedule('actions', null, K);
}
}

return renderSettledDeferred.promise;
}

function resolveRenderPromise() {
if (renderSettledDeferred !== null) {
let resolve = renderSettledDeferred.resolve;
renderSettledDeferred = null;

backburner.join(null, resolve);
}
}

let loops = 0;
function loopEnd() {
for (let i = 0; i < renderers.length; i++) {
Expand All @@ -196,6 +230,7 @@ function loopEnd() {
}
}
loops = 0;
resolveRenderPromise();
}

backburner.on('begin', loopBegin);
Expand Down
73 changes: 73 additions & 0 deletions packages/ember-glimmer/tests/integration/render-settled-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
RenderingTestCase,
moduleFor,
strip
} from 'internal-test-helpers';
import { renderSettled } from 'ember-glimmer';
import { all } from 'rsvp';
import { run } from 'ember-metal';

moduleFor('renderSettled', class extends RenderingTestCase {
['@test resolves when no rendering is happening'](assert) {
return renderSettled().then(() => {
assert.ok(true, 'resolved even without rendering');
});
}

['@test resolves renderers exist but no runloops are triggered'](assert) {
this.render(strip`{{foo}}`, { foo: 'bar' });

return renderSettled().then(() => {
assert.ok(true, 'resolved even without runloops');
});
}

['@test does not create extraneous promises'](assert) {
let first = renderSettled();
let second = renderSettled();

assert.strictEqual(first, second);

return all([first, second]);
}

['@test resolves when rendering has completed (after property update)']() {
this.render(strip`{{foo}}`, { foo: 'bar' });

this.assertText('bar');
this.component.set('foo', 'baz');
this.assertText('bar');

return renderSettled().then(() => {
this.assertText('baz');
});
}

['@test resolves in run loop when renderer has settled'](assert) {
assert.expect(3);

this.render(strip`{{foo}}`, { foo: 'bar' });

this.assertText('bar');
let promise;

return run(() => {
run.schedule('actions', null, () => {
this.component.set('foo', 'set in actions');

promise = renderSettled().then(() => {
this.assertText('set in afterRender');
});

run.schedule('afterRender', null, () => {
this.component.set('foo', 'set in afterRender');
});
});

// still not updated here
this.assertText('bar');

return promise;
});
}
});
4 changes: 3 additions & 1 deletion packages/ember-metal/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ interface IBackburner {
join(...args: any[]): void;
on(...args: any[]): void;
scheduleOnce(...args: any[]): void;
schedule(queueName: string, target: Object | null, method: Function | string): void;
}
interface IRun {
(...args: any[]): any;
schedule(...args: any[]): void;
later(...args: any[]): void;
join(...args: any[]): void;
backburner: IBackburner
backburner: IBackburner;
currentRunLoop: boolean;
}

export const run: IRun;
Expand Down

0 comments on commit 65819a8

Please sign in to comment.