Ember App Kit and testing model hook - ember.js

In Ember App Kit, there are a number of testing examples that ship with the initial repo. One of those is a basic Route Unit test. This test is trivial, if the data is hard-coded in the model hook, like this:
test("#model", function(){
deepEqual(route.model(), ['red', 'yellow', 'blue']);
});
How do you use the isolated container to test the model hook if it returns a promise from ember-data?
Here's the test:
import Activities from 'appkit/routes/activities';
var route;
module("Unit - ActivitiesRoute", {
setup: function(){
var container = isolatedContainer([
'route:activities'
]);
route = container.lookup('route:activities');
}
});
test("#model", function(){
deepEqual(route.model(), ['activity', 'activity2', 'activity3']);
});
And the actual Route's model hook:
export default Ember.Route.extend({
model: function() {
return this.get('store').find('activity');
}
});
UPDATE:
After implementing the different approaches below from kingpin2k. Here is a summary of the outcomes.
First approach: works great ... yet no promise.
Second approach: returns the promise object (appears to be resolved), but the array, and correct values are assigned to _detail property.
test("#model", function(){
deepEqual(route.model()['_detail'], ['activity', 'activity2', 'activity3']); //passes
});
I'd like for store creation to be taken care of within the module setup().
...
module("Unit - ActivitiesRoute", {
setup: function(){
var container = isolatedContainer([
'route:activities'
]);
route = container.lookup('route:activities');
var store = {
find: function(type){
return new Em.RSVP.Promise(function(resolve){
resolve(['activity', 'activity2', 'activity3']); // or made up model(s) here
});
}
};
route.set('store', store);
}
});
And the test:
test("#model", function(){
deepEqual(route.model(), ['activity', 'activity2', 'activity3']); // ???
});
Third approach:
...
module('Unit - ActivitiesRoute', {
setup: function() {
var container = isolatedContainer([
'route:activities'
]);
route = container.lookup('route:activities');
var store = {
find: function() {
var promise = new Ember.RSVP.Promise(function(resolve) {
Em.run.later(function() {
resolve(Activity.FIXTURES);
}, 10);
});
return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin).create({
promise: promise
});
}
};
route.set('store', store);
}
});
And in the test, calling route.model() returns an empty object {} :
test("#model", function(){
deepEqual(route.model(), Activity.FIXTURES); // returns {}
});
UPDATE #2
It was also necessary to add asyncTest() instead of test() and to also call start() to prevent the test runner from hanging.
asyncTest('#model', function(){
Em.run(function(){
route.model().then(function(result){
ok(result);
equal(result, Activity.FIXTURES);
start();
});
});
});

Simple approach, it's a unit test, so really you aren't testing the store, so setup a mock store and result.
route = container.lookup('route:activities');
var store = {
find: function(type){
equal(type, 'activity', 'type is activity');
return ['activity', 'activity2', 'activity3'];
}
}
route.set('store', store);
Even better you can also replicate the promise
route = container.lookup('route:activities');
var store = {
find: function(type){
equal(type, 'activity', 'type is activity');
return new Em.RSVP.Promise(function(resolve){
resolve(['activity', 'activity2', 'activity3']); // or made up model(s) here
});
}
}
route.set('store', store);
If you want to more closely replicate Ember Data you might use an ArrayProxy implementing the PromiseProxyMixin...
route = container.lookup('route:activities');
var store = {
find: function(type){
equal(type, 'activity', 'type is activity');
var promise = new Ember.RSVP.Promise(function(resolve){
Em.run.later(function(){
resolve(['activity', 'activity2', 'activity3']);
}, 10);
});
return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin).create({
promise: promise
});
}
}
route.set('store', store);
Update
Using your last approach you should implement it like this
test("#model", function(){
route.model().then(function(result){
deepEqual(result, Activity.FIXTURES); // returns {}
});
});
But, there is something tricky here, since it has an async response you'll want to wrap it in an Ember run loop
test("#model", function(){
Em.run(function(){
route.model().then(function(result){
deepEqual(result, Activity.FIXTURES); // returns {}
});
});
});

Related

Ember 2.0 get another router from router/controller

Is there any way to call route action from another router/controller? Let's say I have two routes:
App.RouteOne = Ember.Object.extend({
actions: {
someCommonFunctionality: function() {
// ...
}
}
});
App.RouteTwo = Ember.Object.extend({
actions: {
// Here I want to call someCommonFunctionality function from RouteOne
}
});
Is this somehow possible? I have an AJAX get method that I do not want to repeat in RouteTwo as I have it already in RouteOne

Ember, mixin to detect click outside of view/component

I'm writing a Mixin to handle when user clicks outside of a view/component.
This is the mixin:
App.ClickElsewhereMixin = Ember.Mixin.create({
onClickElsewhere: Ember.K,
didRender: function() {
this._super.apply(this, arguments);
return $(document).on('click', this.get('onClickElsewhere'));
},
willDestroyElement: function() {
this._super.apply(this, arguments);
$(document).off('click', this.get('onClickElsewhere'));
},
});
I use it in my component:
onClickElsewhere: function() {
this.send('exitEditMode');
},
But when I run it, I get:
TypeError: this.send is not a function
How can I keep the this context?
Solution:
just to make it easier for the reader, here the working Mixin:
App.ClickElsewhereMixin = Ember.Mixin.create({
onClickElsewhere: Ember.K,
setupListener: Ember.on('didRender', function() {
// Set an event that will be fired when user clicks outside of the component/view
return $(document).on('click', $.proxy(this.get('onClickElsewhere'), this));
}),
removeListener: Ember.on('willDestroyElement', function() {
// Clean the previously defined event to keep events stack clean
return $(document).off('click', $.proxy(this.get('onClickElsewhere'), this));
}),
});
The current answer doesn't check whether the click was actually outside of the element – a click on the component will also trigger the callback.
Here's an updated version:
export default Ember.Mixin.create({
onOutsideClick: Ember.K,
handleOutsideClick: function(event) {
let $element = this.$();
let $target = $(event.target);
if (!$target.closest($element).length) {
this.onOutsideClick();
}
},
setupOutsideClickListener: Ember.on('didInsertElement', function() {
let clickHandler = this.get('handleOutsideClick').bind(this);
return Ember.$(document).on('click', clickHandler);
}),
removeOutsideClickListener: Ember.on('willDestroyElement', function() {
let clickHandler = this.get('handleOutsideClick').bind(this);
return Ember.$(document).off('click', clickHandler);
})
});
Greg answer have a mistake, that makes removing the clickHandler event not working. Which means that your clickevent will fire even if you destroy the component.
Here is proper version
import Ember from 'ember';
export default Ember.Mixin.create({
onOutsideClick: Ember.K,
handleOutsideClick: function(event) {
let $element = this.$();
let $target = $(event.target);
if (!$target.closest($element).length) {
this.onOutsideClick();
}
},
setupOutsideClickListener: Ember.on('didInsertElement', function() {
let clickHandler = this.get('handleOutsideClick').bind(this);
return Ember.$(document).on('click', clickHandler);
}),
removeOutsideClickListener: Ember.on('willDestroyElement', function() {
let clickHandler = this.get('handleOutsideClick').bind(this);
return Ember.$(document).off('click', Ember.run.cancel(this, clickHandler));
})
});
The ember way of doing it is Ember.run.bind. This takes care of binding and the run loop.
App.ClickElsewhereMixin = Ember.Mixin.create({
onClickElsewhere: Ember.K,
setupListener: Ember.on('didRender', function() {
this.set('clickHandler', Ember.run.bind(this, this.onClickElsewhere));
Ember.$(document).click(this.get('clickHandler'));
}),
removeListener: Ember.on('willDestroyElement', function() {
Ember.$(document).off('click', this.get('clickHandler'));
}),
});
You have two options:
Use a closure
Use bind
Closure
App.ClickElsewhereMixin = Ember.Mixin.create({
onClickElsewhere: Ember.K,
didRender: function() {
this._super.apply(this, arguments);
return $(document).on('click', function(this){ return this.get('onClickElsewhere'); }(this));
},
willDestroyElement: function() {
this._super.apply(this, arguments);
$(document).off('click', function(this){ return this.get('onClickElsewhere'); }(this));
},
});
Bind
App.ClickElsewhereMixin = Ember.Mixin.create({
onClickElsewhere: Ember.K,
didRender: function() {
this._super.apply(this, arguments);
return $(document).on('click', this.get('onClickElsewhere').bind(this));
},
willDestroyElement: function() {
this._super.apply(this, arguments);
$(document).off('click', this.get('onClickElsewhere').bind(this));
},
});
However, not all browsers support bind yet.
Also, I think you need to use sendAction instead of send in the component (http://guides.emberjs.com/v1.10.0/components/sending-actions-from-components-to-your-application/)
Edit:
jQuery.proxy uses call/apply underneath the covers. See this post for a discussion of call/apply vs bind.
You can use the lib ember-click-outside. Worked for me.

ember data reload() undefined

I am trying to reload a model that has changed on the server. My code is as follows:
App.CustomersController = Ember.ArrayController.extend({
intervalId: undefined,
startRefreshing: function() {
var self = this;
if ( self.get( 'intervalId' ) ) {
return;
}
self.set( 'intervalId', setInterval( function() {
//self.get('model').update();
self.get('model').reload();
}, 30000 ) );
}
});
App.CustomersRoute = Ember.Route.extend({
model: function() {
return this.store.find('customer');
},
setupController: function( controller, model ){
this._super( controller, model );
controller.startRefreshing();
},
actions: {
reload: function() {
this.get('model' ).reload();
}
}
});
You can see that I have two mechanisms for reloading the data - one a timer, and also an action triggered by a button in the UI. The latter is exactly what is shown in the ember-data documentation here: http://emberjs.com/api/data/classes/DS.Model.html#method_reload
Neither works. I get undefined in both cases i.e. the model returned does not have a reload() method. update() sort of works, except it does not remove deleted records and it is not what is recommended in the documentation. What am I doing wrong here in trying to use reload?
My stack:
DEBUG: -------------------------------
DEBUG: Ember : 1.5.1+pre.07fafb84
DEBUG: Ember Data : 1.0.0-beta.7.f87cba88
DEBUG: Handlebars : 1.3.0
DEBUG: jQuery : 1.11.0
DEBUG: -------------------------------
and I am using the following adapter in case that makes any difference:
App.Store = DS.Store.extend({
// Override the default adapter with the `DS.ActiveModelAdapter` which
// is built to work nicely with the ActiveModel::Serializers gem.
adapter: '-active-model'
});
reload exists on a record, not a collection.
You would need to iterate the collection and call reload on each record.
self.get('model').forEach(function(record){
record.reload();
});
But I'm guessing you don't want to waste the callbacks to the server. In this case I'd recommend returning a filter as your model, then make another call to the server for all records.
App.CustomersRoute = Ember.Route.extend({
model: function() {
this.store.find('customer');
return this.store.all('customer');
},
setupController: function( controller, model ){
this._super( controller, model );
controller.startRefreshing();
},
actions: {
reload: function() {
this.get('model' ).reload();
}
}
});
App.CustomersController = Ember.ArrayController.extend({
intervalId: undefined,
startRefreshing: function() {
var self = this;
if ( self.get( 'intervalId' ) ) {
return;
}
self.set( 'intervalId', setInterval( function() {
self.store.find('customer'); // get all customers again, updating the ones we have
}, 30000 ) );
}
});

Ember: View is not updating after request, when using RESTAdapter

As starting point we used http://todomvc.com/architecture-examples/emberjs/ .
We changed the FixtureAdapter to a RESTAdapter and performed the following changes:
Todos.TodosActiveRoute = Ember.Route.extend({
model: function(){
// ** OLD CODE **
// return this.store.filter('todo', function (todo) {
// return !todo.get('isCompleted');
// });
// ** NEW CODE **
return this.store.findQuery('todo', {isCompleted: false})
},
renderTemplate: function(controller){
this.render('todos/index', {controller: controller});
}
});
We can load the todo items correctly, but if we want to delete one of them a DELETE request is successfully sent to the backend but the todo-item is not removed from the UI.
EDIT:
The delete action is:
removeTodo: function () {
var todo = this.get('model');
todo.deleteRecord();
todo.save();
}
The problem is with using findQuery here. It results in a non live array. That's why the view is not updated after delete (I think if you add a todo, it should neither work)
You can use store.filter passing it the query and the filter function. I think it should work as you expect.
Todos.TodosActiveRoute = Ember.Route.extend({
model: function(){
return this.store.filter('todo', {isCompleted: false}, function (todo) {
return !todo.get('isCompleted');
});
},
renderTemplate: function(controller){
this.render('todos/index', {controller: controller});
}
});

emberjs: transitionToRoute Error no method 'addArrayObserver

From the 'job' route I am trying to transition to 'careers' route using following code.
<script type="text/x-handlebars" data-template-name="job">
<button {{action 'backToCareers' this}}>Back</button>
</script>
The controller with following gives 'Uncaught TypeError: Object # has no method 'addArrayObserver' ' error.
CareerApp.JobController = Ember.ObjectController.extend({
backToCareers: function(){
this.transitionToRoute('careers');
}
});
If I change the code(see below) to provide model object the error changes to 'Uncaught More context objects were passed than there are dynamic segments for the route: careers '
CareerApp.JobController = Ember.ObjectController.extend({
backToCareers: function(){
var jobs = CareerApp.Job.findAll();
this.transitionToRoute('careers', jobs);
}
});
Following is the code of my Model and the router
CareerApp.Job = Ember.Model.extend({
refNo: '',
title: ''
});
CareerApp.Job.reopenClass({
findAll: function(){
return $.getJSON("http://site/jobs").then(
function(response){
var jobs = Ember.A();
response.forEach(function(child){
jobs.pushObject(CareerApp.Job.create(child));
});
return jobs;
}
);
}
});
Router code
CareerApp.Router.map(function(){
this.resource('careers', {path: '/'});
this.resource('job', {path: '/jobs/:job_id'});
});
CareerApp.CareersRoute = Ember.Route.extend({
model:function(){
return CareerApp.Job.findAll();
}
});
CareerApp.CareersController = Ember.ArrayController.extend({
gradJobCount: function () {
return this.filterProperty('isExp', false).get('length');
}.property('#each.isExp')
});
The model hook is expected to return an array but you return a jQuery promise object. findAll should return an empty array which is filled when the callback is executed.
findAll: function() {
var jobs = [];
$.getJSON("http://site/jobs").then(function(response){
response.forEach(function(child){
jobs.pushObject(CareerApp.Job.create(child));
});
});
return jobs;
}
As you pass jobs to CarreersController, this one needs to be an ArrayController, maybe you have to define it manually