diff --git a/packages/@ember/-internals/routing/lib/system/router.ts b/packages/@ember/-internals/routing/lib/system/router.ts
index a31c86797cd..8d308366159 100644
--- a/packages/@ember/-internals/routing/lib/system/router.ts
+++ b/packages/@ember/-internals/routing/lib/system/router.ts
@@ -1397,9 +1397,15 @@ function findRouteStateName(route: Route, state: string) {
return routeHasBeenDefined(owner, router, stateName, stateNameFull) ? stateNameFull : '';
}
+function isPromise(p: any): boolean {
+ return p !== null && typeof p === 'object' && typeof p.then === 'function';
+}
+
/**
- Determines whether or not a route has been defined by checking that the route
- is in the Router's map and the owner has a registration for that route.
+ Determines whether or not a route has been defined by checking
+ - that the route is in the Router's map and
+ - the owner has a registration for that route and
+ - it has been fully resolved (think of aync assets loading)
@private
@param {Owner} owner
@@ -1410,9 +1416,15 @@ function findRouteStateName(route: Route, state: string) {
*/
function routeHasBeenDefined(owner: Owner, router: any, localName: string, fullName: string) {
let routerHasRoute = router.hasRoute(fullName);
- let ownerHasRoute =
- owner.hasRegistration(`template:${localName}`) || owner.hasRegistration(`route:${localName}`);
- return routerHasRoute && ownerHasRoute;
+
+ if (routerHasRoute && !isPromise(router.getRoute(fullName))) {
+ let ownerHasRoute =
+ owner.hasRegistration(`template:${localName}`) || owner.hasRegistration(`route:${localName}`);
+
+ return ownerHasRoute;
+ }
+
+ return false;
}
export function triggerEvent(
diff --git a/packages/ember/tests/routing/model_loading_test.js b/packages/ember/tests/routing/model_loading_test.js
index 3c614a5b9bb..7911aa00732 100644
--- a/packages/ember/tests/routing/model_loading_test.js
+++ b/packages/ember/tests/routing/model_loading_test.js
@@ -2,11 +2,15 @@
import { Route } from '@ember/-internals/routing';
import Controller from '@ember/controller';
import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime';
-import { moduleFor, ApplicationTestCase, getTextOf } from 'internal-test-helpers';
+import {
+ moduleFor,
+ ApplicationTestCase,
+ getTextOf,
+ runLoopSettled,
+ lazyLoadingRouterOptions,
+} from 'internal-test-helpers';
import { run } from '@ember/runloop';
import { computed, set } from '@ember/-internals/metal';
-import { isDestroying } from '@glimmer/destroyable';
-import RSVP from 'rsvp';
let originalConsoleError;
@@ -502,8 +506,6 @@ class LoadingTests extends ApplicationTestCase {
})
);
- this.add('route:loading', Route.extend({}));
- this.add('route:home', Route.extend({}));
this.add(
'route:special',
Route.extend({
@@ -518,7 +520,6 @@ class LoadingTests extends ApplicationTestCase {
this.addTemplate('root.index', '
Home
');
this.addTemplate('special', '{{@model.id}}
');
- this.addTemplate('loading', 'LOADING!
');
return this.visit('/').then(() => {
rootElement = document.getElementById('qunit-fixture');
@@ -755,7 +756,7 @@ class LoadingTests extends ApplicationTestCase {
});
}
- ['@test Parent route context change'](assert) {
+ async ['@test Parent route context change'](assert) {
let editCount = 0;
let editedPostIds = emberA();
@@ -777,8 +778,8 @@ class LoadingTests extends ApplicationTestCase {
'route:posts',
Route.extend({
actions: {
- showPost(context) {
- expectDeprecation(() => {
+ async showPost(context) {
+ await expectDeprecationAsync(() => {
this.transitionTo('post', context);
}, /Calling transitionTo on a route is deprecated/);
},
@@ -798,8 +799,8 @@ class LoadingTests extends ApplicationTestCase {
},
actions: {
- editPost() {
- expectDeprecation(() => {
+ async editPost() {
+ await expectDeprecationAsync(() => {
this.transitionTo('post.edit');
}, /Calling transitionTo on a route is deprecated/);
},
@@ -815,6 +816,7 @@ class LoadingTests extends ApplicationTestCase {
editedPostIds.push(postId);
return null;
},
+
setup() {
this._super(...arguments);
editCount++;
@@ -822,15 +824,18 @@ class LoadingTests extends ApplicationTestCase {
})
);
- return this.visit('/posts/1').then(() => {
- assert.ok(true, '/posts/1 has been handled');
- let router = this.applicationInstance.lookup('router:main');
- run(() => router.send('editPost'));
- run(() => router.send('showPost', { id: '2' }));
- run(() => router.send('editPost'));
- assert.equal(editCount, 2, 'set up the edit route twice without failure');
- assert.deepEqual(editedPostIds, ['1', '2'], 'modelFor posts.post returns the right context');
- });
+ await this.visit('/posts/1');
+ assert.ok(true, '/posts/1 has been handled');
+ let router = this.applicationInstance.lookup('router:main');
+ run(() => router.send('editPost'));
+ await runLoopSettled();
+ await runLoopSettled();
+ run(() => router.send('showPost', { id: '2' }));
+ await runLoopSettled();
+ run(() => router.send('editPost'));
+ await runLoopSettled();
+ assert.equal(editCount, 2, 'set up the edit route twice without failure');
+ assert.deepEqual(editedPostIds, ['1', '2'], 'modelFor posts.post returns the right context');
}
['@test ApplicationRoute with model does not proxy the currentPath'](assert) {
@@ -941,6 +946,14 @@ class LoadingTests extends ApplicationTestCase {
assert.equal(childcount, 2);
});
}
+
+ ['@test What about loading states'](assert) {
+ assert.expect(1);
+ this.add('route:loading', Route.extend({}));
+ return this.visit('/').then(() => {
+ assert.ok(true);
+ });
+ }
}
moduleFor('Route - model loading', LoadingTests);
@@ -949,43 +962,7 @@ moduleFor(
'Route - model loading (simulated within lazy engine)',
class extends LoadingTests {
get routerOptions() {
- return {
- location: 'none',
- setupRouter() {
- this._super(...arguments);
- let getRoute = this._routerMicrolib.getRoute;
- this._enginePromises = Object.create(null);
- this._resolvedEngines = Object.create(null);
-
- let routes = new Map();
- let routePromises = new Map();
- this._routerMicrolib.getRoute = (name) => {
- if (routes.has(name)) {
- return routes.get(name);
- }
-
- if (routePromises.has(name)) {
- return routePromises.get(name);
- }
-
- let promise = new RSVP.Promise((resolve) => {
- setTimeout(() => {
- if (isDestroying(this)) {
- return;
- }
-
- let route = getRoute(name);
-
- routes.set(name, route);
- resolve(route);
- }, 10);
- });
- routePromises.set(name, promise);
-
- return promise;
- };
- },
- };
+ return lazyLoadingRouterOptions;
}
}
);
diff --git a/packages/ember/tests/routing/substates_test.js b/packages/ember/tests/routing/substates_test.js
index 13330e7d0c1..bfa3a8c457c 100644
--- a/packages/ember/tests/routing/substates_test.js
+++ b/packages/ember/tests/routing/substates_test.js
@@ -2,7 +2,12 @@ import { RSVP } from '@ember/-internals/runtime';
import { Route } from '@ember/-internals/routing';
import Controller from '@ember/controller';
-import { moduleFor, ApplicationTestCase, runTask } from 'internal-test-helpers';
+import {
+ moduleFor,
+ ApplicationTestCase,
+ runTask,
+ lazyLoadingRouterOptions,
+} from 'internal-test-helpers';
let counter;
@@ -10,1275 +15,1280 @@ function step(assert, expectedValue, description) {
assert.equal(counter, expectedValue, 'Step ' + expectedValue + ': ' + description);
counter++;
}
+class LoadingErrorSubstatesTests extends ApplicationTestCase {
+ constructor() {
+ super(...arguments);
+ counter = 1;
-moduleFor(
- 'Loading/Error Substates',
- class extends ApplicationTestCase {
- constructor() {
- super(...arguments);
- counter = 1;
-
- this.addTemplate('application', `{{outlet}}
`);
- this.addTemplate('index', 'INDEX');
- }
+ this.addTemplate('application', `{{outlet}}
`);
+ this.addTemplate('index', 'INDEX');
+ }
- visit(...args) {
- return runTask(() => super.visit(...args));
- }
+ visit(...args) {
+ return runTask(() => super.visit(...args));
+ }
- getController(name) {
- return this.applicationInstance.lookup(`controller:${name}`);
- }
+ getController(name) {
+ return this.applicationInstance.lookup(`controller:${name}`);
+ }
- get currentPath() {
- let currentPath;
- expectDeprecation(() => {
- currentPath = this.getController('application').get('currentPath');
- }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
- return currentPath;
- }
+ get currentPath() {
+ let currentPath;
+ expectDeprecation(() => {
+ currentPath = this.getController('application').get('currentPath');
+ }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
+ return currentPath;
+ }
- ['@test Slow promise from a child route of application enters nested loading state'](assert) {
- let turtleDeferred = RSVP.defer();
+ ['@test Slow promise from a child route of application enters nested loading state'](assert) {
+ let turtleDeferred = RSVP.defer();
- this.router.map(function () {
- this.route('turtle');
- });
+ this.router.map(function () {
+ this.route('turtle');
+ });
- this.add(
- 'route:application',
- Route.extend({
- setupController() {
- step(assert, 2, 'ApplicationRoute#setupController');
- },
- })
- );
+ this.add(
+ 'route:application',
+ Route.extend({
+ setupController() {
+ step(assert, 2, 'ApplicationRoute#setupController');
+ },
+ })
+ );
+
+ this.add(
+ 'route:turtle',
+ Route.extend({
+ model() {
+ step(assert, 1, 'TurtleRoute#model');
+ return turtleDeferred.promise;
+ },
+ })
+ );
+ this.addTemplate('turtle', 'TURTLE');
+ this.addTemplate('loading', 'LOADING');
- this.add(
- 'route:turtle',
- Route.extend({
- model() {
- step(assert, 1, 'TurtleRoute#model');
- return turtleDeferred.promise;
- },
- })
- );
- this.addTemplate('turtle', 'TURTLE');
- this.addTemplate('loading', 'LOADING');
+ let promise = this.visit('/turtle').then(() => {
+ text = this.$('#app').text();
+ assert.equal(text, 'TURTLE', `turtle template has loaded and replaced the loading template`);
+ });
+
+ let text = this.$('#app').text();
+ assert.equal(
+ text,
+ 'LOADING',
+ `The Loading template is nested in application template's outlet`
+ );
+
+ turtleDeferred.resolve();
+ return promise;
+ }
- let promise = this.visit('/turtle').then(() => {
- text = this.$('#app').text();
- assert.equal(
- text,
- 'TURTLE',
- `turtle template has loaded and replaced the loading template`
- );
- });
+ [`@test Slow promises returned from ApplicationRoute#model don't enter LoadingRoute`](assert) {
+ let appDeferred = RSVP.defer();
+
+ this.add(
+ 'route:application',
+ Route.extend({
+ model() {
+ return appDeferred.promise;
+ },
+ })
+ );
+ this.add(
+ 'route:loading',
+ Route.extend({
+ setupController() {
+ assert.ok(false, `shouldn't get here`);
+ },
+ })
+ );
+ let promise = this.visit('/').then(() => {
let text = this.$('#app').text();
- assert.equal(
- text,
- 'LOADING',
- `The Loading template is nested in application template's outlet`
- );
- turtleDeferred.resolve();
- return promise;
+ assert.equal(text, 'INDEX', `index template has been rendered`);
+ });
+
+ if (this.element) {
+ assert.equal(this.element.textContent, '');
}
- [`@test Slow promises returned from ApplicationRoute#model don't enter LoadingRoute`](assert) {
- let appDeferred = RSVP.defer();
+ appDeferred.resolve();
- this.add(
- 'route:application',
- Route.extend({
- model() {
- return appDeferred.promise;
- },
- })
- );
- this.add(
- 'route:loading',
- Route.extend({
- setupController() {
- assert.ok(false, `shouldn't get here`);
- },
- })
- );
+ return promise;
+ }
+
+ [`@test Don't enter loading route unless either route or template defined`](assert) {
+ let deferred = RSVP.defer();
+
+ this.router.map(function () {
+ this.route('dummy');
+ });
+ this.add(
+ 'route:dummy',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
+ this.addTemplate('dummy', 'DUMMY');
- let promise = this.visit('/').then(() => {
+ return this.visit('/').then(() => {
+ let promise = this.visit('/dummy').then(() => {
let text = this.$('#app').text();
- assert.equal(text, 'INDEX', `index template has been rendered`);
+ assert.equal(text, 'DUMMY', `dummy template has been rendered`);
});
- if (this.element) {
- assert.equal(this.element.textContent, '');
- }
-
- appDeferred.resolve();
-
- return promise;
- }
-
- [`@test Don't enter loading route unless either route or template defined`](assert) {
- let deferred = RSVP.defer();
-
- this.router.map(function () {
- this.route('dummy');
- });
- this.add(
- 'route:dummy',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
+ assert.ok(
+ this.currentPath !== 'loading',
+ `
+ loading state not entered
+ `
);
- this.addTemplate('dummy', 'DUMMY');
+ deferred.resolve();
- return this.visit('/').then(() => {
- let promise = this.visit('/dummy').then(() => {
- let text = this.$('#app').text();
+ return promise;
+ });
+ }
- assert.equal(text, 'DUMMY', `dummy template has been rendered`);
- });
+ ['@test Enter loading route only if loadingRoute is defined'](assert) {
+ let deferred = RSVP.defer();
- assert.ok(
- this.currentPath !== 'loading',
- `
- loading state not entered
- `
- );
- deferred.resolve();
+ this.router.map(function () {
+ this.route('dummy');
+ });
- return promise;
- });
- }
+ this.add(
+ 'route:dummy',
+ Route.extend({
+ model() {
+ step(assert, 1, 'DummyRoute#model');
+ return deferred.promise;
+ },
+ })
+ );
+ this.add(
+ 'route:loading',
+ Route.extend({
+ setupController() {
+ step(assert, 2, 'LoadingRoute#setupController');
+ },
+ })
+ );
+ this.addTemplate('dummy', 'DUMMY');
- ['@test Enter loading route only if loadingRoute is defined'](assert) {
- let deferred = RSVP.defer();
+ return this.visit('/').then(() => {
+ let promise = this.visit('/dummy').then(() => {
+ let text = this.$('#app').text();
- this.router.map(function () {
- this.route('dummy');
+ assert.equal(text, 'DUMMY', `dummy template has been rendered`);
});
+ assert.equal(this.currentPath, 'loading', `loading state entered`);
+ deferred.resolve();
- this.add(
- 'route:dummy',
- Route.extend({
- model() {
- step(assert, 1, 'DummyRoute#model');
- return deferred.promise;
- },
- })
- );
- this.add(
- 'route:loading',
- Route.extend({
- setupController() {
- step(assert, 2, 'LoadingRoute#setupController');
- },
- })
- );
- this.addTemplate('dummy', 'DUMMY');
+ return promise;
+ });
+ }
- return this.visit('/').then(() => {
- let promise = this.visit('/dummy').then(() => {
- let text = this.$('#app').text();
+ ['@test Enter loading route with correct query parameters'](assert) {
+ let deferred = RSVP.defer();
- assert.equal(text, 'DUMMY', `dummy template has been rendered`);
- });
- assert.equal(this.currentPath, 'loading', `loading state entered`);
- deferred.resolve();
+ this.router.map(function () {
+ this.route('dummy');
+ });
- return promise;
- });
- }
-
- ['@test Enter loading route with correct query parameters'](assert) {
- let deferred = RSVP.defer();
+ this.add(
+ 'route:dummy',
+ Route.extend({
+ model() {
+ step(assert, 1, 'DummyRoute#model');
+ return deferred.promise;
+ },
+ })
+ );
- this.router.map(function () {
- this.route('dummy');
- });
+ this.add(
+ 'controller:application',
+ class extends Controller {
+ queryParams = ['qux'];
- this.add(
- 'route:dummy',
- Route.extend({
- model() {
- step(assert, 1, 'DummyRoute#model');
- return deferred.promise;
- },
- })
- );
+ qux = 'initial';
+ }
+ );
- this.add(
- 'controller:application',
- class extends Controller {
- queryParams = ['qux'];
+ this.add(
+ 'route:loading',
+ Route.extend({
+ setupController() {
+ step(assert, 2, 'LoadingRoute#setupController');
+ },
+ })
+ );
+ this.addTemplate('dummy', 'DUMMY');
- qux = 'initial';
- }
+ return this.visit('/?qux=updated').then(() => {
+ assert.equal(
+ this.getController('application').qux,
+ 'updated',
+ 'the application controller has the correct qp value'
);
- this.add(
- 'route:loading',
- Route.extend({
- setupController() {
- step(assert, 2, 'LoadingRoute#setupController');
- },
- })
- );
- this.addTemplate('dummy', 'DUMMY');
+ let promise = this.visit('/dummy?qux=updated').then(() => {
+ let text = this.$('#app').text();
- return this.visit('/?qux=updated').then(() => {
+ assert.equal(text, 'DUMMY', `dummy template has been rendered`);
assert.equal(
this.getController('application').qux,
'updated',
'the application controller has the correct qp value'
);
+ });
- let promise = this.visit('/dummy?qux=updated').then(() => {
- let text = this.$('#app').text();
+ assert.equal(this.currentPath, 'loading', `loading state entered`);
+ assert.equal(
+ this.currentURL,
+ '/dummy?qux=updated',
+ `during loading url reflect the correct state`
+ );
+ assert.equal(
+ this.getController('application').qux,
+ 'updated',
+ 'the application controller has the correct qp value'
+ );
- assert.equal(text, 'DUMMY', `dummy template has been rendered`);
- assert.equal(
- this.getController('application').qux,
- 'updated',
- 'the application controller has the correct qp value'
- );
- });
+ deferred.resolve();
- assert.equal(this.currentPath, 'loading', `loading state entered`);
- assert.equal(
- this.currentURL,
- '/dummy?qux=updated',
- `during loading url reflect the correct state`
- );
- assert.equal(
- this.getController('application').qux,
- 'updated',
- 'the application controller has the correct qp value'
- );
+ return promise;
+ });
+ }
- deferred.resolve();
+ ['@test Enter child-loading route with correct query parameters'](assert) {
+ assert.expect(9);
+ let deferred = RSVP.defer();
- return promise;
+ this.router.map(function () {
+ this.route('parent', function () {
+ this.route('child');
});
- }
+ });
+
+ this.add(
+ 'route:parent.child',
+ Route.extend({
+ model() {
+ step(assert, 1, 'ChildRoute#model');
+ return deferred.promise;
+ },
+ })
+ );
- ['@test Enter child-loading route with correct query parameters'](assert) {
- assert.expect(9);
- let deferred = RSVP.defer();
+ this.add(
+ 'controller:parent',
+ class extends Controller {
+ queryParams = ['qux'];
- this.router.map(function () {
- this.route('parent', function () {
- this.route('child');
- });
- });
-
- this.add(
- 'route:parent.child',
- Route.extend({
- model() {
- step(assert, 1, 'ChildRoute#model');
- return deferred.promise;
- },
- })
- );
+ qux = 'initial';
+ }
+ );
- this.add(
- 'controller:parent',
- class extends Controller {
- queryParams = ['qux'];
+ this.add(
+ 'route:parent.child_loading',
+ Route.extend({
+ setupController() {
+ step(assert, 2, 'ChildLoadingRoute#setupController');
+ },
+ })
+ );
+ this.addTemplate('parent', 'PARENT {{outlet}}');
- qux = 'initial';
- }
- );
+ this.addTemplate('parent.child', 'CHILD');
- this.add(
- 'route:parent.child_loading',
- Route.extend({
- setupController() {
- step(assert, 2, 'ChildLoadingRoute#setupController');
- },
- })
+ return this.visit('/parent?qux=updated').then(() => {
+ assert.equal(
+ this.getController('parent').qux,
+ 'updated',
+ 'in the parent route, the parent controller has the correct qp value'
);
- this.addTemplate('parent', 'PARENT {{outlet}}');
- this.addTemplate('parent.child', 'CHILD');
-
- return this.visit('/parent?qux=updated').then(() => {
- assert.equal(
- this.getController('parent').qux,
- 'updated',
- 'in the parent route, the parent controller has the correct qp value'
- );
-
- let promise = this.visit('/parent/child?qux=updated').then(() => {
- let text = this.$('#app').text();
-
- assert.equal(text, 'PARENT CHILD', `child template has been rendered`);
- assert.equal(
- this.getController('parent').qux,
- 'updated',
- 'after entered in the parent.child route, the parent controller has the correct qp value'
- );
- });
+ let promise = this.visit('/parent/child?qux=updated').then(() => {
+ let text = this.$('#app').text();
- assert.equal(this.currentPath, 'parent.child_loading', `child loading state entered`);
- assert.equal(
- this.currentURL,
- '/parent/child?qux=updated',
- `during child loading, url reflect the correct state`
- );
+ assert.equal(text, 'PARENT CHILD', `child template has been rendered`);
assert.equal(
this.getController('parent').qux,
'updated',
- 'in the child_loading route, the parent controller has the correct qp value'
+ 'after entered in the parent.child route, the parent controller has the correct qp value'
);
-
- deferred.resolve();
-
- return promise;
});
- }
- ['@test Slow promises returned from ApplicationRoute#model enter ApplicationLoadingRoute if present'](
- assert
- ) {
- let appDeferred = RSVP.defer();
-
- this.add(
- 'route:application',
- Route.extend({
- model() {
- return appDeferred.promise;
- },
- })
+ assert.equal(this.currentPath, 'parent.child_loading', `child loading state entered`);
+ assert.equal(
+ this.currentURL,
+ '/parent/child?qux=updated',
+ `during child loading, url reflect the correct state`
);
- let loadingRouteEntered = false;
- this.add(
- 'route:application_loading',
- Route.extend({
- setupController() {
- loadingRouteEntered = true;
- },
- })
+ assert.equal(
+ this.getController('parent').qux,
+ 'updated',
+ 'in the child_loading route, the parent controller has the correct qp value'
);
- let promise = this.visit('/').then(() => {
- assert.equal(this.$('#app').text(), 'INDEX', 'index route loaded');
- });
- assert.ok(loadingRouteEntered, 'ApplicationLoadingRoute was entered');
- appDeferred.resolve();
+ deferred.resolve();
return promise;
- }
-
- ['@test Slow promises returned from ApplicationRoute#model enter application_loading if template present'](
- assert
- ) {
- let appDeferred = RSVP.defer();
+ });
+ }
- this.addTemplate(
- 'application_loading',
- `
- TOPLEVEL LOADING
- `
- );
- this.add(
- 'route:application',
- Route.extend({
- model() {
- return appDeferred.promise;
- },
- })
- );
+ ['@test Slow promises returned from ApplicationRoute#model enter ApplicationLoadingRoute if present'](
+ assert
+ ) {
+ let appDeferred = RSVP.defer();
- let promise = this.visit('/').then(() => {
- let length = this.$('#toplevel-loading').length;
- text = this.$('#app').text();
+ this.add(
+ 'route:application',
+ Route.extend({
+ model() {
+ return appDeferred.promise;
+ },
+ })
+ );
+ let loadingRouteEntered = false;
+ this.add(
+ 'route:application_loading',
+ Route.extend({
+ setupController() {
+ loadingRouteEntered = true;
+ },
+ })
+ );
- assert.equal(length, 0, `top-level loading view has been entirely removed from the DOM`);
- assert.equal(text, 'INDEX', 'index has fully rendered');
- });
- let text = this.$('#toplevel-loading').text();
+ let promise = this.visit('/').then(() => {
+ assert.equal(this.$('#app').text(), 'INDEX', 'index route loaded');
+ });
+ assert.ok(loadingRouteEntered, 'ApplicationLoadingRoute was entered');
+ appDeferred.resolve();
- assert.equal(text, 'TOPLEVEL LOADING', 'still loading the top level');
- appDeferred.resolve();
+ return promise;
+ }
- return promise;
- }
+ ['@test Slow promises returned from ApplicationRoute#model enter application_loading if template present'](
+ assert
+ ) {
+ let appDeferred = RSVP.defer();
- ['@test Prioritized substate entry works with preserved-namespace nested routes'](assert) {
- let deferred = RSVP.defer();
+ this.addTemplate(
+ 'application_loading',
+ `
+ TOPLEVEL LOADING
+ `
+ );
+ this.add(
+ 'route:application',
+ Route.extend({
+ model() {
+ return appDeferred.promise;
+ },
+ })
+ );
- this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
- this.addTemplate('foo.bar.index', 'YAY');
+ let promise = this.visit('/').then(() => {
+ let length = this.$('#toplevel-loading').length;
+ text = this.$('#app').text();
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar', { path: '/bar' }, function () {});
- });
- });
+ assert.equal(length, 0, `top-level loading view has been entirely removed from the DOM`);
+ assert.equal(text, 'INDEX', 'index has fully rendered');
+ });
+ let text = this.$('#toplevel-loading').text();
- this.add(
- 'route:foo.bar',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
- );
+ assert.equal(text, 'TOPLEVEL LOADING', 'still loading the top level');
+ appDeferred.resolve();
- return this.visit('/').then(() => {
- let promise = this.visit('/foo/bar').then(() => {
- text = this.$('#app').text();
+ return promise;
+ }
- assert.equal(text, 'YAY', 'foo.bar.index fully loaded');
- });
- let text = this.$('#app').text();
+ ['@test Prioritized substate entry works with preserved-namespace nested routes'](assert) {
+ let deferred = RSVP.defer();
- assert.equal(
- text,
- 'FOOBAR LOADING',
- `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
- );
- deferred.resolve();
+ this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
+ this.addTemplate('foo.bar.index', 'YAY');
- return promise;
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar', { path: '/bar' }, function () {});
});
- }
+ });
- ['@test Prioritized substate entry works with reset-namespace nested routes'](assert) {
- let deferred = RSVP.defer();
+ this.add(
+ 'route:foo.bar',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
- this.addTemplate('bar_loading', 'BAR LOADING');
- this.addTemplate('bar.index', 'YAY');
+ return this.visit('/').then(() => {
+ let promise = this.visit('/foo/bar').then(() => {
+ text = this.$('#app').text();
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar', { path: '/bar', resetNamespace: true }, function () {});
- });
+ assert.equal(text, 'YAY', 'foo.bar.index fully loaded');
});
+ let text = this.$('#app').text();
- this.add(
- 'route:bar',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
+ assert.equal(
+ text,
+ 'FOOBAR LOADING',
+ `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
);
+ deferred.resolve();
- return this.visit('/').then(() => {
- let promise = this.visit('/foo/bar').then(() => {
- text = this.$('#app').text();
-
- assert.equal(text, 'YAY', 'bar.index fully loaded');
- });
-
- let text = this.$('#app').text();
-
- assert.equal(
- text,
- 'BAR LOADING',
- `foo.bar_loading was entered (as opposed to something likefoo/foo/bar_loading)`
- );
- deferred.resolve();
-
- return promise;
- });
- }
+ return promise;
+ });
+ }
- ['@test Prioritized loading substate entry works with preserved-namespace nested routes'](
- assert
- ) {
- let deferred = RSVP.defer();
+ ['@test Prioritized substate entry works with reset-namespace nested routes'](assert) {
+ let deferred = RSVP.defer();
- this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
- this.addTemplate('foo.bar', 'YAY');
+ this.addTemplate('bar_loading', 'BAR LOADING');
+ this.addTemplate('bar.index', 'YAY');
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar');
- });
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar', { path: '/bar', resetNamespace: true }, function () {});
});
+ });
- this.add(
- 'route:foo.bar',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
- );
+ this.add(
+ 'route:bar',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
+ return this.visit('/').then(() => {
let promise = this.visit('/foo/bar').then(() => {
text = this.$('#app').text();
- assert.equal(text, 'YAY', 'foo.bar has rendered');
+ assert.equal(text, 'YAY', 'bar.index fully loaded');
});
+
let text = this.$('#app').text();
assert.equal(
text,
- 'FOOBAR LOADING',
- `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
+ 'BAR LOADING',
+ `foo.bar_loading was entered (as opposed to something likefoo/foo/bar_loading)`
);
deferred.resolve();
return promise;
- }
+ });
+ }
- async ['@test Prioritized error substate entry works with preserved-namespace nested routes'](
- assert
- ) {
- this.addTemplate('foo.bar_error', 'FOOBAR ERROR: {{@model.msg}}');
- this.addTemplate('foo.bar', 'YAY');
+ ['@test Prioritized loading substate entry works with preserved-namespace nested routes'](
+ assert
+ ) {
+ let deferred = RSVP.defer();
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar');
- });
+ this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
+ this.addTemplate('foo.bar', 'YAY');
+
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar');
});
+ });
- this.add(
- 'route:foo.bar',
- Route.extend({
- model() {
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- })
- );
+ this.add(
+ 'route:foo.bar',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
- await this.visit('/');
+ let promise = this.visit('/foo/bar').then(() => {
+ text = this.$('#app').text();
- await this.visit('/foo/bar');
+ assert.equal(text, 'YAY', 'foo.bar has rendered');
+ });
+ let text = this.$('#app').text();
- assert.equal(
- this.$('#app').text(),
- 'FOOBAR ERROR: did it broke?',
- `foo.bar_error was entered (as opposed to something like foo/foo/bar_error)`
- );
- }
+ assert.equal(
+ text,
+ 'FOOBAR LOADING',
+ `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
+ );
+ deferred.resolve();
- ['@test Prioritized loading substate entry works with auto-generated index routes'](assert) {
- let deferred = RSVP.defer();
- this.addTemplate('foo.index_loading', 'FOO LOADING');
- this.addTemplate('foo.index', 'YAY');
- this.addTemplate('foo', '{{outlet}}');
+ return promise;
+ }
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar');
- });
+ async ['@test Prioritized error substate entry works with preserved-namespace nested routes'](
+ assert
+ ) {
+ this.addTemplate('foo.bar_error', 'FOOBAR ERROR: {{@model.msg}}');
+ this.addTemplate('foo.bar', 'YAY');
+
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar');
});
+ });
+
+ this.add(
+ 'route:foo.bar',
+ Route.extend({
+ model() {
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ })
+ );
- this.add(
- 'route:foo.index',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
- );
- this.add(
- 'route:foo',
- Route.extend({
- model() {
- return true;
- },
- })
- );
+ await this.visit('/');
- let promise = this.visit('/foo').then(() => {
- text = this.$('#app').text();
+ await this.visit('/foo/bar');
- assert.equal(text, 'YAY', 'foo.index was rendered');
+ assert.equal(
+ this.$('#app').text(),
+ 'FOOBAR ERROR: did it broke?',
+ `foo.bar_error was entered (as opposed to something like foo/foo/bar_error)`
+ );
+ }
+
+ ['@test Prioritized loading substate entry works with auto-generated index routes'](assert) {
+ let deferred = RSVP.defer();
+ this.addTemplate('foo.index_loading', 'FOO LOADING');
+ this.addTemplate('foo.index', 'YAY');
+ this.addTemplate('foo', '{{outlet}}');
+
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar');
});
- let text = this.$('#app').text();
- assert.equal(text, 'FOO LOADING', 'foo.index_loading was entered');
+ });
- deferred.resolve();
+ this.add(
+ 'route:foo.index',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
+ this.add(
+ 'route:foo',
+ Route.extend({
+ model() {
+ return true;
+ },
+ })
+ );
- return promise;
- }
+ let promise = this.visit('/foo').then(() => {
+ text = this.$('#app').text();
- async ['@test Prioritized error substate entry works with auto-generated index routes'](
- assert
- ) {
- this.addTemplate('foo.index_error', 'FOO ERROR: {{@model.msg}}');
- this.addTemplate('foo.index', 'YAY');
- this.addTemplate('foo', '{{outlet}}');
+ assert.equal(text, 'YAY', 'foo.index was rendered');
+ });
+ let text = this.$('#app').text();
+ assert.equal(text, 'FOO LOADING', 'foo.index_loading was entered');
- this.router.map(function () {
- this.route('foo', function () {
- this.route('bar');
- });
- });
+ deferred.resolve();
- this.add(
- 'route:foo.index',
- Route.extend({
- model() {
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- })
- );
- this.add(
- 'route:foo',
- Route.extend({
- model() {
- return true;
- },
- })
- );
+ return promise;
+ }
- await this.visit('/');
+ async ['@test Prioritized error substate entry works with auto-generated index routes'](assert) {
+ this.addTemplate('foo.index_error', 'FOO ERROR: {{@model.msg}}');
+ this.addTemplate('foo.index', 'YAY');
+ this.addTemplate('foo', '{{outlet}}');
- await this.visit('/foo');
+ this.router.map(function () {
+ this.route('foo', function () {
+ this.route('bar');
+ });
+ });
+
+ this.add(
+ 'route:foo.index',
+ Route.extend({
+ model() {
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ })
+ );
+ this.add(
+ 'route:foo',
+ Route.extend({
+ model() {
+ return true;
+ },
+ })
+ );
- assert.equal(
- this.$('#app').text(),
- 'FOO ERROR: did it broke?',
- 'foo.index_error was entered'
- );
- }
+ await this.visit('/');
- async ['@test Rejected promises returned from ApplicationRoute transition into top-level application_error'](
- assert
- ) {
- let reject = true;
-
- this.addTemplate('index', 'INDEX
');
- this.add(
- 'route:application',
- Route.extend({
- init() {
- this._super(...arguments);
- },
- model() {
- if (reject) {
- return RSVP.reject({ msg: 'BAD NEWS BEARS' });
- } else {
- return {};
- }
- },
- })
- );
+ await this.visit('/foo');
- this.addTemplate(
- 'application_error',
- `TOPLEVEL ERROR: {{@model.msg}}
`
- );
+ assert.equal(this.$('#app').text(), 'FOO ERROR: did it broke?', 'foo.index_error was entered');
+ }
- await this.visit('/');
+ async ['@test Rejected promises returned from ApplicationRoute transition into top-level application_error'](
+ assert
+ ) {
+ let reject = true;
+
+ this.addTemplate('index', 'INDEX
');
+ this.add(
+ 'route:application',
+ Route.extend({
+ init() {
+ this._super(...arguments);
+ },
+ model() {
+ if (reject) {
+ return RSVP.reject({ msg: 'BAD NEWS BEARS' });
+ } else {
+ return {};
+ }
+ },
+ })
+ );
- assert.equal(
- this.$('#toplevel-error').text(),
- 'TOPLEVEL ERROR: BAD NEWS BEARS',
- 'toplevel error rendered'
- );
+ this.addTemplate(
+ 'application_error',
+ `TOPLEVEL ERROR: {{@model.msg}}
`
+ );
- reject = false;
+ await this.visit('/');
- await this.visit('/');
+ assert.equal(
+ this.$('#toplevel-error').text(),
+ 'TOPLEVEL ERROR: BAD NEWS BEARS',
+ 'toplevel error rendered'
+ );
- assert.equal(this.$('#index').text(), 'INDEX', 'the index route resolved');
- }
+ reject = false;
+
+ await this.visit('/');
+
+ assert.equal(this.$('#index').text(), 'INDEX', 'the index route resolved');
}
-);
+}
-moduleFor(
- 'Loading/Error Substates - nested routes',
- class extends ApplicationTestCase {
- constructor() {
- super(...arguments);
-
- counter = 1;
-
- this.addTemplate('application', `{{outlet}}
`);
- this.addTemplate('index', 'INDEX');
- this.addTemplate('grandma', 'GRANDMA {{outlet}}');
- this.addTemplate('mom', 'MOM');
-
- this.router.map(function () {
- this.route('grandma', function () {
- this.route('mom', { resetNamespace: true }, function () {
- this.route('sally');
- this.route('this-route-throws');
- });
- this.route('puppies');
- });
- this.route('memere', { path: '/memere/:seg' }, function () {});
- });
- }
+class LoadingErrorSubstatesNestedTests extends ApplicationTestCase {
+ constructor() {
+ super(...arguments);
- getController(name) {
- return this.applicationInstance.lookup(`controller:${name}`);
- }
+ counter = 1;
- get currentPath() {
- let currentPath;
- expectDeprecation(() => {
- currentPath = this.getController('application').get('currentPath');
- }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
- return currentPath;
- }
+ this.addTemplate('application', `{{outlet}}
`);
+ this.addTemplate('index', 'INDEX');
+ this.addTemplate('grandma', 'GRANDMA {{outlet}}');
+ this.addTemplate('mom', 'MOM');
- async ['@test ApplicationRoute#currentPath reflects loading state path'](assert) {
- await this.visit('/');
+ this.router.map(function () {
+ this.route('grandma', function () {
+ this.route('mom', { resetNamespace: true }, function () {
+ this.route('sally');
+ this.route('this-route-throws');
+ });
+ this.route('puppies');
+ });
+ this.route('memere', { path: '/memere/:seg' }, function () {});
+ });
+ }
- let momDeferred = RSVP.defer();
+ getController(name) {
+ return this.applicationInstance.lookup(`controller:${name}`);
+ }
- this.addTemplate('grandma.loading', 'GRANDMALOADING');
+ get currentPath() {
+ let currentPath;
+ expectDeprecation(() => {
+ currentPath = this.getController('application').get('currentPath');
+ }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
+ return currentPath;
+ }
- this.add(
- 'route:mom',
- Route.extend({
- model() {
- return momDeferred.promise;
- },
- })
- );
+ async ['@test ApplicationRoute#currentPath reflects loading state path'](assert) {
+ await this.visit('/');
- let promise = runTask(() => this.visit('/grandma/mom')).then(() => {
- text = this.$('#app').text();
+ let momDeferred = RSVP.defer();
- assert.equal(text, 'GRANDMA MOM', `Grandma.mom loaded text is displayed`);
- assert.equal(this.currentPath, 'grandma.mom.index', `currentPath reflects final state`);
- });
- let text = this.$('#app').text();
+ this.addTemplate('grandma.loading', 'GRANDMALOADING');
- assert.equal(text, 'GRANDMA GRANDMALOADING', `Grandma.mom loading text displayed`);
+ this.add(
+ 'route:mom',
+ Route.extend({
+ model() {
+ return momDeferred.promise;
+ },
+ })
+ );
- assert.equal(this.currentPath, 'grandma.loading', `currentPath reflects loading state`);
+ let promise = runTask(() => this.visit('/grandma/mom')).then(() => {
+ text = this.$('#app').text();
- momDeferred.resolve();
+ assert.equal(text, 'GRANDMA MOM', `Grandma.mom loaded text is displayed`);
+ assert.equal(this.currentPath, 'grandma.mom.index', `currentPath reflects final state`);
+ });
+ let text = this.$('#app').text();
- return promise;
- }
+ assert.equal(text, 'GRANDMA GRANDMALOADING', `Grandma.mom loading text displayed`);
- async [`@test Loading actions bubble to root but don't enter substates above pivot `](assert) {
- await this.visit('/');
+ assert.equal(this.currentPath, 'grandma.loading', `currentPath reflects loading state`);
- let sallyDeferred = RSVP.defer();
- let puppiesDeferred = RSVP.defer();
+ momDeferred.resolve();
- this.add(
- 'route:application',
- Route.extend({
- actions: {
- loading() {
- assert.ok(true, 'loading action received on ApplicationRoute');
- },
- },
- })
- );
+ return promise;
+ }
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- return sallyDeferred.promise;
- },
- })
- );
+ async [`@test Loading actions bubble to root but don't enter substates above pivot `](assert) {
+ await this.visit('/');
+
+ let sallyDeferred = RSVP.defer();
+ let puppiesDeferred = RSVP.defer();
- this.add(
- 'route:grandma.puppies',
- Route.extend({
- model() {
- return puppiesDeferred.promise;
+ this.add(
+ 'route:application',
+ Route.extend({
+ actions: {
+ loading() {
+ assert.ok(true, 'loading action received on ApplicationRoute');
},
- })
- );
+ },
+ })
+ );
+
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ return sallyDeferred.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'route:grandma.puppies',
+ Route.extend({
+ model() {
+ return puppiesDeferred.promise;
+ },
+ })
+ );
- let promise = this.visit('/grandma/mom/sally');
- assert.equal(this.currentPath, 'index', 'Initial route fully loaded');
+ let promise = this.visit('/grandma/mom/sally');
+ assert.equal(this.currentPath, 'index', 'Initial route fully loaded');
- sallyDeferred.resolve();
+ sallyDeferred.resolve();
- promise
- .then(() => {
- assert.equal(this.currentPath, 'grandma.mom.sally', 'transition completed');
+ promise
+ .then(() => {
+ assert.equal(this.currentPath, 'grandma.mom.sally', 'transition completed');
- let visit = this.visit('/grandma/puppies');
- assert.equal(
- this.currentPath,
- 'grandma.mom.sally',
- 'still in initial state because the only loading state is above the pivot route'
- );
+ let visit = this.visit('/grandma/puppies');
+ assert.equal(
+ this.currentPath,
+ 'grandma.mom.sally',
+ 'still in initial state because the only loading state is above the pivot route'
+ );
- return visit;
- })
- .then(() => {
- runTask(() => puppiesDeferred.resolve());
+ return visit;
+ })
+ .then(() => {
+ runTask(() => puppiesDeferred.resolve());
- assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
- });
+ assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
+ });
- return promise;
- }
+ return promise;
+ }
- async ['@test Default error event moves into nested route'](assert) {
- await this.visit('/');
+ async ['@test Default error event moves into nested route'](assert) {
+ await this.visit('/');
- this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
+ this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error() {
- step(assert, 2, 'MomSallyRoute#actions.error');
- return true;
- },
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error() {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ return true;
},
- })
- );
+ },
+ })
+ );
- await this.visit('/grandma/mom/sally');
+ await this.visit('/grandma/mom/sally');
- step(assert, 3, 'App finished loading');
+ step(assert, 3, 'App finished loading');
- assert.equal(this.$('#app').text(), 'GRANDMA ERROR: did it broke?', 'error bubbles');
- assert.equal(this.currentPath, 'grandma.error', 'Initial route fully loaded');
- }
+ assert.equal(this.$('#app').text(), 'GRANDMA ERROR: did it broke?', 'error bubbles');
+ assert.equal(this.currentPath, 'grandma.error', 'Initial route fully loaded');
+ }
- async [`@test Non-bubbled errors that re-throw aren't swallowed`](assert) {
- await this.visit('/');
+ async [`@test Non-bubbled errors that re-throw aren't swallowed`](assert) {
+ await this.visit('/');
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error(err) {
- // returns undefined which is falsey
- throw err;
- },
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error(err) {
+ // returns undefined which is falsey
+ throw err;
},
- })
- );
-
- await assert.rejects(
- this.visit('/grandma/mom/sally'),
- function (err) {
- return err.msg === 'did it broke?';
},
- 'it broke'
- );
- }
+ })
+ );
+
+ await assert.rejects(
+ this.visit('/grandma/mom/sally'),
+ function (err) {
+ return err.msg === 'did it broke?';
+ },
+ 'it broke'
+ );
+ }
- async [`@test Handled errors that re-throw aren't swallowed`](assert) {
- await this.visit('/');
+ async [`@test Handled errors that re-throw aren't swallowed`](assert) {
+ await this.visit('/');
- let handledError;
+ let handledError;
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error(err) {
- step(assert, 2, 'MomSallyRoute#actions.error');
- handledError = err;
- expectDeprecation(() => {
- this.transitionTo('mom.this-route-throws');
- }, /Calling transitionTo on a route is deprecated/);
-
- return false;
- },
- },
- })
- );
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error(err) {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ handledError = err;
+ expectDeprecation(() => {
+ this.transitionTo('mom.this-route-throws');
+ }, /Calling transitionTo on a route is deprecated/);
- this.add(
- 'route:mom.this-route-throws',
- Route.extend({
- model() {
- step(assert, 3, 'MomThisRouteThrows#model');
- throw handledError;
+ return false;
},
- })
- );
-
- await assert.rejects(
- this.visit('/grandma/mom/sally'),
- function (err) {
- return err.msg === 'did it broke?';
},
- `it broke`
- );
- }
+ })
+ );
+
+ this.add(
+ 'route:mom.this-route-throws',
+ Route.extend({
+ model() {
+ step(assert, 3, 'MomThisRouteThrows#model');
+ throw handledError;
+ },
+ })
+ );
+
+ await assert.rejects(
+ this.visit('/grandma/mom/sally'),
+ function (err) {
+ return err.msg === 'did it broke?';
+ },
+ `it broke`
+ );
+ }
- async ['@test errors that are bubbled are thrown at a higher level if not handled'](assert) {
- await this.visit('/');
-
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error() {
- step(assert, 2, 'MomSallyRoute#actions.error');
- return true;
- },
- },
- })
- );
+ async ['@test errors that are bubbled are thrown at a higher level if not handled'](assert) {
+ await this.visit('/');
- await assert.rejects(
- this.visit('/grandma/mom/sally'),
- function (err) {
- return err.msg == 'did it broke?';
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
},
- 'Correct error was thrown'
- );
- }
+ actions: {
+ error() {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ return true;
+ },
+ },
+ })
+ );
+
+ await assert.rejects(
+ this.visit('/grandma/mom/sally'),
+ function (err) {
+ return err.msg == 'did it broke?';
+ },
+ 'Correct error was thrown'
+ );
+ }
- async [`@test Handled errors that are thrown through rejection aren't swallowed`](assert) {
- await this.visit('/');
+ async [`@test Handled errors that are thrown through rejection aren't swallowed`](assert) {
+ await this.visit('/');
- let handledError;
+ let handledError;
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error(err) {
- step(assert, 2, 'MomSallyRoute#actions.error');
- handledError = err;
- expectDeprecation(() => {
- this.transitionTo('mom.this-route-throws');
- }, /Calling transitionTo on a route is deprecated/);
-
- return false;
- },
- },
- })
- );
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error(err) {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ handledError = err;
+ expectDeprecation(() => {
+ this.transitionTo('mom.this-route-throws');
+ }, /Calling transitionTo on a route is deprecated/);
- this.add(
- 'route:mom.this-route-throws',
- Route.extend({
- model() {
- step(assert, 3, 'MomThisRouteThrows#model');
- return RSVP.reject(handledError);
+ return false;
},
- })
- );
-
- await assert.rejects(
- this.visit('/grandma/mom/sally'),
- function (err) {
- return err.msg === 'did it broke?';
},
- 'it broke'
- );
- }
+ })
+ );
+
+ this.add(
+ 'route:mom.this-route-throws',
+ Route.extend({
+ model() {
+ step(assert, 3, 'MomThisRouteThrows#model');
+ return RSVP.reject(handledError);
+ },
+ })
+ );
+
+ await assert.rejects(
+ this.visit('/grandma/mom/sally'),
+ function (err) {
+ return err.msg === 'did it broke?';
+ },
+ 'it broke'
+ );
+ }
- async ['@test Default error events move into nested route, prioritizing more specifically named error routes - NEW'](
- assert
- ) {
- await this.visit('/');
-
- this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
- this.addTemplate('mom_error', 'MOM ERROR: {{@model.msg}}');
-
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error() {
- step(assert, 2, 'MomSallyRoute#actions.error');
- return true;
- },
+ async ['@test Default error events move into nested route, prioritizing more specifically named error routes - NEW'](
+ assert
+ ) {
+ await this.visit('/');
+
+ this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
+ this.addTemplate('mom_error', 'MOM ERROR: {{@model.msg}}');
+
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error() {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ return true;
},
- })
- );
-
- await this.visit('/grandma/mom/sally');
-
- step(assert, 3, 'Application finished booting');
+ },
+ })
+ );
- assert.equal(
- this.$('#app').text(),
- 'GRANDMA MOM ERROR: did it broke?',
- 'the more specifically named mome error substate was entered over the other error route'
- );
+ await this.visit('/grandma/mom/sally');
- assert.equal(this.currentPath, 'grandma.mom_error', 'Initial route fully loaded');
- }
+ step(assert, 3, 'Application finished booting');
- async ['@test Slow promises waterfall on startup'](assert) {
- await this.visit('/');
+ assert.equal(
+ this.$('#app').text(),
+ 'GRANDMA MOM ERROR: did it broke?',
+ 'the more specifically named mome error substate was entered over the other error route'
+ );
- let grandmaDeferred = RSVP.defer();
- let sallyDeferred = RSVP.defer();
+ assert.equal(this.currentPath, 'grandma.mom_error', 'Initial route fully loaded');
+ }
- this.addTemplate('loading', 'LOADING');
- this.addTemplate('mom', 'MOM {{outlet}}');
- this.addTemplate('mom.loading', 'MOMLOADING');
- this.addTemplate('mom.sally', 'SALLY');
+ async ['@test Slow promises waterfall on startup'](assert) {
+ await this.visit('/');
- this.add(
- 'route:grandma',
- Route.extend({
- model() {
- step(assert, 1, 'GrandmaRoute#model');
- return grandmaDeferred.promise;
- },
- })
- );
+ let grandmaDeferred = RSVP.defer();
+ let sallyDeferred = RSVP.defer();
- this.add(
- 'route:mom',
- Route.extend({
- model() {
- step(assert, 2, 'MomRoute#model');
- return {};
- },
- })
- );
+ this.addTemplate('loading', 'LOADING');
+ this.addTemplate('mom', 'MOM {{outlet}}');
+ this.addTemplate('mom.loading', 'MOMLOADING');
+ this.addTemplate('mom.sally', 'SALLY');
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 3, 'SallyRoute#model');
- return sallyDeferred.promise;
- },
- setupController() {
- step(assert, 4, 'SallyRoute#setupController');
- },
- })
- );
+ this.add(
+ 'route:grandma',
+ Route.extend({
+ model() {
+ step(assert, 1, 'GrandmaRoute#model');
+ return grandmaDeferred.promise;
+ },
+ })
+ );
+
+ this.add(
+ 'route:mom',
+ Route.extend({
+ model() {
+ step(assert, 2, 'MomRoute#model');
+ return {};
+ },
+ })
+ );
+
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 3, 'SallyRoute#model');
+ return sallyDeferred.promise;
+ },
+ setupController() {
+ step(assert, 4, 'SallyRoute#setupController');
+ },
+ })
+ );
- let promise = runTask(() => this.visit('/grandma/mom/sally')).then(() => {
- text = this.$('#app').text();
+ let promise = runTask(() => this.visit('/grandma/mom/sally')).then(() => {
+ text = this.$('#app').text();
- assert.equal(text, 'GRANDMA MOM SALLY', `Sally template displayed`);
- });
- let text = this.$('#app').text();
+ assert.equal(text, 'GRANDMA MOM SALLY', `Sally template displayed`);
+ });
+ let text = this.$('#app').text();
- assert.equal(
- text,
- 'LOADING',
- `The loading template is nested in application template's outlet`
- );
+ assert.equal(
+ text,
+ 'LOADING',
+ `The loading template is nested in application template's outlet`
+ );
- runTask(() => grandmaDeferred.resolve());
- text = this.$('#app').text();
+ runTask(() => grandmaDeferred.resolve());
+ text = this.$('#app').text();
- assert.equal(
- text,
- 'GRANDMA MOM MOMLOADING',
- `Mom's child loading route is displayed due to sally's slow promise`
- );
+ assert.equal(
+ text,
+ 'GRANDMA MOM MOMLOADING',
+ `Mom's child loading route is displayed due to sally's slow promise`
+ );
- sallyDeferred.resolve();
+ sallyDeferred.resolve();
- return promise;
- }
+ return promise;
+ }
- async ['@test Enter child loading state of pivot route'](assert) {
- await this.visit('/');
+ async ['@test Enter child loading state of pivot route'](assert) {
+ await this.visit('/');
- let deferred = RSVP.defer();
- this.addTemplate('grandma.loading', 'GMONEYLOADING');
+ let deferred = RSVP.defer();
+ this.addTemplate('grandma.loading', 'GMONEYLOADING');
- this.add(
- 'route:mom.sally',
- Route.extend({
- setupController() {
- step(assert, 1, 'SallyRoute#setupController');
- },
- })
- );
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ setupController() {
+ step(assert, 1, 'SallyRoute#setupController');
+ },
+ })
+ );
+
+ this.add(
+ 'route:grandma.puppies',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
- this.add(
- 'route:grandma.puppies',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
- );
+ await this.visit('/grandma/mom/sally');
+ assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded');
- await this.visit('/grandma/mom/sally');
- assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded');
+ let promise = runTask(() => this.visit('/grandma/puppies')).then(() => {
+ assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
+ });
- let promise = runTask(() => this.visit('/grandma/puppies')).then(() => {
- assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
- });
+ assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`);
+ deferred.resolve();
- assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`);
- deferred.resolve();
+ return promise;
+ }
- return promise;
- }
+ async [`@test Error events that aren't bubbled don't throw application assertions`](assert) {
+ await this.visit('/');
- async [`@test Error events that aren't bubbled don't throw application assertions`](assert) {
- await this.visit('/');
-
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
- },
- actions: {
- error(err) {
- step(assert, 2, 'MomSallyRoute#actions.error');
- assert.equal(err.msg, 'did it broke?', `it didn't break`);
- return false;
- },
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error(err) {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ assert.equal(err.msg, 'did it broke?', `it didn't break`);
+ return false;
},
- })
- );
+ },
+ })
+ );
- return this.visit('/grandma/mom/sally');
- }
+ return this.visit('/grandma/mom/sally');
+ }
- ['@test Handled errors that bubble can be handled at a higher level'](assert) {
- let handledError;
-
- this.add(
- 'route:mom',
- Route.extend({
- actions: {
- error(err) {
- step(assert, 3, 'MomRoute#actions.error');
- assert.equal(
- err,
- handledError,
- `error handled and rebubbled is handleable at higher route`
- );
- },
+ ['@test Handled errors that bubble can be handled at a higher level'](assert) {
+ let handledError;
+
+ this.add(
+ 'route:mom',
+ Route.extend({
+ actions: {
+ error(err) {
+ step(assert, 3, 'MomRoute#actions.error');
+ assert.equal(
+ err,
+ handledError,
+ `error handled and rebubbled is handleable at higher route`
+ );
},
- })
- );
+ },
+ })
+ );
+
+ this.add(
+ 'route:mom.sally',
+ Route.extend({
+ model() {
+ step(assert, 1, 'MomSallyRoute#model');
+ return RSVP.reject({
+ msg: 'did it broke?',
+ });
+ },
+ actions: {
+ error(err) {
+ step(assert, 2, 'MomSallyRoute#actions.error');
+ handledError = err;
- this.add(
- 'route:mom.sally',
- Route.extend({
- model() {
- step(assert, 1, 'MomSallyRoute#model');
- return RSVP.reject({
- msg: 'did it broke?',
- });
+ return true;
},
- actions: {
- error(err) {
- step(assert, 2, 'MomSallyRoute#actions.error');
- handledError = err;
+ },
+ })
+ );
- return true;
- },
- },
- })
- );
+ return this.visit('/grandma/mom/sally');
+ }
- return this.visit('/grandma/mom/sally');
- }
+ async ['@test Setting a query param during a slow transition should work'](assert) {
+ await this.visit('/');
- async ['@test Setting a query param during a slow transition should work'](assert) {
- await this.visit('/');
+ let deferred = RSVP.defer();
+ this.addTemplate('memere.loading', 'MMONEYLOADING');
- let deferred = RSVP.defer();
- this.addTemplate('memere.loading', 'MMONEYLOADING');
+ this.add(
+ 'route:grandma',
+ Route.extend({
+ beforeModel: function () {
+ expectDeprecation(() => {
+ this.transitionTo('memere', 1);
+ }, /Calling transitionTo on a route is deprecated/);
+ },
+ })
+ );
+
+ this.add(
+ 'route:memere',
+ Route.extend({
+ queryParams: {
+ test: { defaultValue: 1 },
+ },
+ })
+ );
+
+ this.add(
+ 'route:memere.index',
+ Route.extend({
+ model() {
+ return deferred.promise;
+ },
+ })
+ );
- this.add(
- 'route:grandma',
- Route.extend({
- beforeModel: function () {
- expectDeprecation(() => {
- this.transitionTo('memere', 1);
- }, /Calling transitionTo on a route is deprecated/);
- },
- })
- );
+ let promise = runTask(() => this.visit('/grandma')).then(() => {
+ assert.equal(this.currentPath, 'memere.index', 'Transition should be complete');
+ });
+ let memereController = this.getController('memere');
- this.add(
- 'route:memere',
- Route.extend({
- queryParams: {
- test: { defaultValue: 1 },
- },
- })
- );
+ assert.equal(this.currentPath, 'memere.loading', 'Initial route should be loading');
- this.add(
- 'route:memere.index',
- Route.extend({
- model() {
- return deferred.promise;
- },
- })
- );
+ memereController.set('test', 3);
- let promise = runTask(() => this.visit('/grandma')).then(() => {
- assert.equal(this.currentPath, 'memere.index', 'Transition should be complete');
- });
- let memereController = this.getController('memere');
+ assert.equal(this.currentPath, 'memere.loading', 'Initial route should still be loading');
- assert.equal(this.currentPath, 'memere.loading', 'Initial route should be loading');
+ assert.equal(
+ memereController.get('test'),
+ 3,
+ 'Controller query param value should have changed'
+ );
+ deferred.resolve();
- memereController.set('test', 3);
+ return promise;
+ }
+}
- assert.equal(this.currentPath, 'memere.loading', 'Initial route should still be loading');
+moduleFor('Loading/Error Substates', LoadingErrorSubstatesTests);
- assert.equal(
- memereController.get('test'),
- 3,
- 'Controller query param value should have changed'
- );
- deferred.resolve();
+moduleFor('Loading/Error Substates - nested routes', LoadingErrorSubstatesNestedTests);
- return promise;
+moduleFor(
+ 'Loading/Error Substates (simulated within lazy engine)',
+ class extends LoadingErrorSubstatesTests {
+ get routerOptions() {
+ return lazyLoadingRouterOptions;
+ }
+ }
+);
+
+moduleFor(
+ 'Loading/Error Substates - nested routes (simulated within lazy engine)',
+ class extends LoadingErrorSubstatesNestedTests {
+ get routerOptions() {
+ return lazyLoadingRouterOptions;
}
}
);
diff --git a/packages/internal-test-helpers/index.js b/packages/internal-test-helpers/index.js
index bad3852df84..9000da6f269 100644
--- a/packages/internal-test-helpers/index.js
+++ b/packages/internal-test-helpers/index.js
@@ -7,6 +7,7 @@ export { default as moduleFor, setupTestClass } from './lib/module-for';
export { default as strip } from './lib/strip';
export { default as applyMixins } from './lib/apply-mixins';
export { default as getTextOf } from './lib/get-text-of';
+export { default as lazyLoadingRouterOptions } from './lib/lazy-router-options';
export {
expectDeprecation,
expectNoDeprecation,
diff --git a/packages/internal-test-helpers/lib/lazy-router-options.ts b/packages/internal-test-helpers/lib/lazy-router-options.ts
new file mode 100644
index 00000000000..ee5bd0f12af
--- /dev/null
+++ b/packages/internal-test-helpers/lib/lazy-router-options.ts
@@ -0,0 +1,42 @@
+import { Router } from '@ember/-internals/routing';
+import { isDestroying } from '@glimmer/destroyable';
+import RSVP from 'rsvp';
+
+export default class LazyRouter extends Router {
+ location = 'none';
+ setupRouter(): boolean {
+ const result = super.setupRouter();
+ let getRoute = this._routerMicrolib.getRoute;
+ this._enginePromises = Object.create(null);
+ this._resolvedEngines = Object.create(null);
+
+ let routes = new Map();
+ let routePromises = new Map();
+ this._routerMicrolib.getRoute = (name) => {
+ if (routes.has(name)) {
+ return routes.get(name);
+ }
+
+ if (routePromises.has(name)) {
+ return routePromises.get(name);
+ }
+
+ let promise = new RSVP.Promise((resolve) => {
+ setTimeout(() => {
+ if (isDestroying(this)) {
+ return;
+ }
+
+ let route = getRoute(name);
+
+ routes.set(name, route);
+ resolve(route);
+ }, 10);
+ });
+ routePromises.set(name, promise);
+
+ return promise;
+ };
+ return result;
+ }
+}
diff --git a/packages/internal-test-helpers/lib/test-cases/application.js b/packages/internal-test-helpers/lib/test-cases/application.js
index 53ce885d1ce..a19182e26d3 100644
--- a/packages/internal-test-helpers/lib/test-cases/application.js
+++ b/packages/internal-test-helpers/lib/test-cases/application.js
@@ -15,7 +15,10 @@ export default class ApplicationTestCase extends TestResolverApplicationTestCase
this.resolver = this.application.__registry__.resolver;
if (this.resolver) {
- this.resolver.add('router:main', Router.extend(this.routerOptions));
+ let { routerOptions } = this;
+ let RouterSubclass =
+ typeof routerOptions === 'function' ? routerOptions : Router.extend(routerOptions);
+ this.resolver.add('router:main', RouterSubclass);
}
}
diff --git a/packages/internal-test-helpers/lib/test-cases/autoboot-application.js b/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
index a1b09057290..c2cbbf1cbab 100644
--- a/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
+++ b/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
@@ -10,7 +10,10 @@ export default class AutobootApplicationTestCase extends TestResolverApplication
this.resolver = application.__registry__.resolver;
if (this.resolver) {
- this.resolver.add('router:main', Router.extend(this.routerOptions));
+ let { routerOptions } = this;
+ let RouterSubclass =
+ typeof routerOptions === 'function' ? routerOptions : Router.extend(routerOptions);
+ this.resolver.add('router:main', RouterSubclass);
}
return application;