Backbone.js and Jasmine Spys Not Getting Called - backbone.js

I'm trying to test that when an element is clicked, that a function is called. Easy enough it would seem, but I must be missing something stupid because I can't seem to get this simple example to work.
Here is my View
(function($) {
window.LaserMonitor = {
Views: {}
};
window.LaserMonitor.Views.WorkstationSummary = Backbone.View.extend({
tagName: 'li',
className: 'StationItem',
initialize: function() {
_.bindAll(this, 'showDetails');
this.template = _.template($("#WorkstationSummary").html());
},
events: {
'click h3' : 'showDetails'
},
showDetails: function() {
},
render: function() {
var renderedTmpl = this.template(this.model.toJSON());
$(this.el).append(renderedTmpl);
return this;
}
});
})(jQuery);
and here is my Jasmine test:
describe('WorkstationSummary Item', function() {
beforeEach(function() {
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g,
evaluate: /\{\{(.+?)\}\}/g
};
loadFixtures('LaserMonitorFixture.html');
this.model = new Backbone.Model({
id: 1,
name: 'D8',
assigned: 1900,
inProgress: 4,
completed: 5
});
this.view = new window.LaserMonitor.Views.WorkstationSummary({model: this.model});
});
describe('Events', function() {
beforeEach(function() {
this.view.render();
});
it('should trigger click event', function() {
this.header = this.view.$('h3');
spyOn(this.view, 'showDetails');
this.header.click();
expect(this.view.showDetails).toHaveBeenCalled();
});
});
});
The result of this running is:
Error: Expected spy on showDetails to have been called.
at new (http://localhost:57708/JobMgr2/test-js/lib/jasmine-1.0.2/jasmine.js:102:32)
at [object Object].toHaveBeenCalled (http://localhost:57708/JobMgr2/test-js/lib/jasmine-1.0.2/jasmine.js:1171:29)
at [object Object]. (http://localhost:57708/JobMgr2/test-js/spec/LaserMonitorSpec.js:33:34)
at [object Object].execute (http://localhost:57708/JobMgr2/test-js/lib/jasmine-1.0.2/jasmine.js:1001:15)
at [object Object].next_ (http://localhost:57708/JobMgr2/test-js/lib/jasmine-1.0.2/jasmine.js:1790:31)
at http://localhost:57708/JobMgr2/test-js/lib/jasmine-1.0.2/jasmine.js:1780:18
EDIT: Adding the fixture template for completness:
<script type="text/template" id="WorkstationSummary">
<h3>{{ name }} ({{ assigned }}/{{ inProgress }}/{{ completed }})</h3>
<ul>
</ul>
</script>

If you create a spy for method, while running the test, instead of calling the actual method the spy get called. The spy is a wrapper around the method. But here the problem is you have created the view before you created the spy. So the actual method is getting called instead of the spy. What you have to do is create the spy before creating the view object. I have used sinon.js to spy on the methods. And you have to use the prototype of the view to spy on a method of that view:
var workStationSpy = sinon.spy(window.LaserMonitor.Views.WorkstationSummary.prototype, "showDetails");
this.view = new window.LaserMonitor.Views.WorkstationSummary({model: this.model});
this.view.render();
expect(workStationSpy).toHaveBeenCalled();
workStationSpy.restore();

I would change it to the following and try:
it('should trigger click event', function() {
var viewToTest = this.view;
this.header = viewToTest.$('h3');
spyOn(viewToTest, 'showDetails');
this.header.click();
expect(viewToTest.showDetails).toHaveBeenCalled();
});
My worry with calling "this.view" could lead to scoping issues. Its a wild guess that I have not tested but I think its worth a try. Good luck!

Related

Backbone.js model.save() fire a "too much recursion" error in underscore

I've got a problem trying to use backbone on saving my Model from a form. Here I want my my view to actually be an editing form:
(function() {
'use strict';
var YachtEditor = {};
window.YachtEditor = YachtEditor;
var template = function(name) {
return Mustache.compile($('#' + name + 'Template').html());
};
YachtEditor.Tank = Backbone.Model.extend({
defaults : {
dCapacity : "",
sType : ""
}
});
YachtEditor.Tanks = Backbone.Collection.extend({
// url: "/rest/tanks",
localStorage: new Store("tanks"),
model : YachtEditor.Tank
});
YachtEditor.TankView = Backbone.View.extend({
template: template("tank"),
events: {
'click .save' : 'save',
'click .remove' : 'remove'
},
initialize: function() {
console.log("initialize tank View :");
console.log(this.model.get("id"));
},
render: function() {
this.$el.html(this.template(this));
return this;
},
save: function() {
console.log('change');
var self = this;
var values = {
sType: self.$("#sType").val(),
dCapacity: self.$("#dCapacity").val()
};
console.log("dCapacity : " + values.dCapacity);
console.log("sType : " + values.sType);
this.model.save(values);
},
remove: function() {
this.model.destroy();
},
dCapacity : function() {
return this.model.get("dCapacity");
},
sType : function() {
return this.model.get("sType");
}
});
YachtEditor.TanksView = Backbone.View.extend({
el: $("div.tankZone"),
template: template("tanks"),
events: {
"click .add" : "addTank",
"click .clear" : "clear"
},
initialize: function() {
this.tanks = new YachtEditor.Tanks();
// this.tanks.on('all', this.render, this);
this.tanks.fetch();
this.render();
},
render: function() {
this.$el.html(this.template(this));
this.tanks.each(this.renderTank, this);
return this;
},
renderTank: function(tank) {
var view = new YachtEditor.TankView({model: tank});
$(".tanks").append(view.render().el);
return this;
},
addTank: function() {
this.tanks.create({});
this.render();
},
clear: function() {
this.tanks.each(function(tank) {
tank.destroy();
});
this.render();
}
});
...
})();
Here is the mustache template i use for each tank
<script id="tankTemplate" type="text/x-mustache-template">
<div class="tankView">
<h1>Tank</h1>
<select id="sType" value="{{ sType }}">
#for(option <- Tank.Type.values().toList) {
<option>#option.toString</option>
}
</select>
<input id="dCapacity" type="text" value="{{ dCapacity }}">
<button class="destroy">x</button>
</div>
</script>
My problem here is that this.model.save() triggers a 'too much recursion' in underscore. js. (chrome is displaying an error also.
Here is the call stack on error:
_.extend
_.clone
_.extend.toJSON
_.extend.save
_.extend.update
Backbone.sync
_.extend.sync
_.extend.save
YachtEditor.TankView.Backbone.View.extend.save
st.event.dispatch
y.handle
I suspect the save to recall the blur event but i cannot find a way to explicit it... Maybe I'm not using backbone as i should?
My problem, aside of some pointed out by Yurui Ray Zhang (thank you), was that I was using a backbone-localstorage.js from an example I found here : git://github.com/ngauthier/intro-to-backbone-js.git
The "too much recursion error" stopped to appear as soon a I replaced it with a storage I found here : https://github.com/jeromegn/Backbone.localStorage
a few things. you defined your tank model as
app.Tank = ...
but in your collection you are referencing it as:
model : YachtEditor.Tank
and in your view, you are trying to assign elements before they are rendered on the page:
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
I'm not sure how your view is rendered to the page, some people, like me, like to use render() to render the template directly to the page:
render: function() {
this.$el.html(this.template(this));
//done, you should be able to see the form on the page now.
},
some others, will use something else to insert the el, eg:
//in another view
tankView.render().$el.appendTo('body');
but either way, if you want to cache your elements, you need to do it after they are rendered to the page, not in initialize.
//this method is only called after render() is called!
cacheElements: function() {
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
}
I'd suggest, first, try to fix this things, and then, try to add some console log or debuggers in your readForm method to see if the values are grabbed correctly:
readForm: function() {
var input = this.input;
console.log(input.sType.val());
console.log(input.dCapacity.val());
this.model.save({
sType: input.sType.val(),
dCapacity: input.dCapacity.val()
});
},

Backbone.js Uncaught ReferenceError: x is not defined

I am getting Uncaught ReferenceError: _auditNumber is not defined error while trying to bind my model to the view using backbone.js and underscore.js
<script id="searchTemplate" type="text/template">
<div class="span4">
<p>"<%= _auditNumber %>"</p>
</div>
<div class="span4">
<p>"<%= _aic %>"</p>
</script>
Collection
//Collection
var AuditsCollection = Backbone.Collection.extend({
initialize: function() {
this.on('add', this.render);
},
render: function() {
_.each(this.models, function (item) {
var _auditView = new AuditView({
model: item
});
$("#audits").append(_auditView.render().el);
});
},
});
Model
var Audit = Backbone.Model.extend({
url: function () {
return myUrl;
},
defaults: {
_auditNumber: "",
_aic: "",
},
parse: function (data) {
data.forEach(function (auditItem) {
var auditsCollection = new AuditsCollection();
auditsCollection.add(JSON.stringify(auditItem));
});
}
});
// Sub View
var AuditView = Backbone.View.extend({
className: 'row-fluid',
template: $("#searchTemplate").html(),
render: function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
I know I am missing something simple, any help is appreciated.
2 problems (at least - you're kind of off in the weeds given how many backbone tutorials there are).
Your model URL is returning a list of results. That's what collections are for. Your model should fetch a single record and the parse method has to return the model's attribute data. If you stick with the tutorials, you won't need a custom url function and you won't need a custom parse function at all.
var Audit = Backbone.Model.extend({
url: function () {
//This needs to be a url like /audits/42 for a single record
return myUrl;
},
defaults: {
_auditNumber: "",
_aic: "",
},
parse: function (data) {
//this needs to return an object
return data[0];
}
});
You aren't passing a valid data object to your template function.
// Sub View
var AuditView = Backbone.View.extend({
className: 'row-fluid',
//compile template string into function once
template: _.template($("#searchTemplate").html()),
render: function () {
//render template into unique HTML each time
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});

backbone.js example doesn't run upon page load but does when invoked in debugger

I have a simple backbone.js example I am working on. The problem is upon page load it is not displaying anything on the page. However, in the Chrome debugger console, if I explicitly make a call to the view and it's render() method then the results show up on the screen with the correct json data.
Any help would be really, really appreciated!
var Clients = Backbone.Collection.extend({
model: Client,
url: 'api/Contacts'
});
var clients = new Clients();
var UserItemView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#contacts-template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var UserListView = Backbone.View.extend({
el: $('#contacts'),
render: function() {
this.$el.empty();
var self = this;
_.each(this.collection.models, function(model) {
self.renderItem(model);
});
},
renderItem: function(item) {
var itemView = new UserItemView({
model: item
});
this.$el.append(itemView.render().el);
}
});
Here's the code for the index.html page:
<ul id="contacts"></ul>
<script id="contacts-template" type="text/template">
<%= FirstName %> <%= LastName %>
</script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.3/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script src="scripts/app/app.js"></script>
<script>
$(document).ready(function () {
alert('HI'); // I verified this alert works
clients.fetch();
var userListView = new UserListView({ collection: clients });
userListView.render();
});
</script>
Every asynchronous call should have a callback when it's done, here you're trying to use clients collection before it has data from the server. I would change the code to:
$(document).ready(function () {
alert('HI'); // I verified this alert works
clients.fetch(
success: function() {
var userListView = new UserListView({ collection: clients });
userListView.render();
},
error: function() {
alert('An error has occurred');
},
);
});
Regards,

Backbone - View not updating page

I'm trying to use the render function in questionListView, and it appears to be running, but is not updating the page.
The template:
<script id="myTemplate" type="text/template">
<p>Test</p>
</script>
Part of the JS:
$(function(){
//Test data
var initialListData = [
{ listName: "Sample Questions", listID: 1},
{ listName: "Default questions", listID: 2}
];
// MODELS ----------------------------------------------------
var questionList = Backbone.Model.extend({
defaults: {
listName: "Name of the list",
listID: 0
}
});
// COLLECTIONS ----------------------------------------------------
var populateList = Backbone.Collection.extend({
model: questionList
});
// VIEWS ----------------------------------------------------
var questionListView = Backbone.View.extend({
template: $("#myTemplate").html(),
render: function () {
console.log('I can see this, but nothing happens...');
var tmpl = _.template(this.template);
$(this.el).append(tmpl(this.model.toJSON()));
return this;
}
});
var AppView = Backbone.View.extend({
el : $("#content"),
initialize: function (){
this.collection=new populateList(initialListData);
this.render();
},
render: function (){
_.each(this.collection.models, function (item) {
this.renderSelect(item);
}, this);
},
renderSelect: function(item){
var populateQuestionList = new questionListView({
model: item
});
this.$el.append(populateQuestionList.render().el);
}
});
var app = new AppView();
} (jQuery));
Thanks!
Are you triggering this in a callback to the document.ready event? If not, your code could be executing before the DOM is actually loaded and ready. Try:
$(function () {
var app = new AppView();
});
A few misc points.
You can do template: _.template($("#myTemplate").html()) to cache the template function as a micro-optimization
You can use this.$el instead of $(this.el) in recent version of backbone. You are already doing this in one place but not both.

When using Backbone.View a "parent.apply is not a function" error is returned

Consider this markup
<div id="controls" class="controls">
Home -
get -
new
<input type="text" val="" id="input">
</div>
And this piece of javascript:
$(document).ready(function() {
"use strict";
// this is used on my code as root.App,
// but the code was omitted here for clarity purposes
var root = this,
undefined;
var controller = Backbone.Controller.extend({
routes : {
// static
},
});
var view = new Backbone.View.extend({
el : $('#controls'),
events : {
'click a' : 'updateOnEnter'
},
updateOnEnter : function(el) {
alert('sss');
return this;
},
initialize : function() {
_.bindAll(this, 'render', 'updateOnEnter');
},
render : function() {
return this;
}
});
new view;
new controller;
Backbone.history.start();
)};
When view is called (with new view), Firebug fires this error:
parent.apply is not a function
error backbone.js (line 1043): child = function(){ return parent.apply(this, arguments); };
Any ideas to why this is happening? Thanks.
Never mind.
The problem is on line 16 of the above js code:
var view = new Backbone.View.extend({
it should instead be:
var view = Backbone.View.extend({
I'm not deleting this question since somebody may find it useful. The pitfalls of not coming from a CS background, I guess.

Resources