import { RSVP } from 'ember-runtime';
import { Route } from 'ember-routing';
import { moduleFor, ApplicationTestCase } from 'internal-test-helpers';
let counter;
function step(assert, expectedValue, description) {
assert.equal(counter, expectedValue, 'Step ' + expectedValue + ': ' + description);
counter++;
}
moduleFor(
'Loading/Error Substates',
class extends ApplicationTestCase {
constructor() {
super(...arguments);
counter = 1;
this.addTemplate('application', `
{{outlet}}
`);
this.addTemplate('index', 'INDEX');
}
getController(name) {
return this.applicationInstance.lookup(`controller:${name}`);
}
get currentPath() {
return this.getController('application').get('currentPath');
}
['@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;
}
[`@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 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;
});
}
['@test Enter loading route only if loadingRoute is defined'](assert) {
let deferred = RSVP.defer();
this.router.map(function() {
this.route('dummy');
});
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 this.visit('/').then(() => {
let promise = this.visit('/dummy').then(() => {
let text = this.$('#app').text();
assert.equal(text, 'DUMMY', `dummy template has been rendered`);
});
assert.equal(this.currentPath, 'loading', `loading state entered`);
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 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;
},
})
);
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;
}
['@test Prioritized substate entry works with preserved-namespace nested routes'](assert) {
let deferred = RSVP.defer();
this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
this.addTemplate('foo.bar.index', 'YAY');
this.router.map(function() {
this.route('foo', function() {
this.route('bar', { path: '/bar' }, function() {});
});
});
this.add(
'route:foo.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.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)`
);
deferred.resolve();
return promise;
});
}
['@test Prioritized substate entry works with reset-namespace nested routes'](assert) {
let deferred = RSVP.defer();
this.addTemplate('bar_loading', 'BAR LOADING');
this.addTemplate('bar.index', 'YAY');
this.router.map(function() {
this.route('foo', function() {
this.route('bar', { path: '/bar', resetNamespace: true }, function() {});
});
});
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', '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;
});
}
['@test Prioritized loading substate entry works with preserved-namespace nested routes'](
assert
) {
let deferred = RSVP.defer();
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 deferred.promise;
},
})
);
let promise = this.visit('/foo/bar').then(() => {
text = this.$('#app').text();
assert.equal(text, 'YAY', 'foo.bar has rendered');
});
let text = this.$('#app').text();
assert.equal(
text,
'FOOBAR LOADING',
`foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
);
deferred.resolve();
return promise;
}
['@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 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;
}
['@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}}');
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;
},
})
);
return this.visit('/').then(() => {
return this.visit('/foo').then(() => {
let text = this.$('#app').text();
assert.equal(text, 'FOO ERROR: did it broke?', 'foo.index_error was entered');
});
});
}
['@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 {};
}
},
})
);
this.addTemplate(
'application_error',
`
TOPLEVEL ERROR: {{model.msg}}
`
);
return this.visit('/')
.then(() => {
let text = this.$('#toplevel-error').text();
assert.equal(text, 'TOPLEVEL ERROR: BAD NEWS BEARS', 'toplevel error rendered');
reject = false;
})
.then(() => {
return this.visit('/');
})
.then(() => {
let text = this.$('#index').text();
assert.equal(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() {});
});
this.visit('/');
}
getController(name) {
return this.applicationInstance.lookup(`controller:${name}`);
}
get currentPath() {
return this.getController('application').get('currentPath');
}
['@test ApplicationRoute#currentPath reflects loading state path'](assert) {
let momDeferred = RSVP.defer();
this.addTemplate('grandma.loading', 'GRANDMALOADING');
this.add(
'route:mom',
Route.extend({
model() {
return momDeferred.promise;
},
})
);
let promise = 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 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;
}
['@test Default error event moves into nested route'](assert) {
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');
});
}
[`@test Non-bubbled errors that re-throw aren't swallowed`](assert) {
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;
},
},
})
);
assert.throws(
() => {
this.visit('/grandma/mom/sally');
},
function(err) {
return err.msg === 'did it broke?';
},
'it broke'
);
return this.runLoopSettled();
}
[`@test Handled errors that re-throw 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');
throw handledError;
},
})
);
assert.throws(
() => {
this.visit('/grandma/mom/sally');
},
function(err) {
return err.msg === 'did it broke?';
},
`it broke`
);
return this.runLoopSettled();
}
['@test errors that are bubbled are thrown at a higher level if not handled'](assert) {
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;
},
},
})
);
assert.throws(
() => {
this.visit('/grandma/mom/sally');
},
function(err) {
return err.msg == 'did it broke?';
},
'Correct error was thrown'
);
return this.runLoopSettled();
}
[`@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 Default error events move into nested route, prioritizing more specifically named error routes - NEW'](
assert
) {
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;
},
},
})
);
return this.visit('/grandma/mom/sally').then(() => {
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'
);
assert.equal(this.currentPath, 'grandma.mom_error', 'Initial route fully loaded');
});
}
['@test Slow promises waterfall on startup'](assert) {
let grandmaDeferred = RSVP.defer();
let sallyDeferred = RSVP.defer();
this.addTemplate('loading', 'LOADING');
this.addTemplate('mom', 'MOM {{outlet}}');
this.addTemplate('mom.loading', 'MOMLOADING');
this.addTemplate('mom.sally', 'SALLY');
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 = 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,
'LOADING',
`The loading template is nested in application template's outlet`
);
this.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`
);
sallyDeferred.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 Error events that aren't bubbled don't throw application assertions`](assert) {
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');
}
['@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 Setting a query param during a slow transition should work'](assert) {
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 = 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;
}
}
);