mirror of
https://github.com/jashkenas/backbone.git
synced 2026-04-30 03:00:06 -04:00
Merge branch 'master' of github.com:documentcloud/backbone
This commit is contained in:
@@ -9,3 +9,5 @@
|
||||
* Use the same coding style as the rest of the [codebase](https://github.com/documentcloud/backbone/blob/master/backbone.js).
|
||||
|
||||
* In your pull request, do not re-build the minified `backbone-min.js` file. We'll do that before cutting a new release.
|
||||
|
||||
* All pull requests should be made to `master`. If the patch is for documentation of the currently released version, please note this so that it can be cherry picked into `gh-pages`.
|
||||
|
||||
194
backbone.js
194
backbone.js
@@ -72,32 +72,39 @@
|
||||
// in terms of the existing API.
|
||||
var eventsApi = function(obj, action, name, rest) {
|
||||
if (!name) return true;
|
||||
|
||||
// Handle event maps.
|
||||
if (typeof name === 'object') {
|
||||
for (var key in name) {
|
||||
obj[action].apply(obj, [key, name[key]].concat(rest));
|
||||
}
|
||||
} else if (eventSplitter.test(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle space separated event names.
|
||||
if (eventSplitter.test(name)) {
|
||||
var names = name.split(eventSplitter);
|
||||
for (var i = 0, l = names.length; i < l; i++) {
|
||||
obj[action].apply(obj, [names[i]].concat(rest));
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Optimized internal dispatch function for triggering events. Tries to
|
||||
// keep the usual cases speedy (most Backbone events have 3 arguments).
|
||||
var triggerEvents = function(events, args) {
|
||||
var ev, i = -1, l = events.length;
|
||||
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
|
||||
switch (args.length) {
|
||||
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
|
||||
return;
|
||||
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
|
||||
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, args[0], args[1]);
|
||||
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, args[0], args[1], args[2]);
|
||||
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);
|
||||
}
|
||||
@@ -136,8 +143,7 @@
|
||||
callback.apply(this, arguments);
|
||||
});
|
||||
once._callback = callback;
|
||||
this.on(name, once, context);
|
||||
return this;
|
||||
return this.on(name, once, context);
|
||||
},
|
||||
|
||||
// Remove one or many callbacks. If `context` is null, removes all
|
||||
@@ -194,15 +200,12 @@
|
||||
stopListening: function(obj, name, callback) {
|
||||
var listeners = this._listeners;
|
||||
if (!listeners) return this;
|
||||
if (obj) {
|
||||
obj.off(name, typeof name === 'object' ? this : callback, this);
|
||||
if (!name && !callback) delete listeners[obj._listenerId];
|
||||
} else {
|
||||
if (typeof name === 'object') callback = this;
|
||||
for (var id in listeners) {
|
||||
listeners[id].off(name, callback, this);
|
||||
}
|
||||
this._listeners = {};
|
||||
var deleteListener = !name && !callback;
|
||||
if (typeof name === 'object') callback = this;
|
||||
if (obj) (listeners = {})[obj._listenerId] = obj;
|
||||
for (var id in listeners) {
|
||||
listeners[id].off(name, callback, this);
|
||||
if (deleteListener) delete this._listeners[id];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -217,7 +220,8 @@
|
||||
var listeners = this._listeners || (this._listeners = {});
|
||||
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
|
||||
listeners[id] = obj;
|
||||
obj[implementation](name, typeof name === 'object' ? this : callback, this);
|
||||
if (typeof name === 'object') callback = this;
|
||||
obj[implementation](name, callback, this);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
@@ -298,7 +302,7 @@
|
||||
// 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, unset, changes, silent, changing, prev, current;
|
||||
var attr, attrs, unset, changes, changing, prev, current;
|
||||
if (key == null) return this;
|
||||
|
||||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||||
@@ -316,7 +320,6 @@
|
||||
|
||||
// Extract attributes and options.
|
||||
unset = options.unset;
|
||||
silent = options.silent;
|
||||
changes = [];
|
||||
changing = this._changing;
|
||||
this._changing = true;
|
||||
@@ -343,19 +346,15 @@
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if (changing) return this;
|
||||
if (!silent) {
|
||||
while (this._pending) {
|
||||
this._pending = false;
|
||||
this.trigger('change', this, options);
|
||||
}
|
||||
while (this._pending) {
|
||||
this._pending = false;
|
||||
this.trigger('change', this, options);
|
||||
}
|
||||
this._pending = false;
|
||||
this._changing = false;
|
||||
@@ -416,16 +415,19 @@
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Fetch the model from the server. If the server's representation of the
|
||||
// model differs from its current attributes, they will be overriden,
|
||||
// model differs from its current attributes, they will be overridden,
|
||||
// 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(model, resp, options) {
|
||||
options.success = function(resp) {
|
||||
if (!model.set(model.parse(resp, options), options)) return false;
|
||||
if (success) success(model, resp, options);
|
||||
model.trigger('sync', model, resp, options);
|
||||
};
|
||||
wrapError(this, options);
|
||||
return this.sync('read', this, options);
|
||||
},
|
||||
|
||||
@@ -433,7 +435,7 @@
|
||||
// If the server returns an attributes hash that differs, the model's
|
||||
// state will be `set` again.
|
||||
save: function(key, val, options) {
|
||||
var attrs, success, method, xhr, attributes = this.attributes;
|
||||
var attrs, method, xhr, attributes = this.attributes;
|
||||
|
||||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||||
if (key == null || typeof key === 'object') {
|
||||
@@ -459,8 +461,9 @@
|
||||
// After a successful server-side save, the client is (optionally)
|
||||
// updated with the server-side state.
|
||||
if (options.parse === void 0) options.parse = true;
|
||||
success = options.success;
|
||||
options.success = function(model, resp, options) {
|
||||
var model = this;
|
||||
var success = options.success;
|
||||
options.success = function(resp) {
|
||||
// Ensure attributes are restored during synchronous saves.
|
||||
model.attributes = attributes;
|
||||
var serverAttrs = model.parse(resp, options);
|
||||
@@ -469,9 +472,10 @@
|
||||
return false;
|
||||
}
|
||||
if (success) success(model, resp, options);
|
||||
model.trigger('sync', model, resp, options);
|
||||
};
|
||||
wrapError(this, options);
|
||||
|
||||
// Finish configuring and sending the Ajax request.
|
||||
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
||||
if (method === 'patch') options.attrs = attrs;
|
||||
xhr = this.sync(method, this, options);
|
||||
@@ -494,15 +498,17 @@
|
||||
model.trigger('destroy', model, model.collection, options);
|
||||
};
|
||||
|
||||
options.success = function(model, resp, options) {
|
||||
options.success = function(resp) {
|
||||
if (options.wait || model.isNew()) destroy();
|
||||
if (success) success(model, resp, options);
|
||||
if (!model.isNew()) model.trigger('sync', model, resp, options);
|
||||
};
|
||||
|
||||
if (this.isNew()) {
|
||||
options.success(this, null, options);
|
||||
options.success();
|
||||
return false;
|
||||
}
|
||||
wrapError(this, options);
|
||||
|
||||
var xhr = this.sync('delete', this, options);
|
||||
if (!options.wait) destroy();
|
||||
@@ -599,25 +605,26 @@
|
||||
at = options.at;
|
||||
sort = this.comparator && (at == null) && options.sort !== false;
|
||||
sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
||||
var modelMap = {};
|
||||
|
||||
// Turn bare objects into model references, and prevent invalid models
|
||||
// from being added.
|
||||
for (i = 0, l = models.length; i < l; i++) {
|
||||
if (!(model = this._prepareModel(attrs = models[i], options))) {
|
||||
this.trigger('invalid', this, attrs, options);
|
||||
continue;
|
||||
}
|
||||
if (!(model = this._prepareModel(attrs = models[i], options))) continue;
|
||||
|
||||
// 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.merge) {
|
||||
existing.set(attrs === model ? model.attributes : attrs, options);
|
||||
modelMap[existing.cid] = true;
|
||||
if (options.merge && model !== existing) {
|
||||
existing.set(model === attrs ? model.attributes : attrs, options);
|
||||
if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.add === false) continue;
|
||||
|
||||
// This is a new model, push it to the `add` list.
|
||||
add.push(model);
|
||||
|
||||
@@ -628,6 +635,14 @@
|
||||
if (model.id != null) this._byId[model.id] = model;
|
||||
}
|
||||
|
||||
if (options.remove) {
|
||||
var remove = [];
|
||||
for (i = 0, l = this.length; i < l; ++i) {
|
||||
if (!modelMap[(model = this.models[i]).cid]) remove.push(model);
|
||||
}
|
||||
if (remove.length) this.remove(remove, options);
|
||||
}
|
||||
|
||||
// See if sorting is needed, update `length` and splice in new models.
|
||||
if (add.length) {
|
||||
if (sort) doSort = true;
|
||||
@@ -713,8 +728,7 @@
|
||||
// Get a model from the set by id.
|
||||
get: function(obj) {
|
||||
if (obj == null) return void 0;
|
||||
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
|
||||
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
|
||||
return this._byId[obj.id != null ? obj.id : obj.cid || obj];
|
||||
},
|
||||
|
||||
// Get the model at the given index.
|
||||
@@ -722,10 +736,11 @@
|
||||
return this.models[index];
|
||||
},
|
||||
|
||||
// Return models with matching attributes. Useful for simple cases of `filter`.
|
||||
where: function(attrs) {
|
||||
if (_.isEmpty(attrs)) return [];
|
||||
return this.filter(function(model) {
|
||||
// Return models with matching attributes. Useful for simple cases of
|
||||
// `filter`.
|
||||
where: function(attrs, first) {
|
||||
if (_.isEmpty(attrs)) return first ? void 0 : [];
|
||||
return this[first ? 'find' : 'filter'](function(model) {
|
||||
for (var key in attrs) {
|
||||
if (attrs[key] !== model.get(key)) return false;
|
||||
}
|
||||
@@ -733,6 +748,12 @@
|
||||
});
|
||||
},
|
||||
|
||||
// Return the first model with matching attributes. Useful for simple cases
|
||||
// of `find`.
|
||||
findWhere: function(attrs) {
|
||||
return this.where(attrs, true);
|
||||
},
|
||||
|
||||
// Force the collection to re-sort itself. You don't need to call this under
|
||||
// normal circumstances, as the set will maintain sort order as each item
|
||||
// is added.
|
||||
@@ -761,36 +782,9 @@
|
||||
// Smartly update a collection with a change set of models, adding,
|
||||
// removing, and merging as necessary.
|
||||
update: function(models, options) {
|
||||
options = _.extend({add: true, merge: true, remove: true}, options);
|
||||
options = _.extend({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] : [];
|
||||
|
||||
// Proxy to `add` for this case, no need to iterate...
|
||||
if (options.add && !options.remove) return this.add(models, options);
|
||||
|
||||
// Determine which models to add and merge, and which to remove.
|
||||
for (i = 0, l = models.length; i < l; i++) {
|
||||
model = models[i];
|
||||
existing = this.get(model);
|
||||
if (options.remove && existing) modelMap[existing.cid] = true;
|
||||
if ((options.add && !existing) || (options.merge && existing)) {
|
||||
add.push(model);
|
||||
}
|
||||
}
|
||||
if (options.remove) {
|
||||
for (i = 0, l = this.models.length; i < l; i++) {
|
||||
model = this.models[i];
|
||||
if (!modelMap[model.cid]) remove.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove models (if applicable) before we add and merge the rest.
|
||||
if (remove.length) this.remove(remove, options);
|
||||
if (add.length) this.add(add, options);
|
||||
this.add(models, options);
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -798,7 +792,7 @@
|
||||
// you can reset the entire set with a new list of models, without firing
|
||||
// any `add` or `remove` events. Fires `reset` when finished.
|
||||
reset: function(models, options) {
|
||||
options || (options = {});
|
||||
options = options ? _.clone(options) : {};
|
||||
if (options.parse) models = this.parse(models, options);
|
||||
for (var i = 0, l = this.models.length; i < l; i++) {
|
||||
this._removeReference(this.models[i]);
|
||||
@@ -817,11 +811,14 @@
|
||||
options = options ? _.clone(options) : {};
|
||||
if (options.parse === void 0) options.parse = true;
|
||||
var success = options.success;
|
||||
options.success = function(collection, resp, options) {
|
||||
var collection = this;
|
||||
options.success = function(resp) {
|
||||
var method = options.update ? 'update' : 'reset';
|
||||
collection[method](resp, options);
|
||||
if (success) success(collection, resp, options);
|
||||
collection.trigger('sync', collection, resp, options);
|
||||
};
|
||||
wrapError(this, options);
|
||||
return this.sync('read', this, options);
|
||||
},
|
||||
|
||||
@@ -834,7 +831,7 @@
|
||||
if (!options.wait) this.add(model, options);
|
||||
var collection = this;
|
||||
var success = options.success;
|
||||
options.success = function(model, resp, options) {
|
||||
options.success = function(resp) {
|
||||
if (options.wait) collection.add(model, options);
|
||||
if (success) success(model, resp, options);
|
||||
};
|
||||
@@ -869,7 +866,10 @@
|
||||
options || (options = {});
|
||||
options.collection = this;
|
||||
var model = new this.model(attrs, options);
|
||||
if (!model._validate(attrs, options)) return false;
|
||||
if (!model._validate(attrs, options)) {
|
||||
this.trigger('invalid', this, attrs, options);
|
||||
return false;
|
||||
}
|
||||
return model;
|
||||
},
|
||||
|
||||
@@ -1334,8 +1334,9 @@
|
||||
},
|
||||
|
||||
// Performs the initial configuration of a View with a set of options.
|
||||
// Keys with special meaning *(model, collection, id, className)*, are
|
||||
// attached directly to the view.
|
||||
// Keys with special meaning *(e.g. model, collection, id, className)* are
|
||||
// attached directly to the view. See `viewOptions` for an exhaustive
|
||||
// list.
|
||||
_configure: function(options) {
|
||||
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
|
||||
_.extend(this, _.pick(options, viewOptions));
|
||||
@@ -1433,18 +1434,6 @@
|
||||
params.processData = false;
|
||||
}
|
||||
|
||||
var success = options.success;
|
||||
options.success = function(resp) {
|
||||
if (success) success(model, resp, options);
|
||||
model.trigger('sync', model, resp, options);
|
||||
};
|
||||
|
||||
var error = options.error;
|
||||
options.error = function(xhr) {
|
||||
if (error) error(model, xhr, options);
|
||||
model.trigger('error', model, xhr, options);
|
||||
};
|
||||
|
||||
// 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);
|
||||
@@ -1503,4 +1492,13 @@
|
||||
throw new Error('A "url" property or function must be specified');
|
||||
};
|
||||
|
||||
// Wrap an optional error callback with a fallback error event.
|
||||
var wrapError = function (model, options) {
|
||||
var error = options.error;
|
||||
options.error = function(resp) {
|
||||
if (error) error(model, resp, options);
|
||||
model.trigger('error', model, resp, options);
|
||||
};
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -110,9 +110,9 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m
|
||||
}
|
||||
|
||||
if (resp) {
|
||||
options.success(model, resp, options);
|
||||
options.success(resp);
|
||||
} else {
|
||||
options.error(model, "Record not found", options);
|
||||
options.error('Record not found.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
123
index.html
123
index.html
@@ -151,6 +151,9 @@
|
||||
a img {
|
||||
border: 0;
|
||||
}
|
||||
a.travis-badge {
|
||||
display: block;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
@@ -367,6 +370,7 @@
|
||||
<li>– <a href="#Collection-pop">pop</a></li>
|
||||
<li>– <a href="#Collection-unshift">unshift</a></li>
|
||||
<li>– <a href="#Collection-shift">shift</a></li>
|
||||
<li>– <a href="#Collection-slice">slice</a></li>
|
||||
<li>– <a href="#Collection-length">length</a></li>
|
||||
<li>– <a href="#Collection-comparator">comparator</a></li>
|
||||
<li>– <a href="#Collection-sort">sort</a></li>
|
||||
@@ -559,8 +563,8 @@
|
||||
<td><a class="punch" href="https://raw.github.com/documentcloud/backbone/master/backbone.js">Edge Version (master)</a></td>
|
||||
<td>
|
||||
<i>Unreleased, use at your own risk</i>
|
||||
<a href="https://travis-ci.org/documentcloud/backbone">
|
||||
<img width="89" height="13" src="https://travis-ci.org/documentcloud/backbone.png" />
|
||||
<a class="travis-badge" href="https://travis-ci.org/documentcloud/backbone">
|
||||
<img src="https://travis-ci.org/documentcloud/backbone.png" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -970,12 +974,11 @@ new Book({
|
||||
<p id="Model-set">
|
||||
<b class="header">set</b><code>model.set(attributes, [options])</code>
|
||||
<br />
|
||||
Set a hash of attributes (one or many) on the model. If any of the attributes
|
||||
change the model's state, a <tt>"change"</tt> event will be triggered, unless
|
||||
<tt>{silent: true}</tt> is passed as an option. Change events for specific
|
||||
attributes are also triggered, and you can bind to those as well, for example:
|
||||
<tt>change:title</tt>, and <tt>change:content</tt>. You may also pass
|
||||
individual keys and values.
|
||||
Set a hash of attributes (one or many) on the model. If any of the
|
||||
attributes change the model's state, a <tt>"change"</tt> event will be
|
||||
triggered. Change events for specific attributes are also triggered, and
|
||||
you can bind to those as well, for example: <tt>change:title</tt>, and
|
||||
<tt>change:content</tt>. You may also pass individual keys and values.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
@@ -1017,15 +1020,15 @@ if (note.has("title")) {
|
||||
<p id="Model-unset">
|
||||
<b class="header">unset</b><code>model.unset(attribute, [options])</code>
|
||||
<br />
|
||||
Remove an attribute by deleting it from the internal attributes hash.
|
||||
Fires a <tt>"change"</tt> event unless <tt>silent</tt> is passed as an option.
|
||||
Remove an attribute by deleting it from the attributes hash. Fires
|
||||
<tt>"change"</tt> and <tt>"change:attr"</tt> events.
|
||||
</p>
|
||||
|
||||
<p id="Model-clear">
|
||||
<b class="header">clear</b><code>model.clear([options])</code>
|
||||
<br />
|
||||
Removes all attributes from the model, including the <tt>id</tt> attribute. Fires a <tt>"change"</tt> event unless
|
||||
<tt>silent</tt> is passed as an option.
|
||||
Removes all attributes from the model, including the <tt>id</tt>
|
||||
attribute. Fires <tt>"change"</tt> and <tt>"change:attr"</tt> events.
|
||||
</p>
|
||||
|
||||
<p id="Model-id">
|
||||
@@ -1234,10 +1237,8 @@ book.save({author: "Teddy"});
|
||||
<b>save</b> accepts <tt>success</tt> and <tt>error</tt> callbacks in the
|
||||
options hash, which are passed <tt>(model, response, options)</tt> and
|
||||
<tt>(model, xhr, options)</tt> as arguments, respectively.
|
||||
The <tt>error</tt> callback will also be invoked if the model has a
|
||||
<tt>validate</tt> method, and validation fails. If a server-side
|
||||
validation fails, return a non-<tt>200</tt> HTTP response code, along with
|
||||
an error response in text or JSON.
|
||||
If a server-side validation fails, return a non-<tt>200</tt>
|
||||
HTTP response code, along with an error response in text or JSON.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
@@ -1360,7 +1361,7 @@ alert(solaris.url());
|
||||
</pre>
|
||||
|
||||
<p id="Model-parse">
|
||||
<b class="header">parse</b><code>model.parse(response)</code>
|
||||
<b class="header">parse</b><code>model.parse(response, options)</code>
|
||||
<br />
|
||||
<b>parse</b> is called whenever a model's data is returned by the
|
||||
server, in <a href="#Model-fetch">fetch</a>, and <a href="#Model-save">save</a>.
|
||||
@@ -1492,6 +1493,25 @@ bill.set({name : "Bill Jones"});
|
||||
var Library = Backbone.Collection.extend({
|
||||
model: Book
|
||||
});
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
A collection can also contain polymorphic models by overriding this property
|
||||
with a function that returns a model.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
var Library = Backbone.Collection.extend({
|
||||
|
||||
model: function(attrs, options) {
|
||||
if (condition) {
|
||||
return new PublicDocument(attrs, options);
|
||||
} else {
|
||||
return new PrivateDocument(attrs, options);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</pre>
|
||||
|
||||
<p id="Collection-constructor">
|
||||
@@ -1561,9 +1581,9 @@ alert(JSON.stringify(collection));
|
||||
<li><a href="http://underscorejs.org/#find">find (detect)</a></li>
|
||||
<li><a href="http://underscorejs.org/#filter">filter (select)</a></li>
|
||||
<li><a href="http://underscorejs.org/#reject">reject</a></li>
|
||||
<li><a href="http://underscorejs.org/#all">every (all)</a></li>
|
||||
<li><a href="http://underscorejs.org/#any">some (any)</a></li>
|
||||
<li><a href="http://underscorejs.org/#include">include (contains)</a></li>
|
||||
<li><a href="http://underscorejs.org/#every">every (all)</a></li>
|
||||
<li><a href="http://underscorejs.org/#some">some (any)</a></li>
|
||||
<li><a href="http://underscorejs.org/#contains">contains (include)</a></li>
|
||||
<li><a href="http://underscorejs.org/#invoke">invoke</a></li>
|
||||
<li><a href="http://underscorejs.org/#max">max</a></li>
|
||||
<li><a href="http://underscorejs.org/#min">min</a></li>
|
||||
@@ -1585,19 +1605,19 @@ alert(JSON.stringify(collection));
|
||||
</ul>
|
||||
|
||||
<pre>
|
||||
Books.each(function(book) {
|
||||
books.each(function(book) {
|
||||
book.publish();
|
||||
});
|
||||
|
||||
var titles = Books.map(function(book) {
|
||||
var titles = books.map(function(book) {
|
||||
return book.get("title");
|
||||
});
|
||||
|
||||
var publishedBooks = Books.filter(function(book) {
|
||||
var publishedBooks = books.filter(function(book) {
|
||||
return book.get("published") === true;
|
||||
});
|
||||
|
||||
var alphabetical = Books.sortBy(function(book) {
|
||||
var alphabetical = books.sortBy(function(book) {
|
||||
return book.author.get("name").toLowerCase();
|
||||
});
|
||||
</pre>
|
||||
@@ -1638,10 +1658,9 @@ ships.add([
|
||||
<b class="header">remove</b><code>collection.remove(models, [options])</code>
|
||||
<br />
|
||||
Remove a model (or an array of models) from the collection. Fires a
|
||||
<tt>"remove"</tt> event, which you can use <tt>silent</tt>
|
||||
to suppress. If you're a callback listening to the <tt>"remove"</tt> event,
|
||||
the index at which the model is being removed from the collection is available
|
||||
as <tt>options.index</tt>.
|
||||
<tt>"remove"</tt> event, which you can use <tt>silent</tt> to suppress.
|
||||
The model's index before removal is available to listeners as
|
||||
<tt>options.index</tt>.
|
||||
</p>
|
||||
|
||||
<p id="Collection-reset">
|
||||
@@ -1663,8 +1682,8 @@ ships.add([
|
||||
|
||||
<pre>
|
||||
<script>
|
||||
var Accounts = new Backbone.Collection;
|
||||
Accounts.reset(<%= @accounts.to_json %>);
|
||||
var accounts = new Backbone.Collection;
|
||||
accounts.reset(<%= @accounts.to_json %>);
|
||||
</script>
|
||||
</pre>
|
||||
|
||||
@@ -1743,6 +1762,14 @@ var book = Library.get(110);
|
||||
<a href="#Collection-remove">remove</a>.
|
||||
</p>
|
||||
|
||||
<p id="Collection-slice">
|
||||
<b class="header">slice</b><code>collection.slice(begin, end)</code>
|
||||
<br />
|
||||
Return a shallow copy of this collection's models, using the same options as
|
||||
native
|
||||
<a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/slice">Array#slice</a>.
|
||||
</p>
|
||||
|
||||
<p id="Collection-length">
|
||||
<b class="header">length</b><code>collection.length</code>
|
||||
<br />
|
||||
@@ -1874,7 +1901,7 @@ var Notes = Backbone.Collection.extend({
|
||||
</pre>
|
||||
|
||||
<p id="Collection-parse">
|
||||
<b class="header">parse</b><code>collection.parse(response)</code>
|
||||
<b class="header">parse</b><code>collection.parse(response, options)</code>
|
||||
<br />
|
||||
<b>parse</b> is called by Backbone whenever a collection's models are
|
||||
returned by the server, in <a href="#Collection-fetch">fetch</a>.
|
||||
@@ -1924,10 +1951,10 @@ Backbone.sync = function(method, model) {
|
||||
alert(method + ": " + model.url);
|
||||
};
|
||||
|
||||
var Accounts = new Backbone.Collection;
|
||||
Accounts.url = '/accounts';
|
||||
var accounts = new Backbone.Collection;
|
||||
accounts.url = '/accounts';
|
||||
|
||||
Accounts.fetch();
|
||||
accounts.fetch();
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
@@ -1980,9 +2007,9 @@ var Library = Backbone.Collection.extend({
|
||||
model: Book
|
||||
});
|
||||
|
||||
var NYPL = new Library;
|
||||
var nypl = new Library;
|
||||
|
||||
var othello = NYPL.create({
|
||||
var othello = nypl.create({
|
||||
title: "Othello",
|
||||
author: "William Shakespeare"
|
||||
});
|
||||
@@ -3616,11 +3643,11 @@ var Mailbox = Backbone.Model.extend({
|
||||
|
||||
});
|
||||
|
||||
var Inbox = new Mailbox;
|
||||
var inbox = new Mailbox;
|
||||
|
||||
// And then, when the Inbox is opened:
|
||||
|
||||
Inbox.messages.fetch();
|
||||
inbox.messages.fetch();
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
@@ -3674,10 +3701,10 @@ Inbox.messages.fetch();
|
||||
|
||||
<pre>
|
||||
<script>
|
||||
var Accounts = new Backbone.Collection;
|
||||
Accounts.reset(<%= @accounts.to_json %>);
|
||||
var Projects = new Backbone.Collection;
|
||||
Projects.reset(<%= @projects.to_json(:collaborators => true) %>);
|
||||
var accounts = new Backbone.Collection;
|
||||
accounts.reset(<%= @accounts.to_json %>);
|
||||
var projects = new Backbone.Collection;
|
||||
projects.reset(<%= @projects.to_json(:collaborators => true) %>);
|
||||
</script>
|
||||
</pre>
|
||||
|
||||
@@ -3811,6 +3838,15 @@ ActiveRecord::Base.include_root_in_json = false
|
||||
|
||||
<h2 id="changelog">Change Log</h2>
|
||||
|
||||
<b class="header">Edge</b> — <small><i>Unreleased</i></small><br/>
|
||||
|
||||
<ul style="margin-top: 5px;">
|
||||
<li>
|
||||
The <tt>silent</tt> option has been removed from <tt>Model#set</tt>.
|
||||
Custom options should be used to accomplish this effect instead.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<b class="header">0.9.10</b> — <small><i>Jan. 15, 2013</i></small> — <a href="https://github.com/documentcloud/backbone/compare/0.9.9...0.9.10">Diff</a><br />
|
||||
<ul style="margin-top: 5px;">
|
||||
<li>
|
||||
@@ -3843,6 +3879,9 @@ ActiveRecord::Base.include_root_in_json = false
|
||||
Bug fix where an empty response from the server on save would not call
|
||||
the success function.
|
||||
</li>
|
||||
<li>
|
||||
<tt>parse</tt> now receives <tt>options</tt> as its second argument.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<b class="header">0.9.9</b> — <small><i>Dec. 13, 2012</i></small> — <a href="https://github.com/documentcloud/backbone/compare/0.9.2...0.9.9">Diff</a> — <a href="http://htmlpreview.github.com/?https://raw.github.com/documentcloud/backbone/0.9.9/index.html">Docs</a><br />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"underscore" : ">=1.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"phantomjs": "0.2.2"
|
||||
"phantomjs": "1.8.1-3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phantomjs test/vendor/runner.js test/index.html?noglobals=true"
|
||||
|
||||
@@ -62,30 +62,29 @@ $(document).ready(function() {
|
||||
strictEqual(collection.last().get('a'), 4);
|
||||
});
|
||||
|
||||
test("get", 5, function() {
|
||||
test("get", 6, function() {
|
||||
equal(col.get(0), d);
|
||||
equal(col.get(d.clone()), d);
|
||||
equal(col.get(2), b);
|
||||
equal(col.get({id: 1}), c);
|
||||
equal(col.get(c.clone()), c);
|
||||
equal(col.get(col.first().cid), col.first());
|
||||
});
|
||||
|
||||
test("get with non-default ids", 4, function() {
|
||||
test("get with non-default ids", 5, function() {
|
||||
var col = new Backbone.Collection();
|
||||
var MongoModel = Backbone.Model.extend({
|
||||
idAttribute: '_id'
|
||||
});
|
||||
var MongoModel = Backbone.Model.extend({idAttribute: '_id'});
|
||||
var model = new MongoModel({_id: 100});
|
||||
col.push(model);
|
||||
col.add(model);
|
||||
equal(col.get(100), model);
|
||||
model.set({_id: 101});
|
||||
equal(col.get(101), model);
|
||||
equal(col.get(model.cid), model);
|
||||
equal(col.get(model), model);
|
||||
equal(col.get(101), void 0);
|
||||
|
||||
var Col2 = Backbone.Collection.extend({ model: MongoModel });
|
||||
var col2 = new Col2();
|
||||
col2.push(model);
|
||||
equal(col2.get({_id: 101}), model);
|
||||
equal(col2.get(model.clone()), model);
|
||||
var col2 = new Backbone.Collection();
|
||||
col2.model = MongoModel;
|
||||
col2.add(model.attributes);
|
||||
equal(col2.get(model.clone()), col2.first());
|
||||
});
|
||||
|
||||
test("update index when id changes", 3, function() {
|
||||
@@ -362,9 +361,7 @@ $(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(model, [], options);
|
||||
};
|
||||
e.sync = function(method, model, options) { options.success(); };
|
||||
var colE = new Backbone.Collection([e]);
|
||||
var colF = new Backbone.Collection([e]);
|
||||
e.destroy();
|
||||
@@ -396,6 +393,15 @@ $(document).ready(function() {
|
||||
equal(this.syncArgs.options.parse, false);
|
||||
});
|
||||
|
||||
test("fetch with an error response triggers an error event", 1, function () {
|
||||
var collection = new Backbone.Collection();
|
||||
collection.on('error', function () {
|
||||
ok(true);
|
||||
});
|
||||
collection.sync = function (method, model, options) { options.error(); };
|
||||
collection.fetch();
|
||||
});
|
||||
|
||||
test("ensure fetch only parses once", 1, function() {
|
||||
var collection = new Backbone.Collection;
|
||||
var counter = 0;
|
||||
@@ -405,7 +411,7 @@ $(document).ready(function() {
|
||||
};
|
||||
collection.url = '/test';
|
||||
collection.fetch();
|
||||
this.syncArgs.options.success([]);
|
||||
this.syncArgs.options.success();
|
||||
equal(counter, 1);
|
||||
});
|
||||
|
||||
@@ -461,9 +467,10 @@ $(document).ready(function() {
|
||||
equal(JSON.stringify(col), '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]');
|
||||
});
|
||||
|
||||
test("where", 6, function() {
|
||||
test("where and findWhere", 8, function() {
|
||||
var model = new Backbone.Model({a: 1});
|
||||
var coll = new Backbone.Collection([
|
||||
{a: 1},
|
||||
model,
|
||||
{a: 1},
|
||||
{a: 1, b: 2},
|
||||
{a: 2, b: 2},
|
||||
@@ -475,6 +482,8 @@ $(document).ready(function() {
|
||||
equal(coll.where({b: 1}).length, 0);
|
||||
equal(coll.where({b: 2}).length, 2);
|
||||
equal(coll.where({a: 1, b: 2}).length, 1);
|
||||
equal(coll.findWhere({a: 1}), model);
|
||||
equal(coll.findWhere({a: 4}), void 0);
|
||||
});
|
||||
|
||||
test("Underscore methods", 13, function() {
|
||||
@@ -484,10 +493,10 @@ $(document).ready(function() {
|
||||
equal(col.indexOf(b), 1);
|
||||
equal(col.size(), 4);
|
||||
equal(col.rest().length, 3);
|
||||
ok(!_.include(col.rest()), a);
|
||||
ok(!_.include(col.rest()), d);
|
||||
ok(!_.include(col.rest(), a));
|
||||
ok(_.include(col.rest(), d));
|
||||
ok(!col.isEmpty());
|
||||
ok(!_.include(col.without(d)), d);
|
||||
ok(!_.include(col.without(d), d));
|
||||
equal(col.max(function(model){ return model.id; }).id, 3);
|
||||
equal(col.min(function(model){ return model.id; }).id, 0);
|
||||
deepEqual(col.chain()
|
||||
@@ -702,9 +711,7 @@ $(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, [], options);
|
||||
};
|
||||
model.sync = function(method, model, options){ options.success(); };
|
||||
collection.on('add', function(){ ok(true); });
|
||||
collection.create(model, {wait: true});
|
||||
});
|
||||
@@ -928,6 +935,35 @@ $(document).ready(function() {
|
||||
equal(col.length, 1);
|
||||
});
|
||||
|
||||
test("`update` and model level `parse`", function() {
|
||||
var Model = Backbone.Model.extend({
|
||||
parse: function (res) { return res.model; }
|
||||
});
|
||||
var Collection = Backbone.Collection.extend({
|
||||
model: Model,
|
||||
parse: function (res) { return res.models; }
|
||||
});
|
||||
var model = new Model({id: 1});
|
||||
var collection = new Collection(model);
|
||||
collection.update({models: [
|
||||
{model: {id: 1}},
|
||||
{model: {id: 2}}
|
||||
]}, {parse: true});
|
||||
equal(collection.first(), model);
|
||||
});
|
||||
|
||||
test("`update` data is only parsed once", function() {
|
||||
var collection = new Backbone.Collection();
|
||||
collection.model = Backbone.Model.extend({
|
||||
parse: function (data) {
|
||||
equal(data.parsed, void 0);
|
||||
data.parsed = true;
|
||||
return data;
|
||||
}
|
||||
});
|
||||
collection.update({}, {parse: true});
|
||||
});
|
||||
|
||||
test("#1894 - Push should not trigger a sort", 0, function() {
|
||||
var Collection = Backbone.Collection.extend({
|
||||
comparator: 'id',
|
||||
|
||||
@@ -99,6 +99,11 @@ $(document).ready(function() {
|
||||
a.listenTo(b, 'event2', cb);
|
||||
a.stopListening(null, {event: cb});
|
||||
b.trigger('event event2');
|
||||
b.off();
|
||||
a.listenTo(b, 'event event2', cb);
|
||||
a.stopListening(null, 'event');
|
||||
a.stopListening();
|
||||
b.trigger('event2');
|
||||
});
|
||||
|
||||
test("listenToOnce and stopListening", 1, function() {
|
||||
@@ -291,18 +296,6 @@ $(document).ready(function() {
|
||||
obj.trigger('x y');
|
||||
});
|
||||
|
||||
test("off is chainable", 3, function() {
|
||||
var obj = _.extend({}, Backbone.Events);
|
||||
// With no events
|
||||
ok(obj.off() === obj);
|
||||
// When removing all events
|
||||
obj.on('event', function(){}, obj);
|
||||
ok(obj.off() === obj);
|
||||
// When removing some events
|
||||
obj.on('event', function(){}, obj);
|
||||
ok(obj.off('event') === obj);
|
||||
});
|
||||
|
||||
test("#1310 - off does not skip consecutive events", 0, function() {
|
||||
var obj = _.extend({}, Backbone.Events);
|
||||
obj.on('event', function() { ok(false); }, obj);
|
||||
@@ -432,4 +425,21 @@ $(document).ready(function() {
|
||||
_.extend({}, Backbone.Events).once('event').trigger('event');
|
||||
});
|
||||
|
||||
});
|
||||
test("event functions are chainable", function() {
|
||||
var obj = _.extend({}, Backbone.Events);
|
||||
var obj2 = _.extend({}, Backbone.Events);
|
||||
var fn = function() {};
|
||||
equal(obj, obj.trigger('noeventssetyet'));
|
||||
equal(obj, obj.off('noeventssetyet'));
|
||||
equal(obj, obj.stopListening('noeventssetyet'));
|
||||
equal(obj, obj.on('a', fn));
|
||||
equal(obj, obj.once('c', fn));
|
||||
equal(obj, obj.trigger('a'));
|
||||
equal(obj, obj.listenTo(obj2, 'a', fn));
|
||||
equal(obj, obj.listenToOnce(obj2, 'b', fn));
|
||||
equal(obj, obj.off('a c'));
|
||||
equal(obj, obj.stopListening(obj2, 'a'));
|
||||
equal(obj, obj.stopListening());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
161
test/model.js
161
test/model.js
@@ -46,9 +46,9 @@ $(document).ready(function() {
|
||||
|
||||
test("initialize with parsed attributes", 1, function() {
|
||||
var Model = Backbone.Model.extend({
|
||||
parse: function(obj) {
|
||||
obj.value += 1;
|
||||
return obj;
|
||||
parse: function(attrs) {
|
||||
attrs.value += 1;
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
var model = new Model({value: 1}, {parse: true});
|
||||
@@ -69,8 +69,8 @@ $(document).ready(function() {
|
||||
|
||||
test("parse can return null", 1, function() {
|
||||
var Model = Backbone.Model.extend({
|
||||
parse: function(obj) {
|
||||
obj.value += 1;
|
||||
parse: function(attrs) {
|
||||
attrs.value += 1;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -242,14 +242,11 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
test("set falsy values in the correct order", 2, function() {
|
||||
var model = new Backbone.Model({result: 'result'});
|
||||
var model = new Backbone.Model({result: false});
|
||||
model.on('change', function() {
|
||||
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.set({result: void 0});
|
||||
});
|
||||
|
||||
@@ -324,7 +321,7 @@ $(document).ready(function() {
|
||||
"two": 2
|
||||
}
|
||||
});
|
||||
var model = new Defaulted({two: null});
|
||||
var model = new Defaulted({two: undefined});
|
||||
equal(model.get('one'), 1);
|
||||
equal(model.get('two'), 2);
|
||||
Defaulted = Backbone.Model.extend({
|
||||
@@ -335,7 +332,7 @@ $(document).ready(function() {
|
||||
};
|
||||
}
|
||||
});
|
||||
model = new Defaulted({two: null});
|
||||
model = new Defaulted({two: undefined});
|
||||
equal(model.get('one'), 3);
|
||||
equal(model.get('two'), 4);
|
||||
});
|
||||
@@ -401,7 +398,7 @@ $(document).ready(function() {
|
||||
if (attrs.admin) return "Can't change admin status.";
|
||||
};
|
||||
model.sync = function(method, model, options) {
|
||||
options.success.call(this, this, {admin: true}, options);
|
||||
options.success.call(this, {admin: true});
|
||||
};
|
||||
model.on('invalid', function(model, error) {
|
||||
lastError = error;
|
||||
@@ -418,6 +415,19 @@ $(document).ready(function() {
|
||||
ok(_.isEqual(this.syncArgs.model, doc));
|
||||
});
|
||||
|
||||
test("save, fetch, destroy triggers error event when an error occurs", 3, function () {
|
||||
var model = new Backbone.Model();
|
||||
model.on('error', function () {
|
||||
ok(true);
|
||||
});
|
||||
model.sync = function (method, model, options) {
|
||||
options.error();
|
||||
};
|
||||
model.save({data: 2, id: 1});
|
||||
model.fetch();
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
test("save with PATCH", function() {
|
||||
doc.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4});
|
||||
doc.save();
|
||||
@@ -435,7 +445,7 @@ $(document).ready(function() {
|
||||
test("save in positional style", 1, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.sync = function(method, model, options) {
|
||||
options.success(model, {}, options);
|
||||
options.success();
|
||||
};
|
||||
model.save('title', 'Twelfth Night');
|
||||
equal(model.get('title'), 'Twelfth Night');
|
||||
@@ -444,8 +454,8 @@ $(document).ready(function() {
|
||||
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);
|
||||
options.success('', options);
|
||||
options.success(null, options);
|
||||
};
|
||||
model.save({testing:'empty'}, {
|
||||
success: function (model) {
|
||||
@@ -649,15 +659,12 @@ $(document).ready(function() {
|
||||
ok('x' in model.attributes);
|
||||
});
|
||||
|
||||
test("hasChanged works outside of change events, and true within", 6, function() {
|
||||
var model = new Backbone.Model({x: 1});
|
||||
test("hasChanged works outside of change events, and true within", 4, function() {
|
||||
var model = new Backbone.Model;
|
||||
model.on('change:x', function() {
|
||||
ok(model.hasChanged('x'));
|
||||
equal(model.get('x'), 1);
|
||||
});
|
||||
model.set({x: 2}, {silent: true});
|
||||
ok(model.hasChanged());
|
||||
equal(model.hasChanged('x'), true);
|
||||
model.set({x: 1});
|
||||
ok(model.hasChanged());
|
||||
equal(model.hasChanged('x'), true);
|
||||
@@ -684,14 +691,14 @@ $(document).ready(function() {
|
||||
|
||||
test("`hasChanged` for falsey keys", 2, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.set({x: true}, {silent: true});
|
||||
model.set({x: true});
|
||||
ok(!model.hasChanged(0));
|
||||
ok(!model.hasChanged(''));
|
||||
});
|
||||
|
||||
test("`previous` for falsey keys", 2, function() {
|
||||
var model = new Backbone.Model({0: true, '': true});
|
||||
model.set({0: false, '': false}, {silent: true});
|
||||
model.set({0: false, '': false});
|
||||
equal(model.previous(0), true);
|
||||
equal(model.previous(''), true);
|
||||
});
|
||||
@@ -720,7 +727,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(model, {}, options);
|
||||
options.success();
|
||||
};
|
||||
model.on("change:x", function() { ok(true); });
|
||||
model.save({x: 3}, {wait: true});
|
||||
@@ -741,21 +748,24 @@ $(document).ready(function() {
|
||||
new Model().save();
|
||||
});
|
||||
|
||||
test("nested `set` during `'change:attr'`", 2, function() {
|
||||
test("nested `set` during `'change:attr'`", 1, function() {
|
||||
var events = [];
|
||||
var model = new Backbone.Model();
|
||||
var model = new Backbone.Model;
|
||||
model.on('all', function(event) { events.push(event); });
|
||||
model.on('change', function() {
|
||||
model.set({z: true}, {silent:true});
|
||||
model.set({z: true});
|
||||
});
|
||||
model.on('change:x', function() {
|
||||
model.set({y: true});
|
||||
});
|
||||
model.set({x: true});
|
||||
deepEqual(events, ['change:y', 'change:x', 'change']);
|
||||
events = [];
|
||||
model.set({z: true});
|
||||
deepEqual(events, []);
|
||||
deepEqual(events, [
|
||||
'change:y',
|
||||
'change:x',
|
||||
'change:z',
|
||||
'change',
|
||||
'change'
|
||||
]);
|
||||
});
|
||||
|
||||
test("nested `change` only fires once", 1, function() {
|
||||
@@ -793,16 +803,14 @@ $(document).ready(function() {
|
||||
model.set({x: true});
|
||||
});
|
||||
|
||||
test("nested `change` with silent", 3, function() {
|
||||
test("nested change", 3, function() {
|
||||
var count = 0;
|
||||
var model = new Backbone.Model();
|
||||
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});
|
||||
model.set({y: true, z: true});
|
||||
break;
|
||||
case 1:
|
||||
deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
|
||||
@@ -818,29 +826,7 @@ $(document).ready(function() {
|
||||
model.set({z: false});
|
||||
});
|
||||
|
||||
test("nested `change:attr` with silent", 0, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:y', function(){ ok(false); });
|
||||
model.on('change', function() {
|
||||
model.set({y: true}, {silent: true});
|
||||
model.set({z: true});
|
||||
});
|
||||
model.set({x: true});
|
||||
});
|
||||
|
||||
test("multiple nested changes with silent", 1, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:x', function() {
|
||||
model.set({y: 1}, {silent: true});
|
||||
model.set({y: 2});
|
||||
});
|
||||
model.on('change:y', function(model, val) {
|
||||
equal(val, 2);
|
||||
});
|
||||
model.set({x: true});
|
||||
});
|
||||
|
||||
test("multiple nested changes with silent", 1, function() {
|
||||
test("multiple nested changes", 1, function() {
|
||||
var changes = [];
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:b', function(model, val) { changes.push(val); });
|
||||
@@ -851,14 +837,6 @@ $(document).ready(function() {
|
||||
deepEqual(changes, [0, 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() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:b', function() {
|
||||
@@ -893,7 +871,7 @@ $(document).ready(function() {
|
||||
}
|
||||
};
|
||||
model.sync = function(method, model, options) {
|
||||
options.success(model, {}, options);
|
||||
options.success();
|
||||
};
|
||||
model.save({id: 1}, opts);
|
||||
model.fetch(opts);
|
||||
@@ -902,9 +880,8 @@ $(document).ready(function() {
|
||||
|
||||
test("#1412 - Trigger 'sync' event.", 3, function() {
|
||||
var model = new Backbone.Model({id: 1});
|
||||
model.url = '/test';
|
||||
model.sync = function (method, model, options) { options.success(); };
|
||||
model.on('sync', function(){ ok(true); });
|
||||
Backbone.ajax = function(settings){ settings.success(); };
|
||||
model.fetch();
|
||||
model.save();
|
||||
model.destroy();
|
||||
@@ -950,7 +927,7 @@ $(document).ready(function() {
|
||||
var Model = Backbone.Model.extend({
|
||||
sync: function(method, model, options) {
|
||||
setTimeout(function(){
|
||||
options.success(model, {}, options);
|
||||
options.success();
|
||||
start();
|
||||
}, 0);
|
||||
}
|
||||
@@ -960,28 +937,6 @@ $(document).ready(function() {
|
||||
.save(null, {wait: true});
|
||||
});
|
||||
|
||||
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(true); });
|
||||
model.set({x:2},{silent:true});
|
||||
model.set({x:3},{silent:true});
|
||||
model.set({x:1});
|
||||
});
|
||||
|
||||
test("#1664 - multiple silent changes nested inside a change event", 2, function() {
|
||||
var changes = [];
|
||||
var model = new Backbone.Model();
|
||||
model.on('change', function() {
|
||||
model.set({a:'c'}, {silent:true});
|
||||
model.set({b:2}, {silent:true});
|
||||
model.unset('c', {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']);
|
||||
deepEqual(model.attributes, {a: 'c', b: 2});
|
||||
});
|
||||
|
||||
test("#1791 - `attributes` is available for `parse`", function() {
|
||||
var Model = Backbone.Model.extend({
|
||||
parse: function() { this.has('a'); } // shouldn't throw an error
|
||||
@@ -990,22 +945,9 @@ $(document).ready(function() {
|
||||
expect(0);
|
||||
});
|
||||
|
||||
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); });
|
||||
model.on('change', function() {
|
||||
model.set({a:'c'}, {silent:true});
|
||||
});
|
||||
model.set({a:'a'});
|
||||
deepEqual(changes, ['a']);
|
||||
model.set({a:'a'});
|
||||
deepEqual(changes, ['a', 'a']);
|
||||
});
|
||||
|
||||
test("#1943 change calculations should use _.isEqual", function() {
|
||||
var model = new Backbone.Model({a: {key: 'value'}});
|
||||
model.set('a', {key:'value'}, {silent:true});
|
||||
model.set('a', {key:'value'});
|
||||
equal(model.changedAttributes(), false);
|
||||
});
|
||||
|
||||
@@ -1061,13 +1003,4 @@ $(document).ready(function() {
|
||||
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});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
15
test/vendor/qunit.css
vendored
15
test/vendor/qunit.css
vendored
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* QUnit v1.10.0 - A JavaScript Unit Testing Framework
|
||||
* QUnit v1.11.0 - A JavaScript Unit Testing Framework
|
||||
*
|
||||
* http://qunitjs.com
|
||||
*
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
/** Resets */
|
||||
|
||||
#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -111,7 +111,12 @@
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#qunit-tests ol {
|
||||
#qunit-tests li .runtime {
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.qunit-assert-list {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
@@ -122,6 +127,10 @@
|
||||
-webkit-border-radius: 5px;
|
||||
}
|
||||
|
||||
.qunit-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests table {
|
||||
border-collapse: collapse;
|
||||
margin-top: .2em;
|
||||
|
||||
491
test/vendor/qunit.js
vendored
491
test/vendor/qunit.js
vendored
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* QUnit v1.10.0 - A JavaScript Unit Testing Framework
|
||||
* QUnit v1.11.0 - A JavaScript Unit Testing Framework
|
||||
*
|
||||
* http://qunitjs.com
|
||||
*
|
||||
@@ -11,6 +11,7 @@
|
||||
(function( window ) {
|
||||
|
||||
var QUnit,
|
||||
assert,
|
||||
config,
|
||||
onErrorFnPrev,
|
||||
testId = 0,
|
||||
@@ -20,18 +21,67 @@ var QUnit,
|
||||
// Keep a local reference to Date (GH-283)
|
||||
Date = window.Date,
|
||||
defined = {
|
||||
setTimeout: typeof window.setTimeout !== "undefined",
|
||||
sessionStorage: (function() {
|
||||
var x = "qunit-test-string";
|
||||
try {
|
||||
sessionStorage.setItem( x, x );
|
||||
sessionStorage.removeItem( x );
|
||||
return true;
|
||||
} catch( e ) {
|
||||
return false;
|
||||
setTimeout: typeof window.setTimeout !== "undefined",
|
||||
sessionStorage: (function() {
|
||||
var x = "qunit-test-string";
|
||||
try {
|
||||
sessionStorage.setItem( x, x );
|
||||
sessionStorage.removeItem( x );
|
||||
return true;
|
||||
} catch( e ) {
|
||||
return false;
|
||||
}
|
||||
}())
|
||||
},
|
||||
/**
|
||||
* Provides a normalized error string, correcting an issue
|
||||
* with IE 7 (and prior) where Error.prototype.toString is
|
||||
* not properly implemented
|
||||
*
|
||||
* Based on http://es5.github.com/#x15.11.4.4
|
||||
*
|
||||
* @param {String|Error} error
|
||||
* @return {String} error message
|
||||
*/
|
||||
errorString = function( error ) {
|
||||
var name, message,
|
||||
errorString = error.toString();
|
||||
if ( errorString.substring( 0, 7 ) === "[object" ) {
|
||||
name = error.name ? error.name.toString() : "Error";
|
||||
message = error.message ? error.message.toString() : "";
|
||||
if ( name && message ) {
|
||||
return name + ": " + message;
|
||||
} else if ( name ) {
|
||||
return name;
|
||||
} else if ( message ) {
|
||||
return message;
|
||||
} else {
|
||||
return "Error";
|
||||
}
|
||||
} else {
|
||||
return errorString;
|
||||
}
|
||||
}())
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Makes a clone of an object using only Array or Object as base,
|
||||
* and copies over the own enumerable properties.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Object} New object with only the own properties (recursively).
|
||||
*/
|
||||
objectValues = function( obj ) {
|
||||
// Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
|
||||
/*jshint newcap: false */
|
||||
var key, val,
|
||||
vals = QUnit.is( "array", obj ) ? [] : {};
|
||||
for ( key in obj ) {
|
||||
if ( hasOwn.call( obj, key ) ) {
|
||||
val = obj[key];
|
||||
vals[key] = val === Object(val) ? objectValues(val) : val;
|
||||
}
|
||||
}
|
||||
return vals;
|
||||
};
|
||||
|
||||
function Test( settings ) {
|
||||
extend( this, settings );
|
||||
@@ -44,11 +94,11 @@ Test.count = 0;
|
||||
Test.prototype = {
|
||||
init: function() {
|
||||
var a, b, li,
|
||||
tests = id( "qunit-tests" );
|
||||
tests = id( "qunit-tests" );
|
||||
|
||||
if ( tests ) {
|
||||
b = document.createElement( "strong" );
|
||||
b.innerHTML = this.name;
|
||||
b.innerHTML = this.nameHtml;
|
||||
|
||||
// `a` initialized at top of scope
|
||||
a = document.createElement( "a" );
|
||||
@@ -92,6 +142,7 @@ Test.prototype = {
|
||||
teardown: function() {}
|
||||
}, this.moduleTestEnvironment );
|
||||
|
||||
this.started = +new Date();
|
||||
runLoggingCallbacks( "testStart", QUnit, {
|
||||
name: this.testName,
|
||||
module: this.module
|
||||
@@ -111,7 +162,7 @@ Test.prototype = {
|
||||
try {
|
||||
this.testEnvironment.setup.call( this.testEnvironment );
|
||||
} catch( e ) {
|
||||
QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
|
||||
QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
|
||||
}
|
||||
},
|
||||
run: function() {
|
||||
@@ -120,22 +171,28 @@ Test.prototype = {
|
||||
var running = id( "qunit-testresult" );
|
||||
|
||||
if ( running ) {
|
||||
running.innerHTML = "Running: <br/>" + this.name;
|
||||
running.innerHTML = "Running: <br/>" + this.nameHtml;
|
||||
}
|
||||
|
||||
if ( this.async ) {
|
||||
QUnit.stop();
|
||||
}
|
||||
|
||||
this.callbackStarted = +new Date();
|
||||
|
||||
if ( config.notrycatch ) {
|
||||
this.callback.call( this.testEnvironment, QUnit.assert );
|
||||
this.callbackRuntime = +new Date() - this.callbackStarted;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.callback.call( this.testEnvironment, QUnit.assert );
|
||||
this.callbackRuntime = +new Date() - this.callbackStarted;
|
||||
} catch( e ) {
|
||||
QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) );
|
||||
this.callbackRuntime = +new Date() - this.callbackStarted;
|
||||
|
||||
QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
|
||||
// else next test will carry the responsibility
|
||||
saveGlobal();
|
||||
|
||||
@@ -148,38 +205,43 @@ Test.prototype = {
|
||||
teardown: function() {
|
||||
config.current = this;
|
||||
if ( config.notrycatch ) {
|
||||
if ( typeof this.callbackRuntime === "undefined" ) {
|
||||
this.callbackRuntime = +new Date() - this.callbackStarted;
|
||||
}
|
||||
this.testEnvironment.teardown.call( this.testEnvironment );
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
this.testEnvironment.teardown.call( this.testEnvironment );
|
||||
} catch( e ) {
|
||||
QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
|
||||
QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
|
||||
}
|
||||
}
|
||||
checkPollution();
|
||||
},
|
||||
finish: function() {
|
||||
config.current = this;
|
||||
if ( config.requireExpects && this.expected == null ) {
|
||||
if ( config.requireExpects && this.expected === null ) {
|
||||
QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
|
||||
} else if ( this.expected != null && this.expected != this.assertions.length ) {
|
||||
} else if ( this.expected !== null && this.expected !== this.assertions.length ) {
|
||||
QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
|
||||
} else if ( this.expected == null && !this.assertions.length ) {
|
||||
} else if ( this.expected === null && !this.assertions.length ) {
|
||||
QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
|
||||
}
|
||||
|
||||
var assertion, a, b, i, li, ol,
|
||||
var i, assertion, a, b, time, li, ol,
|
||||
test = this,
|
||||
good = 0,
|
||||
bad = 0,
|
||||
tests = id( "qunit-tests" );
|
||||
|
||||
this.runtime = +new Date() - this.started;
|
||||
config.stats.all += this.assertions.length;
|
||||
config.moduleStats.all += this.assertions.length;
|
||||
|
||||
if ( tests ) {
|
||||
ol = document.createElement( "ol" );
|
||||
ol.className = "qunit-assert-list";
|
||||
|
||||
for ( i = 0; i < this.assertions.length; i++ ) {
|
||||
assertion = this.assertions[i];
|
||||
@@ -208,22 +270,22 @@ Test.prototype = {
|
||||
}
|
||||
|
||||
if ( bad === 0 ) {
|
||||
ol.style.display = "none";
|
||||
addClass( ol, "qunit-collapsed" );
|
||||
}
|
||||
|
||||
// `b` initialized at top of scope
|
||||
b = document.createElement( "strong" );
|
||||
b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
|
||||
b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
|
||||
|
||||
addEvent(b, "click", function() {
|
||||
var next = b.nextSibling.nextSibling,
|
||||
display = next.style.display;
|
||||
next.style.display = display === "none" ? "block" : "none";
|
||||
var next = b.parentNode.lastChild,
|
||||
collapsed = hasClass( next, "qunit-collapsed" );
|
||||
( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
|
||||
});
|
||||
|
||||
addEvent(b, "dblclick", function( e ) {
|
||||
var target = e && e.target ? e.target : window.event.srcElement;
|
||||
if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
|
||||
if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
|
||||
target = target.parentNode;
|
||||
}
|
||||
if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
|
||||
@@ -231,13 +293,19 @@ Test.prototype = {
|
||||
}
|
||||
});
|
||||
|
||||
// `time` initialized at top of scope
|
||||
time = document.createElement( "span" );
|
||||
time.className = "runtime";
|
||||
time.innerHTML = this.runtime + " ms";
|
||||
|
||||
// `li` initialized at top of scope
|
||||
li = id( this.id );
|
||||
li.className = bad ? "fail" : "pass";
|
||||
li.removeChild( li.firstChild );
|
||||
a = li.firstChild;
|
||||
li.appendChild( b );
|
||||
li.appendChild ( a );
|
||||
li.appendChild( a );
|
||||
li.appendChild( time );
|
||||
li.appendChild( ol );
|
||||
|
||||
} else {
|
||||
@@ -255,7 +323,8 @@ Test.prototype = {
|
||||
module: this.module,
|
||||
failed: bad,
|
||||
passed: this.assertions.length - bad,
|
||||
total: this.assertions.length
|
||||
total: this.assertions.length,
|
||||
duration: this.runtime
|
||||
});
|
||||
|
||||
QUnit.reset();
|
||||
@@ -321,7 +390,7 @@ QUnit = {
|
||||
|
||||
test: function( testName, expected, callback, async ) {
|
||||
var test,
|
||||
name = "<span class='test-name'>" + escapeInnerText( testName ) + "</span>";
|
||||
nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>";
|
||||
|
||||
if ( arguments.length === 2 ) {
|
||||
callback = expected;
|
||||
@@ -329,11 +398,11 @@ QUnit = {
|
||||
}
|
||||
|
||||
if ( config.currentModule ) {
|
||||
name = "<span class='module-name'>" + config.currentModule + "</span>: " + name;
|
||||
nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml;
|
||||
}
|
||||
|
||||
test = new Test({
|
||||
name: name,
|
||||
nameHtml: nameHtml,
|
||||
testName: testName,
|
||||
expected: expected,
|
||||
async: async,
|
||||
@@ -360,6 +429,18 @@ QUnit = {
|
||||
},
|
||||
|
||||
start: function( count ) {
|
||||
// QUnit hasn't been initialized yet.
|
||||
// Note: RequireJS (et al) may delay onLoad
|
||||
if ( config.semaphore === undefined ) {
|
||||
QUnit.begin(function() {
|
||||
// This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
|
||||
setTimeout(function() {
|
||||
QUnit.start( count );
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
config.semaphore -= count || 1;
|
||||
// don't start until equal number of stop-calls
|
||||
if ( config.semaphore > 0 ) {
|
||||
@@ -368,6 +449,8 @@ QUnit = {
|
||||
// ignore if start is called more often then stop
|
||||
if ( config.semaphore < 0 ) {
|
||||
config.semaphore = 0;
|
||||
QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
|
||||
return;
|
||||
}
|
||||
// A slight delay, to avoid any current callbacks
|
||||
if ( defined.setTimeout ) {
|
||||
@@ -403,11 +486,14 @@ QUnit = {
|
||||
}
|
||||
};
|
||||
|
||||
// `assert` initialized at top of scope
|
||||
// Asssert helpers
|
||||
// All of these must call either QUnit.push() or manually do:
|
||||
// All of these must either call QUnit.push() or manually do:
|
||||
// - runLoggingCallbacks( "log", .. );
|
||||
// - config.current.assertions.push({ .. });
|
||||
QUnit.assert = {
|
||||
// We attach it to the QUnit object *after* we expose the public API,
|
||||
// otherwise `assert` will become a global variable in browsers (#341).
|
||||
assert = {
|
||||
/**
|
||||
* Asserts rough true-ish result.
|
||||
* @name ok
|
||||
@@ -428,14 +514,14 @@ QUnit.assert = {
|
||||
message: msg
|
||||
};
|
||||
|
||||
msg = escapeInnerText( msg || (result ? "okay" : "failed" ) );
|
||||
msg = escapeText( msg || (result ? "okay" : "failed" ) );
|
||||
msg = "<span class='test-message'>" + msg + "</span>";
|
||||
|
||||
if ( !result ) {
|
||||
source = sourceFromStacktrace( 2 );
|
||||
if ( source ) {
|
||||
details.source = source;
|
||||
msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr></table>";
|
||||
msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr></table>";
|
||||
}
|
||||
}
|
||||
runLoggingCallbacks( "log", QUnit, details );
|
||||
@@ -453,6 +539,7 @@ QUnit.assert = {
|
||||
* @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
|
||||
*/
|
||||
equal: function( actual, expected, message ) {
|
||||
/*jshint eqeqeq:false */
|
||||
QUnit.push( expected == actual, actual, expected, message );
|
||||
},
|
||||
|
||||
@@ -461,9 +548,30 @@ QUnit.assert = {
|
||||
* @function
|
||||
*/
|
||||
notEqual: function( actual, expected, message ) {
|
||||
/*jshint eqeqeq:false */
|
||||
QUnit.push( expected != actual, actual, expected, message );
|
||||
},
|
||||
|
||||
/**
|
||||
* @name propEqual
|
||||
* @function
|
||||
*/
|
||||
propEqual: function( actual, expected, message ) {
|
||||
actual = objectValues(actual);
|
||||
expected = objectValues(expected);
|
||||
QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
|
||||
},
|
||||
|
||||
/**
|
||||
* @name notPropEqual
|
||||
* @function
|
||||
*/
|
||||
notPropEqual: function( actual, expected, message ) {
|
||||
actual = objectValues(actual);
|
||||
expected = objectValues(expected);
|
||||
QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
|
||||
},
|
||||
|
||||
/**
|
||||
* @name deepEqual
|
||||
* @function
|
||||
@@ -496,8 +604,9 @@ QUnit.assert = {
|
||||
QUnit.push( expected !== actual, actual, expected, message );
|
||||
},
|
||||
|
||||
throws: function( block, expected, message ) {
|
||||
"throws": function( block, expected, message ) {
|
||||
var actual,
|
||||
expectedOutput = expected,
|
||||
ok = false;
|
||||
|
||||
// 'expected' is optional
|
||||
@@ -518,18 +627,20 @@ QUnit.assert = {
|
||||
// we don't want to validate thrown error
|
||||
if ( !expected ) {
|
||||
ok = true;
|
||||
expectedOutput = null;
|
||||
// expected is a regexp
|
||||
} else if ( QUnit.objectType( expected ) === "regexp" ) {
|
||||
ok = expected.test( actual );
|
||||
ok = expected.test( errorString( actual ) );
|
||||
// expected is a constructor
|
||||
} else if ( actual instanceof expected ) {
|
||||
ok = true;
|
||||
// expected is a validation function which returns true is validation passed
|
||||
} else if ( expected.call( {}, actual ) === true ) {
|
||||
expectedOutput = null;
|
||||
ok = true;
|
||||
}
|
||||
|
||||
QUnit.push( ok, actual, null, message );
|
||||
QUnit.push( ok, actual, expectedOutput, message );
|
||||
} else {
|
||||
QUnit.pushFailure( message, null, 'No exception was thrown.' );
|
||||
}
|
||||
@@ -538,15 +649,16 @@ QUnit.assert = {
|
||||
|
||||
/**
|
||||
* @deprecate since 1.8.0
|
||||
* Kept assertion helpers in root for backwards compatibility
|
||||
* Kept assertion helpers in root for backwards compatibility.
|
||||
*/
|
||||
extend( QUnit, QUnit.assert );
|
||||
extend( QUnit, assert );
|
||||
|
||||
/**
|
||||
* @deprecated since 1.9.0
|
||||
* Kept global "raises()" for backwards compatibility
|
||||
* Kept root "raises()" for backwards compatibility.
|
||||
* (Note that we don't introduce assert.raises).
|
||||
*/
|
||||
QUnit.raises = QUnit.assert.throws;
|
||||
QUnit.raises = assert[ "throws" ];
|
||||
|
||||
/**
|
||||
* @deprecated since 1.0.0, replaced with error pushes since 1.3.0
|
||||
@@ -622,6 +734,15 @@ config = {
|
||||
moduleDone: []
|
||||
};
|
||||
|
||||
// Export global variables, unless an 'exports' object exists,
|
||||
// in that case we assume we're in CommonJS (dealt with on the bottom of the script)
|
||||
if ( typeof exports === "undefined" ) {
|
||||
extend( window, QUnit );
|
||||
|
||||
// Expose QUnit object
|
||||
window.QUnit = QUnit;
|
||||
}
|
||||
|
||||
// Initialize more QUnit.config and QUnit.urlParams
|
||||
(function() {
|
||||
var i,
|
||||
@@ -655,18 +776,11 @@ config = {
|
||||
QUnit.isLocal = location.protocol === "file:";
|
||||
}());
|
||||
|
||||
// Export global variables, unless an 'exports' object exists,
|
||||
// in that case we assume we're in CommonJS (dealt with on the bottom of the script)
|
||||
if ( typeof exports === "undefined" ) {
|
||||
extend( window, QUnit );
|
||||
|
||||
// Expose QUnit object
|
||||
window.QUnit = QUnit;
|
||||
}
|
||||
|
||||
// Extend QUnit object,
|
||||
// these after set here because they should not be exposed as global functions
|
||||
extend( QUnit, {
|
||||
assert: assert,
|
||||
|
||||
config: config,
|
||||
|
||||
// Initialize the configuration options
|
||||
@@ -681,7 +795,7 @@ extend( QUnit, {
|
||||
autorun: false,
|
||||
filter: "",
|
||||
queue: [],
|
||||
semaphore: 0
|
||||
semaphore: 1
|
||||
});
|
||||
|
||||
var tests, banner, result,
|
||||
@@ -689,7 +803,7 @@ extend( QUnit, {
|
||||
|
||||
if ( qunit ) {
|
||||
qunit.innerHTML =
|
||||
"<h1 id='qunit-header'>" + escapeInnerText( document.title ) + "</h1>" +
|
||||
"<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
|
||||
"<h2 id='qunit-banner'></h2>" +
|
||||
"<div id='qunit-testrunner-toolbar'></div>" +
|
||||
"<h2 id='qunit-userAgent'></h2>" +
|
||||
@@ -745,7 +859,7 @@ extend( QUnit, {
|
||||
|
||||
// Safe object type checking
|
||||
is: function( type, obj ) {
|
||||
return QUnit.objectType( obj ) == type;
|
||||
return QUnit.objectType( obj ) === type;
|
||||
},
|
||||
|
||||
objectType: function( obj ) {
|
||||
@@ -757,7 +871,8 @@ extend( QUnit, {
|
||||
return "null";
|
||||
}
|
||||
|
||||
var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || "";
|
||||
var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
|
||||
type = match && match[1] || "";
|
||||
|
||||
switch ( type ) {
|
||||
case "Number":
|
||||
@@ -794,16 +909,16 @@ extend( QUnit, {
|
||||
expected: expected
|
||||
};
|
||||
|
||||
message = escapeInnerText( message ) || ( result ? "okay" : "failed" );
|
||||
message = escapeText( message ) || ( result ? "okay" : "failed" );
|
||||
message = "<span class='test-message'>" + message + "</span>";
|
||||
output = message;
|
||||
|
||||
if ( !result ) {
|
||||
expected = escapeInnerText( QUnit.jsDump.parse(expected) );
|
||||
actual = escapeInnerText( QUnit.jsDump.parse(actual) );
|
||||
expected = escapeText( QUnit.jsDump.parse(expected) );
|
||||
actual = escapeText( QUnit.jsDump.parse(actual) );
|
||||
output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>";
|
||||
|
||||
if ( actual != expected ) {
|
||||
if ( actual !== expected ) {
|
||||
output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>";
|
||||
output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>";
|
||||
}
|
||||
@@ -812,7 +927,7 @@ extend( QUnit, {
|
||||
|
||||
if ( source ) {
|
||||
details.source = source;
|
||||
output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr>";
|
||||
output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
|
||||
}
|
||||
|
||||
output += "</table>";
|
||||
@@ -839,19 +954,19 @@ extend( QUnit, {
|
||||
message: message
|
||||
};
|
||||
|
||||
message = escapeInnerText( message ) || "error";
|
||||
message = escapeText( message ) || "error";
|
||||
message = "<span class='test-message'>" + message + "</span>";
|
||||
output = message;
|
||||
|
||||
output += "<table>";
|
||||
|
||||
if ( actual ) {
|
||||
output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeInnerText( actual ) + "</pre></td></tr>";
|
||||
output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>";
|
||||
}
|
||||
|
||||
if ( source ) {
|
||||
details.source = source;
|
||||
output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr>";
|
||||
output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
|
||||
}
|
||||
|
||||
output += "</table>";
|
||||
@@ -876,7 +991,8 @@ extend( QUnit, {
|
||||
querystring += encodeURIComponent( key ) + "=" +
|
||||
encodeURIComponent( params[ key ] ) + "&";
|
||||
}
|
||||
return window.location.pathname + querystring.slice( 0, -1 );
|
||||
return window.location.protocol + "//" + window.location.host +
|
||||
window.location.pathname + querystring.slice( 0, -1 );
|
||||
},
|
||||
|
||||
extend: extend,
|
||||
@@ -907,7 +1023,7 @@ extend( QUnit.constructor.prototype, {
|
||||
// testStart: { name }
|
||||
testStart: registerLoggingCallback( "testStart" ),
|
||||
|
||||
// testDone: { name, failed, passed, total }
|
||||
// testDone: { name, failed, passed, total, duration }
|
||||
testDone: registerLoggingCallback( "testDone" ),
|
||||
|
||||
// moduleStart: { name }
|
||||
@@ -925,9 +1041,10 @@ QUnit.load = function() {
|
||||
runLoggingCallbacks( "begin", QUnit, {} );
|
||||
|
||||
// Initialize the config, saving the execution queue
|
||||
var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, moduleFilter,
|
||||
numModules = 0,
|
||||
moduleFilterHtml = "",
|
||||
var banner, filter, i, label, len, main, ol, toolbar, userAgent, val,
|
||||
urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter,
|
||||
numModules = 0,
|
||||
moduleFilterHtml = "",
|
||||
urlConfigHtml = "",
|
||||
oldconfig = extend( {}, config );
|
||||
|
||||
@@ -948,14 +1065,24 @@ QUnit.load = function() {
|
||||
};
|
||||
}
|
||||
config[ val.id ] = QUnit.urlParams[ val.id ];
|
||||
urlConfigHtml += "<input id='qunit-urlconfig-" + val.id + "' name='" + val.id + "' type='checkbox'" + ( config[ val.id ] ? " checked='checked'" : "" ) + " title='" + val.tooltip + "'><label for='qunit-urlconfig-" + val.id + "' title='" + val.tooltip + "'>" + val.label + "</label>";
|
||||
urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) +
|
||||
"' name='" + escapeText( val.id ) +
|
||||
"' type='checkbox'" + ( config[ val.id ] ? " checked='checked'" : "" ) +
|
||||
" title='" + escapeText( val.tooltip ) +
|
||||
"'><label for='qunit-urlconfig-" + escapeText( val.id ) +
|
||||
"' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>";
|
||||
}
|
||||
|
||||
moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " + ( config.module === undefined ? "selected" : "" ) + ">< All Modules ></option>";
|
||||
moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " +
|
||||
( config.module === undefined ? "selected='selected'" : "" ) +
|
||||
">< All Modules ></option>";
|
||||
|
||||
for ( i in config.modules ) {
|
||||
if ( config.modules.hasOwnProperty( i ) ) {
|
||||
numModules += 1;
|
||||
moduleFilterHtml += "<option value='" + encodeURIComponent(i) + "' " + ( config.module === i ? "selected" : "" ) + ">" + i + "</option>";
|
||||
moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(i) ) + "' " +
|
||||
( config.module === i ? "selected='selected'" : "" ) +
|
||||
">" + escapeText(i) + "</option>";
|
||||
}
|
||||
}
|
||||
moduleFilterHtml += "</select>";
|
||||
@@ -1014,22 +1141,28 @@ QUnit.load = function() {
|
||||
label.innerHTML = "Hide passed tests";
|
||||
toolbar.appendChild( label );
|
||||
|
||||
urlConfigCheckboxes = document.createElement( 'span' );
|
||||
urlConfigCheckboxes.innerHTML = urlConfigHtml;
|
||||
addEvent( urlConfigCheckboxes, "change", function( event ) {
|
||||
var params = {};
|
||||
params[ event.target.name ] = event.target.checked ? true : undefined;
|
||||
urlConfigCheckboxesContainer = document.createElement("span");
|
||||
urlConfigCheckboxesContainer.innerHTML = urlConfigHtml;
|
||||
urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input");
|
||||
// For oldIE support:
|
||||
// * Add handlers to the individual elements instead of the container
|
||||
// * Use "click" instead of "change"
|
||||
// * Fallback from event.target to event.srcElement
|
||||
addEvents( urlConfigCheckboxes, "click", function( event ) {
|
||||
var params = {},
|
||||
target = event.target || event.srcElement;
|
||||
params[ target.name ] = target.checked ? true : undefined;
|
||||
window.location = QUnit.url( params );
|
||||
});
|
||||
toolbar.appendChild( urlConfigCheckboxes );
|
||||
toolbar.appendChild( urlConfigCheckboxesContainer );
|
||||
|
||||
if (numModules > 1) {
|
||||
moduleFilter = document.createElement( 'span' );
|
||||
moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' );
|
||||
moduleFilter.innerHTML = moduleFilterHtml;
|
||||
addEvent( moduleFilter, "change", function() {
|
||||
addEvent( moduleFilter.lastChild, "change", function() {
|
||||
var selectBox = moduleFilter.getElementsByTagName("select")[0],
|
||||
selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
|
||||
selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
|
||||
|
||||
window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } );
|
||||
});
|
||||
@@ -1106,7 +1239,7 @@ function done() {
|
||||
" milliseconds.<br/>",
|
||||
"<span class='passed'>",
|
||||
passed,
|
||||
"</span> tests of <span class='total'>",
|
||||
"</span> assertions of <span class='total'>",
|
||||
config.stats.all,
|
||||
"</span> passed, <span class='failed'>",
|
||||
config.stats.bad,
|
||||
@@ -1199,7 +1332,7 @@ function validTest( test ) {
|
||||
function extractStacktrace( e, offset ) {
|
||||
offset = offset === undefined ? 3 : offset;
|
||||
|
||||
var stack, include, i, regex;
|
||||
var stack, include, i;
|
||||
|
||||
if ( e.stacktrace ) {
|
||||
// Opera
|
||||
@@ -1213,7 +1346,7 @@ function extractStacktrace( e, offset ) {
|
||||
if ( fileName ) {
|
||||
include = [];
|
||||
for ( i = offset; i < stack.length; i++ ) {
|
||||
if ( stack[ i ].indexOf( fileName ) != -1 ) {
|
||||
if ( stack[ i ].indexOf( fileName ) !== -1 ) {
|
||||
break;
|
||||
}
|
||||
include.push( stack[ i ] );
|
||||
@@ -1242,17 +1375,27 @@ function sourceFromStacktrace( offset ) {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeInnerText( s ) {
|
||||
/**
|
||||
* Escape text for attribute or text content.
|
||||
*/
|
||||
function escapeText( s ) {
|
||||
if ( !s ) {
|
||||
return "";
|
||||
}
|
||||
s = s + "";
|
||||
return s.replace( /[\&<>]/g, function( s ) {
|
||||
// Both single quotes and double quotes (for attributes)
|
||||
return s.replace( /['"<>&]/g, function( s ) {
|
||||
switch( s ) {
|
||||
case "&": return "&";
|
||||
case "<": return "<";
|
||||
case ">": return ">";
|
||||
default: return s;
|
||||
case '\'':
|
||||
return ''';
|
||||
case '"':
|
||||
return '"';
|
||||
case '<':
|
||||
return '<';
|
||||
case '>':
|
||||
return '>';
|
||||
case '&':
|
||||
return '&';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1300,7 +1443,7 @@ function saveGlobal() {
|
||||
}
|
||||
}
|
||||
|
||||
function checkPollution( name ) {
|
||||
function checkPollution() {
|
||||
var newGlobals,
|
||||
deletedGlobals,
|
||||
old = config.pollution;
|
||||
@@ -1349,16 +1492,53 @@ function extend( a, b ) {
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
* @param {string} type
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function addEvent( elem, type, fn ) {
|
||||
// Standards-based browsers
|
||||
if ( elem.addEventListener ) {
|
||||
elem.addEventListener( type, fn, false );
|
||||
} else if ( elem.attachEvent ) {
|
||||
elem.attachEvent( "on" + type, fn );
|
||||
// IE
|
||||
} else {
|
||||
fn();
|
||||
elem.attachEvent( "on" + type, fn );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array|NodeList} elems
|
||||
* @param {string} type
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function addEvents( elems, type, fn ) {
|
||||
var i = elems.length;
|
||||
while ( i-- ) {
|
||||
addEvent( elems[i], type, fn );
|
||||
}
|
||||
}
|
||||
|
||||
function hasClass( elem, name ) {
|
||||
return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
|
||||
}
|
||||
|
||||
function addClass( elem, name ) {
|
||||
if ( !hasClass( elem, name ) ) {
|
||||
elem.className += (elem.className ? " " : "") + name;
|
||||
}
|
||||
}
|
||||
|
||||
function removeClass( elem, name ) {
|
||||
var set = " " + elem.className + " ";
|
||||
// Class name may appear multiple times
|
||||
while ( set.indexOf(" " + name + " ") > -1 ) {
|
||||
set = set.replace(" " + name + " " , " ");
|
||||
}
|
||||
// If possible, trim it for prettiness, but not neccecarily
|
||||
elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set );
|
||||
}
|
||||
|
||||
function id( name ) {
|
||||
return !!( typeof document !== "undefined" && document && document.getElementById ) &&
|
||||
document.getElementById( name );
|
||||
@@ -1372,7 +1552,6 @@ function registerLoggingCallback( key ) {
|
||||
|
||||
// Supports deprecated method of completely overwriting logging callbacks
|
||||
function runLoggingCallbacks( key, scope, args ) {
|
||||
//debugger;
|
||||
var i, callbacks;
|
||||
if ( QUnit.hasOwnProperty( key ) ) {
|
||||
QUnit[ key ].call(scope, args );
|
||||
@@ -1414,6 +1593,7 @@ QUnit.equiv = (function() {
|
||||
|
||||
// for string, boolean, number and null
|
||||
function useStrictEquality( b, a ) {
|
||||
/*jshint eqeqeq:false */
|
||||
if ( b instanceof a.constructor || a instanceof b.constructor ) {
|
||||
// to catch short annotaion VS 'new' annotation of a
|
||||
// declaration
|
||||
@@ -1610,7 +1790,8 @@ QUnit.jsDump = (function() {
|
||||
|
||||
var reName = /^function (\w+)/,
|
||||
jsDump = {
|
||||
parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance
|
||||
// type is used mostly internally, you can fix a (custom)type in advance
|
||||
parse: function( obj, type, stack ) {
|
||||
stack = stack || [ ];
|
||||
var inStack, res,
|
||||
parser = this.parsers[ type || this.typeOf(obj) ];
|
||||
@@ -1618,18 +1799,16 @@ QUnit.jsDump = (function() {
|
||||
type = typeof parser;
|
||||
inStack = inArray( obj, stack );
|
||||
|
||||
if ( inStack != -1 ) {
|
||||
if ( inStack !== -1 ) {
|
||||
return "recursion(" + (inStack - stack.length) + ")";
|
||||
}
|
||||
//else
|
||||
if ( type == "function" ) {
|
||||
if ( type === "function" ) {
|
||||
stack.push( obj );
|
||||
res = parser.call( this, obj, stack );
|
||||
stack.pop();
|
||||
return res;
|
||||
}
|
||||
// else
|
||||
return ( type == "string" ) ? parser : this.parsers.error;
|
||||
return ( type === "string" ) ? parser : this.parsers.error;
|
||||
},
|
||||
typeOf: function( obj ) {
|
||||
var type;
|
||||
@@ -1656,6 +1835,8 @@ QUnit.jsDump = (function() {
|
||||
( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
|
||||
) {
|
||||
type = "array";
|
||||
} else if ( obj.constructor === Error.prototype.constructor ) {
|
||||
type = "error";
|
||||
} else {
|
||||
type = typeof obj;
|
||||
}
|
||||
@@ -1664,7 +1845,8 @@ QUnit.jsDump = (function() {
|
||||
separator: function() {
|
||||
return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " ";
|
||||
},
|
||||
indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
|
||||
// extra can be a number, shortcut for increasing-calling-decreasing
|
||||
indent: function( extra ) {
|
||||
if ( !this.multiline ) {
|
||||
return "";
|
||||
}
|
||||
@@ -1693,13 +1875,16 @@ QUnit.jsDump = (function() {
|
||||
parsers: {
|
||||
window: "[Window]",
|
||||
document: "[Document]",
|
||||
error: "[ERROR]", //when no parser is found, shouldn"t happen
|
||||
error: function(error) {
|
||||
return "Error(\"" + error.message + "\")";
|
||||
},
|
||||
unknown: "[Unknown]",
|
||||
"null": "null",
|
||||
"undefined": "undefined",
|
||||
"function": function( fn ) {
|
||||
var ret = "function",
|
||||
name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE
|
||||
// functions never have name in IE
|
||||
name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
|
||||
|
||||
if ( name ) {
|
||||
ret += " " + name;
|
||||
@@ -1715,13 +1900,9 @@ QUnit.jsDump = (function() {
|
||||
object: function( map, stack ) {
|
||||
var ret = [ ], keys, key, val, i;
|
||||
QUnit.jsDump.up();
|
||||
if ( Object.keys ) {
|
||||
keys = Object.keys( map );
|
||||
} else {
|
||||
keys = [];
|
||||
for ( key in map ) {
|
||||
keys.push( key );
|
||||
}
|
||||
keys = [];
|
||||
for ( key in map ) {
|
||||
keys.push( key );
|
||||
}
|
||||
keys.sort();
|
||||
for ( i = 0; i < keys.length; i++ ) {
|
||||
@@ -1733,21 +1914,34 @@ QUnit.jsDump = (function() {
|
||||
return join( "{", ret, "}" );
|
||||
},
|
||||
node: function( node ) {
|
||||
var a, val,
|
||||
var len, i, val,
|
||||
open = QUnit.jsDump.HTML ? "<" : "<",
|
||||
close = QUnit.jsDump.HTML ? ">" : ">",
|
||||
tag = node.nodeName.toLowerCase(),
|
||||
ret = open + tag;
|
||||
ret = open + tag,
|
||||
attrs = node.attributes;
|
||||
|
||||
for ( a in QUnit.jsDump.DOMAttrs ) {
|
||||
val = node[ QUnit.jsDump.DOMAttrs[a] ];
|
||||
if ( val ) {
|
||||
ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" );
|
||||
if ( attrs ) {
|
||||
for ( i = 0, len = attrs.length; i < len; i++ ) {
|
||||
val = attrs[i].nodeValue;
|
||||
// IE6 includes all attributes in .attributes, even ones not explicitly set.
|
||||
// Those have values like undefined, null, 0, false, "" or "inherit".
|
||||
if ( val && val !== "inherit" ) {
|
||||
ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret + close + open + "/" + tag + close;
|
||||
ret += close;
|
||||
|
||||
// Show content of TextNode or CDATASection
|
||||
if ( node.nodeType === 3 || node.nodeType === 4 ) {
|
||||
ret += node.nodeValue;
|
||||
}
|
||||
|
||||
return ret + open + "/" + tag + close;
|
||||
},
|
||||
functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function
|
||||
// function calls it internally, it's the arguments part of the function
|
||||
functionArgs: function( fn ) {
|
||||
var args,
|
||||
l = fn.length;
|
||||
|
||||
@@ -1757,54 +1951,34 @@ QUnit.jsDump = (function() {
|
||||
|
||||
args = new Array(l);
|
||||
while ( l-- ) {
|
||||
args[l] = String.fromCharCode(97+l);//97 is 'a'
|
||||
// 97 is 'a'
|
||||
args[l] = String.fromCharCode(97+l);
|
||||
}
|
||||
return " " + args.join( ", " ) + " ";
|
||||
},
|
||||
key: quote, //object calls it internally, the key part of an item in a map
|
||||
functionCode: "[code]", //function calls it internally, it's the content of the function
|
||||
attribute: quote, //node calls it internally, it's an html attribute value
|
||||
// object calls it internally, the key part of an item in a map
|
||||
key: quote,
|
||||
// function calls it internally, it's the content of the function
|
||||
functionCode: "[code]",
|
||||
// node calls it internally, it's an html attribute value
|
||||
attribute: quote,
|
||||
string: quote,
|
||||
date: quote,
|
||||
regexp: literal, //regex
|
||||
regexp: literal,
|
||||
number: literal,
|
||||
"boolean": literal
|
||||
},
|
||||
DOMAttrs: {
|
||||
//attributes to dump from nodes, name=>realName
|
||||
id: "id",
|
||||
name: "name",
|
||||
"class": "className"
|
||||
},
|
||||
HTML: false,//if true, entities are escaped ( <, >, \t, space and \n )
|
||||
indentChar: " ",//indentation unit
|
||||
multiline: true //if true, items in a collection, are separated by a \n, else just a space.
|
||||
// if true, entities are escaped ( <, >, \t, space and \n )
|
||||
HTML: false,
|
||||
// indentation unit
|
||||
indentChar: " ",
|
||||
// if true, items in a collection, are separated by a \n, else just a space.
|
||||
multiline: true
|
||||
};
|
||||
|
||||
return jsDump;
|
||||
}());
|
||||
|
||||
// from Sizzle.js
|
||||
function getText( elems ) {
|
||||
var i, elem,
|
||||
ret = "";
|
||||
|
||||
for ( i = 0; elems[i]; i++ ) {
|
||||
elem = elems[i];
|
||||
|
||||
// Get the text from text nodes and CDATA nodes
|
||||
if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
|
||||
ret += elem.nodeValue;
|
||||
|
||||
// Traverse everything else, except comment nodes
|
||||
} else if ( elem.nodeType !== 8 ) {
|
||||
ret += getText( elem.childNodes );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// from jquery.js
|
||||
function inArray( elem, array ) {
|
||||
if ( array.indexOf ) {
|
||||
@@ -1835,13 +2009,14 @@ function inArray( elem, array ) {
|
||||
* QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
|
||||
*/
|
||||
QUnit.diff = (function() {
|
||||
/*jshint eqeqeq:false, eqnull:true */
|
||||
function diff( o, n ) {
|
||||
var i,
|
||||
ns = {},
|
||||
os = {};
|
||||
|
||||
for ( i = 0; i < n.length; i++ ) {
|
||||
if ( ns[ n[i] ] == null ) {
|
||||
if ( !hasOwn.call( ns, n[i] ) ) {
|
||||
ns[ n[i] ] = {
|
||||
rows: [],
|
||||
o: null
|
||||
@@ -1851,7 +2026,7 @@ QUnit.diff = (function() {
|
||||
}
|
||||
|
||||
for ( i = 0; i < o.length; i++ ) {
|
||||
if ( os[ o[i] ] == null ) {
|
||||
if ( !hasOwn.call( os, o[i] ) ) {
|
||||
os[ o[i] ] = {
|
||||
rows: [],
|
||||
n: null
|
||||
@@ -1864,7 +2039,7 @@ QUnit.diff = (function() {
|
||||
if ( !hasOwn.call( ns, i ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) {
|
||||
if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
|
||||
n[ ns[i].rows[0] ] = {
|
||||
text: n[ ns[i].rows[0] ],
|
||||
row: os[i].rows[0]
|
||||
@@ -1970,7 +2145,7 @@ QUnit.diff = (function() {
|
||||
|
||||
// for CommonJS enviroments, export everything
|
||||
if ( typeof exports !== "undefined" ) {
|
||||
extend(exports, QUnit);
|
||||
extend( exports, QUnit );
|
||||
}
|
||||
|
||||
// get at whatever the global object is, like window in browsers
|
||||
|
||||
171
test/vendor/runner.js
vendored
171
test/vendor/runner.js
vendored
@@ -1,98 +1,127 @@
|
||||
/*
|
||||
* Qt+WebKit powered headless test runner using Phantomjs
|
||||
* QtWebKit-powered headless test runner using PhantomJS
|
||||
*
|
||||
* Phantomjs installation: http://code.google.com/p/phantomjs/wiki/BuildInstructions
|
||||
* PhantomJS binaries: http://phantomjs.org/download.html
|
||||
* Requires PhantomJS 1.6+ (1.7+ recommended)
|
||||
*
|
||||
* Run with:
|
||||
* phantomjs runner.js [url-of-your-qunit-testsuite]
|
||||
* phantomjs runner.js [url-of-your-qunit-testsuite]
|
||||
*
|
||||
* E.g.
|
||||
* phantomjs runner.js http://localhost/qunit/test
|
||||
* e.g.
|
||||
* phantomjs runner.js http://localhost/qunit/test/index.html
|
||||
*/
|
||||
|
||||
/*jshint latedef:false */
|
||||
/*global phantom:true require:true console:true */
|
||||
var url = phantom.args[0],
|
||||
page = require('webpage').create();
|
||||
/*global phantom:false, require:false, console:false, window:false, QUnit:false */
|
||||
|
||||
// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
|
||||
page.onConsoleMessage = function(msg) {
|
||||
console.log(msg);
|
||||
};
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
page.onInitialized = function() {
|
||||
page.evaluate(addLogging);
|
||||
};
|
||||
page.open(url, function(status){
|
||||
if (status !== "success") {
|
||||
console.log("Unable to access network: " + status);
|
||||
var args = require('system').args;
|
||||
|
||||
// arg[0]: scriptName, args[1...]: arguments
|
||||
if (args.length !== 2) {
|
||||
console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite]');
|
||||
phantom.exit(1);
|
||||
} else {
|
||||
// page.evaluate(addLogging);
|
||||
var interval = setInterval(function() {
|
||||
if (finished()) {
|
||||
clearInterval(interval);
|
||||
onfinishedTests();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
function finished() {
|
||||
return page.evaluate(function(){
|
||||
return !!window.qunitDone;
|
||||
});
|
||||
}
|
||||
var url = args[1],
|
||||
page = require('webpage').create();
|
||||
|
||||
function onfinishedTests() {
|
||||
var output = page.evaluate(function() {
|
||||
return JSON.stringify(window.qunitDone);
|
||||
});
|
||||
phantom.exit(JSON.parse(output).failed > 0 ? 1 : 0);
|
||||
}
|
||||
// Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`)
|
||||
page.onConsoleMessage = function(msg) {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
function addLogging() {
|
||||
window.document.addEventListener( "DOMContentLoaded", function() {
|
||||
var current_test_assertions = [];
|
||||
page.onInitialized = function() {
|
||||
page.evaluate(addLogging);
|
||||
};
|
||||
|
||||
QUnit.testDone(function(result) {
|
||||
var i,
|
||||
name = result.module + ': ' + result.name;
|
||||
page.onCallback = function(message) {
|
||||
var result,
|
||||
failed;
|
||||
|
||||
if (result.failed) {
|
||||
console.log('Assertion Failed: ' + name);
|
||||
if (message) {
|
||||
if (message.name === 'QUnit.done') {
|
||||
result = message.data;
|
||||
failed = !result || result.failed;
|
||||
|
||||
for (i = 0; i < current_test_assertions.length; i++) {
|
||||
console.log(' ' + current_test_assertions[i]);
|
||||
}
|
||||
phantom.exit(failed ? 1 : 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
page.open(url, function(status) {
|
||||
if (status !== 'success') {
|
||||
console.error('Unable to access network: ' + status);
|
||||
phantom.exit(1);
|
||||
} else {
|
||||
// Cannot do this verification with the 'DOMContentLoaded' handler because it
|
||||
// will be too late to attach it if a page does not have any script tags.
|
||||
var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); });
|
||||
if (qunitMissing) {
|
||||
console.error('The `QUnit` object is not present on this page.');
|
||||
phantom.exit(1);
|
||||
}
|
||||
|
||||
current_test_assertions = [];
|
||||
});
|
||||
// Do nothing... the callback mechanism will handle everything!
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.log(function(details) {
|
||||
var response;
|
||||
function addLogging() {
|
||||
window.document.addEventListener('DOMContentLoaded', function() {
|
||||
var current_test_assertions = [];
|
||||
|
||||
if (details.result) {
|
||||
return;
|
||||
}
|
||||
QUnit.log(function(details) {
|
||||
var response;
|
||||
|
||||
response = details.message || '';
|
||||
|
||||
if (typeof details.expected !== 'undefined') {
|
||||
if (response) {
|
||||
response += ', ';
|
||||
// Ignore passing assertions
|
||||
if (details.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
response += 'expected: ' + details.expected + ', but was: ' + details.actual;
|
||||
}
|
||||
response = details.message || '';
|
||||
|
||||
current_test_assertions.push('Failed assertion: ' + response);
|
||||
});
|
||||
if (typeof details.expected !== 'undefined') {
|
||||
if (response) {
|
||||
response += ', ';
|
||||
}
|
||||
|
||||
QUnit.done(function(result){
|
||||
console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
|
||||
window.qunitDone = result;
|
||||
});
|
||||
}, false );
|
||||
}
|
||||
response += 'expected: ' + details.expected + ', but was: ' + details.actual;
|
||||
if (details.source) {
|
||||
response += "\n" + details.source;
|
||||
}
|
||||
}
|
||||
|
||||
current_test_assertions.push('Failed assertion: ' + response);
|
||||
});
|
||||
|
||||
QUnit.testDone(function(result) {
|
||||
var i,
|
||||
len,
|
||||
name = result.module + ': ' + result.name;
|
||||
|
||||
if (result.failed) {
|
||||
console.log('Test failed: ' + name);
|
||||
|
||||
for (i = 0, len = current_test_assertions.length; i < len; i++) {
|
||||
console.log(' ' + current_test_assertions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
current_test_assertions.length = 0;
|
||||
});
|
||||
|
||||
QUnit.done(function(result) {
|
||||
console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
|
||||
|
||||
if (typeof window.callPhantom === 'function') {
|
||||
window.callPhantom({
|
||||
'name': 'QUnit.done',
|
||||
'data': result
|
||||
});
|
||||
}
|
||||
});
|
||||
}, false);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user