<!
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