Update vendors.

Former-commit-id: baf89d2c3bd7077462995bffa7f8bff1e1cf28f9
This commit is contained in:
John-David Dalton
2012-12-28 19:47:44 -06:00
parent 8ec7b84a78
commit 87f880ca52
12 changed files with 499 additions and 381 deletions

View File

@@ -160,7 +160,8 @@
if (callback || context) {
for (j = 0, k = list.length; j < k; j++) {
ev = list[j];
if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
if ((callback && callback !== ev.callback &&
callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
events.push(ev);
}
@@ -190,27 +191,25 @@
// An inversion-of-control version of `on`. Tell *this* object to listen to
// an event in another object ... keeping track of what it's listening to.
listenTo: function(object, events, callback, context) {
context = context || this;
listenTo: function(object, events, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = object._listenerId || (object._listenerId = _.uniqueId('l'));
listeners[id] = object;
object.on(events, callback || context, context);
object.on(events, callback || this, this);
return this;
},
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function(object, events, callback, context) {
context = context || this;
stopListening: function(object, events, callback) {
var listeners = this._listeners;
if (!listeners) return;
if (object) {
object.off(events, callback, context);
object.off(events, callback, this);
if (!events && !callback) delete listeners[object._listenerId];
} else {
for (var id in listeners) {
listeners[id].off(null, null, context);
listeners[id].off(null, null, this);
}
this._listeners = {};
}
@@ -235,15 +234,14 @@
var defaults;
var attrs = attributes || {};
this.cid = _.uniqueId('c');
this.changed = {};
this.attributes = {};
this._changes = [];
if (options && options.collection) this.collection = options.collection;
if (options && options.parse) attrs = this.parse(attrs, options);
if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
this.set(attrs, {silent: true});
this._currentAttributes = _.clone(this.attributes);
this._previousAttributes = _.clone(this.attributes);
if (options && options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) {
attrs = _.defaults({}, attrs, defaults);
}
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
@@ -287,47 +285,72 @@
return this.get(attr) != null;
},
// ----------------------------------------------------------------------
// Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it.
set: function(key, val, options) {
var attr, attrs;
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key)) {
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// Extract attributes and options.
var silent = options && options.silent;
var unset = options && options.unset;
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
var now = this.attributes;
// For each `set` attribute...
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
// Update or delete the current value, and track the change.
unset ? delete now[attr] : now[attr] = val;
this._changes.push(attr, val);
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Signal that the model's state has potentially changed, and we need
// to recompute the actual changes.
this._hasComputed = false;
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// Fire the `"change"` events.
if (!silent) this.change(options);
if (changing) return this;
if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
@@ -345,15 +368,53 @@
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// ---------------------------------------------------------------------
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overriden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
options.success = function(model, resp, options) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
};
@@ -364,55 +425,50 @@
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
var attrs, current, done;
var attrs, model, success, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || _.isObject(key)) {
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else if (key != null) {
} else {
(attrs = {})[key] = val;
}
options = options ? _.clone(options) : {};
// If we're "wait"-ing to set changed attributes, validate early.
if (options.wait) {
if (attrs && !this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
// If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
// Regular saves `set` attributes before persisting to the server.
var silentOptions = _.extend({}, options, {silent: true});
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
options = _.extend({validate: true}, options);
// Do not persist invalid models.
if (!attrs && !this._validate(null, options)) return false;
if (!this._validate(attrs, options)) return false;
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
done = true;
success = options.success;
options.success = function(model, resp, options) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (!model.set(serverAttrs, options)) return false;
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
};
// Finish configuring and sending the Ajax request.
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method == 'patch') options.attrs = attrs;
var xhr = this.sync(method, this, options);
xhr = this.sync(method, this, options);
// When using `wait`, reset attributes to original values unless
// `success` has been called already.
if (!done && options.wait) {
this.clear(silentOptions);
this.set(current, silentOptions);
}
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
@@ -429,13 +485,13 @@
model.trigger('destroy', model, model.collection, options);
};
options.success = function(resp) {
options.success = function(model, resp, options) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
};
if (this.isNew()) {
options.success();
options.success(this, null, options);
return false;
}
@@ -469,115 +525,20 @@
return this.id == null;
},
// Call this method to manually fire a `"change"` event for this model and
// a `"change:attribute"` event for each changed attribute.
// Calling this will cause all objects observing the model to update.
change: function(options) {
var changing = this._changing;
this._changing = true;
// Generate the changes to be triggered on the model.
var triggers = this._computeChanges(true);
this._pending = !!triggers.length;
for (var i = triggers.length - 2; i >= 0; i -= 2) {
this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
}
if (changing) return this;
// Trigger a `change` while there have been changes.
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
this._previousAttributes = _.clone(this.attributes);
}
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (!this._hasComputed) this._computeChanges();
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false, old = this._previousAttributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Looking at the built up list of `set` attribute changes, compute how
// many of the attributes have actually changed. If `loud`, return a
// boiled-down list of only the real changes.
_computeChanges: function(loud) {
this.changed = {};
var already = {};
var triggers = [];
var current = this._currentAttributes;
var changes = this._changes;
// Loop through the current queue of potential model changes.
for (var i = changes.length - 2; i >= 0; i -= 2) {
var key = changes[i], val = changes[i + 1];
if (already[key]) continue;
already[key] = true;
// Check if the attribute has been modified since the last change,
// and update `this.changed` accordingly. If we're inside of a `change`
// call, also add a trigger to the list.
if (!_.isEqual(current[key], val)) {
this.changed[key] = val;
if (!loud) continue;
triggers.push(key, val);
current[key] = val;
}
}
if (loud) this._changes = [];
// Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
this._hasComputed = true;
return triggers;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
// Check if the model is currently in a valid state.
isValid: function(options) {
return !this.validate || !this.validate(this.attributes, options);
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire a general
// `"error"` event and call the error callback, if specified.
_validate: function(attrs, options) {
if (!this.validate) return true;
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validate(attrs, options);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
if (options && options.error) options.error(this, error, options);
this.trigger('error', this, error, options);
this.trigger('invalid', this, error, options || {});
return false;
}
@@ -620,20 +581,22 @@
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set. Pass **silent** to avoid
// firing the `add` event for every new model.
// Add a model, or list of models to the set.
add: function(models, options) {
var i, args, length, model, existing, needsSort;
var at = options && options.at;
var sort = ((options && options.sort) == null ? true : options.sort);
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, model, attrs, existing, sort, doSort, sortAttr, at, add;
add = [];
at = options.at;
sort = this.comparator && (at == null) && (options.sort == null || options.sort);
sortAttr = _.isString(this.comparator) ? this.comparator : null;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = models.length - 1; i >= 0; i--) {
if(!(model = this._prepareModel(models[i], options))) {
this.trigger("error", this, models[i], options);
models.splice(i, 1);
for (i = 0, l = models.length; i < l; i++) {
attrs = models[i];
if(!(model = this._prepareModel(attrs, options))) {
this.trigger('invalid', this, attrs, options);
continue;
}
models[i] = model;
@@ -641,14 +604,16 @@
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(model)) {
if (options && options.merge) {
existing.set(model.attributes, options);
needsSort = sort;
if (options.merge) {
existing.set(attrs === model ? model.attributes : attrs, options);
if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
}
models.splice(i, 1);
continue;
}
// This is a new model, push it to the `add` list.
add.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
@@ -657,31 +622,37 @@
}
// See if sorting is needed, update `length` and splice in new models.
if (models.length) needsSort = sort;
this.length += models.length;
args = [at != null ? at : this.models.length, 0];
push.apply(args, models);
splice.apply(this.models, args);
if (add.length) {
if (sort) doSort = true;
this.length += add.length;
if (at != null) {
splice.apply(this.models, [at, 0].concat(add));
} else {
push.apply(this.models, add);
}
}
// Sort the collection if appropriate.
if (needsSort && this.comparator && at == null) this.sort({silent: true});
// Silently sort the collection if appropriate.
if (doSort) this.sort({silent: true});
if (options && options.silent) return this;
if (options.silent) return this;
// Trigger `add` events.
while (model = models.shift()) {
model.trigger('add', model, this, options);
for (i = 0, l = add.length; i < l; i++) {
(model = add[i]).trigger('add', model, this, options);
}
// Trigger `sort` if the collection was sorted.
if (doSort) this.trigger('sort', this, options);
return this;
},
// Remove a model, or a list of models from the set. Pass silent to avoid
// firing the `remove` event for every model removed.
// Remove a model, or a list of models from the set.
remove: function(models, options) {
var i, l, index, model;
options || (options = {});
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]);
if (!model) continue;
@@ -762,14 +733,16 @@
if (!this.comparator) {
throw new Error('Cannot sort a set without a comparator');
}
options || (options = {});
// Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) {
this.models = this.sortBy(this.comparator, this);
} else {
this.models.sort(_.bind(this.comparator, this));
}
if (!options || !options.silent) this.trigger('sort', this, options);
if (!options.silent) this.trigger('sort', this, options);
return this;
},
@@ -781,10 +754,10 @@
// Smartly update a collection with a change set of models, adding,
// removing, and merging as necessary.
update: function(models, options) {
var model, i, l, existing;
var add = [], remove = [], modelMap = {};
options = _.extend({add: true, merge: true, remove: true}, options);
if (options.parse) models = this.parse(models, options);
var model, i, l, existing;
var add = [], remove = [], modelMap = {};
// Allow a single model (or no argument) to be passed.
if (!_.isArray(models)) models = models ? [models] : [];
@@ -836,9 +809,8 @@
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var collection = this;
var success = options.success;
options.success = function(resp, status, xhr) {
options.success = function(collection, resp, options) {
var method = options.update ? 'update' : 'reset';
collection[method](resp, options);
if (success) success(collection, resp, options);
@@ -850,9 +822,9 @@
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
var collection = this;
options = options ? _.clone(options) : {};
model = this._prepareModel(model, options);
var collection = this;
if (!model) return false;
if (!options.wait) collection.add(model, options);
var success = options.success;
@@ -929,7 +901,7 @@
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take',
'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle',
'initial', 'rest', 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle',
'lastIndexOf', 'isEmpty'];
// Mix in each Underscore method as a proxy to `Collection#models`.
@@ -969,7 +941,7 @@
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /:\w+/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
@@ -1020,7 +992,9 @@
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, '([^\/]+)')
.replace(namedParam, function(match, optional){
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
@@ -1121,9 +1095,9 @@
// 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).bind('popstate', this.checkUrl);
Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).bind('hashchange', this.checkUrl);
Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
@@ -1155,7 +1129,7 @@
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval);
History.started = false;
},
@@ -1298,18 +1272,6 @@
return this;
},
// For small amounts of DOM Elements, where a full-blown template isn't
// needed, use **make** to manufacture elements, one at a time.
//
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
//
make: function(tagName, attributes, content) {
var el = document.createElement(tagName);
if (attributes) Backbone.$(el).attr(attributes);
if (content != null) Backbone.$(el).html(content);
return el;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
@@ -1347,9 +1309,9 @@
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.bind(eventName, method);
this.$el.on(eventName, method);
} else {
this.$el.delegate(selector, eventName, method);
this.$el.on(eventName, selector, method);
}
}
},
@@ -1358,7 +1320,7 @@
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.unbind('.delegateEvents' + this.cid);
this.$el.off('.delegateEvents' + this.cid);
},
// Performs the initial configuration of a View with a set of options.
@@ -1379,7 +1341,8 @@
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
@@ -1461,13 +1424,13 @@
}
var success = options.success;
options.success = function(resp, status, xhr) {
if (success) success(resp, status, xhr);
options.success = function(resp) {
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
var error = options.error;
options.error = function(xhr, status, thrown) {
options.error = function(xhr) {
if (error) error(model, xhr, options);
model.trigger('error', model, xhr, options);
};

View File

@@ -82,7 +82,7 @@ $(document).ready(function() {
equal(col.get(101), model);
var Col2 = Backbone.Collection.extend({ model: MongoModel });
col2 = new Col2();
var col2 = new Col2();
col2.push(model);
equal(col2.get({_id: 101}), model);
equal(col2.get(model.clone()), model);
@@ -362,7 +362,9 @@ $(document).ready(function() {
test("model destroy removes from all collections", 3, function() {
var e = new Backbone.Model({id: 5, title: 'Othello'});
e.sync = function(method, model, options) { options.success({}); };
e.sync = function(method, model, options) {
options.success(model, [], options);
};
var colE = new Backbone.Collection([e]);
var colF = new Backbone.Collection([e]);
e.destroy();
@@ -417,7 +419,7 @@ $(document).ready(function() {
equal(model.collection, collection);
});
test("create enforces validation", 1, function() {
test("create with validate:true enforces validation", 1, function() {
var ValidatingModel = Backbone.Model.extend({
validate: function(attrs) {
return "fail";
@@ -427,10 +429,10 @@ $(document).ready(function() {
model: ValidatingModel
});
var col = new ValidatingCollection();
equal(col.create({"foo":"bar"}), false);
equal(col.create({"foo":"bar"}, {validate:true}), false);
});
test("a failing create runs the error callback", 1, function() {
test("a failing create returns model with errors", function() {
var ValidatingModel = Backbone.Model.extend({
validate: function(attrs) {
return "fail";
@@ -439,11 +441,10 @@ $(document).ready(function() {
var ValidatingCollection = Backbone.Collection.extend({
model: ValidatingModel
});
var flag = false;
var callback = function(model, error) { flag = true; };
var col = new ValidatingCollection();
col.create({"foo":"bar"}, { error: callback });
equal(flag, true);
var m = col.create({"foo":"bar"});
equal(m.validationError, 'fail');
equal(col.length, 1);
});
test("initialize", 1, function() {
@@ -567,7 +568,7 @@ $(document).ready(function() {
equal(col.length, 0);
});
test("#861, adding models to a collection which do not pass validation", function() {
test("#861, adding models to a collection which do not pass validation, with validate:true", function() {
var Model = Backbone.Model.extend({
validate: function(attrs) {
if (attrs.id == 3) return "id can't be 3";
@@ -581,18 +582,18 @@ $(document).ready(function() {
var collection = new Collection;
collection.on("error", function() { ok(true); });
collection.add([{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}]);
collection.add([{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}], {validate:true});
deepEqual(collection.pluck('id'), [1, 2, 4, 5, 6]);
});
test("Invalid models are discarded.", 5, function() {
test("Invalid models are discarded with validate:true.", 5, function() {
var collection = new Backbone.Collection;
collection.on('test', function() { ok(true); });
collection.model = Backbone.Model.extend({
validate: function(attrs){ if (!attrs.valid) return 'invalid'; }
});
var model = new collection.model({id: 1, valid: true});
collection.add([model, {id: 2}]);
collection.add([model, {id: 2}], {validate:true});
model.trigger('test');
ok(collection.get(model.cid));
ok(collection.get(1));
@@ -656,7 +657,7 @@ $(document).ready(function() {
}
};
col.sync = m.sync = function( method, collection, options ){
options.success();
options.success(collection, [], options);
};
col.fetch(opts);
col.create(m, opts);
@@ -674,7 +675,9 @@ $(document).ready(function() {
test("#1447 - create with wait adds model.", 1, function() {
var collection = new Backbone.Collection;
var model = new Backbone.Model;
model.sync = function(method, model, options){ options.success(); };
model.sync = function(method, model, options){
options.success(model, [], options);
};
collection.on('add', function(){ ok(true); });
collection.create(model, {wait: true});
});
@@ -880,6 +883,24 @@ $(document).ready(function() {
equal(c.length, 2);
});
test("update + merge with default values defined", function() {
var Model = Backbone.Model.extend({
defaults: {
key: 'value'
}
});
var m = new Model({id: 1});
var col = new Backbone.Collection([m], {model: Model});
equal(col.first().get('key'), 'value');
col.update({id: 1, key: 'other'});
equal(col.first().get('key'), 'other');
col.update({id: 1, other: 'value'});
equal(col.first().get('key'), 'other');
equal(col.length, 1);
});
test("#1894 - Push should not trigger a sort", 0, function() {
var Collection = Backbone.Collection.extend({
comparator: 'id',
@@ -937,4 +958,32 @@ $(document).ready(function() {
Backbone.ajax = ajax;
});
test("`add` only `sort`s when necessary", 2, function () {
var collection = new (Backbone.Collection.extend({
comparator: 'a'
}))([{id: 1}, {id: 2}, {id: 3}]);
collection.on('sort', function () { ok(true); });
collection.add({id: 4}); // do sort, new model
collection.add({id: 1, a: 1}, {merge: true}); // do sort, comparator change
collection.add({id: 1, b: 1}, {merge: true}); // don't sort, no comparator change
collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no comparator change
collection.add(collection.models); // don't sort, nothing new
collection.add(collection.models, {merge: true}); // don't sort
});
test("`add` only `sort`s when necessary with comparator function", 3, function () {
var collection = new (Backbone.Collection.extend({
comparator: function(a, b) {
a.get('a') > b.get('a') ? 1 : (a.get('a') < b.get('a') ? -1 : 0);
}
}))([{id: 1}, {id: 2}, {id: 3}]);
collection.on('sort', function () { ok(true); });
collection.add({id: 4}); // do sort, new model
collection.add({id: 1, a: 1}, {merge: true}); // do sort, model change
collection.add({id: 1, b: 1}, {merge: true}); // do sort, model change
collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no model change
collection.add(collection.models); // don't sort, nothing new
collection.add(collection.models, {merge: true}); // don't sort
});
});

View File

@@ -86,30 +86,6 @@ $(document).ready(function() {
b.trigger('change');
});
test("listenTo with context", 1, function() {
var a = _.extend({}, Backbone.Events);
var ctx = {};
a.listenTo(a, 'foo', function(){ equal(this, ctx); }, ctx);
a.trigger('foo');
});
test("stopListening with context", 2, function() {
var a = _.extend({}, Backbone.Events);
var ctx = {};
var calledWithContext = false;
var calledWithoutContext = false;
a.listenTo(a, 'foo', function(){ calledWithContext = true; }, ctx);
a.listenTo(a, 'foo', function(){ calledWithoutContext = true; });
a.stopListening(a, 'foo', null, ctx);
a.trigger('foo');
equal(false, calledWithContext);
equal(true, calledWithoutContext);
});
test("trigger all for each event", 3, function() {
var a, b, obj = { counter: 0 };
_.extend(obj, Backbone.Events);
@@ -384,4 +360,8 @@ $(document).ready(function() {
Backbone.trigger('all');
});
test("once without a callback is a noop", 0, function() {
_.extend({}, Backbone.Events).once('event').trigger('event');
});
});

View File

@@ -202,9 +202,9 @@ $(document).ready(function() {
ok(changeCount == 1, "Change count should NOT have incremented.");
a.validate = function(attrs) {
equal(attrs.foo, void 0, "don't ignore values when unsetting");
equal(attrs.foo, void 0, "validate:true passed while unsetting");
};
a.unset('foo');
a.unset('foo', {validate: true});
equal(a.get('foo'), void 0, "Foo should have changed");
delete a.validate;
ok(changeCount == 2, "Change count should have incremented for unset.");
@@ -213,6 +213,24 @@ $(document).ready(function() {
equal(a.id, undefined, "Unsetting the id should remove the id property.");
});
test("#2030 - set with failed validate, followed by another set triggers change", function () {
var attr = 0, main = 0, error = 0;
var Model = Backbone.Model.extend({
validate: function (attr) {
if (attr.x > 1) {
error++;
return "this is an error";
}
}
});
var model = new Model({x:0});
model.on('change:x', function () { attr++; });
model.on('change', function () { main++; });
model.set({x:2}, {validate:true});
model.set({x:1}, {validate:true});
deepEqual([attr, main, error], [1, 1, 1]);
});
test("set triggers changes in the correct order", function() {
var value = null;
var model = new Backbone.Model;
@@ -223,15 +241,16 @@ $(document).ready(function() {
equal(value, 'last');
});
test("set falsy values in the correct order", 1, function() {
test("set falsy values in the correct order", 2, function() {
var model = new Backbone.Model({result: 'result'});
model.on('change', function() {
equal(model.changed.result, false);
equal(model.changed.result, void 0);
equal(model.previous('result'), false);
});
model.set({result: void 0}, {silent: true});
model.set({result: null}, {silent: true});
model.set({result: false}, {silent: true});
model.change();
model.set({result: void 0});
});
test("multiple unsets", 1, function() {
@@ -245,14 +264,12 @@ $(document).ready(function() {
equal(i, 2, 'Unset does not fire an event for missing attributes.');
});
test("unset and changedAttributes", 2, function() {
test("unset and changedAttributes", 1, function() {
var model = new Backbone.Model({a: 1});
model.unset('a', {silent: true});
var changedAttributes = model.changedAttributes();
ok('a' in changedAttributes, 'changedAttributes should contain unset properties');
changedAttributes = model.changedAttributes();
ok('a' in changedAttributes, 'changedAttributes should contain unset properties when running changedAttributes again after an unset.');
model.on('change', function() {
ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties');
});
model.unset('a');
});
test("using a non-default id attribute.", 5, function() {
@@ -272,6 +289,21 @@ $(document).ready(function() {
equal(model.get('name'), '');
});
test("setting an object", 1, function() {
var model = new Backbone.Model({
custom: { foo: 1 }
});
model.on('change', function() {
ok(1);
});
model.set({
custom: { foo: 1 } // no change should be fired
});
model.set({
custom: { foo: 2 } // change event should be fired
});
});
test("clear", 3, function() {
var changed;
var model = new Backbone.Model({id: 1, name : "Model"});
@@ -308,9 +340,9 @@ $(document).ready(function() {
equal(model.get('two'), 4);
});
test("change, hasChanged, changedAttributes, previous, previousAttributes", 12, function() {
var model = new Backbone.Model({name : "Tim", age : 10});
equal(model.changedAttributes(), false);
test("change, hasChanged, changedAttributes, previous, previousAttributes", 9, function() {
var model = new Backbone.Model({name: "Tim", age: 10});
deepEqual(model.changedAttributes(), false);
model.on('change', function() {
ok(model.hasChanged('name'), 'name changed');
ok(!model.hasChanged('age'), 'age did not');
@@ -320,17 +352,13 @@ $(document).ready(function() {
});
equal(model.hasChanged(), false);
equal(model.hasChanged(undefined), false);
model.set({name : 'Rob'}, {silent : true});
equal(model.hasChanged(), true);
equal(model.hasChanged(undefined), true);
equal(model.hasChanged('name'), true);
model.change();
model.set({name : 'Rob'});
equal(model.get('name'), 'Rob');
});
test("changedAttributes", 3, function() {
var model = new Backbone.Model({a: 'a', b: 'b'});
equal(model.changedAttributes(), false);
deepEqual(model.changedAttributes(), false);
equal(model.changedAttributes({a: 'a'}), false);
equal(model.changedAttributes({a: 'b'}).a, 'b');
});
@@ -341,8 +369,7 @@ $(document).ready(function() {
model.on('change', function(model, options) {
value = options.prefix + model.get('name');
});
model.set({name: 'Bob'}, {silent: true});
model.change({prefix: 'Mr. '});
model.set({name: 'Bob'}, {prefix: 'Mr. '});
equal(value, 'Mr. Bob');
model.set({name: 'Sue'}, {prefix: 'Ms. '});
equal(value, 'Ms. Sue');
@@ -368,19 +395,21 @@ $(document).ready(function() {
model.set({lastName: 'Hicks'});
});
test("validate after save", 1, function() {
test("validate after save", 2, function() {
var lastError, model = new Backbone.Model();
model.validate = function(attrs) {
if (attrs.admin) return "Can't change admin status.";
};
model.sync = function(method, model, options) {
options.success.call(this, {admin: true});
options.success.call(this, this, {admin: true}, options);
};
model.save(null, {error: function(model, error) {
model.on('invalid', function(model, error) {
lastError = error;
}});
});
model.save(null);
equal(lastError, "Can't change admin status.");
equal(model.validationError, "Can't change admin status.");
});
test("save", 2, function() {
@@ -406,13 +435,24 @@ $(document).ready(function() {
test("save in positional style", 1, function() {
var model = new Backbone.Model();
model.sync = function(method, model, options) {
options.success();
options.success(model, {}, options);
};
model.save('title', 'Twelfth Night');
equal(model.get('title'), 'Twelfth Night');
});
test("save with non-object success response", 2, function () {
var model = new Backbone.Model();
model.sync = function(method, model, options) {
options.success(model, '', options);
options.success(model, null, options);
};
model.save({testing:'empty'}, {
success: function (model) {
deepEqual(model.attributes, {testing:'empty'});
}
});
});
test("fetch", 2, function() {
doc.fetch();
@@ -442,14 +482,16 @@ $(document).ready(function() {
model.validate = function(attrs) {
if (attrs.admin != this.get('admin')) return "Can't change admin status.";
};
model.on('error', function(model, error) {
model.on('invalid', function(model, error) {
lastError = error;
});
var result = model.set({a: 100});
equal(result, model);
equal(model.get('a'), 100);
equal(lastError, undefined);
result = model.set({a: 200, admin: false});
result = model.set({admin: true});
equal(model.get('admin'), true);
result = model.set({a: 200, admin: false}, {validate:true});
equal(lastError, "Can't change admin status.");
equal(result, false);
equal(model.get('a'), 100);
@@ -467,10 +509,10 @@ $(document).ready(function() {
model.set({name: "Two"});
equal(model.get('name'), 'Two');
equal(error, undefined);
model.unset('name');
model.unset('name', {validate: true});
equal(error, true);
equal(model.get('name'), 'Two');
model.clear();
model.clear({validate:true});
equal(model.get('name'), 'Two');
delete model.validate;
model.clear();
@@ -483,21 +525,18 @@ $(document).ready(function() {
model.validate = function(attrs) {
if (attrs.admin) return "Can't change admin status.";
};
var callback = function(model, error) {
lastError = error;
};
model.on('error', function(model, error) {
model.on('invalid', function(model, error) {
boundError = true;
});
var result = model.set({a: 100}, {error: callback});
var result = model.set({a: 100}, {validate:true});
equal(result, model);
equal(model.get('a'), 100);
equal(lastError, undefined);
equal(model.validationError, null);
equal(boundError, undefined);
result = model.set({a: 200, admin: true}, {error: callback});
result = model.set({a: 200, admin: true}, {validate:true});
equal(result, false);
equal(model.get('a'), 100);
equal(lastError, "Can't change admin status.");
equal(model.validationError, "Can't change admin status.");
equal(boundError, true);
});
@@ -592,6 +631,13 @@ $(document).ready(function() {
ok(model.get('x') === a);
});
test("set same value does not trigger change", 0, function() {
var model = new Backbone.Model({x: 1});
model.on('change change:x', function() { ok(false); });
model.set({x: 1});
model.set({x: 1});
});
test("unset does not fire a change for undefined attributes", 0, function() {
var model = new Backbone.Model({x: undefined});
model.on('change:x', function(){ ok(false); });
@@ -603,19 +649,29 @@ $(document).ready(function() {
ok('x' in model.attributes);
});
test("change fires change:attr", 1, function() {
test("hasChanged works outside of change events, and true within", 6, function() {
var model = new Backbone.Model({x: 1});
model.set({x: 2}, {silent: true});
model.on('change:x', function(){ ok(true); });
model.change();
});
test("hasChanged is false after original values are set", 2, function() {
var model = new Backbone.Model({x: 1});
model.on('change:x', function(){ ok(false); });
model.on('change:x', function() {
ok(model.hasChanged('x'));
equal(model.get('x'), 1);
});
model.set({x: 2}, {silent: true});
ok(model.hasChanged());
model.set({x: 1}, {silent: true});
equal(model.hasChanged('x'), true);
model.set({x: 1});
ok(model.hasChanged());
equal(model.hasChanged('x'), true);
});
test("hasChanged gets cleared on the following set", 4, function() {
var model = new Backbone.Model;
model.set({x: 1});
ok(model.hasChanged());
model.set({x: 1});
ok(!model.hasChanged());
model.set({x: 2});
ok(model.hasChanged());
model.set({});
ok(!model.hasChanged());
});
@@ -664,7 +720,7 @@ $(document).ready(function() {
test("#1030 - `save` with `wait` results in correct attributes if success is called during sync", 2, function() {
var model = new Backbone.Model({x: 1, y: 2});
model.sync = function(method, model, options) {
options.success();
options.success(model, {}, options);
};
model.on("change:x", function() { ok(true); });
model.save({x: 3}, {wait: true});
@@ -691,25 +747,19 @@ $(document).ready(function() {
model.set({x: true});
deepEqual(events, ['change:y', 'change:x', 'change']);
events = [];
model.change();
deepEqual(events, ['change:z', 'change']);
model.set({z: true});
deepEqual(events, []);
});
test("nested `change` only fires once", 1, function() {
var model = new Backbone.Model();
model.on('change', function() {
ok(true);
model.change();
model.set({x: true});
});
model.set({x: true});
});
test("no `'change'` event if no changes", 0, function() {
var model = new Backbone.Model();
model.on('change', function() { ok(false); });
model.change();
});
test("nested `set` during `'change'`", 6, function() {
var count = 0;
var model = new Backbone.Model();
@@ -721,13 +771,13 @@ $(document).ready(function() {
model.set({y: true});
break;
case 1:
deepEqual(this.changedAttributes(), {y: true});
equal(model.previous('x'), true);
deepEqual(this.changedAttributes(), {x: true, y: true});
equal(model.previous('x'), undefined);
model.set({z: true});
break;
case 2:
deepEqual(this.changedAttributes(), {z: true});
equal(model.previous('y'), true);
deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
equal(model.previous('y'), undefined);
break;
default:
ok(false);
@@ -736,30 +786,34 @@ $(document).ready(function() {
model.set({x: true});
});
test("nested `'change'` with silent", 3, function() {
test("nested `change` with silent", 3, function() {
var count = 0;
var model = new Backbone.Model();
model.on('change:y', function() { ok(true); });
model.on('change:y', function() { ok(false); });
model.on('change', function() {
switch(count++) {
case 0:
deepEqual(this.changedAttributes(), {x: true});
model.set({y: true}, {silent: true});
model.set({z: true});
break;
case 1:
deepEqual(this.changedAttributes(), {y: true, z: true});
deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
break;
case 2:
deepEqual(this.changedAttributes(), {z: false});
break;
default:
ok(false);
}
});
model.set({x: true});
model.set({z: true});
model.set({z: false});
});
test("nested `'change:attr'` with silent", 1, function() {
test("nested `change:attr` with silent", 0, function() {
var model = new Backbone.Model();
model.on('change:y', function(){ ok(true); });
model.on('change:y', function(){ ok(false); });
model.on('change', function() {
model.set({y: true}, {silent: true});
model.set({z: true});
@@ -777,21 +831,25 @@ $(document).ready(function() {
equal(val, 2);
});
model.set({x: true});
model.change();
});
test("multiple nested changes with silent", 2, function() {
test("multiple nested changes with silent", 1, function() {
var changes = [];
var model = new Backbone.Model();
model.on('change:b', function(model, val) { changes.push(val); });
model.on('change', function() {
model.set({b: 1});
model.set({b: 2}, {silent: true});
});
model.set({b: 0});
deepEqual(changes, [0, 1]);
model.change();
deepEqual(changes, [0, 1, 2, 1]);
});
test("basic silent change semantics", 1, function() {
var model = new Backbone.Model;
model.set({x: 1});
model.on('change', function(){ ok(true); });
model.set({x: 2}, {silent: true});
model.set({x: 1});
});
test("nested set multiple times", 1, function() {
@@ -828,7 +886,7 @@ $(document).ready(function() {
}
};
model.sync = function(method, model, options) {
options.success();
options.success(model, {}, options);
};
model.save({id: 1}, opts);
model.fetch(opts);
@@ -866,7 +924,7 @@ $(document).ready(function() {
validate: function(){ return 'invalid'; }
});
var model = new Model({id: 1});
model.on('error', function(){ ok(true); });
model.on('invalid', function(){ ok(true); });
model.save();
});
@@ -885,7 +943,7 @@ $(document).ready(function() {
var Model = Backbone.Model.extend({
sync: function(method, model, options) {
setTimeout(function(){
options.success();
options.success(model, {}, options);
start();
}, 0);
}
@@ -895,9 +953,9 @@ $(document).ready(function() {
.save(null, {wait: true});
});
test("#1664 - Changing from one value, silently to another, back to original does not trigger change.", 0, function() {
test("#1664 - Changing from one value, silently to another, back to original triggers a change.", 1, function() {
var model = new Backbone.Model({x:1});
model.on('change:x', function() { ok(false); });
model.on('change:x', function() { ok(true); });
model.set({x:2},{silent:true});
model.set({x:3},{silent:true});
model.set({x:1});
@@ -910,15 +968,11 @@ $(document).ready(function() {
model.set({a:'c'}, {silent:true});
model.set({b:2}, {silent:true});
model.unset('c', {silent:true});
model.set({a:'a'}, {silent:true});
model.set({b:1}, {silent:true});
model.set({c:'item'}, {silent:true});
});
model.on('change:a change:b change:c', function(model, val) { changes.push(val); });
model.set({a:'a', b:1, c:'item'});
deepEqual(changes, ['a',1,'item']);
model.change();
deepEqual(changes, ['a',1,'item']);
deepEqual(model.attributes, {a: 'c', b: 2});
});
test("#1791 - `attributes` is available for `parse`", function() {
@@ -929,7 +983,7 @@ $(document).ready(function() {
expect(0);
});
test("silent changes in last `change` event back to original does not trigger change", 2, function() {
test("silent changes in last `change` event back to original triggers change", 2, function() {
var changes = [];
var model = new Backbone.Model();
model.on('change:a change:b change:c', function(model, val) { changes.push(val); });
@@ -938,9 +992,8 @@ $(document).ready(function() {
});
model.set({a:'a'});
deepEqual(changes, ['a']);
model.set({a:'a'}, {silent:true});
model.change();
deepEqual(changes, ['a']);
model.set({a:'a'});
deepEqual(changes, ['a', 'a']);
});
test("#1943 change calculations should use _.isEqual", function() {
@@ -949,4 +1002,65 @@ $(document).ready(function() {
equal(model.changedAttributes(), false);
});
test("#1964 - final `change` event is always fired, regardless of interim changes", 1, function () {
var model = new Backbone.Model();
model.on('change:property', function() {
model.set('property', 'bar');
});
model.on('change', function() {
ok(true);
});
model.set('property', 'foo');
});
test("isValid", function() {
var model = new Backbone.Model({valid: true});
model.validate = function(attrs) {
if (!attrs.valid) return "invalid";
};
equal(model.isValid(), true);
equal(model.set({valid: false}, {validate:true}), false);
equal(model.isValid(), true);
model.set({valid:false});
equal(model.isValid(), false);
ok(!model.set('valid', false, {validate: true}));
});
test("#1179 - isValid returns true in the absence of validate.", 1, function() {
var model = new Backbone.Model();
model.validate = null;
ok(model.isValid());
});
test("#1961 - Creating a model with {validate:true} will call validate and use the error callback", function () {
var Model = Backbone.Model.extend({
validate: function (attrs) {
if (attrs.id === 1) return "This shouldn't happen";
}
});
var model = new Model({id: 1}, {validate: true});
equal(model.validationError, "This shouldn't happen");
});
test("toJSON receives attrs during save(..., {wait: true})", 1, function() {
var Model = Backbone.Model.extend({
url: '/test',
toJSON: function() {
strictEqual(this.attributes.x, 1);
return _.clone(this.attributes);
}
});
var model = new Model;
model.save({x: 1}, {wait: true});
});
test("#2034 - nested set with silent only triggers one change", 1, function() {
var model = new Backbone.Model();
model.on('change', function() {
model.set({b: true}, {silent: true});
ok(true);
});
model.set({a: true});
});
});

View File

@@ -70,6 +70,7 @@ $(document).ready(function() {
"contacts/new": "newContact",
"contacts/:id": "loadContact",
"optional(/:item)": "optionalItem",
"named/optional/(y:z)": "namedOptional",
"splat/*args/end": "splat",
"*first/complex-:part/*rest": "complex",
":entity?*args": "query",
@@ -110,23 +111,27 @@ $(document).ready(function() {
this.arg = arg != void 0 ? arg : null;
},
splat : function(args) {
splat: function(args) {
this.args = args;
},
complex : function(first, part, rest) {
complex: function(first, part, rest) {
this.first = first;
this.part = part;
this.rest = rest;
},
query : function(entity, args) {
query: function(entity, args) {
this.entity = entity;
this.queryArgs = args;
},
anything : function(whatever) {
anything: function(whatever) {
this.anything = whatever;
},
namedOptional: function(z) {
this.z = z;
}
});
@@ -502,4 +507,13 @@ $(document).ready(function() {
strictEqual(history.getFragment('/fragment '), 'fragment');
});
test("#1980 - Optional parameters.", 2, function() {
location.replace('http://example.com#named/optional/y');
Backbone.history.checkUrl();
strictEqual(router.z, undefined);
location.replace('http://example.com#named/optional/y123');
Backbone.history.checkUrl();
strictEqual(router.z, '123');
});
});

View File

@@ -29,22 +29,6 @@ $(document).ready(function() {
strictEqual(view.$('a b').html(), 'test');
});
test("make", 3, function() {
var div = view.make('div', {id: 'test-div'}, "one two three");
equal(div.tagName.toLowerCase(), 'div');
equal(div.id, 'test-div');
equal($(div).text(), 'one two three');
});
test("make can take falsy values for content", 2, function() {
var div = view.make('div', {id: 'test-div'}, 0);
equal($(div).text(), '0');
div = view.make('div', {id: 'test-div'}, '');
equal($(div).text(), '');
});
test("initialize", 1, function() {
var View = Backbone.View.extend({
initialize: function() {

View File

@@ -182,7 +182,7 @@ $(document).ready(function() {
equal(_.indexOf(null, 2), -1, 'handles nulls properly');
numbers = [1, 2, 3, 1, 2, 3, 1, 2, 3];
index = _.lastIndexOf(numbers, 2, 2);
var index = _.lastIndexOf(numbers, 2, 2);
equal(index, 1, 'supports the fromIndex argument');
});

View File

@@ -22,7 +22,7 @@ $(document).ready(function() {
equal(answers.join(", "), 'one, two, three', 'iterating over objects works, and ignores the object prototype.');
delete obj.constructor.prototype.four;
answer = null;
var answer = null;
_.each([1, 2, 3], function(num, index, arr){ if (_.include(arr, num)) answer = true; });
ok(answer, 'can reference the original collection from inside the iterator');

View File

@@ -555,7 +555,7 @@ $(document).ready(function() {
value();
ok(returned == 6 && intercepted == 6, 'can use tapped objects in a chain');
});
test("has", function () {
var obj = {foo: "bar", func: function () {} };
ok (_.has(obj, "foo"), "has() checks that the object has a property.");
@@ -563,7 +563,7 @@ $(document).ready(function() {
ok (_.has(obj, "func"), "has() works for functions too.");
obj.hasOwnProperty = null;
ok (_.has(obj, "foo"), "has() works even when the hasOwnProperty method is deleted.");
child = {};
var child = {};
child.prototype = obj;
ok (_.has(child, "foo") == false, "has() does not check the prototype chain for a property.")
});

View File

@@ -25,6 +25,20 @@ $(document).ready(function() {
equal(_.identity(moe), moe, 'moe is the same as his identity');
});
test("random", function() {
var array = _.range(1000);
var min = Math.pow(2, 31);
var max = Math.pow(2, 62);
ok(_.every(array, function() {
return _.random(min, max) >= min;
}), "should produce a random number greater than or equal to the minimum number");
ok(_.some(array, function() {
return _.random(Number.MAX_VALUE) > 0;
}), "should produce a random number when passed `Number.MAX_VALUE`");
});
test("uniqueId", function() {
var ids = [], i = 0;
while(i++ < 100) ids.push(_.uniqueId());
@@ -82,7 +96,7 @@ $(document).ready(function() {
equal(escapeTemplate({a: true}), 'checked="checked"', 'can handle slash escapes in interpolations.');
var fancyTemplate = _.template("<ul><% \
for (key in people) { \
for (var key in people) { \
%><li><%= people[key] %></li><% } %></ul>");
result = fancyTemplate({people : {moe : "Moe", larry : "Larry", curly : "Curly"}});
equal(result, "<ul><li>Moe</li><li>Larry</li><li>Curly</li></ul>", 'can run arbitrary javascript in templates');
@@ -137,7 +151,7 @@ $(document).ready(function() {
interpolate : /\{\{=([\s\S]+?)\}\}/g
};
var custom = _.template("<ul>{{ for (key in people) { }}<li>{{= people[key] }}</li>{{ } }}</ul>");
var custom = _.template("<ul>{{ for (var key in people) { }}<li>{{= people[key] }}</li>{{ } }}</ul>");
result = custom({people : {moe : "Moe", larry : "Larry", curly : "Curly"}});
equal(result, "<ul><li>Moe</li><li>Larry</li><li>Curly</li></ul>", 'can run arbitrary javascript in templates');
@@ -152,7 +166,7 @@ $(document).ready(function() {
interpolate : /<\?=([\s\S]+?)\?>/g
};
var customWithSpecialChars = _.template("<ul><? for (key in people) { ?><li><?= people[key] ?></li><? } ?></ul>");
var customWithSpecialChars = _.template("<ul><? for (var key in people) { ?><li><?= people[key] ?></li><? } ?></ul>");
result = customWithSpecialChars({people : {moe : "Moe", larry : "Larry", curly : "Curly"}});
equal(result, "<ul><li>Moe</li><li>Larry</li><li>Curly</li></ul>", 'can run arbitrary javascript in templates');

File diff suppressed because one or more lines are too long

View File

@@ -1019,7 +1019,7 @@
max = min;
min = 0;
}
return min + (0 | Math.random() * (max - min + 1));
return min + Math.floor(Math.random() * (max - min + 1));
};
// List of HTML entities for escaping.