From dfd38c8bd79d98bc3cfb9b7fba9a414f5bfb6fe4 Mon Sep 17 00:00:00 2001 From: sma01 Date: Fri, 5 Mar 2021 14:18:30 +0100 Subject: [PATCH 1/2] make model loading test compatible with async loading --- .../ember/tests/routing/model_loading_test.js | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/ember/tests/routing/model_loading_test.js b/packages/ember/tests/routing/model_loading_test.js index 3c614a5b9bb..c53d876f544 100644 --- a/packages/ember/tests/routing/model_loading_test.js +++ b/packages/ember/tests/routing/model_loading_test.js @@ -2,7 +2,7 @@ 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 } from 'internal-test-helpers'; import { run } from '@ember/runloop'; import { computed, set } from '@ember/-internals/metal'; import { isDestroying } from '@glimmer/destroyable'; @@ -502,8 +502,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 +516,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 +752,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 +774,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 +795,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 +812,7 @@ class LoadingTests extends ApplicationTestCase { editedPostIds.push(postId); return null; }, + setup() { this._super(...arguments); editCount++; @@ -822,15 +820,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 +942,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); @@ -968,6 +977,12 @@ moduleFor( return routePromises.get(name); } + // if (name.indexOf('loading')) { + // let route = getRoute(name); + // routes.set(name, route); + // return route; + // } + let promise = new RSVP.Promise((resolve) => { setTimeout(() => { if (isDestroying(this)) { From 0fba94d5194a581e14036a6f7dbbb44a165a4918 Mon Sep 17 00:00:00 2001 From: sly7-7 Date: Wed, 10 Mar 2021 16:38:21 +0100 Subject: [PATCH 2/2] add tests with async loaded substates. WIP fixing #19266 --- .../-internals/routing/lib/system/router.ts | 22 +- .../ember/tests/routing/model_loading_test.js | 54 +- .../ember/tests/routing/substates_test.js | 2080 +++++++++-------- packages/internal-test-helpers/index.js | 1 + .../lib/lazy-router-options.ts | 42 + .../lib/test-cases/application.js | 5 +- .../lib/test-cases/autoboot-application.js | 5 +- 7 files changed, 1121 insertions(+), 1088 deletions(-) create mode 100644 packages/internal-test-helpers/lib/lazy-router-options.ts 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 c53d876f544..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, runLoopSettled } 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; @@ -958,49 +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); - } - - // if (name.indexOf('loading')) { - // let route = getRoute(name); - // routes.set(name, route); - // return route; - // } - - 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;