Update Backbone tests to 1.1.1.

This commit is contained in:
John-David Dalton
2014-02-15 14:32:56 -08:00
parent 486ba5fe0a
commit 88d5f5d76c
8 changed files with 300 additions and 121 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Copyright (c) 2010-2014 Jeremy Ashkenas, DocumentCloud
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation

View File

@@ -1,20 +1,36 @@
// Backbone.js 1.1.0 // Backbone.js 1.1.1
// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
// For all details and documentation: // For all details and documentation:
// http://backbonejs.org // http://backbonejs.org
(function(){ (function(root, factory) {
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch(e) {}
factory(root, exports, _, $);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(this, function(root, Backbone, _, $) {
// Initial Setup // Initial Setup
// ------------- // -------------
// Save a reference to the global object (`window` in the browser, `exports`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be // Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used. // restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone; var previousBackbone = root.Backbone;
@@ -25,25 +41,12 @@
var slice = array.slice; var slice = array.slice;
var splice = array.splice; var splice = array.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both the browser and the server.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Current version of the library. Keep in sync with `package.json`. // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.1.0'; Backbone.VERSION = '1.1.1';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable. // the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object. // to its previous owner. Returns a reference to this Backbone object.
@@ -109,7 +112,7 @@
var retain, ev, events, names, i, l, j, k; var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) { if (!name && !callback && !context) {
this._events = {}; this._events = void 0;
return this; return this;
} }
names = name ? [name] : _.keys(this._events); names = name ? [name] : _.keys(this._events);
@@ -205,7 +208,7 @@
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
} }
}; };
@@ -350,7 +353,7 @@
// Trigger all relevant attribute changes. // Trigger all relevant attribute changes.
if (!silent) { if (!silent) {
if (changes.length) this._pending = true; if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) { for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options); this.trigger('change:' + changes[i], this, current[changes[i]], options);
} }
@@ -361,6 +364,7 @@
if (changing) return this; if (changing) return this;
if (!silent) { if (!silent) {
while (this._pending) { while (this._pending) {
options = this._pending;
this._pending = false; this._pending = false;
this.trigger('change', this, options); this.trigger('change', this, options);
} }
@@ -528,9 +532,12 @@
// using Backbone's restful methods, override this to change the endpoint // using Backbone's restful methods, override this to change the endpoint
// that will be called. // that will be called.
url: function() { url: function() {
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base; if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
}, },
// **parse** converts a response into the hash of attributes to be `set` on // **parse** converts a response into the hash of attributes to be `set` on
@@ -546,7 +553,7 @@
// A model is new if it has never been saved to the server, and lacks an id. // A model is new if it has never been saved to the server, and lacks an id.
isNew: function() { isNew: function() {
return this.id == null; return !this.has(this.idAttribute);
}, },
// Check if the model is currently in a valid state. // Check if the model is currently in a valid state.
@@ -650,7 +657,7 @@
options.index = index; options.index = index;
model.trigger('remove', model, this, options); model.trigger('remove', model, this, options);
} }
this._removeReference(model); this._removeReference(model, options);
} }
return singular ? models[0] : models; return singular ? models[0] : models;
}, },
@@ -676,11 +683,11 @@
// Turn bare objects into model references, and prevent invalid models // Turn bare objects into model references, and prevent invalid models
// from being added. // from being added.
for (i = 0, l = models.length; i < l; i++) { for (i = 0, l = models.length; i < l; i++) {
attrs = models[i]; attrs = models[i] || {};
if (attrs instanceof Model) { if (attrs instanceof Model) {
id = model = attrs; id = model = attrs;
} else { } else {
id = attrs[targetModel.prototype.idAttribute]; id = attrs[targetModel.prototype.idAttribute || 'id'];
} }
// If a duplicate is found, prevent it from being added and // If a duplicate is found, prevent it from being added and
@@ -700,14 +707,13 @@
model = models[i] = this._prepareModel(attrs, options); model = models[i] = this._prepareModel(attrs, options);
if (!model) continue; if (!model) continue;
toAdd.push(model); toAdd.push(model);
this._addReference(model, options);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
} }
if (order) order.push(existing || model);
// Do not add multiple models with the same `id`.
model = existing || model;
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
modelMap[model.id] = true;
} }
// Remove nonexistent models if appropriate. // Remove nonexistent models if appropriate.
@@ -745,7 +751,7 @@
} }
if (sort || (order && order.length)) this.trigger('sort', this, options); if (sort || (order && order.length)) this.trigger('sort', this, options);
} }
// Return the added (or merged) model (or models). // Return the added (or merged) model (or models).
return singular ? models[0] : models; return singular ? models[0] : models;
}, },
@@ -757,7 +763,7 @@
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) { for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]); this._removeReference(this.models[i], options);
} }
options.previousModels = this.models; options.previousModels = this.models;
this._reset(); this._reset();
@@ -798,7 +804,7 @@
// Get a model from the set by id. // Get a model from the set by id.
get: function(obj) { get: function(obj) {
if (obj == null) return void 0; if (obj == null) return void 0;
return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
}, },
// Get the model at the given index. // Get the model at the given index.
@@ -874,7 +880,7 @@
if (!options.wait) this.add(model, options); if (!options.wait) this.add(model, options);
var collection = this; var collection = this;
var success = options.success; var success = options.success;
options.success = function(model, resp, options) { options.success = function(model, resp) {
if (options.wait) collection.add(model, options); if (options.wait) collection.add(model, options);
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
@@ -904,10 +910,7 @@
// Prepare a hash of attributes (or other model) to be added to this // Prepare a hash of attributes (or other model) to be added to this
// collection. // collection.
_prepareModel: function(attrs, options) { _prepareModel: function(attrs, options) {
if (attrs instanceof Model) { if (attrs instanceof Model) return attrs;
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
options.collection = this; options.collection = this;
var model = new this.model(attrs, options); var model = new this.model(attrs, options);
@@ -916,8 +919,16 @@
return false; return false;
}, },
// Internal method to create a model's ties to a collection.
_addReference: function(model, options) {
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
if (!model.collection) model.collection = this;
model.on('all', this._onModelEvent, this);
},
// Internal method to sever a model's ties to a collection. // Internal method to sever a model's ties to a collection.
_removeReference: function(model) { _removeReference: function(model, options) {
if (this === model.collection) delete model.collection; if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this); model.off('all', this._onModelEvent, this);
}, },
@@ -946,7 +957,7 @@
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
'lastIndexOf', 'isEmpty', 'chain']; 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
// Mix in each Underscore method as a proxy to `Collection#models`. // Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) { _.each(methods, function(method) {
@@ -958,7 +969,7 @@
}); });
// Underscore methods that take a property name as an argument. // Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy']; var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties. // Use attributes instead of properties.
_.each(attributeMethods, function(method) { _.each(attributeMethods, function(method) {
@@ -1180,7 +1191,9 @@
return xhr; return xhr;
}; };
var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); var noXhrPatch =
typeof window !== 'undefined' && !!window.ActiveXObject &&
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
// Map from CRUD to HTTP for our default `Backbone.sync` implementation. // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = { var methodMap = {
@@ -1239,7 +1252,7 @@
var router = this; var router = this;
Backbone.history.route(route, function(fragment) { Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment); var args = router._extractParameters(route, fragment);
callback && callback.apply(router, args); router.execute(callback, args);
router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args); router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args); Backbone.history.trigger('route', router, name, args);
@@ -1247,6 +1260,12 @@
return this; return this;
}, },
// Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup.
execute: function(callback, args) {
if (callback) callback.apply(this, args);
},
// Simple proxy to `Backbone.history` to save a fragment into the history. // Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) { navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options); Backbone.history.navigate(fragment, options);
@@ -1271,10 +1290,10 @@
route = route.replace(escapeRegExp, '\\$&') route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?') .replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional) { .replace(namedParam, function(match, optional) {
return optional ? match : '([^\/]+)'; return optional ? match : '([^/?]+)';
}) })
.replace(splatParam, '(.*?)'); .replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '$'); return new RegExp('^' + route + '(?:\\?(.*))?$');
}, },
// Given a route, and a URL fragment that it matches, return the array of // Given a route, and a URL fragment that it matches, return the array of
@@ -1282,7 +1301,9 @@
// treated as `null` to normalize cross-browser behavior. // treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) { _extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1); var params = route.exec(fragment).slice(1);
return _.map(params, function(param) { return _.map(params, function(param, i) {
// Don't decode the search params.
if (i === params.length - 1) return param || null;
return param ? decodeURIComponent(param) : null; return param ? decodeURIComponent(param) : null;
}); });
} }
@@ -1320,8 +1341,8 @@
// Cached regex for removing a trailing slash. // Cached regex for removing a trailing slash.
var trailingSlash = /\/$/; var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash and query. // Cached regex for stripping urls of hash.
var pathStripper = /[?#].*$/; var pathStripper = /#.*$/;
// Has the history handling already been started? // Has the history handling already been started?
History.started = false; History.started = false;
@@ -1333,6 +1354,11 @@
// twenty times a second. // twenty times a second.
interval: 50, interval: 50,
// Are we at the app root?
atRoot: function() {
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
},
// Gets the true hash value. Cannot use location.hash directly due to bug // Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded. // in Firefox where location.hash will always be decoded.
getHash: function(window) { getHash: function(window) {
@@ -1345,7 +1371,7 @@
getFragment: function(fragment, forcePushState) { getFragment: function(fragment, forcePushState) {
if (fragment == null) { if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) { if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = this.location.pathname; fragment = decodeURI(this.location.pathname + this.location.search);
var root = this.root.replace(trailingSlash, ''); var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else { } else {
@@ -1376,7 +1402,8 @@
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) { if (oldIE && this._wantsHashChange) {
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment); this.navigate(fragment);
} }
@@ -1394,7 +1421,6 @@
// opened by a non-pushState browser. // opened by a non-pushState browser.
this.fragment = fragment; this.fragment = fragment;
var loc = this.location; var loc = this.location;
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
// Transition from hashChange to pushState or vice versa if both are // Transition from hashChange to pushState or vice versa if both are
// requested. // requested.
@@ -1402,17 +1428,17 @@
// If we've started off with a route from a `pushState`-enabled // If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it... // browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !atRoot) { if (!this._hasPushState && !this.atRoot()) {
this.fragment = this.getFragment(null, true); this.fragment = this.getFragment(null, true);
this.location.replace(this.root + this.location.search + '#' + this.fragment); this.location.replace(this.root + '#' + this.fragment);
// Return immediately as browser will do redirect to new url // Return immediately as browser will do redirect to new url
return true; return true;
// Or if we've started out with a hash-based route, but we're currently // Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead... // in a browser where it could be `pushState`-based instead...
} else if (this._hasPushState && atRoot && loc.hash) { } else if (this._hasPushState && this.atRoot() && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, ''); this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); this.history.replaceState({}, document.title, this.root + this.fragment);
} }
} }
@@ -1472,7 +1498,7 @@
var url = this.root + (fragment = this.getFragment(fragment || '')); var url = this.root + (fragment = this.getFragment(fragment || ''));
// Strip the fragment of the query and hash for matching. // Strip the hash for matching.
fragment = fragment.replace(pathStripper, ''); fragment = fragment.replace(pathStripper, '');
if (this.fragment === fragment) return; if (this.fragment === fragment) return;
@@ -1578,4 +1604,6 @@
}; };
}; };
}).call(this); return Backbone;
}));

View File

@@ -85,6 +85,11 @@
equal(col2.get(model.clone()), col2.first()); equal(col2.get(model.clone()), col2.first());
}); });
test('get with "undefined" id', function() {
var collection = new Backbone.Collection([{id: 1}, {id: 'undefined'}]);
equal(collection.get(1).id, 1);
}),
test("update index when id changes", 4, function() { test("update index when id changes", 4, function() {
var col = new Backbone.Collection(); var col = new Backbone.Collection();
col.add([ col.add([
@@ -107,7 +112,7 @@
equal(col.pluck('label').join(' '), 'a b c d'); equal(col.pluck('label').join(' '), 'a b c d');
}); });
test("add", 10, function() { test("add", 14, function() {
var added, opts, secondAdded; var added, opts, secondAdded;
added = opts = secondAdded = null; added = opts = secondAdded = null;
e = new Backbone.Model({id: 10, label : 'e'}); e = new Backbone.Model({id: 10, label : 'e'});
@@ -136,6 +141,18 @@
equal(atCol.length, 4); equal(atCol.length, 4);
equal(atCol.at(1), e); equal(atCol.at(1), e);
equal(atCol.last(), h); equal(atCol.last(), h);
var coll = new Backbone.Collection(new Array(2));
var addCount = 0;
coll.on('add', function(){
addCount += 1;
});
coll.add([undefined, f, g]);
equal(coll.length, 5);
equal(addCount, 3);
coll.add(new Array(4));
equal(coll.length, 9);
equal(addCount, 7);
}); });
test("add multiple models", 6, function() { test("add multiple models", 6, function() {
@@ -535,7 +552,7 @@
equal(coll.findWhere({a: 4}), void 0); equal(coll.findWhere({a: 4}), void 0);
}); });
test("Underscore methods", 14, function() { test("Underscore methods", 16, function() {
equal(col.map(function(model){ return model.get('label'); }).join(' '), 'a b c d'); equal(col.map(function(model){ return model.get('label'); }).join(' '), 'a b c d');
equal(col.any(function(model){ return model.id === 100; }), false); equal(col.any(function(model){ return model.id === 100; }), false);
equal(col.any(function(model){ return model.id === 0; }), true); equal(col.any(function(model){ return model.id === 0; }), true);
@@ -554,9 +571,12 @@
.value(), .value(),
[4, 0]); [4, 0]);
deepEqual(col.difference([c, d]), [a, b]); deepEqual(col.difference([c, d]), [a, b]);
ok(col.include(col.sample()));
var first = col.first();
ok(col.indexBy('id')[first.id] === first);
}); });
test("reset", 12, function() { test("reset", 16, function() {
var resetCount = 0; var resetCount = 0;
var models = col.models; var models = col.models;
col.on('reset', function() { resetCount += 1; }); col.on('reset', function() { resetCount += 1; });
@@ -576,6 +596,15 @@
col.reset(); col.reset();
equal(col.length, 0); equal(col.length, 0);
equal(resetCount, 4); equal(resetCount, 4);
var f = new Backbone.Model({id: 20, label : 'f'});
col.reset([undefined, f]);
equal(col.length, 2);
equal(resetCount, 5);
col.reset(new Array(4));
equal(col.length, 4);
equal(resetCount, 6);
}); });
test ("reset with different values", function(){ test ("reset with different values", function(){
@@ -942,20 +971,6 @@
strictEqual(c.length, 0); strictEqual(c.length, 0);
}); });
test("set with many models does not overflow the stack", function() {
var n = 150000;
var collection = new Backbone.Collection();
var models = [];
for (var i = 0; i < n; i++) {
models.push({id: i});
}
collection.set(models);
equal(collection.length, n);
collection.reset();
collection.set(models, {at: 0});
equal(collection.length, n);
});
test("set with only cids", 3, function() { test("set with only cids", 3, function() {
var m1 = new Backbone.Model; var m1 = new Backbone.Model;
var m2 = new Backbone.Model; var m2 = new Backbone.Model;
@@ -1274,4 +1289,50 @@
equal(job.items.get(2).subItems.get(3).get('subName'), 'NewThree'); equal(job.items.get(2).subItems.get(3).get('subName'), 'NewThree');
}); });
test('_addReference binds all collection events & adds to the lookup hashes', 9, function() {
var calls = {add: 0, remove: 0};
var Collection = Backbone.Collection.extend({
_addReference: function(model) {
Backbone.Collection.prototype._addReference.apply(this, arguments);
calls.add++;
equal(model, this._byId[model.id]);
equal(model, this._byId[model.cid]);
equal(model._events.all.length, 1);
},
_removeReference: function(model) {
Backbone.Collection.prototype._removeReference.apply(this, arguments);
calls.remove++;
equal(this._byId[model.id], void 0);
equal(this._byId[model.cid], void 0);
equal(model.collection, void 0);
equal(model._events.all, void 0);
}
});
var collection = new Collection();
var model = collection.add({id: 1});
collection.remove(model);
equal(calls.add, 1);
equal(calls.remove, 1);
});
test('Do not allow duplicate models to be `add`ed or `set`', function() {
var c = new Backbone.Collection();
c.add([{id: 1}, {id: 1}]);
equal(c.length, 1);
equal(c.models.length, 1);
c.set([{id: 1}, {id: 1}]);
equal(c.length, 1);
equal(c.models.length, 1);
});
})(); })();

View File

@@ -4,10 +4,16 @@
var ajax = Backbone.ajax; var ajax = Backbone.ajax;
var emulateHTTP = Backbone.emulateHTTP; var emulateHTTP = Backbone.emulateHTTP;
var emulateJSON = Backbone.emulateJSON; var emulateJSON = Backbone.emulateJSON;
var history = window.history;
var pushState = history.pushState;
var replaceState = history.replaceState;
QUnit.testStart(function() { QUnit.testStart(function() {
var env = this.config.current.testEnvironment; var env = this.config.current.testEnvironment;
// We never want to actually call these during tests.
history.pushState = history.replaceState = function(){};
// Capture ajax settings for comparison. // Capture ajax settings for comparison.
Backbone.ajax = function(settings) { Backbone.ajax = function(settings) {
env.ajaxSettings = settings; env.ajaxSettings = settings;
@@ -30,6 +36,8 @@
Backbone.ajax = ajax; Backbone.ajax = ajax;
Backbone.emulateHTTP = emulateHTTP; Backbone.emulateHTTP = emulateHTTP;
Backbone.emulateJSON = emulateJSON; Backbone.emulateJSON = emulateJSON;
history.pushState = pushState;
history.replaceState = replaceState;
}); });
})(); })();

View File

@@ -305,7 +305,7 @@
test("if callback is truthy but not a function, `on` should throw an error just like jQuery", 1, function() { test("if callback is truthy but not a function, `on` should throw an error just like jQuery", 1, function() {
var view = _.extend({}, Backbone.Events).on('test', 'noop'); var view = _.extend({}, Backbone.Events).on('test', 'noop');
throws(function() { raises(function() {
view.trigger('test'); view.trigger('test');
}); });
}); });

View File

@@ -262,6 +262,26 @@
model.set({result: void 0}); model.set({result: void 0});
}); });
test("nested set triggers with the correct options", function() {
var model = new Backbone.Model();
var o1 = {};
var o2 = {};
var o3 = {};
model.on('change', function(__, options) {
switch (model.get('a')) {
case 1:
equal(options, o1);
return model.set('a', 2, o2);
case 2:
equal(options, o2);
return model.set('a', 3, o3);
case 3:
equal(options, o3);
}
});
model.set('a', 1, o1);
});
test("multiple unsets", 1, function() { test("multiple unsets", 1, function() {
var i = 0; var i = 0;
var counter = function(){ i++; }; var counter = function(){ i++; };

View File

@@ -5,10 +5,10 @@
var lastRoute = null; var lastRoute = null;
var lastArgs = []; var lastArgs = [];
function onRoute(router, route, args) { var onRoute = function(router, route, args) {
lastRoute = route; lastRoute = route;
lastArgs = args; lastArgs = args;
} };
var Location = function(href) { var Location = function(href) {
this.replace(href); this.replace(href);
@@ -16,8 +16,11 @@
_.extend(Location.prototype, { _.extend(Location.prototype, {
parser: document.createElement('a'),
replace: function(href) { replace: function(href) {
_.extend(this, _.pick($('<a></a>', {href: href})[0], this.parser.href = href;
_.extend(this, _.pick(this.parser,
'href', 'href',
'hash', 'hash',
'host', 'host',
@@ -64,7 +67,7 @@
this.value = value; this.value = value;
} }
}; };
_.bindAll(ExternalObject); _.bindAll(ExternalObject, 'routingFunction');
var Router = Backbone.Router.extend({ var Router = Backbone.Router.extend({
@@ -87,7 +90,7 @@
":repo/compare/*from...*to": "github", ":repo/compare/*from...*to": "github",
"decode/:named/*splat": "decode", "decode/:named/*splat": "decode",
"*first/complex-*part/*rest": "complex", "*first/complex-*part/*rest": "complex",
":entity?*args": "query", "query/:entity": "query",
"function/:value": ExternalObject.routingFunction, "function/:value": ExternalObject.routingFunction,
"*anything": "anything" "*anything": "anything"
}, },
@@ -208,6 +211,11 @@
equal(router.page, '20'); equal(router.page, '20');
}); });
test("routes via navigate with params", 1, function() {
Backbone.history.navigate('query/test?a=b', {trigger: true});
equal(router.queryArgs, 'a=b');
});
test("routes via navigate for backwards-compatibility", 2, function() { test("routes via navigate for backwards-compatibility", 2, function() {
Backbone.history.navigate('search/manhattan/p20', true); Backbone.history.navigate('search/manhattan/p20', true);
equal(router.query, 'manhattan'); equal(router.query, 'manhattan');
@@ -285,7 +293,7 @@
}); });
test("routes (query)", 5, function() { test("routes (query)", 5, function() {
location.replace('http://example.com#mandel?a=b&c=d'); location.replace('http://example.com#query/mandel?a=b&c=d');
Backbone.history.checkUrl(); Backbone.history.checkUrl();
equal(router.entity, 'mandel'); equal(router.entity, 'mandel');
equal(router.queryArgs, 'a=b&c=d'); equal(router.queryArgs, 'a=b&c=d');
@@ -535,7 +543,7 @@
Backbone.history.stop(); Backbone.history.stop();
location.replace('http://example.com/root/x/y?a=b'); location.replace('http://example.com/root/x/y?a=b');
location.replace = function(url) { location.replace = function(url) {
strictEqual(url, '/root/?a=b#x/y'); strictEqual(url, '/root/#x/y?a=b');
}; };
Backbone.history = _.extend(new Backbone.History, { Backbone.history = _.extend(new Backbone.History, {
location: location, location: location,
@@ -552,7 +560,7 @@
test("#1695 - hashChange to pushState with search.", 1, function() { test("#1695 - hashChange to pushState with search.", 1, function() {
Backbone.history.stop(); Backbone.history.stop();
location.replace('http://example.com/root?a=b#x/y'); location.replace('http://example.com/root#x/y?a=b');
Backbone.history = _.extend(new Backbone.History, { Backbone.history = _.extend(new Backbone.History, {
location: location, location: location,
history: { history: {
@@ -601,7 +609,7 @@
test("#2062 - Trigger 'route' event on router instance.", 2, function() { test("#2062 - Trigger 'route' event on router instance.", 2, function() {
router.on('route', function(name, args) { router.on('route', function(name, args) {
strictEqual(name, 'routeEvent'); strictEqual(name, 'routeEvent');
deepEqual(args, ['x']); deepEqual(args, ['x', null]);
}); });
location.replace('http://example.com#route-event/x'); location.replace('http://example.com#route-event/x');
Backbone.history.checkUrl(); Backbone.history.checkUrl();
@@ -684,7 +692,7 @@
} }
}); });
location.replace('http://example.com/root/path'); location.replace('http://example.com/root/path');
Backbone.history.start({pushState: true, root: 'root'}); Backbone.history.start({pushState: true, hashChange: false, root: 'root'});
Backbone.history.navigate(''); Backbone.history.navigate('');
}); });
@@ -699,7 +707,7 @@
} }
}); });
location.replace('http://example.com/path'); location.replace('http://example.com/path');
Backbone.history.start({pushState: true}); Backbone.history.start({pushState: true, hashChange: false});
Backbone.history.navigate(''); Backbone.history.navigate('');
}); });
@@ -722,8 +730,66 @@
var router = new Router; var router = new Router;
location.replace('http://example.com/'); location.replace('http://example.com/');
Backbone.history.start({pushState: true}); Backbone.history.start({pushState: true, hashChange: false});
Backbone.history.navigate('path?query#hash', true); Backbone.history.navigate('path?query#hash', true);
}); });
test('Do not decode the search params.', function() {
var Router = Backbone.Router.extend({
routes: {
path: function(params){
strictEqual(params, 'x=y%20z');
}
}
});
var router = new Router;
Backbone.history.navigate('path?x=y%20z', true);
});
test('Navigate to a hash url.', function() {
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
Backbone.history.start({pushState: true});
var Router = Backbone.Router.extend({
routes: {
path: function(params) {
strictEqual(params, 'x=y');
}
}
});
var router = new Router;
location.replace('http://example.com/path?x=y#hash');
Backbone.history.checkUrl();
});
test('#navigate to a hash url.', function() {
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
Backbone.history.start({pushState: true});
var Router = Backbone.Router.extend({
routes: {
path: function(params) {
strictEqual(params, 'x=y');
}
}
});
var router = new Router;
Backbone.history.navigate('path?x=y#hash', true);
});
test('unicode pathname', 1, function() {
location.replace('http://example.com/myyjä');
Backbone.history.stop();
Backbone.history = _.extend(new Backbone.History, {location: location});
var Router = Backbone.Router.extend({
routes: {
myyjä: function() {
ok(true);
}
}
});
var router = new Router;
Backbone.history.start({pushState: true});
});
})(); })();

View File

@@ -39,23 +39,23 @@
test("delegateEvents", 6, function() { test("delegateEvents", 6, function() {
var counter1 = 0, counter2 = 0; var counter1 = 0, counter2 = 0;
var view = new Backbone.View({el: '<p><a id="test"></a></p>'}); var view = new Backbone.View({el: '#testElement'});
view.increment = function(){ counter1++; }; view.increment = function(){ counter1++; };
view.$el.on('click', function(){ counter2++; }); view.$el.on('click', function(){ counter2++; });
var events = {'click #test': 'increment'}; var events = {'click h1': 'increment'};
view.delegateEvents(events); view.delegateEvents(events);
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 1); equal(counter1, 1);
equal(counter2, 1); equal(counter2, 1);
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 2); equal(counter1, 2);
equal(counter2, 2); equal(counter2, 2);
view.delegateEvents(events); view.delegateEvents(events);
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 3); equal(counter1, 3);
equal(counter2, 3); equal(counter2, 3);
}); });
@@ -92,24 +92,24 @@
test("undelegateEvents", 6, function() { test("undelegateEvents", 6, function() {
var counter1 = 0, counter2 = 0; var counter1 = 0, counter2 = 0;
var view = new Backbone.View({el: '<p><a id="test"></a></p>'}); var view = new Backbone.View({el: '#testElement'});
view.increment = function(){ counter1++; }; view.increment = function(){ counter1++; };
view.$el.on('click', function(){ counter2++; }); view.$el.on('click', function(){ counter2++; });
var events = {'click #test': 'increment'}; var events = {'click h1': 'increment'};
view.delegateEvents(events); view.delegateEvents(events);
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 1); equal(counter1, 1);
equal(counter2, 1); equal(counter2, 1);
view.undelegateEvents(); view.undelegateEvents();
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 1); equal(counter1, 1);
equal(counter2, 2); equal(counter2, 2);
view.delegateEvents(events); view.delegateEvents(events);
view.$('#test').trigger('click'); view.$('h1').trigger('click');
equal(counter1, 2); equal(counter1, 2);
equal(counter2, 3); equal(counter2, 3);
}); });
@@ -218,7 +218,7 @@
$('body').trigger('fake$event').trigger('fake$event'); $('body').trigger('fake$event').trigger('fake$event');
equal(count, 2); equal(count, 2);
$('body').unbind('.namespaced'); $('body').off('.namespaced');
$('body').trigger('fake$event'); $('body').trigger('fake$event');
equal(count, 2); equal(count, 2);
}); });
@@ -304,28 +304,24 @@
ok(view.$el.has('a')); ok(view.$el.has('a'));
}); });
test("events passed in options", 2, function() { test("events passed in options", 1, function() {
var counter = 0; var counter = 0;
var View = Backbone.View.extend({ var View = Backbone.View.extend({
el: '<p><a id="test"></a></p>', el: '#testElement',
increment: function() { increment: function() {
counter++; counter++;
} }
}); });
var view = new View({events:{'click #test':'increment'}}); var view = new View({
var view2 = new View({events:function(){ events: {
return {'click #test':'increment'}; 'click h1': 'increment'
}}); }
});
view.$('#test').trigger('click'); view.$('h1').trigger('click').trigger('click');
view2.$('#test').trigger('click');
equal(counter, 2); equal(counter, 2);
view.$('#test').trigger('click');
view2.$('#test').trigger('click');
equal(counter, 4);
}); });
})(); })();