0% found this document useful (0 votes)
13K views

Developing Backbone - Js Applications 2

The document contains code for testing a jQuery plugin called enumerate that numbers list items. It includes the plugin code, sample HTML, and multiple tests with QUnit to test different scenarios including passing start values and asynchronous behavior.
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
13K views

Developing Backbone - Js Applications 2

The document contains code for testing a jQuery plugin called enumerate that numbers list items. It includes the plugin code, sample HTML, and multiple tests with QUnit to test different scenarios including passing start values and asynchronous behavior.
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 25

<!

DOCTYPE html>
<html>
<head>
<title>QUnit Test</title>
<link rel="stylesheet" href="qunit.css">
<script src="qunit.js"></script>
<script src="app.js"></script>
<script src="tests.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit Test</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture"></div>
</body>
</html>

$.fn.enumerate = function( start ) {


if ( typeof start !== 'undefined' ) {
// Since `start` value was provided, enumerate and return
// the initial jQuery object to allow chaining.

return this.each(function(i){
$(this).prepend( '<b>' + ( i + start ) + '</b> ' );
});

} else {
// Since no `start` value was provided, function as a
// getter, returning the appropriate value from the first
// selected element.

var val = this.eq( 0 ).children( 'b' ).eq( 0 ).text();

1
return Number( val );
}
};

/*
<ul>
<li>1. hello</li>
<li>2. world</li>
<li>3. i</li>
<li>4. am</li>
<li>5. foo</li>
</ul>
*/

$.fn.enumerate = function( start ) {


if ( typeof start !== 'undefined' ) {
// Since `start` value was provided, enumerate and return
// the initial jQuery object to allow chaining.

return this.each(function(i){
$(this).prepend( '<b>' + ( i + start ) + '</b> ' );
});

} else {
// Since no `start` value was provided, function as a
// getter, returning the appropriate value from the first
// selected element.

var val = this.eq( 0 ).children( 'b' ).eq( 0 ).text();


return Number( val );
}
};

/*
<ul>
<li>1. hello</li>
<li>2. world</li>
<li>3. i</li>
<li>4. am</li>
<li>5. foo</li>
</ul>

2
*/

<div id="qunit-fixture">
<ul>
<li>hello</li>
<li>world</li>
<li>i</li>
<li>am</li>
<li>foo</li>
</ul>
</div>

module('jQuery#enumerate');

test( 'No arguments passed', 5, function() {


var items = $('#qunit-fixture li').enumerate(); // 0
equal( items.eq(0).text(), '0. hello', 'first item should have index 0' );
equal( items.eq(1).text(), '1. world', 'second item should have index 1' );
equal( items.eq(2).text(), '2. i', 'third item should have index 2' );
equal( items.eq(3).text(), '3. am', 'fourth item should have index 3' );
equal( items.eq(4).text(), '4. foo', 'fifth item should have index 4' );
});

test( '0 passed as an argument', 5, function() {


var items = $('#qunit-fixture li').enumerate( 0 );
equal( items.eq(0).text(), '0. hello', 'first item should have index 0' );
equal( items.eq(1).text(), '1. world', 'second item should have index 1' );
equal( items.eq(2).text(), '2. i', 'third item should have index 2' );
equal( items.eq(3).text(), '3. am', 'fourth item should have index 3' );
equal( items.eq(4).text(), '4. foo', 'fifth item should have index 4' );
});

test( '1 passed as an argument', 3, function() {

3
var items = $('#qunit-fixture li').enumerate( 1 );
equal( items.eq(0).text(), '1. hello', 'first item should have index 1' );
equal( items.eq(1).text(), '2. world', 'second item should have index 2' );
equal( items.eq(2).text(), '3. i', 'third item should have index 3' );
equal( items.eq(3).text(), '4. am', 'fourth item should have index 4' );
equal( items.eq(4).text(), '5. foo', 'fifth item should have index 5' );
});

test('An async test', function(){


stop();
expect( 1 );
$.ajax({
url: '/test',
dataType: 'json',
success: function( data ){
deepEqual(data, {
topic: 'hello',
message: 'hi there!''
});
ok(true, 'Asynchronous test passed!');
start();
}
});
});

test('should call all subscribers for a message exactly once', function () {


var message = getUniqueString();
var spy = this.spy();

PubSub.subscribe( message, spy );


PubSub.publishSync( message, 'Hello World' );

ok( spy.calledOnce, 'the subscriber was called once' );


});

4
test( 'should inspect the jQuery.getJSON usage of jQuery.ajax', function () {
this.spy( jQuery, 'ajax' );

jQuery.getJSON( '/todos/completed' );

ok( jQuery.ajax.calledOnce );
equals( jQuery.ajax.getCall(0).args[0].url, '/todos/completed' );
equals( jQuery.ajax.getCall(0).args[0].dataType, 'json' );
});

test( 'Should call a subscriber with standard matching': function () {


var spy = sinon.spy();

PubSub.subscribe( 'message', spy );


PubSub.publishSync( 'message', { id: 45 } );

assertTrue( spy.calledWith( { id: 45 } ) );


});

test( 'Should call a subscriber with strict matching': function () {


var spy = sinon.spy();

PubSub.subscribe( 'message', spy );


PubSub.publishSync( 'message', 'many', 'arguments' );
PubSub.publishSync( 'message', 12, 34 );

// This passes
assertTrue( spy.calledWith('many') );

5
// This however, fails
assertTrue( spy.calledWithExactly( 'many' ) );
});

test( 'Should call a subscriber and maintain call order': function () {


var a = sinon.spy();
var b = sinon.spy();

PubSub.subscribe( 'message', a );
PubSub.subscribe( 'event', b );

PubSub.publishSync( 'message', { id: 45 } );


PubSub.publishSync( 'event', [1, 2, 3] );

assertTrue( a.calledBefore(b) );
assertTrue( b.calledAfter(a) );
});

test( 'Should call a subscriber and check call counts', function () {


var message = getUniqueString();
var spy = this.spy();

PubSub.subscribe( message, spy );


PubSub.publishSync( message, 'some payload' );

// Passes if spy was called once and only once.


ok( spy.calledOnce ); // calledTwice and calledThrice are also supported

// The number of recorded calls.


equal( spy.callCount, 1 );

6
// Directly checking the arguments of the call
equals( spy.getCall(0).args[0], message );
});

var TodoList = Backbone.Collection.extend({


model: Todo
});

// Let's assume our instance of this collection is


this.todoList;

this.todoStub = sinon.stub( window, 'Todo' );

this.todoStub.restore();

setup: function() {
this.model = new Backbone.Model({
id: 2,
title: 'Hello world'
});
this.todoStub.returns( this.model );
});

this.todoList.model = Todo;

7
module( 'Should function when instantiated with model literals', {

setup:function() {

this.todoStub = sinon.stub(window, 'Todo');


this.model = new Backbone.Model({
id: 2,
title: 'Hello world'
});

this.todoStub.returns(this.model);
this.todos = new TodoList();

// Let's reset the relationship to use a stub


this.todos.model = Todo;

// add a model
this.todos.add({
id: 2,
title: 'Hello world'
});
},

teardown: function() {
this.todoStub.restore();
}

});

test('should add a model', function() {


equal( this.todos.length, 1 );
});

test('should find a model by id', function() {


equal( this.todos.get(5).get('id'), 5 );
});
});
test('should call all subscribers when exceptions', function () {
var myAPI = { clearTodo: function () {} };

8
var spy = this.spy();
var mock = this.mock( myAPI );
mock.expects( 'clearTodo' ).once().throws();

PubSub.subscribe( 'message', myAPI.clearTodo );


PubSub.subscribe( 'message', spy );
PubSub.publishSync( 'message', undefined );

mock.verify();
ok( spy.calledOnce );
});

module( 'About Backbone.Model');

test('Can be created with default values for its attributes.', function() {


expect( 3 );

var todo = new Todo();


equal( todo.get('text'), '' );
equal( todo.get('done'), false );
equal( todo.get('order'), 0 );
});

test('Will set attributes on the model instance when created.', function() {


expect( 1 );

var todo = new Todo( { text: 'Get oil change for car.' } );
equal( todo.get('text'), 'Get oil change for car.' );

});

test('Will call a custom initialize function on the model instance when created.',
function() {
expect( 1 );

var toot = new Todo({ text: 'Stop monkeys from throwing their own crap!' });
equal( toot.get('text'), 'Stop monkeys from throwing their own rainbows!' );
});

9
test('Fires a custom event when the state changes.', function() {
expect( 1 );

var spy = this.spy();


var todo = new Todo();

todo.on( 'change', spy );


// Change the model state
todo.set( { text: 'new text' } );

ok( spy.calledOnce, 'A change event callback was correctly triggered' );


});

test('Can contain custom validation rules, and will trigger an invalid event on failed
validation.', function() {
expect( 3 );

var errorCallback = this.spy();


var todo = new Todo();

todo.on('invalid', errorCallback);
// Change the model state in such a way that validation will fail
todo.set( { done: 'not a boolean' } );

ok( errorCallback.called, 'A failed validation correctly triggered an error' );


notEqual( errorCallback.getCall(0), undefined );
equal( errorCallback.getCall(0).args[1], 'Todo.done must be a boolean value.' );

});

module('Test Collection', {

setup: function() {

// Define new todos


this.todoOne = new Todo;
this.todoTwo = new Todo({
title: "Buy some milk"

10
});

// Create a new collection of todos for testing


this.todos = new TodoList([this.todoOne, this.todoTwo]);
}
});

test('Has the Todo model', function() {


expect( 1 );
equal(this.todos.model, Todo);
});

test('Uses local storage', function() {


expect( 1 );
equal(this.todos.localStorage, new Store('todos-backbone'));
});

// done
test('returns an array of the todos that are done', function() {
expect( 1 );
this.todoTwo.done = true;
deepEqual(this.todos.done(), [this.todoTwo]);
});

// remaining
test('returns an array of the todos that are not done', function() {
expect( 1 );
this.todoTwo.done = true;
deepEqual(this.todos.remaining(), [this.todoOne]);
});

// clear
test('destroys the current todo from local storage', function() {
expect( 2 );
deepEqual(this.todos.models, [this.todoOne, this.todoTwo]);
this.todos.clear(this.todoOne);
deepEqual(this.todos.models, [this.todoTwo]);
});

// Order sets the order on todos ascending numerically


test('defaults to one when there arent any items in the collection', function() {
expect( 1 );

11
this.emptyTodos = new TodoApp.Collections.TodoList;
equal(this.emptyTodos.order(), 0);
});

test('Increments the order by one each time', function() {


expect( 2 );
equal(this.todos.order(this.todoOne), 1);
equal(this.todos.order(this.todoTwo), 2);
});

module( 'About Backbone.View', {


setup: function() {
$('body').append('<ul id="todoList"></ul>');
this.todoView = new TodoView({ model: new Todo() });
},
teardown: function() {
this.todoView.remove();
$('#todoList').remove();
}
});

test('Should be tied to a DOM element when created, based off the property provided.',
function() {
expect( 1 );
equal( this.todoView.el.tagName.toLowerCase(), 'li' );
});

test('Is backed by a model instance, which provides the data.', function() {


expect( 2 );
notEqual( this.todoView.model, undefined );
equal( this.todoView.model.get('done'), false );
});

test('Can render, after which the DOM representation of the view will be visible.',
function() {
this.todoView.render();

12
// Append the DOM representation of the view to ul#todoList
$('ul#todoList').append(this.todoView.el);

// Check the number of li items rendered to the list


equal($('#todoList').find('li').length, 1);
});

asyncTest('Can wire up view methods to DOM elements.', function() {


expect( 2 );
var viewElt;

$('#todoList').append( this.todoView.render().el );

setTimeout(function() {
viewElt = $('#todoList li input.check').filter(':first');

equal(viewElt.length > 0, true);

// Ensure QUnit knows we can continue


start();
}, 1000, 'Expected DOM Elt to exist');

// Trigger the view to toggle the 'done' status on an item or items


$('#todoList li input.check').click();

// Check the done status for the model is true


equal( this.todoView.model.get('done'), true );
});

module( 'About Backbone Applications' , {


setup: function() {
Backbone.localStorageDB = new Store('testTodos');
$('#qunit-fixture').append('<div id="app"></div>');
this.App = new TodoApp({ appendTo: $('#app') });
},

teardown: function() {
this.App.todos.reset();

13
$('#app').remove();
}
});

test('Should bootstrap the application by initializing the Collection.', function() {


expect( 2 );

// The todos collection should not be undefined


notEqual( this.App.todos, undefined );

// The initial length of our todos should however be zero


equal( this.App.todos.length, 0 );
});

test( 'Should bind Collection events to View creation.' , function() {

// Set the value of a brand new todo within the input box
$('#new-todo').val( 'Buy some milk' );

// Trigger the enter (return) key to be pressed inside #new-todo


// causing the new item to be added to the todos collection
$('#new-todo').trigger(new $.Event( 'keypress', { keyCode: 13 } ));

// The length of our collection should now be 1


equal( this.App.todos.length, 1 );
});

// cranium.js - Cranium.Events

var Cranium = Cranium || {};

// Set DOM selection utility


var $ = this.jQuery || this.Zepto || document.querySelectorAll.bind(document);

// Mix in to any object in order to provide it with custom events.


var Events = Cranium.Events = {
// Keeps list of events and associated listeners
channels: {},

14
// Counter
eventNumber: 0,

// Announce events and passes data to the listeners;


trigger: function (events, data) {
for (var topic in Cranium.Events.channels){
if (Cranium.Events.channels.hasOwnProperty(topic)) {
if (topic.split("-")[0] == events){
Cranium.Events.channels[topic](data) !== false || delete
Cranium.Events.channels[topic];
}
}
}
},
// Registers an event type and its listener
on: function (events, callback) {
Cranium.Events.channels[events + --Cranium.Events.eventNumber] = callback;
},
// Unregisters an event type and its listener
off: function(topic) {
var topic;
for (topic in Cranium.Events.channels) {
if (Cranium.Events.channels.hasOwnProperty(topic)) {
if (topic.split("-")[0] == events) {
delete Cranium.Events.channels[topic];
}
}
}
}
};

// cranium.js - Cranium.Model

// Attributes represents data, model's properties.


// These are to be passed at Model instantiation.
// Also we are creating id for each Model instance

15
// so that it can identify itself (e.g. on chage
// announcements)
var Model = Cranium.Model = function (attributes) {
this.id = _.uniqueId('model');
this.attributes = attributes || {};
};

// Getter (accessor) method;


// returns named data item
Cranium.Model.prototype.get = function(attrName) {
return this.attributes[attrName];
};

// Setter (mutator) method;


// Set/mix in into model mapped data (e.g.{name: "John"})
// and publishes the change event
Cranium.Model.prototype.set = function(attrs){
if (_.isObject(attrs)) {
_.extend(this.attributes, attrs);
this.change(this.attributes);
}
return this;
};

// Returns clone of the Models data object


// (used for view template rendering)
Cranium.Model.prototype.toJSON = function(options) {
return _.clone(this.attributes);
};

// Helper function that announces changes to the Model


// and passes the new data
Cranium.Model.prototype.change = function(attrs){
this.trigger(this.id + 'update', attrs);
};

// Mix in Event system


_.extend(Cranium.Model.prototype, Cranium.Events);

16
// DOM View
var View = Cranium.View = function (options) {
// Mix in options object (e.g extending functionality)
_.extend(this, options);
this.id = _.uniqueId('view');
};

// Mix in Event system


_.extend(Cranium.View.prototype, Cranium.Events);

// cranium.js - Cranium.Controller

// Controller tying together a model and view


var Controller = Cranium.Controller = function(options){
// Mix in options object (e.g extending functionality)
_.extend(this, options);
this.id = _.uniqueId('controller');
var parts, selector, eventType;

// Parses Events object passed during the definition of the


// controller and maps it to the defined method to handle it;
if(this.events){
_.each(this.events, function(method, eventName){
parts = eventName.split('.');
selector = parts[0];
eventType = parts[1];
$(selector)['on' + eventType] = this[method];
}.bind(this));
}
};

<!doctype html>
<html lang="en">

17
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
</head>
<body>
<div id="todo">
</div>
<script type="text/template" class="todo-template">
<div>
<input id="todo_complete" type="checkbox" <%= completed %>>
<%= title %>
</div>
</script>
<script src="underscore-min.js"></script>
<script src="cranium.js"></script>
<script src="example.js"></script>
</body>
</html>

// example.js - usage of Cranium MVC

// And todo instance


var todo1 = new Cranium.Model({
title: "",
completed: ""
});

console.log("First todo title - nothing set: " + todo1.get('title'));


todo1.set({title: "Do something"});
console.log("Its changed now: " + todo1.get('title'));
''
// View instance
var todoView = new Cranium.View({
// DOM element selector
el: '#todo',

// Todo template; Underscore temlating used


template: _.template($('.todo-template').innerHTML),

18
init: function (model) {
this.render( model.attributes );

this.on(model.id + 'update', this.render.bind(this));


},
render: function (data) {
console.log("View about to render.");
$(this.el).innerHTML = this.template( data );
}
});

var todoController = new Cranium.Controller({


// Specify the model to update
model: todo1,

// and the view to observe this model


view: todoView,

events: {
"#todo.click" : "toggleComplete"
},

// Initialize everything
initialize: function () {
this.view.init(this.model);
return this;
},
// Toggles the value of the todo in the Model
toggleComplete: function () {
var completed = todoController.model.get('completed');
console.log("Todo old 'completed' value?", completed);
todoController.model.set({ completed: (!completed) ? 'checked': '' });
console.log("Todo new 'completed' value?", todoController.model.get('completed'));
return this;
}
});

// Let's kick start things off


todoController.initialize();

19
todo1.set({ title: "Due to this change Model will notify View and it will re-render"})

// The DOM element for a todo item...


app.TodoView = Backbone.View.extend({

//... is a list tag.


tagName: 'li',

// Pass the contents of the todo template through a templating


// function, cache it for a single todo
template: _.template( $('#item-template').html() ),

// The DOM events specific to an item.


events: {
'click .toggle': 'togglecompleted'
},

// The TodoView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **Todo** and a **TodoView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
this.listenTo( this.model, 'change', this.render );
this.listenTo( this.model, 'destroy', this.remove );
},

// Re-render the titles of the todo item.


render: function() {
this.$el.html( this.template( this.model.attributes ) );
return this;
},

// Toggle the `"completed"` state of the model.


togglecompleted: function() {
this.model.toggle();
},
});

20
var myApplication = (function(){
function(){
// ...
},
return {
// ...
}
})();

var myViews = (function(){


return {
TodoView: Backbone.View.extend({ .. }),
TodosView: Backbone.View.extend({ .. }),
AboutView: Backbone.View.extend({ .. })
//etc.
};
})();

var myApplication_todoView = Backbone.View.extend({}),


myApplication_todosView = Backbone.View.extend({});
};
})();

/* Doesn't check for existence of myApplication */


var myApplication = {};

/*
Does check for existence. If already defined, we use that instance.
Option 1: if(!myApplication) myApplication = {};

21
Option 2: var myApplication = myApplication || {};
We can then populate our object literal to support models, views and collections (or any
data, really):
*/

var myApplication = {
models : {},
views : {
pages : {}
},
collections : {}
};

var myTodosViews = myTodosViews || {};


myTodosViews.todoView = Backbone.View.extend({});
myTodosViews.todosView = Backbone.View.extend({});

var myConfig = {
language: 'english',
defaults: {
enableDelegation: true,
maxTodos: 40
},
theme: {
skin: 'a',
toolbars: {
index: 'ui-navigation-toolbar',
pages: 'ui-custom-toolbar'
}
}
}

YAHOO.util.Dom.getElementsByClassName('test');

22
var todoApp = todoApp || {};

// perform similar check for nested children


todoApp.routers = todoApp.routers || {};
todoApp.model = todoApp.model || {};
todoApp.model.special = todoApp.model.special || {};

// routers
todoApp.routers.Workspace = Backbone.Router.extend({});
todoApp.routers.TodoSearch = Backbone.Router.extend({});

// models
todoApp.model.Todo = Backbone.Model.extend({});
todoApp.model.Notes = Backbone.Model.extend({});

// special models
todoApp.model.special.Admin = Backbone.Model.extend({});

// Provide top-level namespaces for our javascript.


(function() {
window.dc = {};
dc.controllers = {};
dc.model = {};
dc.app = {};
dc.ui = {};
})();

// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns

23
// the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;

// Underscore methods that we want to implement on the Collection.


// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject',
'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all',
'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first',
'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'indexOf',
'shuffle', 'lastIndexOf', 'isEmpty', 'chain'];

// Mix in each Underscore method as a proxy to `Collection#models`.


_.each(methods, function(method) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});

var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};

Backbone.sync = function(method, model, options) {


var type = methodMap[method];

// ... Followed by lots of Backbone.js configuration, then..

24
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;

// Depending on whether we're using pushState or hashes, and whether


// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window)
.on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window)
.on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
...

25

You might also like