How to test LoDash debounce in Jasmine with Sinon fakeTimer? - backbone.js

I'm trying to write a test for debouncing user input in a search query. The function is defined on a Backbone View:
SearchView = Backbone.View.extend({
events: {
"input .search-input": "search"
},
// init, render, etc.
search: _.debounce(function() {
this.collection.fetch();
}, 200)
});
Originally, the Backbone library (v0.9.10) used Underscore (v1.4.4), and the test was defined as follows:
describe("SearchView", function() {
var view, $viewContainer;
beforeEach(function() {
appendSetFixtures('<div class="jasmine-container"></div>');
$viewContainer = $(".jasmine-container");
view = new SearchView({
el: $viewContainer
});
});
afterEach(function() {
view.remove();
view.cleanup();
});
//...
describe("wires the search input", function() {
var collectionStub,
fakeTimer;
beforeEach(function() {
collectionStub = sinon.stub(
SearchResultsCollection.prototype,
"fetch"
);
fakeTimer = sinon.useFakeTimers();
});
afterEach(function() {
collectionStub.restore();
fakeTimer.restore();
});
it("should not trigger a search before 200ms", function() {
fakeTimer.tick(199);
expect(collectionStub).not.toHaveBeenCalled();
});
it("should trigger a search after 200ms", function() {
fakeTimer.tick(200);
expect(collectionStub).toHaveBeenCalled();
});
});
});
However, now I want to incorporate LoDash instead of Underscore. Using the latest Underscore compatibility build on their site (LoDash 2.4.1 / Underscore 1.5.6), all my tests pass except for the one using _.debounce!
I did some research and came across these relevant issues to create a LoDash Underscore build with runInContext, but I have no idea how to use it due to lack of examples. How can I use _.runInContext() in my spec(s) to work with sinon.fakeTimer?

SearchView = Backbone.View.extend({
events: {
"input .search-input": function() {
this.search();
}
},
initialize: function() {
this.search = _.debounce(this.search, 200);
}
// init, render, etc.
search: function() {
this.collection.fetch();
}
});
describe("SearchView", function() {
var view;
var $viewContainer;
var clock;
var lodash = window._;
beforeEach(function() {
appendSetFixtures('<div class="jasmine-container"></div>');
$viewContainer = $(".jasmine-container");
clock = sinon.useFakeTimers();
window._ = _.runInContext(window);
view = new SearchView({
el: $viewContainer
});
});
afterEach(function() {
view.remove();
view.cleanup();
clock.restore();
window._ = lodash;
});
//...
describe("wires the search input", function() {
var collectionStub;
beforeEach(function() {
collectionStub = sinon.stub(
SearchResultsCollection.prototype,
"fetch"
);
});
afterEach(function() {
collectionStub.restore();
});
it("should not trigger a search before 200ms", function() {
fakeTimer.tick(199);
expect(collectionStub).not.toHaveBeenCalled();
});
it("should trigger a search after 200ms", function() {
fakeTimer.tick(200);
expect(collectionStub).toHaveBeenCalled();
});
});
});

You need add this line
_ = _.runInContext(window);
before creation (not initialization) of SearchView or any call of _.debounce(). So it should be right after including Lo-Dash.
This allows you to run lodash in global window context so you can use overridden by SinonJS setTimeout.

Related

Backbone doesn't remove View when Model removed

There's a remove method on EventView. When I click on the remove button, the Event (Model) should be removed so as the EventView.
With the following code, I can remove the Model from mongodb by clicking on the remove button. But the Model View won't remove itself until I refresh the page.
I am using express, EJS and mongodb for this demo.
app.js // with express routes settings
events.init = function(req, res) { res.render('index') };
events.all = function(req, res) {
db.event.find({}, function(err, event) {
if (err) return;
res.json(event);
});
}
events.delete = function (req, res) {
var Id = db.ObjectId(req.params.id);
db.event.remove({
"_id": Id
});
}
app.get('/', events.init);
app.get('/events', events.all);
app.del('/events/:id', events.delete);
client.js // Backbone Model, Collection and View setup
var Event = Backbone.Model.extend({
idAttribute: "_id"
});
var EventCollection = Backbone.Collection.extend({
model: Event,
url: "/events"
});
var EventView = Backbone.View.extend({
events: {
"click .remove": "remove"
},
initialize: function () {
this.listenTo(this.model, 'destroy', this.remove);
},
remove: function (e) {
this.model.destroy();
},
render: function () {
var html = new EJS({url: '/partials/event-field.ejs'}).render(this.model);
this.$el.html(html);
return this
}
});
var EventCollectionView = Backbone.View.extend({
render: function () {
this.collection.each(function(event){
var eventView = new EventView({ model: event });
this.$el.append(eventView.render().$el);
}, this);
return this
}
});
init.js // Called on page load
$(function () {
var collection = new EventCollection();
collection.fetch({
success: function(data){
var collectionView = new EventCollectionView({ collection: data})
$('.upcoming .list-group').append(collectionView.render().$el);
}
});
});
Somehow I found out how to make it work.
I renamed the remove method to destroy and changed the destroy method like so.
events: {
"click .remove": "destroy"
},
initialize: function () {
this.listenTo(this.model, 'destroy', this.destroy);
},
destroy: function () {
this.remove();
this.unbind();
this.model.destroy();
},

How to filter a backbone collection and render the results

Total newbie to Backbone so apologize if this is a simple question.
I am have successfully loaded a collection and rendered the view. However I have a dropdown with A tags that I would like to use the filter the data displayed. I'm trying to set an event listener in my VIEW and then trigger a function within the view to filter the results and re-render the view.
Here's my code:
IBA.NewsModel = Backbone.Model.extend({});
IBA.NewsCollection = Backbone.Collection.extend({
model: IBA.NewsModel,
url: "/news/api"
});
IBA.NewsView = Backbone.View.extend({
el: '#main',
template: _.template($("#news-article").html()),
events: {
"change .dropdown-item": "filterNews"
},
initialize: function () {
this.collection = new IBA.NewsCollection();
this.listenTo(this.collection, 'reset', this.render);
this.collection.fetch({
success: function() {
console.log("JSON file load was successful");
view.render();
},
error: function(){
console.log('There was some error in loading and processing the JSON file');
}
});
},
render: function () {
this.$el.html(this.template({
articles: this.collection.toJSON()
})
);
return this;
},
filterNews: function (e){
e.preventDefault();
var items = this.collection.where({cat_name: "interviews"});
console.log(items);
this.$el.html(this.template({articles: this.items.toJSON()}));
}
});
var view = new IBA.NewsView();
Easiest way to do it would be to reset the actual collection with the filtered results:
IBA.NewsModel = Backbone.Model.extend({});
IBA.NewsCollection = Backbone.Collection.extend({
model: IBA.NewsModel,
url: "/news/api"
});
IBA.NewsView = Backbone.View.extend({
el: '#main',
template: _.template($("#news-article").html()),
events: {
"change .dropdown-item": "filterNews"
},
initialize: function() {
this.collection = new IBA.NewsCollection();
this.listenTo(this.collection, 'reset', this.render);
this.fetchNews();
},
fetchNews: function() {
var view = this;
this.collection.fetch({
success: function() {
console.log("JSON file load was successful");
view.render();
},
error: function() {
console.log('There was some error in loading and processing the JSON file');
}
});
},
render: function() {
this.$el.html(this.template({
articles: this.collection.toJSON()
}));
return this;
},
filterNews: function(e) {
e.preventDefault();
this.collection.reset(this.collection.where({
cat_name: "interviews"
}));
}
});
var view = new IBA.NewsView();
When you want to go back to original data, fetch it again using the fetchNews() method.
You also had a syntax error that view was not defined in your initialize

why the drawSomething doesn't work

I use the backbone to test something, but i don't know why the drawSomething just no show ##"
initialize: function() {
setInterval(function() {
//alert("Hello");
this.drawSomething();
}, 1000);
},
drawSomething: function() {
alert('hi');
},
The problem is that inside setInterval callback context this is not what you expect (it's global object window). Simplest fix is to save proper object reference in variable:
var self = this;
setInterval(function() {
//alert("Hello");
self.drawSomething();
}, 1000);
as you are using backbone, so possible you are using underscore too. Bind should help:
initialize: function () {
var foo = function () { this.drawSomething(); };
foo = _.bind(foo, this);
setInterval(foo, 1000);
}
or jQuery analog Proxy:
foo = $.proxy(foo, this);
as quick solution
initialize: function() {
setInterval(function() {
//alert("Hello");
this.drawSomething();
}.bind(this), 1000);
},
drawSomething: function() {
alert('hi');
},
but I would prefer to use additional variable as dfsq methioned, because some old browsers doesn't support bind

inherit backbone view declared in different .js file

Expanding my original question located here
If my Userview is in Userview.js file and I want to inherit from that class in AdminView.js file, how would I go about it.
I tried this, but would not fit my need as I don't have a class.
UPDATE 1:
define([
'modules/userdetail'
],
function(UserView) {
var adminView
adminView.Views.Content = UserView.Views.Content.extend({
initialize: function() {
//looking to override the fn that is declared in UserView
console.log("AAA");
},
});
}
UPDATE 2:
So digging deep, the User Detail is
define(
[ 'modules/baseClass'],
function(BaseClass) {
// Create a new module
//Create Model
//Create View
UserDetails.Views.Content = Backbone.View
.extend({
template :
initialize : function() {
this.model = new UserDetails.Model();
},
events : {
},
render : function(LayOut) {
return LayOut(this).render().then(this.pageReady);
},
pageReady : function() {
},
});
UserDetails.activate = function() {
app.router.navigate('UserDetails', true);
};
UserDetails.configureRouting = function() {
app.router.route('UserDetails', 'UserDetails',
function() {
layoutmanager.setView('#content',
new UserDetails.Views.Content())
.render();
});
};
return UserDetails;
});
ADMIN:
define([
'modules/baseclass',
'modules/UserDetail'
],
function(BaseClass, UserDetails) {
UserDetail.Views.Content = UserDetail.Views.Content.extend({
render:function(){
console.log("rendering");
UserDetail.Views.Content.prototype.render();
}
});
//create admin model
//admin view
AdminView.Views.Content = Backbone.View.extend({
template: "admin-template",
events: {
},
initialize: function() {
this.model = new AdminModel.Model();
},
render: function(manage) {
return manage(this).render().then(this.pageReady);
},
pageReady: function() {
});
},
AdminView.activate = function() {
app.router.navigate('adminview', true);
};
AdminView.configureRouting = function() {
app.router.route('adminview', 'adminview', function() {
layoutmanager.setView('#content', new AdminView.Views.Content()).render();
layoutmanager.setView('#userDetials', new UserDetials.Views.Content()).render();
});
};
if(app.router && app.router.route) {
AdminView.configureRouting();
}
return AdminView;
});
Now if I have to call the render of the userdetails from admin view, the render method fails as the param is undefined.
I am not well versed with where the para in render is defined as I looked through my code and have not found anything
Either include the script tag for Userview.js before the script tag for AdminView.js, or using a module system like requirejs or browserify where you can specify the two modules as dependencies.

Backbone view, initialize and render

I have a backbone view which load subview. When I load a subview, I would like to show a loader when the view fetch needed datas and hide the loader when the view is ready to render.
I did something like this :
var appView = Backbone.View.extend({
showLoader: function() {
// Code to show loader
},
hideLoader: function() {
// Code to hide loader
 },
loadSubView: function() {
this.showLoader();
var myView = new MySubView();
this.$el.html(myView.render().el);
this.hideLoader();
}
});
For now, my sub-view load a collection and is implemented like this :
var mySubView = Backbone.View.extend({
initialize: function() {
this.myCollection.fetch({
async: false
});
},
render: function() {
// Code to render
}
});
My sub view load the collection synchronously because it is the only way I found to know when my view is "ready" to render but I think this is not the best way to use Backbone.
What schould I do ?
There are several ways to do it.
You can explicitly use the pubsub pattern. Something like this:
var AppView = Backbone.View.extend({
showLoader: function() {
console.log('show the spinner');
},
hideLoader: function() {
console.log('hide the spinner');
},
loadSubView: function() {
this.showLoader();
var subView = new SubView();
subView.on('render', this.hideLoader);
this.$el.html(subView.render().el);
}
});
var SubView = Backbone.View.extend({
render: function() {
console.log('a subView render');
this.trigger('render');
return this;
}
});
var appView = new AppView({el: $('body')});
appView.loadSubView();
http://jsfiddle.net/theotheo/qnVhy/
You can attach a function to the ajaxStart/ajaxStop events on the
spinner itself:
var AppView = Backbone.View.extend({
initialize: function() {
var _this = this;
this.$('#spinner')
.hide()
.ajaxStart(_this.showLoader)
.ajaxStop(_this.hideLoader);
}
...
}
Or you can use jQuery.ajaxSetup:
var AppView = Backbone.View.extend({
initialize: function() {
var _this = this;
jQuery.ajaxSetup({
beforeSend: _this.showLoader,
complete: _this.hideLoader,
success: function() {}
});
}
...
}

Resources