Using Jasmine, I have already written tests for my models and controllers and all have passed. However, I am running into a lot of problems if I test my views.
IF I remove views/hf_appView from the define function in base.js, the PostViewSpec test will pass. This is just a very simple and contrived example, but it illustrates an error I keep getting (in the developer tools console of my browser) when trying to test other views as well.
See the second example below in which I can't get to work at all. I did this to see if a different view module would work given there might be something wrong with the base module, but no luck.
Even in the passing collection spec, I then added a reference to views/hf_postView in the define function just to see what happens to the test and then all of a sudden I get the same error as #1 & 2 noted below.
Based on the errors below, one theory I have is that: Perhaps the #listTpl that is attached to the view is not passed through because the DOM isn't ready as the view gets parsed? If so, how can I fix this?
Again, the models and controllers pass, but once I start including the view modules I am having a lot trouble. The error is:
Error in Chrome
Cannot read property 'replace' of undefined
Error in Safari
[Warning] Invalid CSS property declaration at: * (jasmine.css, line 16)
[Error] TypeError: undefined is not an object (evaluating 'n.replace')
template (underscore-min.js, line 5)
(anonymous function) (hf_postView.js, line 12)
execCb (require.js, line 1658)
check (require.js, line 874)
(anonymous function) (require.js, line 624)
each (require.js, line 57)
breakCycle (require.js, line 613)
(anonymous function) (require.js, line 626)
each (require.js, line 57)
breakCycle (require.js, line 613)
(anonymous function) (require.js, line 626)
each (require.js, line 57)
breakCycle (require.js, line 613)
(anonymous function) (require.js, line 626)
each (require.js, line 57)
breakCycle (require.js, line 613)
(anonymous function) (require.js, line 626)
each (require.js, line 57)
breakCycle (require.js, line 613)
(anonymous function) (require.js, line 700)
each (require.js, line 57)
checkLoaded (require.js, line 699)
completeLoad (require.js, line 1576)
onScriptLoad (require.js, line 1679)
The 'anonymous function it is referencing in line 12 of hf_postView.js is
template: _.template($('#listTpl').html()),
--------------------------------------
SpecRunner.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
<title>Jasmine Test Runner</title>
<link rel="stylesheet" type="text/css" href="../../test/jasmine/lib/jasmine.css">
</head>
<body>
SpecRunner.js - Partial!!
specs.push('spec/PostModelSpec');
specs.push('spec/PostCollectionSpec');
specs.push('spec/PostViewSpec');
require(['boot'], function(){
require(specs, function(){
window.onload();
});
});
PostViewSpec.js
define(['views/base'], function(Base) {
describe("View :: PostView", function() {
it('View :: Should have tag name', function() {
var base = new Base();
base.render()
expect(base.tagName).toEqual('div');
});
}); //describe
});//define
views/base.js
define([
'jquery',
'underscore',
'backbone',
'views/hf_appView', //****REMOVE this and the hfAppView below, and the test passes ******//
'collections/hf_collections'], function($, _, Backbone, hfAppView, hfposts){
return Base = Backbone.View.extend({
el: '#securities',
render: function() {
//add code here
}
});
});
--------------------------------------------------
PostViewSpec.js - Second Test Example - THIS DOESNT WORK AT ALL using a different view module.
define(['jquery', 'underscore', 'backbone', 'views/hf_postView'], function($, _, Backbone, hfPostView) {
it('View :: Should have tag name', function() {
var base = new hfPostView();
base.render()
expect(base.tagName).toEqual('li');
});
}); //describe
});//define
views/hf_postView
define([
'jquery',
'underscore',
'backbone',
'models/hf_models',
'views/hf_appView',
'utils'], function($, _, Backbone, PostModel, hfAppView, utils){
return hfPostView = Backbone.View.extend({
tagName: 'li',
className: 'securities',
template: _.template($('#listTpl').html()),
events: {
'click .delete': 'deletePost'
},
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
if(!this.model) {
throw new Error('Must have HFPOST model');
}
},
render: function() {
this.$el.html(this.template(
this.model.toJSON()
));
return this;
},
deletePost: function() {
var confirmed = confirm("Are you sure?");
if (confirmed) {
this.model.destroy();
utils.showAlert('Entry was deleted', '', 'alert-warning');
}
} //delete post
}); //Backbone View
});
--------------------------------------------------
PostCollectionSpec.js - THIS works only if I remove the references to the hf_postView as well.
define(['collections/hf_collections', 'views/hf_postView'], function(hfposts, hfPostView) {
describe('Controller :: Post Controller', function () {
var posts;
beforeEach(function() {
posts = hfposts;
});
it('url should be /api', function() {
expect(posts.url).toEqual('/api');
});
it('should create new model', function() {
var post1 = hfposts.set({name: 'thisPost'});
expect(post1.get("name")).toEqual('thisPost');
});
}); //describe
}); //define
This will happen if you pass _.template(...) an empty DOM element. With un-minified underscore.js, doing this ...
_.template($('#thisDoesNotExist'), { foo: "bar" });
... leads to ...
Uncaught TypeError: Cannot read property 'replace' of null underscore.js:1304 _.template
Line 1304 of underscore.js is doing this:
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escaper, escapeChar);
// ... and so on
Your error of attempting to evaluate n.replace is the minified version of that text.replace(...) call.
It happens because your unit test of the View is attempting to load something from the page that (I assume) is outside of the view itself via $('#listTpl'). The DOM element with ID "listTpl" does not exist on the page that is hosting the unit tests, so it returns nothing. Bummer.
How to fix it? Since you're already using RequireJS, I suggest you also use the text plugin. This would allow you to define your template files wholly outside of the pages that are hosting the views in the application, which is darn nice for testing. It's also really easy to get running in an existing RequireJS app, as there is no extra download or configuration; just use the text! prefix for the dependency you want.
Once you've moved your template out to a separate file, your View might look something like this:
define([
'jquery',
'underscore',
'backbone',
'models/hf_models',
'views/hf_appView',
// Assume you created a separate directory for templates ...
'text!templates/list.template',
'utils'], function($, _, Backbone, PostModel, hfAppView, listTemplate, utils){
return hfPostView = Backbone.View.extend({
tagName: 'li',
className: 'securities',
// Woohoo! The text plugin provides our template content!
template: _.template(listTemplate),
events: {
'click .delete': 'deletePost'
},
// ... and so on ...
After that, your unit test does not have to change. Just requiring in views/hf_postView will automatically bring in the template text & any other dependencies.
For good measure: I'm a big fan of testing Backbone Views by asserting stuff as close to what the real application will see as possible. Some of my View tests look like:
describe('after the View has rendered', function() {
var view;
beforeEach(function() {
view = new MyView({
model: mockModel
});
view.render();
});
it('initially has a "Submit" button', function() {
var content = view.$el.html();
var matched = content.indexOf('<button>Submit</button>');
expect(match).to.be.greaterThan(-1);
});
});
Not exactly that, but you get the idea.
Related
I'm trying to use this code for view animation and calling it BaseView:
https://gist.github.com/brian-mann/3947145
then extending view like this:
define(['underscore',
'handlebars',
'views/BaseView',
'text!templates/components/login.tmpl'
], function (
_,
Handlebars,
BaseView,
loginTemplate
) {
'use strict';
var LoginView, errorMap;
LoginView = BaseView.extend({
compiledTemplate: Handlebars.compile(loginTemplate),
events: {
'submit #loginForm': 'login'
},
initialize : function(options){
this.proxyLoginSuccess = options.loginSuccess;
this.errorMap = options.errorMap;
}...
});
return LoginView;
});
It is giving me this error: Uncaught NoElError: An 'el' must be specified for a region.
I tried to remove this.ensureEl(); but doesn't make any difference. Appreciate any help.
You seem to be unclear about some Marionette concepts. The code you linked isn't a view, it's a Marionette Region, and is therefore used to show views, not to be extended from as in your code. This is how you would use it (e.g.):
myApp.addRegions({
fadeRegion: FadeTransitionRegion.extend({
el: "#some-selector"
})
});
Then, you instantiate a view instance and show it:
var myView = new LoginView({
el: "#another-selector"
});
myApp.fadeRegion.show(myView);
In any case, your view needs to have an el attribute defined, either in the view definition, or when it gets instantiated (as above).
If you're still confused about the attributes and specifying them in the view definition or at run time, I'd suggest you read the free preview to my Marionette book where it's explained in more detail.
I am writing a backbone (with require) application and need to search through a collection to pull out the first model (I'm using a unique id so there will only be one).
The issue I'm having is that I'm getting an error:
Uncaught TypeError: Object [object Object] has no method 'findWhere'
When it get to the line with the findWhere command.
The view initialization is:
initialize: function (models) {
this.locationCollection = new LocationCollection();
this.locationCollection.fetch();
this.render();
},
I then access the locationCollection later in another method, the first line of the method is where the error occurs:
createCrate: function (eventname) {
var setLocationList = this.locationCollection.findWhere({ Name: $('#location').val() });
console.log($('#location').val());
console.log(setLocationList);
},
Here is the declaration code the LocationCollection:
define([
'jquery',
'underscore',
'backbone',
'model/LocationModel'
], function ($, _, Backbone, LocationModel) {
var LocationCollection = Backbone.Collection.extend({
model: LocationModel,
url: "/api/locations"
});
return LocationCollection;
});
I can access the items in this.localCollection elsewhere in the view and output them into a backbone typeahead extension, so the collection has been populated.
Any idea why this collection cannot call findWhere?
_.findWhere was introduced in Underscore 1.4.4
and Backbone proxied it in Backbone 1.0
Make sure you have the adequate versions and your error should go away.
Question Updated
I am implementing a search and result for the same in the same page. i
have two seperate html views and views for search and result.
Here is what is happening..
From my router the below view is loaded. This view will load two other views called search and result. I have included the respective html views in the seperate views. but when i run the proj, it displays nothing.
Am i rendering the view in this file wrong ??
if i am rendering it wrong can somebody please tell me where is the mistake and how to rectify it ??
should i include the respective html templates of search and result also in this file ??
views.driver.search.js :
define(
[ 'jquery',
'underscore',
'backbone',
'app/models/model.driver',
'app/collections/collection.driver',
'app/models/model.driver.searchform',
'app/dashboard/views.driver.searchForm',
'app/models/model.driver.searchresult',
'app/dashboard/views.driver.searchResults',
],
function ($, _, Backbone, DriverModel,SearchResultCollection ,searchFormModel, searchFormView, SearchResultModel,SearchResultView) {
var DriverSearchView = Backbone.View.extend({
el: $('#content'),
initialize: function(){
this.render();
},
render: function () {
// var compiledTemplate = _.template(searchFormTemplate, {});
// this.$el.html(compiledTemplate);
this.searchFormInitModel = new searchFormModel();
var searchformView = new searchFormView({model:this.searchFormInitModel});
this.$("#content").html(searchformView.render());
var driver1 = new DriverModel({id:1,firstName:"driver1",lastName:'last',dob:'12/12/12001',});
var driver2 = new DriverModel({id:2,firstName:"driver2",lastName:'last',dob:'12/12/12001',});
var driver3 = new DriverModel({id:3,firstName:"driver3",lastName:'last',dob:'12/12/12001',});
var driver4 = new DriverModel({id:4,firstName:"driver3",lastName:'last',dob:'12/12/12001',});
this.searchResultCollection= new SearchResultCollection([driver1, driver2, driver3, driver4]);
var searchResultView = new SearchResultView({el:this.$el.find("#searchResult"),collection:this.searchResultCollection});
this.$("#content").html(searchResultView.render());
//this.$el.find("#searchResult").html(searchResultView.render());
}
}); // Our module now returns our view
return DriverSearchView;
});
I don't see an issue with your workflow. Reason you are not seeing your console.logs, is because you are returning an object literal containing your initialize function without actually calling the function. Is there a reason you need to return an object from your initialize module?
Anyhow, I am not entirely sure how you calling your app.js file, but try the following if you do not need to return an object back from your initialize module
define(
[
'jquery',
'underscore',
'backbone',
'app/router',
// 'datatable'
],
function ($, _, backbone, router){ //,datatable) {
var initialize = (function () {
console.log("App1");
debugger;
console.log("App2");
router.initialize();
console.log("App3");
})();
// Depending what you want to do here, you could just
// console.log("Router initialized...");
// router.initialize();
});
I have this collection view
define([
'jquery',
'underscore',
'backbone',
'views/project',
'collections/project-collection',
'templates'
], function ($, _, Backbone, ProjectView, ProjectCollection, JST) {
'use strict';
var ProjectListView = Backbone.View.extend({
template: JST['app/scripts/templates/projectList.ejs'],
el: $('#content'),
render: function() {
var projectCollection = new ProjectCollection();
projectCollection.fetch();
projectCollection.each(this.addOne(),this);
return this;
},
addOne: function(project) {
console.log('addOne function');
var projectView = new ProjectView({model: project});
this.$el.html( projectView.render().el);
}
});
return ProjectListView;
});
No matter what I try the model never gets passed through to the addOne function so the in the view that is instantiated by this method the call to
this.model.toJSON()
results in the old 'cannot call method .toJSON of undefined' error. I tried to inject the collection when this collection view was instantiated and that didn't work either. Obviously here it is in the dependency array and that doesn't work either. The model is definitely there as I can log projectCollection.model to the console inside the render function. I'm stumped.
I see two problems with your render: one you know about and one you don't.
The first problem is right here:
projectCollection.each(this.addOne(), this);
The parentheses on this.addOne() call the addOne method right there rather than passing the this.addOne function to each as a callback. You want this:
projectCollection.each(this.addOne, this);
The second problem is that you have to wait for the collection's fetch to return before anything will be in the collection. You can use the fetch's callbacks:
var _this = this;
projectCollection.fetch({
success: function() {
projectCollection.each(_this.addOne, _this);
}
});
or you can use the various events that fetch will fire, see the fetch documentation for details.
I am building my first Backbone app with similar structure to this Todo MVC example with Require.js and also using Backbone LocalStorage. Problem is when I run TweetsCollection.fetch() in HomeView, firebug gives me error: TypeError: options is undefined var method = options.update ? 'update' : 'reset';
TweetsCollection:
define([
'underscore',
'backbone',
'backboneLocalStorage',
'models/TweetModel'
], function(_, Backbone, Store, TweetModel) {
'use strict';
var TweetsCollection = Backbone.Collection.extend({
model: TweetModel,
localStorage: new Store('tweets-storage'),
initialize: function() {
console.log('Collection init...');
}
});
return new TweetsCollection();
});
HomeView init:
initialize: function() {
this.listenTo(TweetsCollection, 'add', this.addOne);
this.listenTo(TweetsCollection, 'reset', this.addAll);
this.listenTo(TweetsCollection, 'all', this.render);
TweetsCollection.fetch(); // <- Error here
},
I try to follow the example above, but I'm really lost with this.
The line of code where the error occurs is in Backbone's success callback that gets executed by Backbone.sync. Here's what that method looks like in Backbone 0.9.10:
options.success = function(collection, resp, options) {
var method = options.update ? 'update' : 'reset';
collection[method](resp, options);
if (success) success(collection, resp, options);
};
Prior to version 0.9.10, Backbone callback signature was:
options.success = function(resp, status, xhr) { ...
The Backbone.localStorage plugin, which you are evidently using, executes the callback method as follows (line 146):
if (options && options.success)
options.success(resp);
As you can see, it doesn't pass the arguments in correct order, and is missing the options argument altogether, which is where you are seeing the error.
So it would seem that the Backbone.localStorage plugin is currently incompatible with the newest Backbone version.
Edit: I went to report this issue to the author of the localStorage plugin, but looks like there is already a GitHub issue and pull request to fix this. It's not merged yet, so in the meantime you can use phoey's fork or downgrade to Backbone 0.9.9