Merge branch 'master' of github.com:documentcloud/backbone into demos

This commit is contained in:
Samuel Clay
2010-10-15 09:48:17 -04:00
8 changed files with 779 additions and 65 deletions

View File

@@ -8,42 +8,24 @@
// Initial Setup
// -------------
// The top-level namespace.
var Backbone = {};
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = this.Backbone = {};
}
// Keep the version here in sync with `package.json`.
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.1.1';
// Export for both CommonJS and the browser.
(typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;
// Require Underscore, if we're on the server, and it's not already present.
var _ = this._;
if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
// For Backbone's purposes, jQuery owns the `$` variable.
var $ = this.$;
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function(parent, protoProps, classProps) {
var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor :
function(){ return parent.apply(this, arguments); };
var ctor = function(){};
ctor.prototype = parent.prototype;
child.prototype = new ctor();
_.extend(child.prototype, protoProps);
if (classProps) _.extend(child, classProps);
child.prototype.constructor = child;
return child;
};
// Helper function to get a URL from a Model or Collection as a property
// or as a function.
var getUrl = function(object) {
return _.isFunction(object.url) ? object.url() : object.url;
};
var $ = this.jQuery;
// Backbone.Events
// -----------------
@@ -97,11 +79,10 @@
// Listening for `"all"` passes the true event name as the first argument.
trigger : function(ev) {
var list, calls, i, l;
var calls = this._callbacks;
if (!(calls = this._callbacks)) return this;
if (list = calls[ev]) {
for (i = 0, l = list.length; i < l; i++) {
list[i].apply(this, _.rest(arguments));
list[i].apply(this, Array.prototype.slice.call(arguments, 1));
}
}
if (list = calls['all']) {
@@ -154,7 +135,7 @@
// Extract attributes and options.
options || (options = {});
if (!attrs) return this;
attrs = attrs.attributes || attrs;
if (attrs.attributes) attrs = attrs.attributes;
var now = this.attributes;
// Run validation if `validate` is defined.
@@ -441,6 +422,7 @@
model.bind('all', this._boundOnModelEvent);
this.length++;
if (!options.silent) this.trigger('add', model);
return model;
},
// Internal implementation of removing a single model from the set, updating
@@ -456,6 +438,7 @@
model.unbind('all', this._boundOnModelEvent);
this.length--;
if (!options.silent) this.trigger('remove', model);
return model;
},
// Internal method called every time a model in the set fires an event.
@@ -607,6 +590,9 @@
'read' : 'GET'
};
// Backbone.sync
// -------------
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, uses jQuery to make a RESTful Ajax request
@@ -627,4 +613,32 @@
});
};
// Helpers
// -------
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function(parent, protoProps, classProps) {
var child;
if (protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
var ctor = function(){};
ctor.prototype = parent.prototype;
child.prototype = new ctor();
_.extend(child.prototype, protoProps);
if (classProps) _.extend(child, classProps);
child.prototype.constructor = child;
return child;
};
// Helper function to get a URL from a Model or Collection as a property
// or as a function.
var getUrl = function(object) {
return _.isFunction(object.url) ? object.url() : object.url;
};
})();

View File

@@ -1,6 +1,6 @@
$(document).ready(function() {
module("Backbone collections");
module("Backbone.Collection");
window.lastRequest = null;
@@ -15,7 +15,7 @@ $(document).ready(function() {
var e = null;
var col = window.col = new Backbone.Collection([a,b,c,d]);
test("collections: new and sort", function() {
test("Collection: new and sort", function() {
equals(col.first(), a, "a should be first");
equals(col.last(), d, "d should be last");
col.comparator = function(model) { return model.id; };
@@ -25,21 +25,21 @@ $(document).ready(function() {
equals(col.length, 4);
});
test("collections: get, getByCid", function() {
test("Collection: get, getByCid", function() {
equals(col.get(1), d);
equals(col.get(3), b);
equals(col.getByCid(col.first().cid), col.first());
});
test("collections: at", function() {
test("Collection: at", function() {
equals(col.at(2), b);
});
test("collections: pluck", function() {
test("Collection: pluck", function() {
equals(col.pluck('label').join(' '), 'd c b a');
});
test("collections: add", function() {
test("Collection: add", function() {
var added = null;
col.bind('add', function(model){ added = model.get('label'); });
e = new Backbone.Model({id: 0, label : 'e'});
@@ -49,7 +49,7 @@ $(document).ready(function() {
equals(col.first(), e);
});
test("collections: remove", function() {
test("Collection: remove", function() {
var removed = null;
col.bind('remove', function(model){ removed = model.get('label'); });
col.remove(e);
@@ -58,13 +58,13 @@ $(document).ready(function() {
equals(col.first(), d);
});
test("collections: fetch", function() {
test("Collection: fetch", function() {
col.fetch();
equals(lastRequest[0], 'read');
equals(lastRequest[1], col);
});
test("collections: create", function() {
test("Collection: create", function() {
var model = col.create({label: 'f'});
equals(lastRequest[0], 'create');
equals(lastRequest[1], model);
@@ -82,7 +82,7 @@ $(document).ready(function() {
equals(coll.one, 1);
});
test("collections: Underscore methods", function() {
test("Collection: Underscore methods", function() {
equals(col.map(function(model){ return model.get('label'); }).join(' '), 'd c b a');
equals(col.any(function(model){ return model.id === 100; }), false);
equals(col.any(function(model){ return model.id === 1; }), true);
@@ -97,7 +97,7 @@ $(document).ready(function() {
equals(col.min(function(model){ return model.id; }).id, 1);
});
test("collections: refresh", function() {
test("Collection: refresh", function() {
var refreshed = 0;
var models = col.models;
col.bind('refresh', function() { refreshed += 1; });

View File

@@ -1,8 +1,8 @@
$(document).ready(function() {
module("Backbone bindable");
module("Backbone.Events");
test("bindable: bind and trigger", function() {
test("Events: bind and trigger", function() {
var obj = { counter: 0 };
_.extend(obj,Backbone.Events);
obj.bind('event', function() { obj.counter += 1; });
@@ -15,7 +15,7 @@ $(document).ready(function() {
equals(obj.counter, 5, 'counter should be incremented five times.');
});
test("bindable: bind, then unbind all functions", function() {
test("Events: bind, then unbind all functions", function() {
var obj = { counter: 0 };
_.extend(obj,Backbone.Events);
var callback = function() { obj.counter += 1; };
@@ -26,7 +26,7 @@ $(document).ready(function() {
equals(obj.counter, 1, 'counter should have only been incremented once.');
});
test("bindable: bind two callbacks, unbind only one", function() {
test("Events: bind two callbacks, unbind only one", function() {
var obj = { counterA: 0, counterB: 0 };
_.extend(obj,Backbone.Events);
var callback = function() { obj.counterA += 1; };

View File

@@ -1,6 +1,6 @@
$(document).ready(function() {
module("Backbone model");
module("Backbone.Model");
// Variable to catch the last request.
window.lastRequest = null;
@@ -26,7 +26,7 @@ $(document).ready(function() {
var collection = new klass();
collection.add(doc);
test("model: initialize", function() {
test("Model: initialize", function() {
var Model = Backbone.Model.extend({
initialize: function() {
this.one = 1;
@@ -36,11 +36,11 @@ $(document).ready(function() {
equals(model.one, 1);
});
test("model: url", function() {
test("Model: url", function() {
equals(doc.url(), '/collection/1-the-tempest');
});
test("model: clone", function() {
test("Model: clone", function() {
attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
a = new Backbone.Model(attrs);
b = a.clone();
@@ -55,7 +55,7 @@ $(document).ready(function() {
equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone.");
});
test("model: isNew", function() {
test("Model: isNew", function() {
attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
a = new Backbone.Model(attrs);
ok(a.isNew(), "it should be new");
@@ -63,12 +63,12 @@ $(document).ready(function() {
ok(a.isNew(), "any defined ID is legal, negative or positive");
});
test("model: get", function() {
test("Model: get", function() {
equals(doc.get('title'), 'The Tempest');
equals(doc.get('author'), 'Bill Shakespeare');
});
test("model: set and unset", function() {
test("Model: set and unset", function() {
attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
a = new Backbone.Model(attrs);
var changeCount = 0;
@@ -85,7 +85,7 @@ $(document).ready(function() {
ok(changeCount == 2, "Change count should have incremented for unset.");
});
test("model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() {
test("Model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() {
var model = new Backbone.Model({name : "Tim", age : 10});
model.bind('change', function() {
ok(model.hasChanged('name'), 'name changed');
@@ -99,19 +99,19 @@ $(document).ready(function() {
equals(model.get('name'), 'Rob');
});
test("model: save", function() {
test("Model: save", function() {
doc.save({title : "Henry V"});
equals(lastRequest[0], 'update');
ok(_.isEqual(lastRequest[1], doc));
});
test("model: destroy", function() {
test("Model: destroy", function() {
doc.destroy();
equals(lastRequest[0], 'delete');
ok(_.isEqual(lastRequest[1], doc));
});
test("model: validate", function() {
test("Model: validate", function() {
var lastError;
var model = new Backbone.Model();
model.validate = function(attrs) {

45
test/speed.js Normal file
View File

@@ -0,0 +1,45 @@
(function(){
var object = {};
_.extend(object, Backbone.Events);
var fn = function(){};
JSLitmus.test('Events: bind + unbind', function() {
object.bind("event", fn);
object.unbind("event", fn);
});
object.bind('test:trigger', fn);
JSLitmus.test('Events: trigger', function() {
object.trigger('test:trigger');
});
object.bind('test:trigger2', fn);
object.bind('test:trigger2', fn);
JSLitmus.test('Events: trigger 2 functions, passing 5 arguments', function() {
object.trigger('test:trigger2', 1, 2, 3, 4, 5);
});
var model = new Backbone.Model;
JSLitmus.test('Model: set Math.random()', function() {
model.set({number: Math.random()});
});
var eventModel = new Backbone.Model;
eventModel.bind('change', fn);
JSLitmus.test('Model: set Math.random() with a change event', function() {
eventModel.set({number: Math.random()});
});
var keyModel = new Backbone.Model;
keyModel.bind('change:number', fn);
JSLitmus.test('Model: set Math.random() with a key-value observer', function() {
keyModel.set({number: Math.random()});
});
})();

View File

@@ -5,18 +5,23 @@
<link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
<script type="text/javascript" src="vendor/jquery-1.4.2.js"></script>
<script type="text/javascript" src="vendor/qunit.js"></script>
<script type="text/javascript" src="vendor/jslitmus.js"></script>
<script type="text/javascript" src="vendor/underscore-1.1.0.js"></script>
<script type="text/javascript" src="../backbone.js"></script>
<script type="text/javascript" src="bindable.js"></script>
<script type="text/javascript" src="events.js"></script>
<script type="text/javascript" src="model.js"></script>
<script type="text/javascript" src="collection.js"></script>
<script type="text/javascript" src="view.js"></script>
<script type="text/javascript" src="speed.js"></script>
</head>
<body>
<h1 id="qunit-header">Backbone Test Suite</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<br /><br />
<h1 id="qunit-header">Backbone Speed Suite</h1>
<div id="jslitmus_container" style="margin: 20px 10px;"></div>
</body>
</html>

649
test/vendor/jslitmus.js vendored Normal file
View File

@@ -0,0 +1,649 @@
// JSLitmus.js
//
// Copyright (c) 2010, Robert Kieffer, http://broofa.com
// Available under MIT license (http://en.wikipedia.org/wiki/MIT_License)
(function() {
// Private methods and state
// Get platform info but don't go crazy trying to recognize everything
// that's out there. This is just for the major platforms and OSes.
var platform = 'unknown platform', ua = navigator.userAgent;
// Detect OS
var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|');
var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null;
if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null;
// Detect browser
var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null;
// Detect version
var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)');
var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null;
var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform';
/**
* A smattering of methods that are needed to implement the JSLitmus testbed.
*/
var jsl = {
/**
* Enhanced version of escape()
*/
escape: function(s) {
s = s.replace(/,/g, '\\,');
s = escape(s);
s = s.replace(/\+/g, '%2b');
s = s.replace(/ /g, '+');
return s;
},
/**
* Get an element by ID.
*/
$: function(id) {
return document.getElementById(id);
},
/**
* Null function
*/
F: function() {},
/**
* Set the status shown in the UI
*/
status: function(msg) {
var el = jsl.$('jsl_status');
if (el) el.innerHTML = msg || '';
},
/**
* Convert a number to an abbreviated string like, "15K" or "10M"
*/
toLabel: function(n) {
if (n == Infinity) {
return 'Infinity';
} else if (n > 1e9) {
n = Math.round(n/1e8);
return n/10 + 'B';
} else if (n > 1e6) {
n = Math.round(n/1e5);
return n/10 + 'M';
} else if (n > 1e3) {
n = Math.round(n/1e2);
return n/10 + 'K';
}
return n;
},
/**
* Copy properties from src to dst
*/
extend: function(dst, src) {
for (var k in src) dst[k] = src[k]; return dst;
},
/**
* Like Array.join(), but for the key-value pairs in an object
*/
join: function(o, delimit1, delimit2) {
if (o.join) return o.join(delimit1); // If it's an array
var pairs = [];
for (var k in o) pairs.push(k + delimit1 + o[k]);
return pairs.join(delimit2);
},
/**
* Array#indexOf isn't supported in IE, so we use this as a cross-browser solution
*/
indexOf: function(arr, o) {
if (arr.indexOf) return arr.indexOf(o);
for (var i = 0; i < this.length; i++) if (arr[i] === o) return i;
return -1;
}
};
/**
* Test manages a single test (created with
* JSLitmus.test())
*
* @private
*/
var Test = function (name, f) {
if (!f) throw new Error('Undefined test function');
if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) {
throw new Error('"' + name + '" test: Test is not a valid Function object');
}
this.loopArg = RegExp.$1;
this.name = name;
this.f = f;
};
jsl.extend(Test, /** @lends Test */ {
/** Calibration tests for establishing iteration loop overhead */
CALIBRATIONS: [
new Test('calibrating loop', function(count) {while (count--);}),
new Test('calibrating function', jsl.F)
],
/**
* Run calibration tests. Returns true if calibrations are not yet
* complete (in which case calling code should run the tests yet again).
* onCalibrated - Callback to invoke when calibrations have finished
*/
calibrate: function(onCalibrated) {
for (var i = 0; i < Test.CALIBRATIONS.length; i++) {
var cal = Test.CALIBRATIONS[i];
if (cal.running) return true;
if (!cal.count) {
cal.isCalibration = true;
cal.onStop = onCalibrated;
//cal.MIN_TIME = .1; // Do calibrations quickly
cal.run(2e4);
return true;
}
}
return false;
}
});
jsl.extend(Test.prototype, {/** @lends Test.prototype */
/** Initial number of iterations */
INIT_COUNT: 10,
/** Max iterations allowed (i.e. used to detect bad looping functions) */
MAX_COUNT: 1e9,
/** Minimum time a test should take to get valid results (secs) */
MIN_TIME: .5,
/** Callback invoked when test state changes */
onChange: jsl.F,
/** Callback invoked when test is finished */
onStop: jsl.F,
/**
* Reset test state
*/
reset: function() {
delete this.count;
delete this.time;
delete this.running;
delete this.error;
},
/**
* Run the test (in a timeout). We use a timeout to make sure the browser
* has a chance to finish rendering any UI changes we've made, like
* updating the status message.
*/
run: function(count) {
count = count || this.INIT_COUNT;
jsl.status(this.name + ' x ' + count);
this.running = true;
var me = this;
setTimeout(function() {me._run(count);}, 200);
},
/**
* The nuts and bolts code that actually runs a test
*/
_run: function(count) {
var me = this;
// Make sure calibration tests have run
if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return;
this.error = null;
try {
var start, f = this.f, now, i = count;
// Start the timer
start = new Date();
// Now for the money shot. If this is a looping function ...
if (this.loopArg) {
// ... let it do the iteration itself
f(count);
} else {
// ... otherwise do the iteration for it
while (i--) f();
}
// Get time test took (in secs)
this.time = Math.max(1,new Date() - start)/1000;
// Store iteration count and per-operation time taken
this.count = count;
this.period = this.time/count;
// Do we need to do another run?
this.running = this.time <= this.MIN_TIME;
// ... if so, compute how many times we should iterate
if (this.running) {
// Bump the count to the nearest power of 2
var x = this.MIN_TIME/this.time;
var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2))));
count *= pow;
if (count > this.MAX_COUNT) {
throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.');
}
}
} catch (e) {
// Exceptions are caught and displayed in the test UI
this.reset();
this.error = e;
}
// Figure out what to do next
if (this.running) {
me.run(count);
} else {
jsl.status('');
me.onStop(me);
}
// Finish up
this.onChange(this);
},
/**
* Get the number of operations per second for this test.
*
* @param normalize if true, iteration loop overhead taken into account
*/
getHz: function(/**Boolean*/ normalize) {
var p = this.period;
// Adjust period based on the calibration test time
if (normalize && !this.isCalibration) {
var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1];
// If the period is within 20% of the calibration time, then zero the
// it out
p = p < cal.period*1.2 ? 0 : p - cal.period;
}
return Math.round(1/p);
},
/**
* Get a friendly string describing the test
*/
toString: function() {
return this.name + ' - ' + this.time/this.count + ' secs';
}
});
// CSS we need for the UI
var STYLESHEET = '<style> \
#jslitmus {font-family:sans-serif; font-size: 12px;} \
#jslitmus a {text-decoration: none;} \
#jslitmus a:hover {text-decoration: underline;} \
#jsl_status { \
margin-top: 10px; \
font-size: 10px; \
color: #888; \
} \
A IMG {border:none} \
#test_results { \
margin-top: 10px; \
font-size: 12px; \
font-family: sans-serif; \
border-collapse: collapse; \
border-spacing: 0px; \
} \
#test_results th, #test_results td { \
border: solid 1px #ccc; \
vertical-align: top; \
padding: 3px; \
} \
#test_results th { \
vertical-align: bottom; \
background-color: #ccc; \
padding: 1px; \
font-size: 10px; \
} \
#test_results #test_platform { \
color: #444; \
text-align:center; \
} \
#test_results .test_row { \
color: #006; \
cursor: pointer; \
} \
#test_results .test_nonlooping { \
border-left-style: dotted; \
border-left-width: 2px; \
} \
#test_results .test_looping { \
border-left-style: solid; \
border-left-width: 2px; \
} \
#test_results .test_name {white-space: nowrap;} \
#test_results .test_pending { \
} \
#test_results .test_running { \
font-style: italic; \
} \
#test_results .test_done {} \
#test_results .test_done { \
text-align: right; \
font-family: monospace; \
} \
#test_results .test_error {color: #600;} \
#test_results .test_error .error_head {font-weight:bold;} \
#test_results .test_error .error_body {font-size:85%;} \
#test_results .test_row:hover td { \
background-color: #ffc; \
text-decoration: underline; \
} \
#chart { \
margin: 10px 0px; \
width: 250px; \
} \
#chart img { \
border: solid 1px #ccc; \
margin-bottom: 5px; \
} \
#chart #tiny_url { \
height: 40px; \
width: 250px; \
} \
#jslitmus_credit { \
font-size: 10px; \
color: #888; \
margin-top: 8px; \
} \
</style>';
// HTML markup for the UI
var MARKUP = '<div id="jslitmus"> \
<button onclick="JSLitmus.runAll(event)">Run Tests</button> \
<button id="stop_button" disabled="disabled" onclick="JSLitmus.stop()">Stop Tests</button> \
<br \> \
<br \> \
<input type="checkbox" style="vertical-align: middle" id="test_normalize" checked="checked" onchange="JSLitmus.renderAll()""> Normalize results \
<table id="test_results"> \
<colgroup> \
<col /> \
<col width="100" /> \
</colgroup> \
<tr><th id="test_platform" colspan="2">' + platform + '</th></tr> \
<tr><th>Test</th><th>Ops/sec</th></tr> \
<tr id="test_row_template" class="test_row" style="display:none"> \
<td class="test_name"></td> \
<td class="test_result">Ready</td> \
</tr> \
</table> \
<div id="jsl_status"></div> \
<div id="chart" style="display:none"> \
<a id="chart_link" target="_blank"><img id="chart_image"></a> \
TinyURL (for chart): \
<iframe id="tiny_url" frameBorder="0" scrolling="no" src=""></iframe> \
</div> \
<a id="jslitmus_credit" title="JSLitmus home page" href="http://code.google.com/p/jslitmus" target="_blank">Powered by JSLitmus</a> \
</div>';
/**
* The public API for creating and running tests
*/
window.JSLitmus = {
/** The list of all tests that have been registered with JSLitmus.test */
_tests: [],
/** The queue of tests that need to be run */
_queue: [],
/**
* The parsed query parameters the current page URL. This is provided as a
* convenience for test functions - it's not used by JSLitmus proper
*/
params: {},
/**
* Initialize
*/
_init: function() {
// Parse query params into JSLitmus.params[] hash
var match = (location + '').match(/([^?#]*)(#.*)?$/);
if (match) {
var pairs = match[1].split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
if (pair.length > 1) {
var key = pair.shift();
var value = pair.length > 1 ? pair.join('=') : pair[0];
this.params[key] = value;
}
}
}
// Write out the stylesheet. We have to do this here because IE
// doesn't honor sheets written after the document has loaded.
document.write(STYLESHEET);
// Setup the rest of the UI once the document is loaded
if (window.addEventListener) {
window.addEventListener('load', this._setup, false);
} else if (document.addEventListener) {
document.addEventListener('load', this._setup, false);
} else if (window.attachEvent) {
window.attachEvent('onload', this._setup);
}
return this;
},
/**
* Set up the UI
*/
_setup: function() {
var el = jsl.$('jslitmus_container');
if (!el) document.body.appendChild(el = document.createElement('div'));
el.innerHTML = MARKUP;
// Render the UI for all our tests
for (var i=0; i < JSLitmus._tests.length; i++)
JSLitmus.renderTest(JSLitmus._tests[i]);
},
/**
* (Re)render all the test results
*/
renderAll: function() {
for (var i = 0; i < JSLitmus._tests.length; i++)
JSLitmus.renderTest(JSLitmus._tests[i]);
JSLitmus.renderChart();
},
/**
* (Re)render the chart graphics
*/
renderChart: function() {
var url = JSLitmus.chartUrl();
jsl.$('chart_link').href = url;
jsl.$('chart_image').src = url;
jsl.$('chart').style.display = '';
// Update the tiny URL
jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url);
},
/**
* (Re)render the results for a specific test
*/
renderTest: function(test) {
// Make a new row if needed
if (!test._row) {
var trow = jsl.$('test_row_template');
if (!trow) return;
test._row = trow.cloneNode(true);
test._row.style.display = '';
test._row.id = '';
test._row.onclick = function() {JSLitmus._queueTest(test);};
test._row.title = 'Run ' + test.name + ' test';
trow.parentNode.appendChild(test._row);
test._row.cells[0].innerHTML = test.name;
}
var cell = test._row.cells[1];
var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping'];
if (test.error) {
cns.push('test_error');
cell.innerHTML =
'<div class="error_head">' + test.error + '</div>' +
'<ul class="error_body"><li>' +
jsl.join(test.error, ': ', '</li><li>') +
'</li></ul>';
} else {
if (test.running) {
cns.push('test_running');
cell.innerHTML = 'running';
} else if (jsl.indexOf(JSLitmus._queue, test) >= 0) {
cns.push('test_pending');
cell.innerHTML = 'pending';
} else if (test.count) {
cns.push('test_done');
var hz = test.getHz(jsl.$('test_normalize').checked);
cell.innerHTML = hz != Infinity ? hz : '&infin;';
cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds';
} else {
cell.innerHTML = 'ready';
}
}
cell.className = cns.join(' ');
},
/**
* Create a new test
*/
test: function(name, f) {
// Create the Test object
var test = new Test(name, f);
JSLitmus._tests.push(test);
// Re-render if the test state changes
test.onChange = JSLitmus.renderTest;
// Run the next test if this one finished
test.onStop = function(test) {
if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test);
JSLitmus.currentTest = null;
JSLitmus._nextTest();
};
// Render the new test
this.renderTest(test);
},
/**
* Add all tests to the run queue
*/
runAll: function(e) {
e = e || window.event;
var reverse = e && e.shiftKey, len = JSLitmus._tests.length;
for (var i = 0; i < len; i++) {
JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]);
}
},
/**
* Remove all tests from the run queue. The current test has to finish on
* it's own though
*/
stop: function() {
while (JSLitmus._queue.length) {
var test = JSLitmus._queue.shift();
JSLitmus.renderTest(test);
}
},
/**
* Run the next test in the run queue
*/
_nextTest: function() {
if (!JSLitmus.currentTest) {
var test = JSLitmus._queue.shift();
if (test) {
jsl.$('stop_button').disabled = false;
JSLitmus.currentTest = test;
test.run();
JSLitmus.renderTest(test);
if (JSLitmus.onTestStart) JSLitmus.onTestStart(test);
} else {
jsl.$('stop_button').disabled = true;
JSLitmus.renderChart();
}
}
},
/**
* Add a test to the run queue
*/
_queueTest: function(test) {
if (jsl.indexOf(JSLitmus._queue, test) >= 0) return;
JSLitmus._queue.push(test);
JSLitmus.renderTest(test);
JSLitmus._nextTest();
},
/**
* Generate a Google Chart URL that shows the data for all tests
*/
chartUrl: function() {
var n = JSLitmus._tests.length, markers = [], data = [];
var d, min = 0, max = -1e10;
var normalize = jsl.$('test_normalize').checked;
// Gather test data
for (var i=0; i < JSLitmus._tests.length; i++) {
var test = JSLitmus._tests[i];
if (test.count) {
var hz = test.getHz(normalize);
var v = hz != Infinity ? hz : 0;
data.push(v);
markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' +
markers.length + ',10');
max = Math.max(v, max);
}
}
if (markers.length <= 0) return null;
// Build chart title
var title = document.getElementsByTagName('title');
title = (title && title.length) ? title[0].innerHTML : null;
var chart_title = [];
if (title) chart_title.push(title);
chart_title.push('Ops/sec (' + platform + ')');
// Build labels
var labels = [jsl.toLabel(min), jsl.toLabel(max)];
var w = 250, bw = 15;
var bs = 5;
var h = markers.length*(bw + bs) + 30 + chart_title.length*20;
var params = {
chtt: escape(chart_title.join('|')),
chts: '000000,10',
cht: 'bhg', // chart type
chd: 't:' + data.join(','), // data set
chds: min + ',' + max, // max/min of data
chxt: 'x', // label axes
chxl: '0:|' + labels.join('|'), // labels
chsp: '0,1',
chm: markers.join('|'), // test names
chbh: [bw, 0, bs].join(','), // bar widths
// chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient
chs: w + 'x' + h
};
return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&');
}
};
JSLitmus._init();
})();

View File

@@ -1,32 +1,33 @@
$(document).ready(function() {
module("Backbone View");
module("Backbone.View");
var view = new Backbone.View({
id : 'test-view',
className : 'test-view'
});
test("view: constructor", function() {
test("View: constructor", function() {
equals(view.el.id, 'test-view');
equals(view.el.className, 'test-view');
equals(view.options.id, 'test-view');
equals(view.options.className, 'test-view');
});
test("view: jQuery", function() {
test("View: jQuery", function() {
view.el = document.body;
equals(view.$('#qunit-header').text(), 'Backbone Test Suite');
equals(view.$('#qunit-header')[0].innerHTML, 'Backbone Test Suite');
equals(view.$('#qunit-header')[1].innerHTML, 'Backbone Speed Suite');
});
test("view: make", function() {
test("View: make", function() {
var div = view.make('div', {id: 'test-div'}, "one two three");
equals(div.tagName.toLowerCase(), 'div');
equals(div.id, 'test-div');
equals($(div).text(), 'one two three');
});
test("view: initialize", function() {
test("View: initialize", function() {
var View = Backbone.View.extend({
initialize: function() {
this.one = 1;
@@ -36,7 +37,7 @@ $(document).ready(function() {
equals(view.one, 1);
});
test("view: handleEvents", function() {
test("View: handleEvents", function() {
var counter = 0;
view.el = document.body;
view.increment = function() {