[`@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, 'INDEX', `index template has been rendered`); }); if (this.element) { assert.equal(this.element.textContent, ''); } appDeferred.resolve(); return promise; }
['@test Enter child loading state of pivot route'](assert) { 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:grandma.puppies', Route.extend({ model() { return deferred.promise; }, }) ); return this.visit('/grandma/mom/sally').then(() => { assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded'); let promise = 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(); 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; }, }) ); let loadingRouteEntered = false; this.add( 'route:application_loading', Route.extend({ setupController() { loadingRouteEntered = true; }, }) ); let promise = this.visit('/').then(() => { assert.equal(this.$('#app').text(), 'INDEX', 'index route loaded'); }); assert.ok(loadingRouteEntered, 'ApplicationLoadingRoute was entered'); appDeferred.resolve(); return promise; }
[`@test Loading actions bubble to root but don't enter substates above pivot `](assert) { let sallyDeferred = RSVP.defer(); let puppiesDeferred = RSVP.defer(); 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'); sallyDeferred.resolve(); 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' ); return visit; }) .then(() => { this.runTask(() => puppiesDeferred.resolve()); assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition'); }); return promise; }
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'); this.add( 'route:grandma', Route.extend({ beforeModel: function() { this.transitionTo('memere', 1); }, }) ); this.add( 'route:memere', Route.extend({ queryParams: { test: { defaultValue: 1 }, }, }) ); this.add( 'route:memere.index', Route.extend({ model() { return deferred.promise; }, }) ); 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 be loading'); memereController.set('test', 3); assert.equal(this.currentPath, 'memere.loading', 'Initial route should still be loading'); assert.equal( memereController.get('test'), 3, 'Controller query param value should have changed' ); deferred.resolve(); return promise; }
constructor() { super(); this.aboutDefer = RSVP.defer(); this.otherDefer = RSVP.defer(); this.newsDefer = RSVP.defer(); let _this = this; this.router.map(function() { this.route('about'); this.route('other'); this.route('news'); }); this.add( 'route:about', Route.extend({ model() { return _this.aboutDefer.promise; }, }) ); this.add( 'route:other', Route.extend({ model() { return _this.otherDefer.promise; }, }) ); this.add( 'route:news', Route.extend({ model() { return _this.newsDefer.promise; }, }) ); this.addTemplate( 'application', ` {{outlet}} {{link-to 'Index' 'index' id='index-link'}} {{link-to 'About' 'about' id='about-link'}} {{link-to 'Other' 'other' id='other-link'}} {{link-to 'News' 'news' activeClass=false id='news-link'}} ` ); }
['@test it should produce a stable DOM when two routes render the same template']() { this.router.map(function() { this.route('a'); this.route('b'); }); this.add( 'route:a', Route.extend({ model() { return 'A'; }, renderTemplate(controller, model) { this.render('common', { controller: 'common', model }); }, }) ); this.add( 'route:b', Route.extend({ model() { return 'B'; }, renderTemplate(controller, model) { this.render('common', { controller: 'common', model }); }, }) ); this.add( 'controller:common', Controller.extend({ prefix: 'common', }) ); this.addTemplate('common', '{{prefix}} {{model}}'); return this.visit('/a') .then(() => { this.assertInnerHTML('common A'); this.takeSnapshot(); return this.visit('/b'); }) .then(() => { this.assertInnerHTML('common B'); this.assertInvariants(); }); }
['@test Prioritized error substate entry works with preserved-namespaec 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?', }); }, }) ); return this.visit('/').then(() => { return this.visit('/foo/bar').then(() => { let text = this.$('#app').text(); assert.equal( text, 'FOOBAR ERROR: did it broke?', `foo.bar_error was entered (as opposed to something like foo/foo/bar_error)` ); }); }); }
['@test Slow promises returned from ApplicationRoute#model enter application_loading if template present']( assert ) { let appDeferred = RSVP.defer(); this.addTemplate( 'application_loading', ` <div id="toplevel-loading">TOPLEVEL LOADING</div> ` ); this.add( 'route:application', Route.extend({ model() { return appDeferred.promise; }, }) ); let promise = this.visit('/').then(() => { let length = this.$('#toplevel-loading').length; text = this.$('#app').text(); 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(); assert.equal(text, 'TOPLEVEL LOADING', 'still loading the top level'); appDeferred.resolve(); return promise; }
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; }, }, }) ); await assert.rejects( this.visit('/grandma/mom/sally'), function(err) { return err.msg == 'did it broke?'; }, 'Correct error was thrown' ); }
[`@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'); return this.visit('/').then(() => { let promise = this.visit('/dummy').then(() => { let text = this.$('#app').text(); assert.equal(text, 'DUMMY', `dummy template has been rendered`); }); assert.ok( this.currentPath !== 'loading', ` loading state not entered ` ); deferred.resolve(); return promise; }); }
async ['@test Default error event moves into nested route'](assert) { await this.visit('/'); 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; }, }, }) ); return this.visit('/grandma/mom/sally').then(() => { step(assert, 3, 'App finished loading'); let text = this.$('#app').text(); assert.equal(text, 'GRANDMA ERROR: did it broke?', 'error bubbles'); assert.equal(this.currentPath, 'grandma.error', 'Initial route fully loaded'); }); }
this.runTask(() => { this.createApplication(); this.addTemplate( 'people', ` <div> {{#each model as |person|}} <div class="name">{{person.firstName}}</div> {{/each}} </div> ` ); this.router.map(function() { this.route('people', { path: '/' }); }); this.add( 'route:people', Route.extend({ model: () => this.modelContent, }) ); this.application.setupForTesting(); });
['@test it should produce a stable DOM when the model changes']() { this.router.map(function() { this.route('color', { path: '/colors/:color' }); }); this.add( 'route:color', Route.extend({ model(params) { return params.color; }, }) ); this.addTemplate('color', 'color: {{model}}'); return this.visit('/colors/red') .then(() => { this.assertInnerHTML('color: red'); this.takeSnapshot(); return this.visit('/colors/green'); }) .then(() => { this.assertInnerHTML('color: green'); this.assertInvariants(); }); }
'@test rejects if there is an unhandled error'(assert) { this.addTemplate('parent', 'Parent{{outlet}}'); this.addTemplate('parent.child', 'Child'); this.add( 'route:parent.child', Route.extend({ model() { throw Error('Unhandled'); }, }) ); return this.visit('/') .then(() => { return this.routerService.recognizeAndLoad('/child'); }) .then( () => { assert.ok(false, 'never'); }, err => { assert.equal(err.message, 'Unhandled'); } ); }
'@test rejects if url is not recognized'(assert) { this.addTemplate('parent', 'Parent{{outlet}}'); this.addTemplate('parent.child', 'Child'); this.add( 'route:parent.child', Route.extend({ model() { return { name: 'child', data: ['stuff'] }; }, }) ); return this.visit('/') .then(() => { return this.routerService.recognizeAndLoad('/foo'); }) .then( () => { assert.ok(false, 'never'); }, reason => { assert.equal(reason, 'URL /foo was not recognized'); } ); }
['@test it can access the model provided by the route']() { this.add( 'route:application', Route.extend({ model() { return ['red', 'yellow', 'blue']; }, }) ); this.addTemplate( 'application', strip` <ul> {{#each model as |item|}} <li>{{item}}</li> {{/each}} </ul> ` ); return this.visit('/').then(() => { this.assertInnerHTML(strip` <ul> <li>red</li> <li>yellow</li> <li>blue</li> </ul> `); }); }
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; }, }, }) ); await assert.rejects( this.visit('/grandma/mom/sally'), function(err) { return err.msg === 'did it broke?'; }, 'it broke' ); }
function registerRoute(application, name, callback) { let route = EmberRoute.extend({ activate: callback, }); application.register('route:' + name, route); }
async ['@test ApplicationRoute#currentPath reflects loading state path'](assert) { await this.visit('/'); let momDeferred = RSVP.defer(); this.addTemplate('grandma.loading', 'GRANDMALOADING'); this.add( 'route:mom', Route.extend({ model() { return momDeferred.promise; }, }) ); let promise = runTask(() => this.visit('/grandma/mom')).then(() => { text = this.$('#app').text(); 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(); assert.equal(text, 'GRANDMA GRANDMALOADING', `Grandma.mom loading text displayed`); assert.equal(this.currentPath, 'grandma.loading', `currentPath reflects loading state`); momDeferred.resolve(); return promise; }
['@test it emits a useful backtracking re-render assertion message']() { this.router.map(function() { this.route('routeWithError'); }); this.add( 'route:routeWithError', Route.extend({ model() { return { name: 'Alex' }; }, }) ); this.addTemplate('routeWithError', 'Hi {{model.name}} {{x-foo person=model}}'); this.addComponent('x-foo', { ComponentClass: Component.extend({ init() { this._super(...arguments); this.set('person.name', 'Ben'); }, }), template: 'Hi {{person.name}} from component', }); let expectedBacktrackingMessage = /modified "model\.name" twice on \[object Object\] in a single render\. It was rendered in "template:my-app\/templates\/routeWithError.hbs" and modified in "component:x-foo"/; return this.visit('/').then(() => { expectAssertion(() => { this.visit('/routeWithError'); }, expectedBacktrackingMessage); }); }
['@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.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'); 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; }
constructor() { super(); this.aboutDefer = RSVP.defer(); this.otherDefer = RSVP.defer(); let _this = this; this.router.map(function() { this.route('parent-route', function() { this.route('about'); this.route('other'); }); }); this.add( 'route:parent-route.about', Route.extend({ model() { return _this.aboutDefer.promise; }, }) ); this.add( 'route:parent-route.other', Route.extend({ model() { return _this.otherDefer.promise; }, }) ); this.addTemplate( 'application', ` {{outlet}} {{#link-to 'index' tagName='li'}} {{link-to 'Index' 'index' id='index-link'}} {{/link-to}} {{#link-to 'parent-route.about' tagName='li'}} {{link-to 'About' 'parent-route.about' id='about-link'}} {{/link-to}} {{#link-to 'parent-route.other' tagName='li'}} {{link-to 'Other' 'parent-route.other' id='other-link'}} {{/link-to}} ` ); }
[`@test Handled errors that are thrown through rejection aren't swallowed`](assert) { 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; this.transitionTo('mom.this-route-throws'); return false; }, }, }) ); this.add( 'route:mom.this-route-throws', Route.extend({ model() { step(assert, 3, 'MomThisRouteThrows#model'); return RSVP.reject(handledError); }, }) ); assert.throws( () => { this.visit('/grandma/mom/sally'); }, function(err) { return err.msg === 'did it broke?'; }, 'it broke' ); return this.runLoopSettled(); }
[`@test visit() rejects if an error occurred during a transition`](assert) { this.router.map(function() { this.route('a'); this.route('b', { path: '/b/:b' }); this.route('c', { path: '/c/:c' }); }); this.add( 'route:a', Route.extend({ afterModel() { this.replaceWith('b', 'zomg'); }, }) ); this.add( 'route:b', Route.extend({ afterModel(params) { this.transitionTo('c', params.b); }, }) ); this.add( 'route:c', Route.extend({ afterModel() { throw new Error('transition failure'); }, }) ); expectAsyncError(); return this.visit('/a').then( () => { assert.ok(false, 'It should not resolve the promise'); }, error => { assert.ok(error instanceof Error, 'It should reject the promise with the boot error'); assert.equal(error.message, 'transition failure'); } ); }
'@test returns a RouteInfoWithAttributes for recognized URL'(assert) { this.add( 'route:dynamicWithChild', Route.extend({ model(params) { return { name: 'dynamicWithChild', data: params.dynamic_id }; }, }) ); this.add( 'route:dynamicWithChild.child', Route.extend({ model(params) { return { name: 'dynamicWithChild.child', data: params.child_id }; }, }) ); return this.visit('/') .then(() => { return this.routerService.recognizeAndLoad('/dynamic-with-child/123/1?a=b'); }) .then(routeInfoWithAttributes => { assert.ok(routeInfoWithAttributes); let { name, localName, parent, attributes, paramNames, params, queryParams, } = routeInfoWithAttributes; assert.equal(name, 'dynamicWithChild.child'); assert.equal(localName, 'child'); assert.equal(parent.name, 'dynamicWithChild'); assert.deepEqual(params, { child_id: '1' }); assert.deepEqual(queryParams, { a: 'b' }); assert.deepEqual(paramNames, ['child_id']); assert.deepEqual(attributes, { name: 'dynamicWithChild.child', data: '1' }); assert.deepEqual(parent.attributes, { name: 'dynamicWithChild', data: '123' }); assert.deepEqual(parent.paramNames, ['dynamic_id']); assert.deepEqual(parent.params, { dynamic_id: '123' }); }); }
async [`@test Handled errors that re-throw aren't swallowed`](assert) { await this.visit('/'); 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; this.transitionTo('mom.this-route-throws'); return false; }, }, }) ); 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` ); }
this.runTask(() => { this.createApplication(); this.router.map(function() { this.route('user', { resetNamespace: true }, function() { this.route('profile'); this.route('edit'); }); }); // Emulate a long-running unscheduled async operation. let resolveLater = () => new RSVP.Promise(resolve => { /* * The wait() helper has a 10ms tick. We should resolve() after * at least one tick to test whether wait() held off while the * async router was still loading. 20ms should be enough. */ later(resolve, { firstName: 'Tom' }, 20); }); this.add( 'route:user', Route.extend({ model() { return resolveLater(); }, }) ); this.add( 'route:user.profile', Route.extend({ beforeModel() { return resolveLater().then(() => this.transitionTo('user.edit')); }, }) ); this.application.setupForTesting(); });
['@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; return true; }, }, }) ); return this.visit('/grandma/mom/sally'); }
['@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'); }); }); this.add( 'route:foo.index', Route.extend({ model() { return deferred.promise; }, }) ); this.add( 'route:foo', Route.extend({ model() { return true; }, }) ); let promise = this.visit('/foo').then(() => { text = this.$('#app').text(); assert.equal(text, 'YAY', 'foo.index was rendered'); }); let text = this.$('#app').text(); assert.equal(text, 'FOO LOADING', 'foo.index_loading was entered'); deferred.resolve(); return promise; }