You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2010/06/02 19:45:57 UTC
svn commit: r950689 [1/2] - in /couchdb/trunk: ./ share/www/script/
share/www/script/jspec/ share/www/spec/
Author: jan
Date: Wed Jun 2 17:45:56 2010
New Revision: 950689
URL: http://svn.apache.org/viewvc?rev=950689&view=rev
Log:
Add tests for couch.js and jquery.couch.js
Patch by Lena Herrmann.
Closes COUCHDB-783.
Added:
couchdb/trunk/share/www/script/jspec/
couchdb/trunk/share/www/script/jspec/jspec.css
couchdb/trunk/share/www/script/jspec/jspec.jquery.js
couchdb/trunk/share/www/script/jspec/jspec.js
couchdb/trunk/share/www/script/jspec/jspec.xhr.js
couchdb/trunk/share/www/spec/
couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js
couchdb/trunk/share/www/spec/couch_js_instance_methods_1_spec.js
couchdb/trunk/share/www/spec/couch_js_instance_methods_2_spec.js
couchdb/trunk/share/www/spec/couch_js_instance_methods_3_spec.js
couchdb/trunk/share/www/spec/custom_helpers.js
couchdb/trunk/share/www/spec/jquery_couch_js_class_methods_spec.js
couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_1_spec.js
couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_2_spec.js
couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_3_spec.js
couchdb/trunk/share/www/spec/run.html
Modified:
couchdb/trunk/README
couchdb/trunk/share/www/script/couch.js
couchdb/trunk/share/www/script/jquery.couch.js
Modified: couchdb/trunk/README
URL: http://svn.apache.org/viewvc/couchdb/trunk/README?rev=950689&r1=950688&r2=950689&view=diff
==============================================================================
--- couchdb/trunk/README (original)
+++ couchdb/trunk/README Wed Jun 2 17:45:56 2010
@@ -39,6 +39,23 @@ The mailing lists provide a wealth of su
Feel free to drop by with your questions or discussion. See the official CouchDB
website for more information about our community resources.
+
+Running the Testsuite
+---------------------
+
+Run the testsuite for couch.js and jquery.couch.js by browsing to this site: http://127.0.0.1:5984/_utils/spec/run.html
+It should work in at least Firefox >= 3.6 and Safari >= 4.0.4.
+
+Read more about JSpec here: http://jspec.info/
+
+Trouble shooting
+~~~~~~~~~~~~~~~~
+
+ * When you change the specs, but your changes have no effect, manually reload the changed spec file in the browser.
+
+ * When the spec that tests erlang views fails, make sure you have enabled erlang views as described here: <http://wiki.apache.org/couchdb/EnableErlangViews>
+
+
Cryptographic Software Notice
-----------------------------
Modified: couchdb/trunk/share/www/script/couch.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch.js?rev=950689&r1=950688&r2=950689&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch.js [utf-8] Wed Jun 2 17:45:56 2010
@@ -22,10 +22,10 @@ function CouchDB(name, httpHeaders) {
this.last_req = null;
this.request = function(method, uri, requestOptions) {
- requestOptions = requestOptions || {}
- requestOptions.headers = combine(requestOptions.headers, httpHeaders)
- return CouchDB.request(method, uri, requestOptions);
- }
+ requestOptions = requestOptions || {}
+ requestOptions.headers = combine(requestOptions.headers, httpHeaders)
+ return CouchDB.request(method, uri, requestOptions);
+ }
// Creates the database on the server
this.createDb = function() {
@@ -198,12 +198,6 @@ function CouchDB(name, httpHeaders) {
return JSON.parse(this.last_req.responseText);
}
- this.viewCleanup = function() {
- this.last_req = this.request("POST", this.uri + "_view_cleanup");
- CouchDB.maybeThrowError(this.last_req);
- return JSON.parse(this.last_req.responseText);
- }
-
this.allDocs = function(options,keys) {
if(!keys) {
this.last_req = this.request("GET", this.uri + "_all_docs"
@@ -223,18 +217,11 @@ function CouchDB(name, httpHeaders) {
return this.allDocs({startkey:"_design", endkey:"_design0"});
};
- this.changes = function(options,keys) {
- var req = null;
- if(!keys) {
- req = this.request("GET", this.uri + "_changes" + encodeOptions(options));
- } else {
- req = this.request("POST", this.uri + "_changes" + encodeOptions(options), {
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify({keys:keys})
- });
- }
- CouchDB.maybeThrowError(req);
- return JSON.parse(req.responseText);
+ this.changes = function(options) {
+ this.last_req = this.request("GET", this.uri + "_changes"
+ + encodeOptions(options));
+ CouchDB.maybeThrowError(this.last_req);
+ return JSON.parse(this.last_req.responseText);
}
this.compact = function() {
Modified: couchdb/trunk/share/www/script/jquery.couch.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jquery.couch.js?rev=950689&r1=950688&r2=950689&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/jquery.couch.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/jquery.couch.js [utf-8] Wed Jun 2 17:45:56 2010
@@ -1,4 +1,4 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+a// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
@@ -276,7 +276,7 @@
}
});
} else {
- alert("please provide an eachApp function for allApps()");
+ alert("Please provide an eachApp function for allApps()");
}
},
openDoc: function(docId, options, ajaxOptions) {
@@ -327,7 +327,7 @@
beforeSend : beforeSend,
complete: function(req) {
var resp = $.httpData(req, "json");
- if (req.status == 201) {
+ if (req.status == 201 || req.status == 202) {
doc._id = resp.id;
doc._rev = resp.rev;
if (versioned) {
@@ -372,13 +372,27 @@
"The document could not be deleted"
);
},
- copyDoc: function(doc, options, ajaxOptions) {
+ bulkRemove: function(docs, options){
+ docs.docs = $.each(
+ docs.docs, function(i, doc){
+ doc._deleted = true;
+ }
+ );
+ $.extend(options, {successStatus: 201});
+ ajax({
+ type: "POST",
+ url: this.uri + "_bulk_docs" + encodeOptions(options),
+ data: toJSON(docs)
+ },
+ options,
+ "The documents could not be deleted"
+ );
+ },
+ copyDoc: function(docId, options, ajaxOptions) {
ajaxOptions = $.extend(ajaxOptions, {
complete: function(req) {
var resp = $.httpData(req, "json");
if (req.status == 201) {
- doc._id = resp.id;
- doc._rev = resp.rev;
if (options.success) options.success(resp);
} else if (options.error) {
options.error(req.status, resp.error, resp.reason);
@@ -389,9 +403,7 @@
});
ajax({
type: "COPY",
- url: this.uri +
- encodeDocId(doc._id) +
- encodeOptions({rev: doc._rev})
+ url: this.uri + encodeDocId(docId)
},
options,
"The document could not be copied",
@@ -490,13 +502,14 @@
);
},
- replicate: function(source, target, options) {
+ replicate: function(source, target, ajaxOptions, replicationOptions) {
+ replicationOptions = $.extend({source: source, target: target}, replicationOptions);
ajax({
type: "POST", url: this.urlPrefix + "/_replicate",
- data: JSON.stringify({source: source, target: target}),
+ data: JSON.stringify(replicationOptions),
contentType: "application/json"
},
- options,
+ ajaxOptions,
"Replication failed"
);
},
@@ -516,7 +529,6 @@
}
return uuidCache.shift();
}
-
});
function ajax(obj, options, errorMessage, ajaxOptions) {
@@ -525,8 +537,18 @@
$.ajax($.extend($.extend({
type: "GET", dataType: "json",
+ beforeSend: function(xhr){
+ if(ajaxOptions && ajaxOptions.headers){
+ for (var header in ajaxOptions.headers){
+ xhr.setRequestHeader(header, ajaxOptions.headers[header]);
+ }
+ }
+ },
complete: function(req) {
var resp = $.httpData(req, "json");
+ if (options.ajaxStart) {
+ options.ajaxStart(resp);
+ }
if (req.status == options.successStatus) {
if (options.beforeSuccess) options.beforeSuccess(req, resp);
if (options.success) options.success(resp);
@@ -556,7 +578,7 @@
var buf = [];
if (typeof(options) === "object" && options !== null) {
for (var name in options) {
- if ($.inArray(name, ["error", "success"]) >= 0)
+ if ($.inArray(name, ["error", "success", "ajaxStart"]) >= 0)
continue;
var value = options[name];
if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) {
Added: couchdb/trunk/share/www/script/jspec/jspec.css
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.css?rev=950689&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/jspec/jspec.css (added)
+++ couchdb/trunk/share/www/script/jspec/jspec.css Wed Jun 2 17:45:56 2010
@@ -0,0 +1,149 @@
+body.jspec {
+ margin: 45px 0;
+ font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+ background: #efefef url(images/bg.png) top left repeat-x;
+ text-align: center;
+}
+#jspec {
+ margin: 0 auto;
+ padding-top: 30px;
+ width: 1008px;
+ background: url(images/vr.png) top left repeat-y;
+ text-align: left;
+}
+#jspec-top {
+ position: relative;
+ margin: 0 auto;
+ width: 1008px;
+ height: 40px;
+ background: url(images/sprites.bg.png) top left no-repeat;
+}
+#jspec-bottom {
+ margin: 0 auto;
+ width: 1008px;
+ height: 15px;
+ background: url(images/sprites.bg.png) bottom left no-repeat;
+}
+#jspec .loading {
+ margin-top: -45px;
+ width: 1008px;
+ height: 80px;
+ background: url(images/loading.gif) 50% 50% no-repeat;
+}
+#jspec-title {
+ position: absolute;
+ top: 15px;
+ left: 20px;
+ width: 160px;
+ font-size: 22px;
+ font-weight: normal;
+ background: url(images/sprites.png) 0 -126px no-repeat;
+ text-align: center;
+}
+#jspec-title em {
+ font-size: 10px;
+ font-style: normal;
+ color: #BCC8D1;
+}
+#jspec-report * {
+ margin: 0;
+ padding: 0;
+ background: none;
+ border: none;
+}
+#jspec-report {
+ padding: 15px 40px;
+ font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+ color: #7B8D9B;
+}
+#jspec-report.has-failures {
+ padding-bottom: 30px;
+}
+#jspec-report .hidden {
+ display: none;
+}
+#jspec-report .heading {
+ margin-bottom: 15px;
+}
+#jspec-report .heading span {
+ padding-right: 10px;
+}
+#jspec-report .heading .passes em {
+ color: #0ea0eb;
+}
+#jspec-report .heading .failures em {
+ color: #FA1616;
+}
+#jspec-report table {
+ font-size: 11px;
+ border-collapse: collapse;
+}
+#jspec-report td {
+ padding: 8px;
+ text-indent: 30px;
+ color: #7B8D9B;
+}
+#jspec-report tr.body {
+ display: none;
+}
+#jspec-report tr.body pre {
+ margin: 0;
+ padding: 0 0 5px 25px;
+}
+#jspec-report tr.even:hover + tr.body,
+#jspec-report tr.odd:hover + tr.body {
+ display: block;
+}
+#jspec-report tr td:first-child em {
+ display: block;
+ clear: both;
+ font-style: normal;
+ font-weight: normal;
+ color: #7B8D9B;
+}
+#jspec-report tr.even:hover,
+#jspec-report tr.odd:hover {
+ text-shadow: 1px 1px 1px #fff;
+ background: #F2F5F7;
+}
+#jspec-report td + td {
+ padding-right: 0;
+ width: 15px;
+}
+#jspec-report td.pass {
+ background: url(images/sprites.png) 3px -7px no-repeat;
+}
+#jspec-report td.fail {
+ background: url(images/sprites.png) 3px -158px no-repeat;
+ font-weight: bold;
+ color: #FC0D0D;
+}
+#jspec-report td.requires-implementation {
+ background: url(images/sprites.png) 3px -333px no-repeat;
+}
+#jspec-report tr.description td {
+ margin-top: 25px;
+ padding-top: 25px;
+ font-size: 12px;
+ font-weight: bold;
+ text-indent: 0;
+ color: #1a1a1a;
+}
+#jspec-report tr.description:first-child td {
+ border-top: none;
+}
+#jspec-report .assertion {
+ display: block;
+ float: left;
+ margin: 0 0 0 1px;
+ padding: 0;
+ width: 1px;
+ height: 5px;
+ background: #7B8D9B;
+}
+#jspec-report .assertion.failed {
+ background: red;
+}
+.jspec-sandbox {
+ display: none;
+}
\ No newline at end of file
Added: couchdb/trunk/share/www/script/jspec/jspec.jquery.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.jquery.js?rev=950689&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/jspec/jspec.jquery.js (added)
+++ couchdb/trunk/share/www/script/jspec/jspec.jquery.js Wed Jun 2 17:45:56 2010
@@ -0,0 +1,72 @@
+
+// JSpec - jQuery - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
+
+JSpec
+.requires('jQuery', 'when using jspec.jquery.js')
+.include({
+ name: 'jQuery',
+
+ // --- Initialize
+
+ init : function() {
+ jQuery.ajaxSetup({ async: false })
+ },
+
+ // --- Utilities
+
+ utilities : {
+ element: jQuery,
+ elements: jQuery,
+ sandbox : function() {
+ return jQuery('<div class="sandbox"></div>')
+ }
+ },
+
+ // --- Matchers
+
+ matchers : {
+ have_tag : "jQuery(expected, actual).length === 1",
+ have_one : "alias have_tag",
+ have_tags : "jQuery(expected, actual).length > 1",
+ have_many : "alias have_tags",
+ have_any : "alias have_tags",
+ have_child : "jQuery(actual).children(expected).length === 1",
+ have_children : "jQuery(actual).children(expected).length > 1",
+ have_text : "jQuery(actual).text() === expected",
+ have_value : "jQuery(actual).val() === expected",
+ be_enabled : "!jQuery(actual).attr('disabled')",
+ have_class : "jQuery(actual).hasClass(expected)",
+
+ be_visible : function(actual) {
+ return jQuery(actual).css('display') != 'none' &&
+ jQuery(actual).css('visibility') != 'hidden' &&
+ jQuery(actual).attr('type') != 'hidden'
+ },
+
+ be_hidden : function(actual) {
+ return !JSpec.does(actual, 'be_visible')
+ },
+
+ have_classes : function(actual) {
+ return !JSpec.any(JSpec.toArray(arguments, 1), function(arg){
+ return !JSpec.does(actual, 'have_class', arg)
+ })
+ },
+
+ have_attr : function(actual, attr, value) {
+ return value ? jQuery(actual).attr(attr) == value:
+ jQuery(actual).attr(attr)
+ },
+
+ 'be disabled selected checked' : function(attr) {
+ return 'jQuery(actual).attr("' + attr + '")'
+ },
+
+ 'have type id title alt href src sel rev name target' : function(attr) {
+ return function(actual, value) {
+ return JSpec.does(actual, 'have_attr', attr, value)
+ }
+ }
+ }
+})
+
Added: couchdb/trunk/share/www/script/jspec/jspec.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.js?rev=950689&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/jspec/jspec.js (added)
+++ couchdb/trunk/share/www/script/jspec/jspec.js Wed Jun 2 17:45:56 2010
@@ -0,0 +1,1756 @@
+
+// JSpec - Core - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
+
+;(function(){
+
+ JSpec = {
+ version : '3.3.2',
+ assert : true,
+ cache : {},
+ suites : [],
+ modules : [],
+ allSuites : [],
+ matchers : {},
+ stubbed : [],
+ options : {},
+ request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
+ stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
+
+ /**
+ * Default context in which bodies are evaluated.
+ *
+ * Replace context simply by setting JSpec.context
+ * to your own like below:
+ *
+ * JSpec.context = { foo : 'bar' }
+ *
+ * Contexts can be changed within any body, this can be useful
+ * in order to provide specific helper methods to specific suites.
+ *
+ * To reset (usually in after hook) simply set to null like below:
+ *
+ * JSpec.context = null
+ *
+ */
+
+ defaultContext : {
+
+ /**
+ * Return an object used for proxy assertions.
+ * This object is used to indicate that an object
+ * should be an instance of _object_, not the constructor
+ * itself.
+ *
+ * @param {function} constructor
+ * @return {hash}
+ * @api public
+ */
+
+ an_instance_of : function(constructor) {
+ return { an_instance_of : constructor }
+ },
+
+ /**
+ * Load fixture at _path_.
+ *
+ * Fixtures are resolved as:
+ *
+ * - <path>
+ * - <path>.html
+ *
+ * @param {string} path
+ * @return {string}
+ * @api public
+ */
+
+ fixture : function(path) {
+ if (JSpec.cache[path]) return JSpec.cache[path]
+ return JSpec.cache[path] =
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) ||
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html')
+ }
+ },
+
+ // --- Objects
+
+ reporters : {
+
+ /**
+ * Report to server.
+ *
+ * Options:
+ * - uri specific uri to report to.
+ * - verbose weither or not to output messages
+ * - failuresOnly output failure messages only
+ *
+ * @api public
+ */
+
+ Server : function(results, options) {
+ var uri = options.uri || 'http://' + window.location.host + '/results'
+ JSpec.post(uri, {
+ stats: JSpec.stats,
+ options: options,
+ results: map(results.allSuites, function(suite) {
+ if (suite.hasSpecs())
+ return {
+ description: suite.description,
+ specs: map(suite.specs, function(spec) {
+ return {
+ description: spec.description,
+ message: !spec.passed() ? spec.failure().message : null,
+ status: spec.requiresImplementation() ? 'pending' :
+ spec.passed() ? 'pass' :
+ 'fail',
+ assertions: map(spec.assertions, function(assertion){
+ return {
+ passed: assertion.passed
+ }
+ })
+ }
+ })
+ }
+ })
+ })
+ if ('close' in main) main.close()
+ },
+
+ /**
+ * Default reporter, outputting to the DOM.
+ *
+ * Options:
+ * - reportToId id of element to output reports to, defaults to 'jspec'
+ * - failuresOnly displays only suites with failing specs
+ *
+ * @api public
+ */
+
+ DOM : function(results, options) {
+ var id = option('reportToId') || 'jspec',
+ report = document.getElementById(id),
+ failuresOnly = option('failuresOnly'),
+ classes = results.stats.failures ? 'has-failures' : ''
+ if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
+
+ function bodyContents(body) {
+ return JSpec.
+ escape(JSpec.contentsOf(body)).
+ replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
+ replace(/\r\n|\r|\n/gm, '<br/>')
+ }
+
+ report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
+ <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
+ <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
+ <span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \
+ </div><table class="suites">' + map(results.allSuites, function(suite) {
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
+ if (displaySuite && suite.hasSpecs())
+ return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
+ map(suite.specs, function(i, spec) {
+ return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
+ (spec.requiresImplementation() ?
+ '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
+ (spec.passed() && !failuresOnly) ?
+ '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
+ !spec.passed() ?
+ '<td class="fail">' + escape(spec.description) +
+ map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') +
+ '</td><td>' + spec.assertionsGraph() + '</td>' :
+ '') +
+ '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
+ }).join('') + '</tr>'
+ }).join('') + '</table></div>'
+ },
+
+ /**
+ * Terminal reporter.
+ *
+ * @api public
+ */
+
+ Terminal : function(results, options) {
+ var failuresOnly = option('failuresOnly')
+ print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
+ color(" Failures: ", 'bold') + color(results.stats.failures, 'red') +
+ color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n")
+
+ function indent(string) {
+ return string.replace(/^(.)/gm, ' $1')
+ }
+
+ each(results.allSuites, function(suite) {
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
+ if (displaySuite && suite.hasSpecs()) {
+ print(color(' ' + suite.description, 'bold'))
+ each(suite.specs, function(spec){
+ var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
+ return graph + color('.', assertion.passed ? 'green' : 'red')
+ })
+ if (spec.requiresImplementation())
+ print(color(' ' + spec.description, 'blue') + assertionsGraph)
+ else if (spec.passed() && !failuresOnly)
+ print(color(' ' + spec.description, 'green') + assertionsGraph)
+ else if (!spec.passed())
+ print(color(' ' + spec.description, 'red') + assertionsGraph +
+ "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n")
+ })
+ print("")
+ }
+ })
+
+ quit(results.stats.failures)
+ }
+ },
+
+ Assertion : function(matcher, actual, expected, negate) {
+ extend(this, {
+ message: '',
+ passed: false,
+ actual: actual,
+ negate: negate,
+ matcher: matcher,
+ expected: expected,
+
+ // Report assertion results
+
+ report : function() {
+ if (JSpec.assert)
+ this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
+ return this
+ },
+
+ // Run the assertion
+
+ run : function() {
+ // TODO: remove unshifting
+ expected.unshift(actual)
+ this.result = matcher.match.apply(this, expected)
+ this.passed = negate ? !this.result : this.result
+ if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
+ return this
+ }
+ })
+ },
+
+ ProxyAssertion : function(object, method, times, negate) {
+ var self = this
+ var old = object[method]
+
+ // Proxy
+
+ object[method] = function(){
+ args = toArray(arguments)
+ result = old.apply(object, args)
+ self.calls.push({ args : args, result : result })
+ return result
+ }
+
+ // Times
+
+ this.times = {
+ once : 1,
+ twice : 2
+ }[times] || times || 1
+
+ extend(this, {
+ calls: [],
+ message: '',
+ defer: true,
+ passed: false,
+ negate: negate,
+ object: object,
+ method: method,
+
+ // Proxy return value
+
+ and_return : function(result) {
+ this.expectedResult = result
+ return this
+ },
+
+ // Proxy arguments passed
+
+ with_args : function() {
+ this.expectedArgs = toArray(arguments)
+ return this
+ },
+
+ // Check if any calls have failing results
+
+ anyResultsFail : function() {
+ return any(this.calls, function(call){
+ return self.expectedResult.an_instance_of ?
+ call.result.constructor != self.expectedResult.an_instance_of:
+ !equal(self.expectedResult, call.result)
+ })
+ },
+
+ // Check if any calls have passing results
+
+ anyResultsPass : function() {
+ return any(this.calls, function(call){
+ return self.expectedResult.an_instance_of ?
+ call.result.constructor == self.expectedResult.an_instance_of:
+ equal(self.expectedResult, call.result)
+ })
+ },
+
+ // Return the passing result
+
+ passingResult : function() {
+ return this.anyResultsPass().result
+ },
+
+ // Return the failing result
+
+ failingResult : function() {
+ return this.anyResultsFail().result
+ },
+
+ // Check if any arguments fail
+
+ anyArgsFail : function() {
+ return any(this.calls, function(call){
+ return any(self.expectedArgs, function(i, arg){
+ if (arg == null) return call.args[i] == null
+ return arg.an_instance_of ?
+ call.args[i].constructor != arg.an_instance_of:
+ !equal(arg, call.args[i])
+
+ })
+ })
+ },
+
+ // Check if any arguments pass
+
+ anyArgsPass : function() {
+ return any(this.calls, function(call){
+ return any(self.expectedArgs, function(i, arg){
+ return arg.an_instance_of ?
+ call.args[i].constructor == arg.an_instance_of:
+ equal(arg, call.args[i])
+
+ })
+ })
+ },
+
+ // Return the passing args
+
+ passingArgs : function() {
+ return this.anyArgsPass().args
+ },
+
+ // Return the failing args
+
+ failingArgs : function() {
+ return this.anyArgsFail().args
+ },
+
+ // Report assertion results
+
+ report : function() {
+ if (JSpec.assert)
+ this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures
+ return this
+ },
+
+ // Run the assertion
+
+ run : function() {
+ var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
+
+ function times(n) {
+ return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n]
+ }
+
+ if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
+ this.message = methodString + ' to return ' + puts(this.expectedResult) +
+ ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
+
+ if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
+ this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
+ ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
+
+ if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times)
+ this.message = methodString + ' to be called ' + times(this.times) +
+ ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
+
+ if (!this.message.length)
+ this.passed = true
+
+ return this
+ }
+ })
+ },
+
+ /**
+ * Specification Suite block object.
+ *
+ * @param {string} description
+ * @param {function} body
+ * @api private
+ */
+
+ Suite : function(description, body) {
+ var self = this
+ extend(this, {
+ body: body,
+ description: description,
+ suites: [],
+ specs: [],
+ ran: false,
+ hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
+
+ // Add a spec to the suite
+
+ addSpec : function(description, body) {
+ var spec = new JSpec.Spec(description, body)
+ this.specs.push(spec)
+ JSpec.stats.specs++ // TODO: abstract
+ spec.suite = this
+ },
+
+ // Add a hook to the suite
+
+ addHook : function(hook, body) {
+ this.hooks[hook].push(body)
+ },
+
+ // Add a nested suite
+
+ addSuite : function(description, body) {
+ var suite = new JSpec.Suite(description, body)
+ JSpec.allSuites.push(suite)
+ suite.name = suite.description
+ suite.description = this.description + ' ' + suite.description
+ this.suites.push(suite)
+ suite.suite = this
+ },
+
+ // Invoke a hook in context to this suite
+
+ hook : function(hook) {
+ if (this.suite) this.suite.hook(hook)
+ each(this.hooks[hook], function(body) {
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
+ })
+ },
+
+ // Check if nested suites are present
+
+ hasSuites : function() {
+ return this.suites.length
+ },
+
+ // Check if this suite has specs
+
+ hasSpecs : function() {
+ return this.specs.length
+ },
+
+ // Check if the entire suite passed
+
+ passed : function() {
+ return !any(this.specs, function(spec){
+ return !spec.passed()
+ })
+ }
+ })
+ },
+
+ /**
+ * Specification block object.
+ *
+ * @param {string} description
+ * @param {function} body
+ * @api private
+ */
+
+ Spec : function(description, body) {
+ extend(this, {
+ body: body,
+ description: description,
+ assertions: [],
+
+ // Add passing assertion
+
+ pass : function(message) {
+ this.assertions.push({ passed: true, message: message })
+ if (JSpec.assert) ++JSpec.stats.passes
+ },
+
+ // Add failing assertion
+
+ fail : function(message) {
+ this.assertions.push({ passed: false, message: message })
+ if (JSpec.assert) ++JSpec.stats.failures
+ },
+
+ // Run deferred assertions
+
+ runDeferredAssertions : function() {
+ each(this.assertions, function(assertion){
+ if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
+ })
+ },
+
+ // Find first failing assertion
+
+ failure : function() {
+ return find(this.assertions, function(assertion){
+ return !assertion.passed
+ })
+ },
+
+ // Find all failing assertions
+
+ failures : function() {
+ return select(this.assertions, function(assertion){
+ return !assertion.passed
+ })
+ },
+
+ // Weither or not the spec passed
+
+ passed : function() {
+ return !this.failure()
+ },
+
+ // Weither or not the spec requires implementation (no assertions)
+
+ requiresImplementation : function() {
+ return this.assertions.length == 0
+ },
+
+ // Sprite based assertions graph
+
+ assertionsGraph : function() {
+ return map(this.assertions, function(assertion){
+ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
+ }).join('')
+ }
+ })
+ },
+
+ Module : function(methods) {
+ extend(this, methods)
+ },
+
+ JSON : {
+
+ /**
+ * Generic sequences.
+ */
+
+ meta : {
+ '\b' : '\\b',
+ '\t' : '\\t',
+ '\n' : '\\n',
+ '\f' : '\\f',
+ '\r' : '\\r',
+ '"' : '\\"',
+ '\\' : '\\\\'
+ },
+
+ /**
+ * Escapable sequences.
+ */
+
+ escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+
+ /**
+ * JSON encode _object_.
+ *
+ * @param {mixed} object
+ * @return {string}
+ * @api private
+ */
+
+ encode : function(object) {
+ var self = this
+ if (object == undefined || object == null) return 'null'
+ if (object === true) return 'true'
+ if (object === false) return 'false'
+ switch (typeof object) {
+ case 'number': return object
+ case 'string': return this.escapable.test(object) ?
+ '"' + object.replace(this.escapable, function (a) {
+ return typeof self.meta[a] === 'string' ? self.meta[a] :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
+ }) + '"' :
+ '"' + object + '"'
+ case 'object':
+ if (object.constructor == Array)
+ return '[' + map(object, function(val){
+ return self.encode(val)
+ }).join(', ') + ']'
+ else if (object)
+ return '{' + map(object, function(key, val){
+ return self.encode(key) + ':' + self.encode(val)
+ }).join(', ') + '}'
+ }
+ return 'null'
+ }
+ },
+
+ // --- DSLs
+
+ DSLs : {
+ snake : {
+ expect : function(actual){
+ return JSpec.expect(actual)
+ },
+
+ describe : function(description, body) {
+ return JSpec.currentSuite.addSuite(description, body)
+ },
+
+ it : function(description, body) {
+ return JSpec.currentSuite.addSpec(description, body)
+ },
+
+ before : function(body) {
+ return JSpec.currentSuite.addHook('before', body)
+ },
+
+ after : function(body) {
+ return JSpec.currentSuite.addHook('after', body)
+ },
+
+ before_each : function(body) {
+ return JSpec.currentSuite.addHook('before_each', body)
+ },
+
+ after_each : function(body) {
+ return JSpec.currentSuite.addHook('after_each', body)
+ },
+
+ should_behave_like : function(description) {
+ return JSpec.shareBehaviorsOf(description)
+ }
+ }
+ },
+
+ // --- Methods
+
+ /**
+ * Check if _value_ is 'stop'. For use as a
+ * utility callback function.
+ *
+ * @param {mixed} value
+ * @return {bool}
+ * @api public
+ */
+
+ haveStopped : function(value) {
+ return value === 'stop'
+ },
+
+ /**
+ * Include _object_ which may be a hash or Module instance.
+ *
+ * @param {hash, Module} object
+ * @return {JSpec}
+ * @api public
+ */
+
+ include : function(object) {
+ var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
+ this.modules.push(module)
+ if ('init' in module) module.init()
+ if ('utilities' in module) extend(this.defaultContext, module.utilities)
+ if ('matchers' in module) this.addMatchers(module.matchers)
+ if ('reporters' in module) extend(this.reporters, module.reporters)
+ if ('DSLs' in module)
+ each(module.DSLs, function(name, methods){
+ JSpec.DSLs[name] = JSpec.DSLs[name] || {}
+ extend(JSpec.DSLs[name], methods)
+ })
+ return this
+ },
+
+ /**
+ * Add a module hook _name_, which is immediately
+ * called per module with the _args_ given. An array of
+ * hook return values is returned.
+ *
+ * @param {name} string
+ * @param {...} args
+ * @return {array}
+ * @api private
+ */
+
+ hook : function(name, args) {
+ args = toArray(arguments, 1)
+ return inject(JSpec.modules, [], function(results, module){
+ if (typeof module[name] == 'function')
+ results.push(JSpec.evalHook(module, name, args))
+ })
+ },
+
+ /**
+ * Eval _module_ hook _name_ with _args_. Evaluates in context
+ * to the module itself, JSpec, and JSpec.context.
+ *
+ * @param {Module} module
+ * @param {string} name
+ * @param {array} args
+ * @return {mixed}
+ * @api private
+ */
+
+ evalHook : function(module, name, args) {
+ hook('evaluatingHookBody', module, name)
+ try { return module[name].apply(module, args) }
+ catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) }
+ },
+
+ /**
+ * Same as hook() however accepts only one _arg_ which is
+ * considered immutable. This function passes the arg
+ * to the first module, then passes the return value of the last
+ * module called, to the following module.
+ *
+ * @param {string} name
+ * @param {mixed} arg
+ * @return {mixed}
+ * @api private
+ */
+
+ hookImmutable : function(name, arg) {
+ return inject(JSpec.modules, arg, function(result, module){
+ if (typeof module[name] == 'function')
+ return JSpec.evalHook(module, name, [result])
+ })
+ },
+
+ /**
+ * Find a suite by its description or name.
+ *
+ * @param {string} description
+ * @return {Suite}
+ * @api private
+ */
+
+ findSuite : function(description) {
+ return find(this.allSuites, function(suite){
+ return suite.name == description || suite.description == description
+ })
+ },
+
+ /**
+ * Share behaviors (specs) of the given suite with
+ * the current suite.
+ *
+ * @param {string} description
+ * @api public
+ */
+
+ shareBehaviorsOf : function(description) {
+ if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
+ else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
+ },
+
+ /**
+ * Copy specs from one suite to another.
+ *
+ * @param {Suite} fromSuite
+ * @param {Suite} toSuite
+ * @api public
+ */
+
+ copySpecs : function(fromSuite, toSuite) {
+ each(fromSuite.specs, function(spec){
+ var newSpec = new Object();
+ extend(newSpec, spec);
+ newSpec.assertions = [];
+ toSuite.specs.push(newSpec);
+ })
+ },
+
+ /**
+ * Convert arguments to an array.
+ *
+ * @param {object} arguments
+ * @param {int} offset
+ * @return {array}
+ * @api public
+ */
+
+ toArray : function(arguments, offset) {
+ return Array.prototype.slice.call(arguments, offset || 0)
+ },
+
+ /**
+ * Return ANSI-escaped colored string.
+ *
+ * @param {string} string
+ * @param {string} color
+ * @return {string}
+ * @api public
+ */
+
+ color : function(string, color) {
+ return "\u001B[" + {
+ bold : 1,
+ black : 30,
+ red : 31,
+ green : 32,
+ yellow : 33,
+ blue : 34,
+ magenta : 35,
+ cyan : 36,
+ white : 37
+ }[color] + 'm' + string + "\u001B[0m"
+ },
+
+ /**
+ * Default matcher message callback.
+ *
+ * @api private
+ */
+
+ defaultMatcherMessage : function(actual, expected, negate, name) {
+ return 'expected ' + puts(actual) + ' to ' +
+ (negate ? 'not ' : '') +
+ name.replace(/_/g, ' ') +
+ ' ' + (expected.length > 1 ?
+ puts.apply(this, expected.slice(1)) :
+ '')
+ },
+
+ /**
+ * Normalize a matcher message.
+ *
+ * When no messge callback is present the defaultMatcherMessage
+ * will be assigned, will suffice for most matchers.
+ *
+ * @param {hash} matcher
+ * @return {hash}
+ * @api public
+ */
+
+ normalizeMatcherMessage : function(matcher) {
+ if (typeof matcher.message != 'function')
+ matcher.message = this.defaultMatcherMessage
+ return matcher
+ },
+
+ /**
+ * Normalize a matcher body
+ *
+ * This process allows the following conversions until
+ * the matcher is in its final normalized hash state.
+ *
+ * - '==' becomes 'actual == expected'
+ * - 'actual == expected' becomes 'return actual == expected'
+ * - function(actual, expected) { return actual == expected } becomes
+ * { match : function(actual, expected) { return actual == expected }}
+ *
+ * @param {mixed} body
+ * @return {hash}
+ * @api public
+ */
+
+ normalizeMatcherBody : function(body) {
+ switch (body.constructor) {
+ case String:
+ if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
+ if (body.length < 4) body = 'actual ' + body + ' expected'
+ return { match: function(actual, expected) { return eval(body) }}
+
+ case Function:
+ return { match: body }
+
+ default:
+ return body
+ }
+ },
+
+ /**
+ * Get option value. This method first checks if
+ * the option key has been set via the query string,
+ * otherwise returning the options hash value.
+ *
+ * @param {string} key
+ * @return {mixed}
+ * @api public
+ */
+
+ option : function(key) {
+ return (value = query(key)) !== null ? value :
+ JSpec.options[key] || null
+ },
+
+ /**
+ * Check if object _a_, is equal to object _b_.
+ *
+ * @param {object} a
+ * @param {object} b
+ * @return {bool}
+ * @api private
+ */
+
+ equal: function(a, b) {
+ if (typeof a != typeof b) return
+ if (a === b) return true
+ if (a instanceof RegExp)
+ return a.toString() === b.toString()
+ if (a instanceof Date)
+ return Number(a) === Number(b)
+ if (typeof a != 'object') return
+ if (a.length !== undefined)
+ if (a.length !== b.length) return
+ else
+ for (var i = 0, len = a.length; i < len; ++i)
+ if (!equal(a[i], b[i]))
+ return
+ for (var key in a)
+ if (!equal(a[key], b[key]))
+ return
+ return true
+ },
+
+ /**
+ * Return last element of an array.
+ *
+ * @param {array} array
+ * @return {object}
+ * @api public
+ */
+
+ last : function(array) {
+ return array[array.length - 1]
+ },
+
+ /**
+ * Convert object(s) to a print-friend string.
+ *
+ * @param {...} object
+ * @return {string}
+ * @api public
+ */
+
+ puts : function(object) {
+ if (arguments.length > 1)
+ return map(toArray(arguments), function(arg){
+ return puts(arg)
+ }).join(', ')
+ if (object === undefined) return 'undefined'
+ if (object === null) return 'null'
+ if (object === true) return 'true'
+ if (object === false) return 'false'
+ if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
+ if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector)
+ if (object.jquery) return object.get(0).outerHTML
+ if (object.nodeName) return object.outerHTML
+ switch (object.constructor) {
+ case Function: return object.name || object
+ case String:
+ return '"' + object
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, '\\n')
+ .replace(/\t/g, '\\t')
+ + '"'
+ case Array:
+ return inject(object, '[', function(b, v){
+ return b + ', ' + puts(v)
+ }).replace('[,', '[') + ' ]'
+ case Object:
+ object.__hit__ = true
+ return inject(object, '{', function(b, k, v) {
+ if (k == '__hit__') return b
+ return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v))
+ }).replace('{,', '{') + ' }'
+ default:
+ return object.toString()
+ }
+ },
+
+ /**
+ * Escape HTML.
+ *
+ * @param {string} html
+ * @return {string}
+ * @api public
+ */
+
+ escape : function(html) {
+ return html.toString()
+ .replace(/&/gmi, '&')
+ .replace(/"/gmi, '"')
+ .replace(/>/gmi, '>')
+ .replace(/</gmi, '<')
+ },
+
+ /**
+ * Perform an assertion without reporting.
+ *
+ * This method is primarily used for internal
+ * matchers in order retain DRYness. May be invoked
+ * like below:
+ *
+ * does('foo', 'eql', 'foo')
+ * does([1,2], 'include', 1, 2)
+ *
+ * External hooks are not run for internal assertions
+ * performed by does().
+ *
+ * @param {mixed} actual
+ * @param {string} matcher
+ * @param {...} expected
+ * @return {mixed}
+ * @api private
+ */
+
+ does : function(actual, matcher, expected) {
+ var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, toArray(arguments, 2))
+ return assertion.run().result
+ },
+
+ /**
+ * Perform an assertion.
+ *
+ * expect(true).to('be', true)
+ * expect('foo').not_to('include', 'bar')
+ * expect([1, [2]]).to('include', 1, [2])
+ *
+ * @param {mixed} actual
+ * @return {hash}
+ * @api public
+ */
+
+ expect : function(actual) {
+ function assert(matcher, args, negate) {
+ var expected = toArray(args, 1)
+ matcher.negate = negate
+ assertion = new JSpec.Assertion(matcher, actual, expected, negate)
+ hook('beforeAssertion', assertion)
+ if (matcher.defer) assertion.run()
+ else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
+ return assertion.result
+ }
+
+ function to(matcher) {
+ return assert(matcher, arguments, false)
+ }
+
+ function not_to(matcher) {
+ return assert(matcher, arguments, true)
+ }
+
+ return {
+ to : to,
+ should : to,
+ not_to: not_to,
+ should_not : not_to
+ }
+ },
+
+ /**
+ * Strim whitespace or chars.
+ *
+ * @param {string} string
+ * @param {string} chars
+ * @return {string}
+ * @api public
+ */
+
+ strip : function(string, chars) {
+ return string.
+ replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
+ replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
+ },
+
+ /**
+ * Call an iterator callback with arguments a, or b
+ * depending on the arity of the callback.
+ *
+ * @param {function} callback
+ * @param {mixed} a
+ * @param {mixed} b
+ * @return {mixed}
+ * @api private
+ */
+
+ callIterator : function(callback, a, b) {
+ return callback.length == 1 ? callback(b) : callback(a, b)
+ },
+
+ /**
+ * Extend an object with another.
+ *
+ * @param {object} object
+ * @param {object} other
+ * @api public
+ */
+
+ extend : function(object, other) {
+ each(other, function(property, value){
+ object[property] = value
+ })
+ },
+
+ /**
+ * Iterate an object, invoking the given callback.
+ *
+ * @param {hash, array} object
+ * @param {function} callback
+ * @return {JSpec}
+ * @api public
+ */
+
+ each : function(object, callback) {
+ if (object.constructor == Array)
+ for (var i = 0, len = object.length; i < len; ++i)
+ callIterator(callback, i, object[i])
+ else
+ for (var key in object)
+ if (object.hasOwnProperty(key))
+ callIterator(callback, key, object[key])
+ },
+
+ /**
+ * Iterate with memo.
+ *
+ * @param {hash, array} object
+ * @param {object} memo
+ * @param {function} callback
+ * @return {object}
+ * @api public
+ */
+
+ inject : function(object, memo, callback) {
+ each(object, function(key, value){
+ memo = (callback.length == 2 ?
+ callback(memo, value):
+ callback(memo, key, value)) ||
+ memo
+ })
+ return memo
+ },
+
+ /**
+ * Destub _object_'s _method_. When no _method_ is passed
+ * all stubbed methods are destubbed. When no arguments
+ * are passed every object found in JSpec.stubbed will be
+ * destubbed.
+ *
+ * @param {mixed} object
+ * @param {string} method
+ * @api public
+ */
+
+ destub : function(object, method) {
+ if (method) {
+ if (object['__prototype__' + method])
+ delete object[method]
+ else
+ object[method] = object['__original__' + method]
+ delete object['__prototype__' + method]
+ delete object['__original____' + method]
+ }
+ else if (object) {
+ for (var key in object)
+ if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
+ destub(object, captures[1])
+ }
+ else
+ while (JSpec.stubbed.length)
+ destub(JSpec.stubbed.shift())
+ },
+
+ /**
+ * Stub _object_'s _method_.
+ *
+ * stub(foo, 'toString').and_return('bar')
+ *
+ * @param {mixed} object
+ * @param {string} method
+ * @return {hash}
+ * @api public
+ */
+
+ stub : function(object, method) {
+ hook('stubbing', object, method)
+ JSpec.stubbed.push(object)
+ var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
+ object[type + method] = object[method]
+ object[method] = function(){}
+ return {
+ and_return : function(value) {
+ if (typeof value == 'function') object[method] = value
+ else object[method] = function(){ return value }
+ }
+ }
+ },
+
+ /**
+ * Map callback return values.
+ *
+ * @param {hash, array} object
+ * @param {function} callback
+ * @return {array}
+ * @api public
+ */
+
+ map : function(object, callback) {
+ return inject(object, [], function(memo, key, value){
+ memo.push(callIterator(callback, key, value))
+ })
+ },
+
+ /**
+ * Returns the first matching expression or null.
+ *
+ * @param {hash, array} object
+ * @param {function} callback
+ * @return {mixed}
+ * @api public
+ */
+
+ any : function(object, callback) {
+ return inject(object, null, function(state, key, value){
+ if (state == undefined)
+ return callIterator(callback, key, value) ? value : state
+ })
+ },
+
+ /**
+ * Returns an array of values collected when the callback
+ * given evaluates to true.
+ *
+ * @param {hash, array} object
+ * @return {function} callback
+ * @return {array}
+ * @api public
+ */
+
+ select : function(object, callback) {
+ return inject(object, [], function(selected, key, value){
+ if (callIterator(callback, key, value))
+ selected.push(value)
+ })
+ },
+
+ /**
+ * Define matchers.
+ *
+ * @param {hash} matchers
+ * @api public
+ */
+
+ addMatchers : function(matchers) {
+ each(matchers, function(name, body){
+ JSpec.addMatcher(name, body)
+ })
+ },
+
+ /**
+ * Define a matcher.
+ *
+ * @param {string} name
+ * @param {hash, function, string} body
+ * @api public
+ */
+
+ addMatcher : function(name, body) {
+ hook('addingMatcher', name, body)
+ if (name.indexOf(' ') != -1) {
+ var matchers = name.split(/\s+/)
+ var prefix = matchers.shift()
+ each(matchers, function(name) {
+ JSpec.addMatcher(prefix + '_' + name, body(name))
+ })
+ }
+ this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
+ this.matchers[name].name = name
+ },
+
+ /**
+ * Add a root suite to JSpec.
+ *
+ * @param {string} description
+ * @param {body} function
+ * @api public
+ */
+
+ describe : function(description, body) {
+ var suite = new JSpec.Suite(description, body)
+ hook('addingSuite', suite)
+ this.allSuites.push(suite)
+ this.suites.push(suite)
+ },
+
+ /**
+ * Return the contents of a function body.
+ *
+ * @param {function} body
+ * @return {string}
+ * @api public
+ */
+
+ contentsOf : function(body) {
+ return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
+ },
+
+ /**
+ * Evaluate a JSpec capture body.
+ *
+ * @param {function} body
+ * @param {string} errorMessage (optional)
+ * @return {Type}
+ * @api private
+ */
+
+ evalBody : function(body, errorMessage) {
+ var dsl = this.DSL || this.DSLs.snake
+ var matchers = this.matchers
+ var context = this.context || this.defaultContext
+ var contents = this.contentsOf(body)
+ hook('evaluatingBody', dsl, matchers, context, contents)
+ try { with (dsl){ with (context) { with (matchers) { eval(contents) }}} }
+ catch(e) { error(errorMessage, e) }
+ },
+
+ /**
+ * Pre-process a string of JSpec.
+ *
+ * @param {string} input
+ * @return {string}
+ * @api private
+ */
+
+ preprocess : function(input) {
+ if (typeof input != 'string') return
+ input = hookImmutable('preprocessing', input)
+ return input.
+ replace(/\t/g, ' ').
+ replace(/\r\n|\n|\r/g, '\n').
+ split('__END__')[0].
+ replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
+ replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
+ replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
+ replace(/^ *(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
+ replace(/^\s*end(?=\s|$)/gm, '});').
+ replace(/-\{/g, 'function(){').
+ replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
+ replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
+ replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
+ replace(/, \)/g, ')').
+ replace(/should\.not/g, 'should_not')
+ },
+
+ /**
+ * Create a range string which can be evaluated to a native array.
+ *
+ * @param {int} start
+ * @param {int} end
+ * @return {string}
+ * @api public
+ */
+
+ range : function(start, end) {
+ var current = parseInt(start), end = parseInt(end), values = [current]
+ if (end > current) while (++current <= end) values.push(current)
+ else while (--current >= end) values.push(current)
+ return '[' + values + ']'
+ },
+
+ /**
+ * Report on the results.
+ *
+ * @api public
+ */
+
+ report : function() {
+ this.duration = Number(new Date) - this.start
+ hook('reporting', JSpec.options)
+ new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options)
+ },
+
+ /**
+ * Run the spec suites. Options are merged
+ * with JSpec options when present.
+ *
+ * @param {hash} options
+ * @return {JSpec}
+ * @api public
+ */
+
+ run : function(options) {
+ if (any(hook('running'), haveStopped)) return this
+ if (options) extend(this.options, options)
+ this.start = Number(new Date)
+ each(this.suites, function(suite) { JSpec.runSuite(suite) })
+ return this
+ },
+
+ /**
+ * Run a suite.
+ *
+ * @param {Suite} suite
+ * @api public
+ */
+
+ runSuite : function(suite) {
+ this.currentSuite = suite
+ this.evalBody(suite.body)
+ suite.ran = true
+ hook('beforeSuite', suite), suite.hook('before')
+ each(suite.specs, function(spec) {
+ hook('beforeSpec', spec)
+ suite.hook('before_each')
+ JSpec.runSpec(spec)
+ hook('afterSpec', spec)
+ suite.hook('after_each')
+ })
+ if (suite.hasSuites()) {
+ each(suite.suites, function(suite) {
+ JSpec.runSuite(suite)
+ })
+ }
+ hook('afterSuite', suite), suite.hook('after')
+ this.stats.suitesFinished++
+ },
+
+ /**
+ * Report a failure for the current spec.
+ *
+ * @param {string} message
+ * @api public
+ */
+
+ fail : function(message) {
+ JSpec.currentSpec.fail(message)
+ },
+
+ /**
+ * Report a passing assertion for the current spec.
+ *
+ * @param {string} message
+ * @api public
+ */
+
+ pass : function(message) {
+ JSpec.currentSpec.pass(message)
+ },
+
+ /**
+ * Run a spec.
+ *
+ * @param {Spec} spec
+ * @api public
+ */
+
+ runSpec : function(spec) {
+ this.currentSpec = spec
+ try { this.evalBody(spec.body) }
+ catch (e) { fail(e) }
+ spec.runDeferredAssertions()
+ destub()
+ this.stats.specsFinished++
+ this.stats.assertions += spec.assertions.length
+ },
+
+ /**
+ * Require a dependency, with optional message.
+ *
+ * @param {string} dependency
+ * @param {string} message (optional)
+ * @return {JSpec}
+ * @api public
+ */
+
+ requires : function(dependency, message) {
+ hook('requiring', dependency, message)
+ try { eval(dependency) }
+ catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
+ return this
+ },
+
+ /**
+ * Query against the current query strings keys
+ * or the queryString specified.
+ *
+ * @param {string} key
+ * @param {string} queryString
+ * @return {string, null}
+ * @api private
+ */
+
+ query : function(key, queryString) {
+ var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
+ return inject(queryString.split('&'), null, function(value, pair){
+ parts = pair.split('=')
+ return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
+ })
+ },
+
+ /**
+ * Throw a JSpec related error.
+ *
+ * @param {string} message
+ * @param {Exception} e
+ * @api public
+ */
+
+ error : function(message, e) {
+ throw (message ? message : '') + e.toString() +
+ (e.line ? ' near line ' + e.line : '')
+ },
+
+ /**
+ * Ad-hoc POST request for JSpec server usage.
+ *
+ * @param {string} uri
+ * @param {string} data
+ * @api private
+ */
+
+ post : function(uri, data) {
+ if (any(hook('posting', uri, data), haveStopped)) return
+ var request = this.xhr()
+ request.open('POST', uri, false)
+ request.setRequestHeader('Content-Type', 'application/json')
+ request.send(JSpec.JSON.encode(data))
+ },
+
+ /**
+ * Instantiate an XMLHttpRequest.
+ *
+ * Here we utilize IE's lame ActiveXObjects first which
+ * allow IE access serve files via the file: protocol, otherwise
+ * we then default to XMLHttpRequest.
+ *
+ * @return {XMLHttpRequest, ActiveXObject}
+ * @api private
+ */
+
+ xhr : function() {
+ return this.ieXhr() || new JSpec.request
+ },
+
+ /**
+ * Return Microsoft piece of crap ActiveXObject.
+ *
+ * @return {ActiveXObject}
+ * @api public
+ */
+
+ ieXhr : function() {
+ function object(str) {
+ try { return new ActiveXObject(str) } catch(e) {}
+ }
+ return object('Msxml2.XMLHTTP.6.0') ||
+ object('Msxml2.XMLHTTP.3.0') ||
+ object('Msxml2.XMLHTTP') ||
+ object('Microsoft.XMLHTTP')
+ },
+
+ /**
+ * Check for HTTP request support.
+ *
+ * @return {bool}
+ * @api private
+ */
+
+ hasXhr : function() {
+ return JSpec.request || 'ActiveXObject' in main
+ },
+
+ /**
+ * Try loading _file_ returning the contents
+ * string or null. Chain to locate / read a file.
+ *
+ * @param {string} file
+ * @return {string}
+ * @api public
+ */
+
+ tryLoading : function(file) {
+ try { return JSpec.load(file) } catch (e) {}
+ },
+
+ /**
+ * Load a _file_'s contents.
+ *
+ * @param {string} file
+ * @param {function} callback
+ * @return {string}
+ * @api public
+ */
+
+ load : function(file, callback) {
+ if (any(hook('loading', file), haveStopped)) return
+ if ('readFile' in main)
+ return readFile(file)
+ else if (this.hasXhr()) {
+ var request = this.xhr()
+ request.open('GET', file, false)
+ request.send(null)
+ if (request.readyState == 4 &&
+ (request.status == 0 ||
+ request.status.toString().charAt(0) == 2))
+ return request.responseText
+ }
+ else
+ error("failed to load `" + file + "'")
+ },
+
+ /**
+ * Load, pre-process, and evaluate a file.
+ *
+ * @param {string} file
+ * @param {JSpec}
+ * @api public
+ */
+
+ exec : function(file) {
+ if (any(hook('executing', file), haveStopped)) return this
+ eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
+ return this
+ }
+ }
+
+ // --- Node.js support
+
+ if (typeof GLOBAL === 'object' && typeof exports === 'object')
+ quit = process.exit,
+ print = require('sys').puts,
+ readFile = require('fs').readFileSync
+
+ // --- Utility functions
+
+ var main = this,
+ find = JSpec.any,
+ utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
+ error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/)
+ while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift())
+ if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
+
+ // --- Matchers
+
+ addMatchers({
+ equal : "===",
+ eql : "equal(actual, expected)",
+ be : "alias equal",
+ be_greater_than : ">",
+ be_less_than : "<",
+ be_at_least : ">=",
+ be_at_most : "<=",
+ be_a : "actual.constructor == expected",
+ be_an : "alias be_a",
+ be_an_instance_of : "actual instanceof expected",
+ be_null : "actual == null",
+ be_true : "actual == true",
+ be_false : "actual == false",
+ be_undefined : "typeof actual == 'undefined'",
+ be_type : "typeof actual == expected",
+ match : "typeof actual == 'string' ? actual.match(expected) : false",
+ respond_to : "typeof actual[expected] == 'function'",
+ have_length : "actual.length == expected",
+ be_within : "actual >= expected[0] && actual <= last(expected)",
+ have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
+
+ receive : { defer : true, match : function(actual, method, times) {
+ proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
+ JSpec.currentSpec.assertions.push(proxy)
+ return proxy
+ }},
+
+ be_empty : function(actual) {
+ if (actual.constructor == Object && actual.length == undefined)
+ for (var key in actual)
+ return false;
+ return !actual.length
+ },
+
+ include : function(actual) {
+ for (state = true, i = 1; i < arguments.length; i++) {
+ arg = arguments[i]
+ switch (actual.constructor) {
+ case String:
+ case Number:
+ case RegExp:
+ case Function:
+ state = actual.toString().indexOf(arg) !== -1
+ break
+
+ case Object:
+ state = arg in actual
+ break
+
+ case Array:
+ state = any(actual, function(value){ return equal(value, arg) })
+ break
+ }
+ if (!state) return false
+ }
+ return true
+ },
+
+ throw_error : { match : function(actual, expected, message) {
+ try { actual() }
+ catch (e) {
+ this.e = e
+ var assert = function(arg) {
+ switch (arg.constructor) {
+ case RegExp : return arg.test(e.message || e.toString())
+ case String : return arg == (e.message || e.toString())
+ case Function : return e instanceof arg || e.name == arg.name
+ }
+ }
+ return message ? assert(expected) && assert(message) :
+ expected ? assert(expected) :
+ true
+ }
+ }, message : function(actual, expected, negate) {
+ // TODO: refactor when actual is not in expected [0]
+ var message_for = function(i) {
+ if (expected[i] == undefined) return 'exception'
+ switch (expected[i].constructor) {
+ case RegExp : return 'exception matching ' + puts(expected[i])
+ case String : return 'exception of ' + puts(expected[i])
+ case Function : return expected[i].name || 'Error'
+ }
+ }
+ exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
+ return 'expected ' + exception + (negate ? ' not ' : '' ) +
+ ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
+ }},
+
+ have : function(actual, length, property) {
+ return actual[property].length == length
+ },
+
+ have_at_least : function(actual, length, property) {
+ return actual[property].length >= length
+ },
+
+ have_at_most :function(actual, length, property) {
+ return actual[property].length <= length
+ },
+
+ have_within : function(actual, range, property) {
+ length = actual[property].length
+ return length >= range.shift() && length <= range.pop()
+ },
+
+ have_prop : function(actual, property, value) {
+ return actual[property] == null ||
+ actual[property] instanceof Function ? false:
+ value == null ? true:
+ does(actual[property], 'eql', value)
+ },
+
+ have_property : function(actual, property, value) {
+ return actual[property] == null ||
+ actual[property] instanceof Function ? false:
+ value == null ? true:
+ value === actual[property]
+ }
+ })
+
+})()
Added: couchdb/trunk/share/www/script/jspec/jspec.xhr.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.xhr.js?rev=950689&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/jspec/jspec.xhr.js (added)
+++ couchdb/trunk/share/www/script/jspec/jspec.xhr.js Wed Jun 2 17:45:56 2010
@@ -0,0 +1,195 @@
+
+// JSpec - XHR - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
+
+(function(){
+
+ var lastRequest
+
+ // --- Original XMLHttpRequest
+
+ var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ?
+ XMLHttpRequest :
+ function(){}
+ var OriginalActiveXObject = 'ActiveXObject' in this ?
+ ActiveXObject :
+ undefined
+
+ // --- MockXMLHttpRequest
+
+ var MockXMLHttpRequest = function() {
+ this.requestHeaders = {}
+ }
+
+ MockXMLHttpRequest.prototype = {
+ status: 0,
+ async: true,
+ readyState: 0,
+ responseText: '',
+ abort: function(){},
+ onreadystatechange: function(){},
+
+ /**
+ * Return response headers hash.
+ */
+
+ getAllResponseHeaders : function(){
+ return this.responseHeaders
+ },
+
+ /**
+ * Return case-insensitive value for header _name_.
+ */
+
+ getResponseHeader : function(name) {
+ return this.responseHeaders[name.toLowerCase()]
+ },
+
+ /**
+ * Set case-insensitive _value_ for header _name_.
+ */
+
+ setRequestHeader : function(name, value) {
+ this.requestHeaders[name.toLowerCase()] = value
+ },
+
+ /**
+ * Open mock request.
+ */
+
+ open : function(method, url, async, user, password) {
+ this.user = user
+ this.password = password
+ this.url = url
+ this.readyState = 1
+ this.method = method.toUpperCase()
+ if (async != undefined) this.async = async
+ if (this.async) this.onreadystatechange()
+ },
+
+ /**
+ * Send request _data_.
+ */
+
+ send : function(data) {
+ var self = this
+ this.data = data
+ this.readyState = 4
+ if (this.method == 'HEAD') this.responseText = null
+ this.responseHeaders['content-length'] = (this.responseText || '').length
+ if(this.async) this.onreadystatechange()
+ lastRequest = function(){
+ return self
+ }
+ }
+ }
+
+ // --- Response status codes
+
+ JSpec.statusCodes = {
+ 100: 'Continue',
+ 101: 'Switching Protocols',
+ 200: 'OK',
+ 201: 'Created',
+ 202: 'Accepted',
+ 203: 'Non-Authoritative Information',
+ 204: 'No Content',
+ 205: 'Reset Content',
+ 206: 'Partial Content',
+ 300: 'Multiple Choice',
+ 301: 'Moved Permanently',
+ 302: 'Found',
+ 303: 'See Other',
+ 304: 'Not Modified',
+ 305: 'Use Proxy',
+ 307: 'Temporary Redirect',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 405: 'Method Not Allowed',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authentication Required',
+ 408: 'Request Timeout',
+ 409: 'Conflict',
+ 410: 'Gone',
+ 411: 'Length Required',
+ 412: 'Precondition Failed',
+ 413: 'Request Entity Too Large',
+ 414: 'Request-URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Requested Range Not Satisfiable',
+ 417: 'Expectation Failed',
+ 422: 'Unprocessable Entity',
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Gateway Timeout',
+ 505: 'HTTP Version Not Supported'
+ }
+
+ /**
+ * Mock XMLHttpRequest requests.
+ *
+ * mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' })
+ *
+ * @return {hash}
+ * @api public
+ */
+
+ function mockRequest() {
+ return { and_return : function(body, type, status, headers) {
+ XMLHttpRequest = MockXMLHttpRequest
+ ActiveXObject = false
+ status = status || 200
+ headers = headers || {}
+ headers['content-type'] = type
+ JSpec.extend(XMLHttpRequest.prototype, {
+ responseText: body,
+ responseHeaders: headers,
+ status: status,
+ statusText: JSpec.statusCodes[status]
+ })
+ }}
+ }
+
+ /**
+ * Unmock XMLHttpRequest requests.
+ *
+ * @api public
+ */
+
+ function unmockRequest() {
+ XMLHttpRequest = OriginalXMLHttpRequest
+ ActiveXObject = OriginalActiveXObject
+ }
+
+ JSpec.include({
+ name: 'Mock XHR',
+
+ // --- Utilities
+
+ utilities : {
+ mockRequest: mockRequest,
+ unmockRequest: unmockRequest
+ },
+
+ // --- Hooks
+
+ afterSpec : function() {
+ unmockRequest()
+ },
+
+ // --- DSLs
+
+ DSLs : {
+ snake : {
+ mock_request: mockRequest,
+ unmock_request: unmockRequest,
+ last_request: function(){ return lastRequest() }
+ }
+ }
+
+ })
+})()
\ No newline at end of file
Added: couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js?rev=950689&view=auto
==============================================================================
--- couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js (added)
+++ couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js Wed Jun 2 17:45:56 2010
@@ -0,0 +1,389 @@
+// Specs for couch.js lines 313-470
+
+describe 'CouchDB class'
+ describe 'session stuff'
+ before
+ useTestUserDb();
+ end
+
+ after
+ useOldUserDb();
+ end
+
+ before_each
+ userDoc = users_db.save(CouchDB.prepareUserDoc({name: "Gaius Baltar", roles: ["president"]}, "secretpass"));
+ end
+
+ after_each
+ users_db.deleteDoc({_id : userDoc.id, _rev : userDoc.rev})
+ end
+
+ describe '.login'
+ it 'should return ok true'
+ CouchDB.login("Gaius Baltar", "secretpass").ok.should.be_true
+ end
+
+ it 'should return the name of the logged in user'
+ CouchDB.login("Gaius Baltar", "secretpass").name.should.eql "Gaius Baltar"
+ end
+
+ it 'should return the roles of the logged in user'
+ CouchDB.login("Gaius Baltar", "secretpass").roles.should.eql ["president"]
+ end
+
+ it 'should post _session'
+ CouchDB.should.receive("request", "once").with_args("POST", "/_session")
+ CouchDB.login("Gaius Baltar", "secretpass");
+ end
+
+ it 'should create a session'
+ CouchDB.login("Gaius Baltar", "secretpass");
+ CouchDB.session().userCtx.name.should.eql "Gaius Baltar"
+ end
+ end
+
+ describe '.logout'
+ before_each
+ CouchDB.login("Gaius Baltar", "secretpass");
+ end
+
+ it 'should return ok true'
+ CouchDB.logout().ok.should.be_true
+ end
+
+ it 'should delete _session'
+ CouchDB.should.receive("request", "once").with_args("DELETE", "/_session")
+ CouchDB.logout();
+ end
+
+ it 'should result in an invalid session'
+ CouchDB.logout();
+ CouchDB.session().name.should.be_null
+ end
+ end
+
+ describe '.session'
+ before_each
+ CouchDB.login("Gaius Baltar", "secretpass");
+ end
+
+ it 'should return ok true'
+ CouchDB.session().ok.should.be_true
+ end
+
+ it 'should return the users name'
+ CouchDB.session().userCtx.name.should.eql "Gaius Baltar"
+ end
+
+ it 'should return the users roles'
+ CouchDB.session().userCtx.roles.should.eql ["president"]
+ end
+
+ it 'should return the name of the authentication db'
+ CouchDB.session().info.authentication_db.should.eql "spec_users_db"
+ end
+
+ it 'should return the active authentication handler'
+ CouchDB.session().info.authenticated.should.eql "cookie"
+ end
+ end
+ end
+
+ describe 'db stuff'
+ before_each
+ db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"});
+ db.createDb();
+ end
+
+ after_each
+ db.deleteDb();
+ end
+
+ describe '.prepareUserDoc'
+ before_each
+ userDoc = CouchDB.prepareUserDoc({name: "Laura Roslin"}, "secretpass");
+ end
+
+ it 'should return the users name'
+ userDoc.name.should.eql "Laura Roslin"
+ end
+
+ it 'should prefix the id with the CouchDB user_prefix'
+ userDoc._id.should.eql "org.couchdb.user:Laura Roslin"
+ end
+
+ it 'should return the users roles'
+ var userDocWithRoles = CouchDB.prepareUserDoc({name: "William Adama", roles: ["admiral", "commander"]}, "secretpass")
+ userDocWithRoles.roles.should.eql ["admiral", "commander"]
+ end
+
+ it 'should return the hashed password'
+ userDoc.password_sha.length.should.be_at_least 30
+ userDoc.password_sha.should.be_a String
+ end
+ end
+
+ describe '.allDbs'
+ it 'should get _all_dbs'
+ CouchDB.should.receive("request", "once").with_args("GET", "/_all_dbs");
+ CouchDB.allDbs();
+ end
+
+ it 'should return an array that includes a created database'
+ temp_db = new CouchDB("temp_spec_db", {"X-Couch-Full-Commit":"false"});
+ temp_db.createDb();
+ CouchDB.allDbs().should.include("temp_spec_db");
+ temp_db.deleteDb();
+ end
+
+ it 'should return an array that does not include a database that does not exist'
+ CouchDB.allDbs().should.not.include("not_existing_temp_spec_db");
+ end
+ end
+
+ describe '.allDesignDocs'
+ it 'should return the total number of documents'
+ CouchDB.allDesignDocs().spec_db.total_rows.should.eql 0
+ db.save({'type':'battlestar', 'name':'galactica'});
+ CouchDB.allDesignDocs().spec_db.total_rows.should.eql 1
+ end
+
+ it 'should return undefined when the db does not exist'
+ CouchDB.allDesignDocs().non_existing_db.should.be_undefined
+ end
+
+ it 'should return no documents when there are no design documents'
+ CouchDB.allDesignDocs().spec_db.rows.should.eql []
+ end
+
+ it 'should return all design documents'
+ var designDoc = {
+ "views" : {
+ "people" : {
+ "map" : "function(doc) { emit(doc._id, doc); }"
+ }
+ },
+ "_id" : "_design/spec_db"
+ };
+ db.save(designDoc);
+
+ var allDesignDocs = CouchDB.allDesignDocs();
+ allDesignDocs.spec_db.rows[0].id.should.eql "_design/spec_db"
+ allDesignDocs.spec_db.rows[0].key.should.eql "_design/spec_db"
+ allDesignDocs.spec_db.rows[0].value.rev.length.should.be_at_least 30
+ end
+ end
+
+ describe '.getVersion'
+ it 'should get the CouchDB version'
+ CouchDB.should.receive("request", "once").with_args("GET", "/")
+ CouchDB.getVersion();
+ end
+
+ it 'should return the CouchDB version'
+ CouchDB.getVersion().should_match /^\d\d?\.\d\d?\.\d\d?.*/
+ end
+ end
+
+ describe '.replicate'
+ before_each
+ db2 = new CouchDB("spec_db_2", {"X-Couch-Full-Commit":"false"});
+ db2.createDb();
+ host = window.location.protocol + "//" + window.location.host ;
+ end
+
+ after_each
+ db2.deleteDb();
+ end
+
+ it 'should return no_changes true when there are no changes between the dbs'
+ CouchDB.replicate(host + db.uri, host + db2.uri).no_changes.should.be_true
+ end
+
+ it 'should return the session ID'
+ db.save({'type':'battlestar', 'name':'galactica'});
+ CouchDB.replicate(host + db.uri, host + db2.uri).session_id.length.should.be_at_least 30
+ end
+
+ it 'should return source_last_seq'
+ db.save({'type':'battlestar', 'name':'galactica'});
+ db.save({'type':'battlestar', 'name':'pegasus'});
+
+ CouchDB.replicate(host + db.uri, host + db2.uri).source_last_seq.should.eql 2
+ end
+
+ it 'should return the replication history'
+ db.save({'type':'battlestar', 'name':'galactica'});
+ db.save({'type':'battlestar', 'name':'pegasus'});
+
+ var result = CouchDB.replicate(host + db.uri, host + db2.uri);
+ result.history[0].docs_written.should.eql 2
+ result.history[0].start_last_seq.should.eql 0
+ end
+
+ it 'should pass through replication options'
+ db.save({'type':'battlestar', 'name':'galactica'});
+ db2.deleteDb();
+ -{CouchDB.replicate(host + db.uri, host + db2.uri)}.should.throw_error
+ var result = CouchDB.replicate(host + db.uri, host + db2.uri, {"body" : {"create_target":true}});
+
+ result.ok.should.eql true
+ result.history[0].docs_written.should.eql 1
+ db2.info().db_name.should.eql "spec_db_2"
+ end
+ end
+
+ describe '.newXhr'
+ it 'should return a XMLHTTPRequest'
+ CouchDB.newXhr().should.have_prop 'readyState'
+ CouchDB.newXhr().should.have_prop 'responseText'
+ CouchDB.newXhr().should.have_prop 'status'
+ end
+ end
+
+ describe '.request'
+ it 'should return a XMLHttpRequest'
+ var req = CouchDB.request("GET", '/');
+ req.should.include "readyState"
+ req.should.include "responseText"
+ req.should.include "statusText"
+ end
+
+ it 'should pass through the options headers'
+ var xhr = CouchDB.newXhr();
+ stub(CouchDB, 'newXhr').and_return(xhr);
+
+ xhr.should.receive("setRequestHeader", "once").with_args("X-Couch-Full-Commit", "true")
+ CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}});
+ end
+
+ it 'should pass through the options body'
+ var xhr = CouchDB.newXhr();
+ stub(CouchDB, 'newXhr').and_return(xhr);
+
+ xhr.should.receive("send", "once").with_args({"body_key":"body_value"})
+ CouchDB.request("GET", "/", {'body': {"body_key":"body_value"}});
+ end
+
+ it 'should prepend the urlPrefix to the uri'
+ var oldPrefix = CouchDB.urlPrefix;
+ CouchDB.urlPrefix = "/_utils";
+
+ var xhr = CouchDB.newXhr();
+ stub(CouchDB, 'newXhr').and_return(xhr);
+
+ xhr.should.receive("open", "once").with_args("GET", "/_utils/", false)
+ CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}});
+
+ CouchDB.urlPrefix = oldPrefix;
+ end
+ end
+
+ describe '.requestStats'
+ it 'should get the stats for specified module and key'
+ var stats = CouchDB.requestStats('couchdb', 'open_databases', null);
+ stats.description.should.eql 'number of open databases'
+ stats.current.should.be_a Number
+ end
+
+ it 'should add flush true to the request when there is a test argument'
+ CouchDB.should.receive("request", "once").with_args("GET", "/_stats/httpd/requests?flush=true")
+ CouchDB.requestStats('httpd', 'requests', 'test');
+ end
+
+ it 'should still work when there is a test argument'
+ var stats = CouchDB.requestStats('httpd_status_codes', '200', 'test');
+ stats.description.should.eql 'number of HTTP 200 OK responses'
+ stats.sum.should.be_a Number
+ end
+ end
+
+ describe '.newUuids'
+ after_each
+ CouchDB.uuids_cache = [];
+ end
+
+ it 'should return the specified amount of uuids'
+ var uuids = CouchDB.newUuids(45);
+ uuids.should.have_length 45
+ end
+
+ it 'should return an array with uuids'
+ var uuids = CouchDB.newUuids(1);
+ uuids[0].should.be_a String
+ uuids[0].should.have_length 32
+ end
+
+ it 'should leave the uuids_cache with 100 uuids when theres no buffer size specified'
+ CouchDB.newUuids(23);
+ CouchDB.uuids_cache.should.have_length 100
+ end
+
+ it 'should leave the uuids_cache with the specified buffer size'
+ CouchDB.newUuids(23, 150);
+ CouchDB.uuids_cache.should.have_length 150
+ end
+
+ it 'should get the uuids from the uuids_cache when there are enough uuids in there'
+ CouchDB.newUuids(10);
+ CouchDB.newUuids(25);
+ CouchDB.uuids_cache.should.have_length 75
+ end
+
+ it 'should create new uuids and add as many as specified to the uuids_cache when there are not enough uuids in the cache'
+ CouchDB.newUuids(10);
+ CouchDB.newUuids(125, 60);
+ CouchDB.uuids_cache.should.have_length 160
+ end
+ end
+
+ describe '.maybeThrowError'
+ it 'should throw an error when the request has status 404'
+ var req = CouchDB.request("GET", "/nonexisting_db");
+ -{CouchDB.maybeThrowError(req)}.should.throw_error
+ end
+
+ it 'should throw an error when the request has status 412'
+ var req = CouchDB.request("PUT", "/spec_db");
+ -{CouchDB.maybeThrowError(req)}.should.throw_error
+ end
+
+ it 'should throw an error when the request has status 405'
+ var req = CouchDB.request("DELETE", "/_utils");
+ -{CouchDB.maybeThrowError(req)}.should.throw_error
+ end
+
+ it 'should throw the responseText of the request'
+ var req = CouchDB.request("GET", "/nonexisting_db");
+ try {
+ CouchDB.maybeThrowError(req)
+ } catch(e) {
+ e.error.should.eql JSON.parse(req.responseText).error
+ e.reason.should.eql JSON.parse(req.responseText).reason
+ }
+ end
+
+ it 'should throw an unknown error when the responseText is invalid json'
+ mock_request().and_return("invalid json...", "application/json", 404, {})
+ try {
+ CouchDB.maybeThrowError(CouchDB.newXhr())
+ } catch(e) {
+ e.error.should.eql "unknown"
+ e.reason.should.eql "invalid json..."
+ }
+ end
+ end
+
+ describe '.params'
+ it 'should turn a json object into a http params string'
+ var params = CouchDB.params({"president":"laura", "cag":"lee"})
+ params.should.eql "president=laura&cag=lee"
+ end
+
+ it 'should return a blank string when the object is empty'
+ var params = CouchDB.params({})
+ params.should.eql ""
+ end
+ end
+ end
+end
\ No newline at end of file
Re: svn commit: r950689 [1/2] - in /couchdb/trunk: ./ share/www/script/ share/www/script/jspec/ share/www/spec/
Posted by Jan Lehnardt <ja...@apache.org>.
Hi All,
I finally got around to commit Lena's excellent test suite. As
she mentioned in her original mail, there are a few open spots.
They've been summed up in these two tickets:
https://issues.apache.org/jira/browse/COUCHDB-725
https://issues.apache.org/jira/browse/COUCHDB-726
And one last error that you see when running the test suite
has an appropriate comment in the source.
If you feel like hacking some JS, this is a good time and
place to dig in :)
Cheers
Jan
--
On 2 Jun 2010, at 19:45, jan@apache.org wrote:
> Author: jan
> Date: Wed Jun 2 17:45:56 2010
> New Revision: 950689
>
> URL: http://svn.apache.org/viewvc?rev=950689&view=rev
> Log:
> Add tests for couch.js and jquery.couch.js
>
> Patch by Lena Herrmann.
>
> Closes COUCHDB-783.
>
> Added:
> couchdb/trunk/share/www/script/jspec/
> couchdb/trunk/share/www/script/jspec/jspec.css
> couchdb/trunk/share/www/script/jspec/jspec.jquery.js
> couchdb/trunk/share/www/script/jspec/jspec.js
> couchdb/trunk/share/www/script/jspec/jspec.xhr.js
> couchdb/trunk/share/www/spec/
> couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js
> couchdb/trunk/share/www/spec/couch_js_instance_methods_1_spec.js
> couchdb/trunk/share/www/spec/couch_js_instance_methods_2_spec.js
> couchdb/trunk/share/www/spec/couch_js_instance_methods_3_spec.js
> couchdb/trunk/share/www/spec/custom_helpers.js
> couchdb/trunk/share/www/spec/jquery_couch_js_class_methods_spec.js
> couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_1_spec.js
> couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_2_spec.js
> couchdb/trunk/share/www/spec/jquery_couch_js_instance_methods_3_spec.js
> couchdb/trunk/share/www/spec/run.html
> Modified:
> couchdb/trunk/README
> couchdb/trunk/share/www/script/couch.js
> couchdb/trunk/share/www/script/jquery.couch.js
>
> Modified: couchdb/trunk/README
> URL: http://svn.apache.org/viewvc/couchdb/trunk/README?rev=950689&r1=950688&r2=950689&view=diff
> ==============================================================================
> --- couchdb/trunk/README (original)
> +++ couchdb/trunk/README Wed Jun 2 17:45:56 2010
> @@ -39,6 +39,23 @@ The mailing lists provide a wealth of su
> Feel free to drop by with your questions or discussion. See the official CouchDB
> website for more information about our community resources.
>
> +
> +Running the Testsuite
> +---------------------
> +
> +Run the testsuite for couch.js and jquery.couch.js by browsing to this site: http://127.0.0.1:5984/_utils/spec/run.html
> +It should work in at least Firefox >= 3.6 and Safari >= 4.0.4.
> +
> +Read more about JSpec here: http://jspec.info/
> +
> +Trouble shooting
> +~~~~~~~~~~~~~~~~
> +
> + * When you change the specs, but your changes have no effect, manually reload the changed spec file in the browser.
> +
> + * When the spec that tests erlang views fails, make sure you have enabled erlang views as described here: <http://wiki.apache.org/couchdb/EnableErlangViews>
> +
> +
> Cryptographic Software Notice
> -----------------------------
>
>
> Modified: couchdb/trunk/share/www/script/couch.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch.js?rev=950689&r1=950688&r2=950689&view=diff
> ==============================================================================
> --- couchdb/trunk/share/www/script/couch.js [utf-8] (original)
> +++ couchdb/trunk/share/www/script/couch.js [utf-8] Wed Jun 2 17:45:56 2010
> @@ -22,10 +22,10 @@ function CouchDB(name, httpHeaders) {
> this.last_req = null;
>
> this.request = function(method, uri, requestOptions) {
> - requestOptions = requestOptions || {}
> - requestOptions.headers = combine(requestOptions.headers, httpHeaders)
> - return CouchDB.request(method, uri, requestOptions);
> - }
> + requestOptions = requestOptions || {}
> + requestOptions.headers = combine(requestOptions.headers, httpHeaders)
> + return CouchDB.request(method, uri, requestOptions);
> + }
>
> // Creates the database on the server
> this.createDb = function() {
> @@ -198,12 +198,6 @@ function CouchDB(name, httpHeaders) {
> return JSON.parse(this.last_req.responseText);
> }
>
> - this.viewCleanup = function() {
> - this.last_req = this.request("POST", this.uri + "_view_cleanup");
> - CouchDB.maybeThrowError(this.last_req);
> - return JSON.parse(this.last_req.responseText);
> - }
> -
> this.allDocs = function(options,keys) {
> if(!keys) {
> this.last_req = this.request("GET", this.uri + "_all_docs"
> @@ -223,18 +217,11 @@ function CouchDB(name, httpHeaders) {
> return this.allDocs({startkey:"_design", endkey:"_design0"});
> };
>
> - this.changes = function(options,keys) {
> - var req = null;
> - if(!keys) {
> - req = this.request("GET", this.uri + "_changes" + encodeOptions(options));
> - } else {
> - req = this.request("POST", this.uri + "_changes" + encodeOptions(options), {
> - headers: {"Content-Type": "application/json"},
> - body: JSON.stringify({keys:keys})
> - });
> - }
> - CouchDB.maybeThrowError(req);
> - return JSON.parse(req.responseText);
> + this.changes = function(options) {
> + this.last_req = this.request("GET", this.uri + "_changes"
> + + encodeOptions(options));
> + CouchDB.maybeThrowError(this.last_req);
> + return JSON.parse(this.last_req.responseText);
> }
>
> this.compact = function() {
>
> Modified: couchdb/trunk/share/www/script/jquery.couch.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jquery.couch.js?rev=950689&r1=950688&r2=950689&view=diff
> ==============================================================================
> --- couchdb/trunk/share/www/script/jquery.couch.js [utf-8] (original)
> +++ couchdb/trunk/share/www/script/jquery.couch.js [utf-8] Wed Jun 2 17:45:56 2010
> @@ -1,4 +1,4 @@
> -// Licensed under the Apache License, Version 2.0 (the "License"); you may not
> +a// Licensed under the Apache License, Version 2.0 (the "License"); you may not
> // use this file except in compliance with the License. You may obtain a copy of
> // the License at
> //
> @@ -276,7 +276,7 @@
> }
> });
> } else {
> - alert("please provide an eachApp function for allApps()");
> + alert("Please provide an eachApp function for allApps()");
> }
> },
> openDoc: function(docId, options, ajaxOptions) {
> @@ -327,7 +327,7 @@
> beforeSend : beforeSend,
> complete: function(req) {
> var resp = $.httpData(req, "json");
> - if (req.status == 201) {
> + if (req.status == 201 || req.status == 202) {
> doc._id = resp.id;
> doc._rev = resp.rev;
> if (versioned) {
> @@ -372,13 +372,27 @@
> "The document could not be deleted"
> );
> },
> - copyDoc: function(doc, options, ajaxOptions) {
> + bulkRemove: function(docs, options){
> + docs.docs = $.each(
> + docs.docs, function(i, doc){
> + doc._deleted = true;
> + }
> + );
> + $.extend(options, {successStatus: 201});
> + ajax({
> + type: "POST",
> + url: this.uri + "_bulk_docs" + encodeOptions(options),
> + data: toJSON(docs)
> + },
> + options,
> + "The documents could not be deleted"
> + );
> + },
> + copyDoc: function(docId, options, ajaxOptions) {
> ajaxOptions = $.extend(ajaxOptions, {
> complete: function(req) {
> var resp = $.httpData(req, "json");
> if (req.status == 201) {
> - doc._id = resp.id;
> - doc._rev = resp.rev;
> if (options.success) options.success(resp);
> } else if (options.error) {
> options.error(req.status, resp.error, resp.reason);
> @@ -389,9 +403,7 @@
> });
> ajax({
> type: "COPY",
> - url: this.uri +
> - encodeDocId(doc._id) +
> - encodeOptions({rev: doc._rev})
> + url: this.uri + encodeDocId(docId)
> },
> options,
> "The document could not be copied",
> @@ -490,13 +502,14 @@
> );
> },
>
> - replicate: function(source, target, options) {
> + replicate: function(source, target, ajaxOptions, replicationOptions) {
> + replicationOptions = $.extend({source: source, target: target}, replicationOptions);
> ajax({
> type: "POST", url: this.urlPrefix + "/_replicate",
> - data: JSON.stringify({source: source, target: target}),
> + data: JSON.stringify(replicationOptions),
> contentType: "application/json"
> },
> - options,
> + ajaxOptions,
> "Replication failed"
> );
> },
> @@ -516,7 +529,6 @@
> }
> return uuidCache.shift();
> }
> -
> });
>
> function ajax(obj, options, errorMessage, ajaxOptions) {
> @@ -525,8 +537,18 @@
>
> $.ajax($.extend($.extend({
> type: "GET", dataType: "json",
> + beforeSend: function(xhr){
> + if(ajaxOptions && ajaxOptions.headers){
> + for (var header in ajaxOptions.headers){
> + xhr.setRequestHeader(header, ajaxOptions.headers[header]);
> + }
> + }
> + },
> complete: function(req) {
> var resp = $.httpData(req, "json");
> + if (options.ajaxStart) {
> + options.ajaxStart(resp);
> + }
> if (req.status == options.successStatus) {
> if (options.beforeSuccess) options.beforeSuccess(req, resp);
> if (options.success) options.success(resp);
> @@ -556,7 +578,7 @@
> var buf = [];
> if (typeof(options) === "object" && options !== null) {
> for (var name in options) {
> - if ($.inArray(name, ["error", "success"]) >= 0)
> + if ($.inArray(name, ["error", "success", "ajaxStart"]) >= 0)
> continue;
> var value = options[name];
> if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) {
>
> Added: couchdb/trunk/share/www/script/jspec/jspec.css
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.css?rev=950689&view=auto
> ==============================================================================
> --- couchdb/trunk/share/www/script/jspec/jspec.css (added)
> +++ couchdb/trunk/share/www/script/jspec/jspec.css Wed Jun 2 17:45:56 2010
> @@ -0,0 +1,149 @@
> +body.jspec {
> + margin: 45px 0;
> + font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
> + background: #efefef url(images/bg.png) top left repeat-x;
> + text-align: center;
> +}
> +#jspec {
> + margin: 0 auto;
> + padding-top: 30px;
> + width: 1008px;
> + background: url(images/vr.png) top left repeat-y;
> + text-align: left;
> +}
> +#jspec-top {
> + position: relative;
> + margin: 0 auto;
> + width: 1008px;
> + height: 40px;
> + background: url(images/sprites.bg.png) top left no-repeat;
> +}
> +#jspec-bottom {
> + margin: 0 auto;
> + width: 1008px;
> + height: 15px;
> + background: url(images/sprites.bg.png) bottom left no-repeat;
> +}
> +#jspec .loading {
> + margin-top: -45px;
> + width: 1008px;
> + height: 80px;
> + background: url(images/loading.gif) 50% 50% no-repeat;
> +}
> +#jspec-title {
> + position: absolute;
> + top: 15px;
> + left: 20px;
> + width: 160px;
> + font-size: 22px;
> + font-weight: normal;
> + background: url(images/sprites.png) 0 -126px no-repeat;
> + text-align: center;
> +}
> +#jspec-title em {
> + font-size: 10px;
> + font-style: normal;
> + color: #BCC8D1;
> +}
> +#jspec-report * {
> + margin: 0;
> + padding: 0;
> + background: none;
> + border: none;
> +}
> +#jspec-report {
> + padding: 15px 40px;
> + font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
> + color: #7B8D9B;
> +}
> +#jspec-report.has-failures {
> + padding-bottom: 30px;
> +}
> +#jspec-report .hidden {
> + display: none;
> +}
> +#jspec-report .heading {
> + margin-bottom: 15px;
> +}
> +#jspec-report .heading span {
> + padding-right: 10px;
> +}
> +#jspec-report .heading .passes em {
> + color: #0ea0eb;
> +}
> +#jspec-report .heading .failures em {
> + color: #FA1616;
> +}
> +#jspec-report table {
> + font-size: 11px;
> + border-collapse: collapse;
> +}
> +#jspec-report td {
> + padding: 8px;
> + text-indent: 30px;
> + color: #7B8D9B;
> +}
> +#jspec-report tr.body {
> + display: none;
> +}
> +#jspec-report tr.body pre {
> + margin: 0;
> + padding: 0 0 5px 25px;
> +}
> +#jspec-report tr.even:hover + tr.body,
> +#jspec-report tr.odd:hover + tr.body {
> + display: block;
> +}
> +#jspec-report tr td:first-child em {
> + display: block;
> + clear: both;
> + font-style: normal;
> + font-weight: normal;
> + color: #7B8D9B;
> +}
> +#jspec-report tr.even:hover,
> +#jspec-report tr.odd:hover {
> + text-shadow: 1px 1px 1px #fff;
> + background: #F2F5F7;
> +}
> +#jspec-report td + td {
> + padding-right: 0;
> + width: 15px;
> +}
> +#jspec-report td.pass {
> + background: url(images/sprites.png) 3px -7px no-repeat;
> +}
> +#jspec-report td.fail {
> + background: url(images/sprites.png) 3px -158px no-repeat;
> + font-weight: bold;
> + color: #FC0D0D;
> +}
> +#jspec-report td.requires-implementation {
> + background: url(images/sprites.png) 3px -333px no-repeat;
> +}
> +#jspec-report tr.description td {
> + margin-top: 25px;
> + padding-top: 25px;
> + font-size: 12px;
> + font-weight: bold;
> + text-indent: 0;
> + color: #1a1a1a;
> +}
> +#jspec-report tr.description:first-child td {
> + border-top: none;
> +}
> +#jspec-report .assertion {
> + display: block;
> + float: left;
> + margin: 0 0 0 1px;
> + padding: 0;
> + width: 1px;
> + height: 5px;
> + background: #7B8D9B;
> +}
> +#jspec-report .assertion.failed {
> + background: red;
> +}
> +.jspec-sandbox {
> + display: none;
> +}
> \ No newline at end of file
>
> Added: couchdb/trunk/share/www/script/jspec/jspec.jquery.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.jquery.js?rev=950689&view=auto
> ==============================================================================
> --- couchdb/trunk/share/www/script/jspec/jspec.jquery.js (added)
> +++ couchdb/trunk/share/www/script/jspec/jspec.jquery.js Wed Jun 2 17:45:56 2010
> @@ -0,0 +1,72 @@
> +
> +// JSpec - jQuery - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
> +
> +JSpec
> +.requires('jQuery', 'when using jspec.jquery.js')
> +.include({
> + name: 'jQuery',
> +
> + // --- Initialize
> +
> + init : function() {
> + jQuery.ajaxSetup({ async: false })
> + },
> +
> + // --- Utilities
> +
> + utilities : {
> + element: jQuery,
> + elements: jQuery,
> + sandbox : function() {
> + return jQuery('<div class="sandbox"></div>')
> + }
> + },
> +
> + // --- Matchers
> +
> + matchers : {
> + have_tag : "jQuery(expected, actual).length === 1",
> + have_one : "alias have_tag",
> + have_tags : "jQuery(expected, actual).length > 1",
> + have_many : "alias have_tags",
> + have_any : "alias have_tags",
> + have_child : "jQuery(actual).children(expected).length === 1",
> + have_children : "jQuery(actual).children(expected).length > 1",
> + have_text : "jQuery(actual).text() === expected",
> + have_value : "jQuery(actual).val() === expected",
> + be_enabled : "!jQuery(actual).attr('disabled')",
> + have_class : "jQuery(actual).hasClass(expected)",
> +
> + be_visible : function(actual) {
> + return jQuery(actual).css('display') != 'none' &&
> + jQuery(actual).css('visibility') != 'hidden' &&
> + jQuery(actual).attr('type') != 'hidden'
> + },
> +
> + be_hidden : function(actual) {
> + return !JSpec.does(actual, 'be_visible')
> + },
> +
> + have_classes : function(actual) {
> + return !JSpec.any(JSpec.toArray(arguments, 1), function(arg){
> + return !JSpec.does(actual, 'have_class', arg)
> + })
> + },
> +
> + have_attr : function(actual, attr, value) {
> + return value ? jQuery(actual).attr(attr) == value:
> + jQuery(actual).attr(attr)
> + },
> +
> + 'be disabled selected checked' : function(attr) {
> + return 'jQuery(actual).attr("' + attr + '")'
> + },
> +
> + 'have type id title alt href src sel rev name target' : function(attr) {
> + return function(actual, value) {
> + return JSpec.does(actual, 'have_attr', attr, value)
> + }
> + }
> + }
> +})
> +
>
> Added: couchdb/trunk/share/www/script/jspec/jspec.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.js?rev=950689&view=auto
> ==============================================================================
> --- couchdb/trunk/share/www/script/jspec/jspec.js (added)
> +++ couchdb/trunk/share/www/script/jspec/jspec.js Wed Jun 2 17:45:56 2010
> @@ -0,0 +1,1756 @@
> +
> +// JSpec - Core - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
> +
> +;(function(){
> +
> + JSpec = {
> + version : '3.3.2',
> + assert : true,
> + cache : {},
> + suites : [],
> + modules : [],
> + allSuites : [],
> + matchers : {},
> + stubbed : [],
> + options : {},
> + request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
> + stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
> +
> + /**
> + * Default context in which bodies are evaluated.
> + *
> + * Replace context simply by setting JSpec.context
> + * to your own like below:
> + *
> + * JSpec.context = { foo : 'bar' }
> + *
> + * Contexts can be changed within any body, this can be useful
> + * in order to provide specific helper methods to specific suites.
> + *
> + * To reset (usually in after hook) simply set to null like below:
> + *
> + * JSpec.context = null
> + *
> + */
> +
> + defaultContext : {
> +
> + /**
> + * Return an object used for proxy assertions.
> + * This object is used to indicate that an object
> + * should be an instance of _object_, not the constructor
> + * itself.
> + *
> + * @param {function} constructor
> + * @return {hash}
> + * @api public
> + */
> +
> + an_instance_of : function(constructor) {
> + return { an_instance_of : constructor }
> + },
> +
> + /**
> + * Load fixture at _path_.
> + *
> + * Fixtures are resolved as:
> + *
> + * - <path>
> + * - <path>.html
> + *
> + * @param {string} path
> + * @return {string}
> + * @api public
> + */
> +
> + fixture : function(path) {
> + if (JSpec.cache[path]) return JSpec.cache[path]
> + return JSpec.cache[path] =
> + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) ||
> + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html')
> + }
> + },
> +
> + // --- Objects
> +
> + reporters : {
> +
> + /**
> + * Report to server.
> + *
> + * Options:
> + * - uri specific uri to report to.
> + * - verbose weither or not to output messages
> + * - failuresOnly output failure messages only
> + *
> + * @api public
> + */
> +
> + Server : function(results, options) {
> + var uri = options.uri || 'http://' + window.location.host + '/results'
> + JSpec.post(uri, {
> + stats: JSpec.stats,
> + options: options,
> + results: map(results.allSuites, function(suite) {
> + if (suite.hasSpecs())
> + return {
> + description: suite.description,
> + specs: map(suite.specs, function(spec) {
> + return {
> + description: spec.description,
> + message: !spec.passed() ? spec.failure().message : null,
> + status: spec.requiresImplementation() ? 'pending' :
> + spec.passed() ? 'pass' :
> + 'fail',
> + assertions: map(spec.assertions, function(assertion){
> + return {
> + passed: assertion.passed
> + }
> + })
> + }
> + })
> + }
> + })
> + })
> + if ('close' in main) main.close()
> + },
> +
> + /**
> + * Default reporter, outputting to the DOM.
> + *
> + * Options:
> + * - reportToId id of element to output reports to, defaults to 'jspec'
> + * - failuresOnly displays only suites with failing specs
> + *
> + * @api public
> + */
> +
> + DOM : function(results, options) {
> + var id = option('reportToId') || 'jspec',
> + report = document.getElementById(id),
> + failuresOnly = option('failuresOnly'),
> + classes = results.stats.failures ? 'has-failures' : ''
> + if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
> +
> + function bodyContents(body) {
> + return JSpec.
> + escape(JSpec.contentsOf(body)).
> + replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
> + replace(/\r\n|\r|\n/gm, '<br/>')
> + }
> +
> + report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
> + <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
> + <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
> + <span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \
> + </div><table class="suites">' + map(results.allSuites, function(suite) {
> + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
> + if (displaySuite && suite.hasSpecs())
> + return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
> + map(suite.specs, function(i, spec) {
> + return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
> + (spec.requiresImplementation() ?
> + '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
> + (spec.passed() && !failuresOnly) ?
> + '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
> + !spec.passed() ?
> + '<td class="fail">' + escape(spec.description) +
> + map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') +
> + '</td><td>' + spec.assertionsGraph() + '</td>' :
> + '') +
> + '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
> + }).join('') + '</tr>'
> + }).join('') + '</table></div>'
> + },
> +
> + /**
> + * Terminal reporter.
> + *
> + * @api public
> + */
> +
> + Terminal : function(results, options) {
> + var failuresOnly = option('failuresOnly')
> + print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
> + color(" Failures: ", 'bold') + color(results.stats.failures, 'red') +
> + color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n")
> +
> + function indent(string) {
> + return string.replace(/^(.)/gm, ' $1')
> + }
> +
> + each(results.allSuites, function(suite) {
> + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
> + if (displaySuite && suite.hasSpecs()) {
> + print(color(' ' + suite.description, 'bold'))
> + each(suite.specs, function(spec){
> + var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
> + return graph + color('.', assertion.passed ? 'green' : 'red')
> + })
> + if (spec.requiresImplementation())
> + print(color(' ' + spec.description, 'blue') + assertionsGraph)
> + else if (spec.passed() && !failuresOnly)
> + print(color(' ' + spec.description, 'green') + assertionsGraph)
> + else if (!spec.passed())
> + print(color(' ' + spec.description, 'red') + assertionsGraph +
> + "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n")
> + })
> + print("")
> + }
> + })
> +
> + quit(results.stats.failures)
> + }
> + },
> +
> + Assertion : function(matcher, actual, expected, negate) {
> + extend(this, {
> + message: '',
> + passed: false,
> + actual: actual,
> + negate: negate,
> + matcher: matcher,
> + expected: expected,
> +
> + // Report assertion results
> +
> + report : function() {
> + if (JSpec.assert)
> + this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
> + return this
> + },
> +
> + // Run the assertion
> +
> + run : function() {
> + // TODO: remove unshifting
> + expected.unshift(actual)
> + this.result = matcher.match.apply(this, expected)
> + this.passed = negate ? !this.result : this.result
> + if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
> + return this
> + }
> + })
> + },
> +
> + ProxyAssertion : function(object, method, times, negate) {
> + var self = this
> + var old = object[method]
> +
> + // Proxy
> +
> + object[method] = function(){
> + args = toArray(arguments)
> + result = old.apply(object, args)
> + self.calls.push({ args : args, result : result })
> + return result
> + }
> +
> + // Times
> +
> + this.times = {
> + once : 1,
> + twice : 2
> + }[times] || times || 1
> +
> + extend(this, {
> + calls: [],
> + message: '',
> + defer: true,
> + passed: false,
> + negate: negate,
> + object: object,
> + method: method,
> +
> + // Proxy return value
> +
> + and_return : function(result) {
> + this.expectedResult = result
> + return this
> + },
> +
> + // Proxy arguments passed
> +
> + with_args : function() {
> + this.expectedArgs = toArray(arguments)
> + return this
> + },
> +
> + // Check if any calls have failing results
> +
> + anyResultsFail : function() {
> + return any(this.calls, function(call){
> + return self.expectedResult.an_instance_of ?
> + call.result.constructor != self.expectedResult.an_instance_of:
> + !equal(self.expectedResult, call.result)
> + })
> + },
> +
> + // Check if any calls have passing results
> +
> + anyResultsPass : function() {
> + return any(this.calls, function(call){
> + return self.expectedResult.an_instance_of ?
> + call.result.constructor == self.expectedResult.an_instance_of:
> + equal(self.expectedResult, call.result)
> + })
> + },
> +
> + // Return the passing result
> +
> + passingResult : function() {
> + return this.anyResultsPass().result
> + },
> +
> + // Return the failing result
> +
> + failingResult : function() {
> + return this.anyResultsFail().result
> + },
> +
> + // Check if any arguments fail
> +
> + anyArgsFail : function() {
> + return any(this.calls, function(call){
> + return any(self.expectedArgs, function(i, arg){
> + if (arg == null) return call.args[i] == null
> + return arg.an_instance_of ?
> + call.args[i].constructor != arg.an_instance_of:
> + !equal(arg, call.args[i])
> +
> + })
> + })
> + },
> +
> + // Check if any arguments pass
> +
> + anyArgsPass : function() {
> + return any(this.calls, function(call){
> + return any(self.expectedArgs, function(i, arg){
> + return arg.an_instance_of ?
> + call.args[i].constructor == arg.an_instance_of:
> + equal(arg, call.args[i])
> +
> + })
> + })
> + },
> +
> + // Return the passing args
> +
> + passingArgs : function() {
> + return this.anyArgsPass().args
> + },
> +
> + // Return the failing args
> +
> + failingArgs : function() {
> + return this.anyArgsFail().args
> + },
> +
> + // Report assertion results
> +
> + report : function() {
> + if (JSpec.assert)
> + this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures
> + return this
> + },
> +
> + // Run the assertion
> +
> + run : function() {
> + var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
> +
> + function times(n) {
> + return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n]
> + }
> +
> + if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
> + this.message = methodString + ' to return ' + puts(this.expectedResult) +
> + ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
> +
> + if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
> + this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
> + ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
> +
> + if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times)
> + this.message = methodString + ' to be called ' + times(this.times) +
> + ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
> +
> + if (!this.message.length)
> + this.passed = true
> +
> + return this
> + }
> + })
> + },
> +
> + /**
> + * Specification Suite block object.
> + *
> + * @param {string} description
> + * @param {function} body
> + * @api private
> + */
> +
> + Suite : function(description, body) {
> + var self = this
> + extend(this, {
> + body: body,
> + description: description,
> + suites: [],
> + specs: [],
> + ran: false,
> + hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
> +
> + // Add a spec to the suite
> +
> + addSpec : function(description, body) {
> + var spec = new JSpec.Spec(description, body)
> + this.specs.push(spec)
> + JSpec.stats.specs++ // TODO: abstract
> + spec.suite = this
> + },
> +
> + // Add a hook to the suite
> +
> + addHook : function(hook, body) {
> + this.hooks[hook].push(body)
> + },
> +
> + // Add a nested suite
> +
> + addSuite : function(description, body) {
> + var suite = new JSpec.Suite(description, body)
> + JSpec.allSuites.push(suite)
> + suite.name = suite.description
> + suite.description = this.description + ' ' + suite.description
> + this.suites.push(suite)
> + suite.suite = this
> + },
> +
> + // Invoke a hook in context to this suite
> +
> + hook : function(hook) {
> + if (this.suite) this.suite.hook(hook)
> + each(this.hooks[hook], function(body) {
> + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
> + })
> + },
> +
> + // Check if nested suites are present
> +
> + hasSuites : function() {
> + return this.suites.length
> + },
> +
> + // Check if this suite has specs
> +
> + hasSpecs : function() {
> + return this.specs.length
> + },
> +
> + // Check if the entire suite passed
> +
> + passed : function() {
> + return !any(this.specs, function(spec){
> + return !spec.passed()
> + })
> + }
> + })
> + },
> +
> + /**
> + * Specification block object.
> + *
> + * @param {string} description
> + * @param {function} body
> + * @api private
> + */
> +
> + Spec : function(description, body) {
> + extend(this, {
> + body: body,
> + description: description,
> + assertions: [],
> +
> + // Add passing assertion
> +
> + pass : function(message) {
> + this.assertions.push({ passed: true, message: message })
> + if (JSpec.assert) ++JSpec.stats.passes
> + },
> +
> + // Add failing assertion
> +
> + fail : function(message) {
> + this.assertions.push({ passed: false, message: message })
> + if (JSpec.assert) ++JSpec.stats.failures
> + },
> +
> + // Run deferred assertions
> +
> + runDeferredAssertions : function() {
> + each(this.assertions, function(assertion){
> + if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
> + })
> + },
> +
> + // Find first failing assertion
> +
> + failure : function() {
> + return find(this.assertions, function(assertion){
> + return !assertion.passed
> + })
> + },
> +
> + // Find all failing assertions
> +
> + failures : function() {
> + return select(this.assertions, function(assertion){
> + return !assertion.passed
> + })
> + },
> +
> + // Weither or not the spec passed
> +
> + passed : function() {
> + return !this.failure()
> + },
> +
> + // Weither or not the spec requires implementation (no assertions)
> +
> + requiresImplementation : function() {
> + return this.assertions.length == 0
> + },
> +
> + // Sprite based assertions graph
> +
> + assertionsGraph : function() {
> + return map(this.assertions, function(assertion){
> + return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
> + }).join('')
> + }
> + })
> + },
> +
> + Module : function(methods) {
> + extend(this, methods)
> + },
> +
> + JSON : {
> +
> + /**
> + * Generic sequences.
> + */
> +
> + meta : {
> + '\b' : '\\b',
> + '\t' : '\\t',
> + '\n' : '\\n',
> + '\f' : '\\f',
> + '\r' : '\\r',
> + '"' : '\\"',
> + '\\' : '\\\\'
> + },
> +
> + /**
> + * Escapable sequences.
> + */
> +
> + escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
> +
> + /**
> + * JSON encode _object_.
> + *
> + * @param {mixed} object
> + * @return {string}
> + * @api private
> + */
> +
> + encode : function(object) {
> + var self = this
> + if (object == undefined || object == null) return 'null'
> + if (object === true) return 'true'
> + if (object === false) return 'false'
> + switch (typeof object) {
> + case 'number': return object
> + case 'string': return this.escapable.test(object) ?
> + '"' + object.replace(this.escapable, function (a) {
> + return typeof self.meta[a] === 'string' ? self.meta[a] :
> + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
> + }) + '"' :
> + '"' + object + '"'
> + case 'object':
> + if (object.constructor == Array)
> + return '[' + map(object, function(val){
> + return self.encode(val)
> + }).join(', ') + ']'
> + else if (object)
> + return '{' + map(object, function(key, val){
> + return self.encode(key) + ':' + self.encode(val)
> + }).join(', ') + '}'
> + }
> + return 'null'
> + }
> + },
> +
> + // --- DSLs
> +
> + DSLs : {
> + snake : {
> + expect : function(actual){
> + return JSpec.expect(actual)
> + },
> +
> + describe : function(description, body) {
> + return JSpec.currentSuite.addSuite(description, body)
> + },
> +
> + it : function(description, body) {
> + return JSpec.currentSuite.addSpec(description, body)
> + },
> +
> + before : function(body) {
> + return JSpec.currentSuite.addHook('before', body)
> + },
> +
> + after : function(body) {
> + return JSpec.currentSuite.addHook('after', body)
> + },
> +
> + before_each : function(body) {
> + return JSpec.currentSuite.addHook('before_each', body)
> + },
> +
> + after_each : function(body) {
> + return JSpec.currentSuite.addHook('after_each', body)
> + },
> +
> + should_behave_like : function(description) {
> + return JSpec.shareBehaviorsOf(description)
> + }
> + }
> + },
> +
> + // --- Methods
> +
> + /**
> + * Check if _value_ is 'stop'. For use as a
> + * utility callback function.
> + *
> + * @param {mixed} value
> + * @return {bool}
> + * @api public
> + */
> +
> + haveStopped : function(value) {
> + return value === 'stop'
> + },
> +
> + /**
> + * Include _object_ which may be a hash or Module instance.
> + *
> + * @param {hash, Module} object
> + * @return {JSpec}
> + * @api public
> + */
> +
> + include : function(object) {
> + var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
> + this.modules.push(module)
> + if ('init' in module) module.init()
> + if ('utilities' in module) extend(this.defaultContext, module.utilities)
> + if ('matchers' in module) this.addMatchers(module.matchers)
> + if ('reporters' in module) extend(this.reporters, module.reporters)
> + if ('DSLs' in module)
> + each(module.DSLs, function(name, methods){
> + JSpec.DSLs[name] = JSpec.DSLs[name] || {}
> + extend(JSpec.DSLs[name], methods)
> + })
> + return this
> + },
> +
> + /**
> + * Add a module hook _name_, which is immediately
> + * called per module with the _args_ given. An array of
> + * hook return values is returned.
> + *
> + * @param {name} string
> + * @param {...} args
> + * @return {array}
> + * @api private
> + */
> +
> + hook : function(name, args) {
> + args = toArray(arguments, 1)
> + return inject(JSpec.modules, [], function(results, module){
> + if (typeof module[name] == 'function')
> + results.push(JSpec.evalHook(module, name, args))
> + })
> + },
> +
> + /**
> + * Eval _module_ hook _name_ with _args_. Evaluates in context
> + * to the module itself, JSpec, and JSpec.context.
> + *
> + * @param {Module} module
> + * @param {string} name
> + * @param {array} args
> + * @return {mixed}
> + * @api private
> + */
> +
> + evalHook : function(module, name, args) {
> + hook('evaluatingHookBody', module, name)
> + try { return module[name].apply(module, args) }
> + catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) }
> + },
> +
> + /**
> + * Same as hook() however accepts only one _arg_ which is
> + * considered immutable. This function passes the arg
> + * to the first module, then passes the return value of the last
> + * module called, to the following module.
> + *
> + * @param {string} name
> + * @param {mixed} arg
> + * @return {mixed}
> + * @api private
> + */
> +
> + hookImmutable : function(name, arg) {
> + return inject(JSpec.modules, arg, function(result, module){
> + if (typeof module[name] == 'function')
> + return JSpec.evalHook(module, name, [result])
> + })
> + },
> +
> + /**
> + * Find a suite by its description or name.
> + *
> + * @param {string} description
> + * @return {Suite}
> + * @api private
> + */
> +
> + findSuite : function(description) {
> + return find(this.allSuites, function(suite){
> + return suite.name == description || suite.description == description
> + })
> + },
> +
> + /**
> + * Share behaviors (specs) of the given suite with
> + * the current suite.
> + *
> + * @param {string} description
> + * @api public
> + */
> +
> + shareBehaviorsOf : function(description) {
> + if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
> + else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
> + },
> +
> + /**
> + * Copy specs from one suite to another.
> + *
> + * @param {Suite} fromSuite
> + * @param {Suite} toSuite
> + * @api public
> + */
> +
> + copySpecs : function(fromSuite, toSuite) {
> + each(fromSuite.specs, function(spec){
> + var newSpec = new Object();
> + extend(newSpec, spec);
> + newSpec.assertions = [];
> + toSuite.specs.push(newSpec);
> + })
> + },
> +
> + /**
> + * Convert arguments to an array.
> + *
> + * @param {object} arguments
> + * @param {int} offset
> + * @return {array}
> + * @api public
> + */
> +
> + toArray : function(arguments, offset) {
> + return Array.prototype.slice.call(arguments, offset || 0)
> + },
> +
> + /**
> + * Return ANSI-escaped colored string.
> + *
> + * @param {string} string
> + * @param {string} color
> + * @return {string}
> + * @api public
> + */
> +
> + color : function(string, color) {
> + return "\u001B[" + {
> + bold : 1,
> + black : 30,
> + red : 31,
> + green : 32,
> + yellow : 33,
> + blue : 34,
> + magenta : 35,
> + cyan : 36,
> + white : 37
> + }[color] + 'm' + string + "\u001B[0m"
> + },
> +
> + /**
> + * Default matcher message callback.
> + *
> + * @api private
> + */
> +
> + defaultMatcherMessage : function(actual, expected, negate, name) {
> + return 'expected ' + puts(actual) + ' to ' +
> + (negate ? 'not ' : '') +
> + name.replace(/_/g, ' ') +
> + ' ' + (expected.length > 1 ?
> + puts.apply(this, expected.slice(1)) :
> + '')
> + },
> +
> + /**
> + * Normalize a matcher message.
> + *
> + * When no messge callback is present the defaultMatcherMessage
> + * will be assigned, will suffice for most matchers.
> + *
> + * @param {hash} matcher
> + * @return {hash}
> + * @api public
> + */
> +
> + normalizeMatcherMessage : function(matcher) {
> + if (typeof matcher.message != 'function')
> + matcher.message = this.defaultMatcherMessage
> + return matcher
> + },
> +
> + /**
> + * Normalize a matcher body
> + *
> + * This process allows the following conversions until
> + * the matcher is in its final normalized hash state.
> + *
> + * - '==' becomes 'actual == expected'
> + * - 'actual == expected' becomes 'return actual == expected'
> + * - function(actual, expected) { return actual == expected } becomes
> + * { match : function(actual, expected) { return actual == expected }}
> + *
> + * @param {mixed} body
> + * @return {hash}
> + * @api public
> + */
> +
> + normalizeMatcherBody : function(body) {
> + switch (body.constructor) {
> + case String:
> + if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
> + if (body.length < 4) body = 'actual ' + body + ' expected'
> + return { match: function(actual, expected) { return eval(body) }}
> +
> + case Function:
> + return { match: body }
> +
> + default:
> + return body
> + }
> + },
> +
> + /**
> + * Get option value. This method first checks if
> + * the option key has been set via the query string,
> + * otherwise returning the options hash value.
> + *
> + * @param {string} key
> + * @return {mixed}
> + * @api public
> + */
> +
> + option : function(key) {
> + return (value = query(key)) !== null ? value :
> + JSpec.options[key] || null
> + },
> +
> + /**
> + * Check if object _a_, is equal to object _b_.
> + *
> + * @param {object} a
> + * @param {object} b
> + * @return {bool}
> + * @api private
> + */
> +
> + equal: function(a, b) {
> + if (typeof a != typeof b) return
> + if (a === b) return true
> + if (a instanceof RegExp)
> + return a.toString() === b.toString()
> + if (a instanceof Date)
> + return Number(a) === Number(b)
> + if (typeof a != 'object') return
> + if (a.length !== undefined)
> + if (a.length !== b.length) return
> + else
> + for (var i = 0, len = a.length; i < len; ++i)
> + if (!equal(a[i], b[i]))
> + return
> + for (var key in a)
> + if (!equal(a[key], b[key]))
> + return
> + return true
> + },
> +
> + /**
> + * Return last element of an array.
> + *
> + * @param {array} array
> + * @return {object}
> + * @api public
> + */
> +
> + last : function(array) {
> + return array[array.length - 1]
> + },
> +
> + /**
> + * Convert object(s) to a print-friend string.
> + *
> + * @param {...} object
> + * @return {string}
> + * @api public
> + */
> +
> + puts : function(object) {
> + if (arguments.length > 1)
> + return map(toArray(arguments), function(arg){
> + return puts(arg)
> + }).join(', ')
> + if (object === undefined) return 'undefined'
> + if (object === null) return 'null'
> + if (object === true) return 'true'
> + if (object === false) return 'false'
> + if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
> + if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector)
> + if (object.jquery) return object.get(0).outerHTML
> + if (object.nodeName) return object.outerHTML
> + switch (object.constructor) {
> + case Function: return object.name || object
> + case String:
> + return '"' + object
> + .replace(/"/g, '\\"')
> + .replace(/\n/g, '\\n')
> + .replace(/\t/g, '\\t')
> + + '"'
> + case Array:
> + return inject(object, '[', function(b, v){
> + return b + ', ' + puts(v)
> + }).replace('[,', '[') + ' ]'
> + case Object:
> + object.__hit__ = true
> + return inject(object, '{', function(b, k, v) {
> + if (k == '__hit__') return b
> + return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v))
> + }).replace('{,', '{') + ' }'
> + default:
> + return object.toString()
> + }
> + },
> +
> + /**
> + * Escape HTML.
> + *
> + * @param {string} html
> + * @return {string}
> + * @api public
> + */
> +
> + escape : function(html) {
> + return html.toString()
> + .replace(/&/gmi, '&')
> + .replace(/"/gmi, '"')
> + .replace(/>/gmi, '>')
> + .replace(/</gmi, '<')
> + },
> +
> + /**
> + * Perform an assertion without reporting.
> + *
> + * This method is primarily used for internal
> + * matchers in order retain DRYness. May be invoked
> + * like below:
> + *
> + * does('foo', 'eql', 'foo')
> + * does([1,2], 'include', 1, 2)
> + *
> + * External hooks are not run for internal assertions
> + * performed by does().
> + *
> + * @param {mixed} actual
> + * @param {string} matcher
> + * @param {...} expected
> + * @return {mixed}
> + * @api private
> + */
> +
> + does : function(actual, matcher, expected) {
> + var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, toArray(arguments, 2))
> + return assertion.run().result
> + },
> +
> + /**
> + * Perform an assertion.
> + *
> + * expect(true).to('be', true)
> + * expect('foo').not_to('include', 'bar')
> + * expect([1, [2]]).to('include', 1, [2])
> + *
> + * @param {mixed} actual
> + * @return {hash}
> + * @api public
> + */
> +
> + expect : function(actual) {
> + function assert(matcher, args, negate) {
> + var expected = toArray(args, 1)
> + matcher.negate = negate
> + assertion = new JSpec.Assertion(matcher, actual, expected, negate)
> + hook('beforeAssertion', assertion)
> + if (matcher.defer) assertion.run()
> + else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
> + return assertion.result
> + }
> +
> + function to(matcher) {
> + return assert(matcher, arguments, false)
> + }
> +
> + function not_to(matcher) {
> + return assert(matcher, arguments, true)
> + }
> +
> + return {
> + to : to,
> + should : to,
> + not_to: not_to,
> + should_not : not_to
> + }
> + },
> +
> + /**
> + * Strim whitespace or chars.
> + *
> + * @param {string} string
> + * @param {string} chars
> + * @return {string}
> + * @api public
> + */
> +
> + strip : function(string, chars) {
> + return string.
> + replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
> + replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
> + },
> +
> + /**
> + * Call an iterator callback with arguments a, or b
> + * depending on the arity of the callback.
> + *
> + * @param {function} callback
> + * @param {mixed} a
> + * @param {mixed} b
> + * @return {mixed}
> + * @api private
> + */
> +
> + callIterator : function(callback, a, b) {
> + return callback.length == 1 ? callback(b) : callback(a, b)
> + },
> +
> + /**
> + * Extend an object with another.
> + *
> + * @param {object} object
> + * @param {object} other
> + * @api public
> + */
> +
> + extend : function(object, other) {
> + each(other, function(property, value){
> + object[property] = value
> + })
> + },
> +
> + /**
> + * Iterate an object, invoking the given callback.
> + *
> + * @param {hash, array} object
> + * @param {function} callback
> + * @return {JSpec}
> + * @api public
> + */
> +
> + each : function(object, callback) {
> + if (object.constructor == Array)
> + for (var i = 0, len = object.length; i < len; ++i)
> + callIterator(callback, i, object[i])
> + else
> + for (var key in object)
> + if (object.hasOwnProperty(key))
> + callIterator(callback, key, object[key])
> + },
> +
> + /**
> + * Iterate with memo.
> + *
> + * @param {hash, array} object
> + * @param {object} memo
> + * @param {function} callback
> + * @return {object}
> + * @api public
> + */
> +
> + inject : function(object, memo, callback) {
> + each(object, function(key, value){
> + memo = (callback.length == 2 ?
> + callback(memo, value):
> + callback(memo, key, value)) ||
> + memo
> + })
> + return memo
> + },
> +
> + /**
> + * Destub _object_'s _method_. When no _method_ is passed
> + * all stubbed methods are destubbed. When no arguments
> + * are passed every object found in JSpec.stubbed will be
> + * destubbed.
> + *
> + * @param {mixed} object
> + * @param {string} method
> + * @api public
> + */
> +
> + destub : function(object, method) {
> + if (method) {
> + if (object['__prototype__' + method])
> + delete object[method]
> + else
> + object[method] = object['__original__' + method]
> + delete object['__prototype__' + method]
> + delete object['__original____' + method]
> + }
> + else if (object) {
> + for (var key in object)
> + if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
> + destub(object, captures[1])
> + }
> + else
> + while (JSpec.stubbed.length)
> + destub(JSpec.stubbed.shift())
> + },
> +
> + /**
> + * Stub _object_'s _method_.
> + *
> + * stub(foo, 'toString').and_return('bar')
> + *
> + * @param {mixed} object
> + * @param {string} method
> + * @return {hash}
> + * @api public
> + */
> +
> + stub : function(object, method) {
> + hook('stubbing', object, method)
> + JSpec.stubbed.push(object)
> + var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
> + object[type + method] = object[method]
> + object[method] = function(){}
> + return {
> + and_return : function(value) {
> + if (typeof value == 'function') object[method] = value
> + else object[method] = function(){ return value }
> + }
> + }
> + },
> +
> + /**
> + * Map callback return values.
> + *
> + * @param {hash, array} object
> + * @param {function} callback
> + * @return {array}
> + * @api public
> + */
> +
> + map : function(object, callback) {
> + return inject(object, [], function(memo, key, value){
> + memo.push(callIterator(callback, key, value))
> + })
> + },
> +
> + /**
> + * Returns the first matching expression or null.
> + *
> + * @param {hash, array} object
> + * @param {function} callback
> + * @return {mixed}
> + * @api public
> + */
> +
> + any : function(object, callback) {
> + return inject(object, null, function(state, key, value){
> + if (state == undefined)
> + return callIterator(callback, key, value) ? value : state
> + })
> + },
> +
> + /**
> + * Returns an array of values collected when the callback
> + * given evaluates to true.
> + *
> + * @param {hash, array} object
> + * @return {function} callback
> + * @return {array}
> + * @api public
> + */
> +
> + select : function(object, callback) {
> + return inject(object, [], function(selected, key, value){
> + if (callIterator(callback, key, value))
> + selected.push(value)
> + })
> + },
> +
> + /**
> + * Define matchers.
> + *
> + * @param {hash} matchers
> + * @api public
> + */
> +
> + addMatchers : function(matchers) {
> + each(matchers, function(name, body){
> + JSpec.addMatcher(name, body)
> + })
> + },
> +
> + /**
> + * Define a matcher.
> + *
> + * @param {string} name
> + * @param {hash, function, string} body
> + * @api public
> + */
> +
> + addMatcher : function(name, body) {
> + hook('addingMatcher', name, body)
> + if (name.indexOf(' ') != -1) {
> + var matchers = name.split(/\s+/)
> + var prefix = matchers.shift()
> + each(matchers, function(name) {
> + JSpec.addMatcher(prefix + '_' + name, body(name))
> + })
> + }
> + this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
> + this.matchers[name].name = name
> + },
> +
> + /**
> + * Add a root suite to JSpec.
> + *
> + * @param {string} description
> + * @param {body} function
> + * @api public
> + */
> +
> + describe : function(description, body) {
> + var suite = new JSpec.Suite(description, body)
> + hook('addingSuite', suite)
> + this.allSuites.push(suite)
> + this.suites.push(suite)
> + },
> +
> + /**
> + * Return the contents of a function body.
> + *
> + * @param {function} body
> + * @return {string}
> + * @api public
> + */
> +
> + contentsOf : function(body) {
> + return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
> + },
> +
> + /**
> + * Evaluate a JSpec capture body.
> + *
> + * @param {function} body
> + * @param {string} errorMessage (optional)
> + * @return {Type}
> + * @api private
> + */
> +
> + evalBody : function(body, errorMessage) {
> + var dsl = this.DSL || this.DSLs.snake
> + var matchers = this.matchers
> + var context = this.context || this.defaultContext
> + var contents = this.contentsOf(body)
> + hook('evaluatingBody', dsl, matchers, context, contents)
> + try { with (dsl){ with (context) { with (matchers) { eval(contents) }}} }
> + catch(e) { error(errorMessage, e) }
> + },
> +
> + /**
> + * Pre-process a string of JSpec.
> + *
> + * @param {string} input
> + * @return {string}
> + * @api private
> + */
> +
> + preprocess : function(input) {
> + if (typeof input != 'string') return
> + input = hookImmutable('preprocessing', input)
> + return input.
> + replace(/\t/g, ' ').
> + replace(/\r\n|\n|\r/g, '\n').
> + split('__END__')[0].
> + replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
> + replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
> + replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
> + replace(/^ *(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
> + replace(/^\s*end(?=\s|$)/gm, '});').
> + replace(/-\{/g, 'function(){').
> + replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
> + replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
> + replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
> + replace(/, \)/g, ')').
> + replace(/should\.not/g, 'should_not')
> + },
> +
> + /**
> + * Create a range string which can be evaluated to a native array.
> + *
> + * @param {int} start
> + * @param {int} end
> + * @return {string}
> + * @api public
> + */
> +
> + range : function(start, end) {
> + var current = parseInt(start), end = parseInt(end), values = [current]
> + if (end > current) while (++current <= end) values.push(current)
> + else while (--current >= end) values.push(current)
> + return '[' + values + ']'
> + },
> +
> + /**
> + * Report on the results.
> + *
> + * @api public
> + */
> +
> + report : function() {
> + this.duration = Number(new Date) - this.start
> + hook('reporting', JSpec.options)
> + new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options)
> + },
> +
> + /**
> + * Run the spec suites. Options are merged
> + * with JSpec options when present.
> + *
> + * @param {hash} options
> + * @return {JSpec}
> + * @api public
> + */
> +
> + run : function(options) {
> + if (any(hook('running'), haveStopped)) return this
> + if (options) extend(this.options, options)
> + this.start = Number(new Date)
> + each(this.suites, function(suite) { JSpec.runSuite(suite) })
> + return this
> + },
> +
> + /**
> + * Run a suite.
> + *
> + * @param {Suite} suite
> + * @api public
> + */
> +
> + runSuite : function(suite) {
> + this.currentSuite = suite
> + this.evalBody(suite.body)
> + suite.ran = true
> + hook('beforeSuite', suite), suite.hook('before')
> + each(suite.specs, function(spec) {
> + hook('beforeSpec', spec)
> + suite.hook('before_each')
> + JSpec.runSpec(spec)
> + hook('afterSpec', spec)
> + suite.hook('after_each')
> + })
> + if (suite.hasSuites()) {
> + each(suite.suites, function(suite) {
> + JSpec.runSuite(suite)
> + })
> + }
> + hook('afterSuite', suite), suite.hook('after')
> + this.stats.suitesFinished++
> + },
> +
> + /**
> + * Report a failure for the current spec.
> + *
> + * @param {string} message
> + * @api public
> + */
> +
> + fail : function(message) {
> + JSpec.currentSpec.fail(message)
> + },
> +
> + /**
> + * Report a passing assertion for the current spec.
> + *
> + * @param {string} message
> + * @api public
> + */
> +
> + pass : function(message) {
> + JSpec.currentSpec.pass(message)
> + },
> +
> + /**
> + * Run a spec.
> + *
> + * @param {Spec} spec
> + * @api public
> + */
> +
> + runSpec : function(spec) {
> + this.currentSpec = spec
> + try { this.evalBody(spec.body) }
> + catch (e) { fail(e) }
> + spec.runDeferredAssertions()
> + destub()
> + this.stats.specsFinished++
> + this.stats.assertions += spec.assertions.length
> + },
> +
> + /**
> + * Require a dependency, with optional message.
> + *
> + * @param {string} dependency
> + * @param {string} message (optional)
> + * @return {JSpec}
> + * @api public
> + */
> +
> + requires : function(dependency, message) {
> + hook('requiring', dependency, message)
> + try { eval(dependency) }
> + catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
> + return this
> + },
> +
> + /**
> + * Query against the current query strings keys
> + * or the queryString specified.
> + *
> + * @param {string} key
> + * @param {string} queryString
> + * @return {string, null}
> + * @api private
> + */
> +
> + query : function(key, queryString) {
> + var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
> + return inject(queryString.split('&'), null, function(value, pair){
> + parts = pair.split('=')
> + return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
> + })
> + },
> +
> + /**
> + * Throw a JSpec related error.
> + *
> + * @param {string} message
> + * @param {Exception} e
> + * @api public
> + */
> +
> + error : function(message, e) {
> + throw (message ? message : '') + e.toString() +
> + (e.line ? ' near line ' + e.line : '')
> + },
> +
> + /**
> + * Ad-hoc POST request for JSpec server usage.
> + *
> + * @param {string} uri
> + * @param {string} data
> + * @api private
> + */
> +
> + post : function(uri, data) {
> + if (any(hook('posting', uri, data), haveStopped)) return
> + var request = this.xhr()
> + request.open('POST', uri, false)
> + request.setRequestHeader('Content-Type', 'application/json')
> + request.send(JSpec.JSON.encode(data))
> + },
> +
> + /**
> + * Instantiate an XMLHttpRequest.
> + *
> + * Here we utilize IE's lame ActiveXObjects first which
> + * allow IE access serve files via the file: protocol, otherwise
> + * we then default to XMLHttpRequest.
> + *
> + * @return {XMLHttpRequest, ActiveXObject}
> + * @api private
> + */
> +
> + xhr : function() {
> + return this.ieXhr() || new JSpec.request
> + },
> +
> + /**
> + * Return Microsoft piece of crap ActiveXObject.
> + *
> + * @return {ActiveXObject}
> + * @api public
> + */
> +
> + ieXhr : function() {
> + function object(str) {
> + try { return new ActiveXObject(str) } catch(e) {}
> + }
> + return object('Msxml2.XMLHTTP.6.0') ||
> + object('Msxml2.XMLHTTP.3.0') ||
> + object('Msxml2.XMLHTTP') ||
> + object('Microsoft.XMLHTTP')
> + },
> +
> + /**
> + * Check for HTTP request support.
> + *
> + * @return {bool}
> + * @api private
> + */
> +
> + hasXhr : function() {
> + return JSpec.request || 'ActiveXObject' in main
> + },
> +
> + /**
> + * Try loading _file_ returning the contents
> + * string or null. Chain to locate / read a file.
> + *
> + * @param {string} file
> + * @return {string}
> + * @api public
> + */
> +
> + tryLoading : function(file) {
> + try { return JSpec.load(file) } catch (e) {}
> + },
> +
> + /**
> + * Load a _file_'s contents.
> + *
> + * @param {string} file
> + * @param {function} callback
> + * @return {string}
> + * @api public
> + */
> +
> + load : function(file, callback) {
> + if (any(hook('loading', file), haveStopped)) return
> + if ('readFile' in main)
> + return readFile(file)
> + else if (this.hasXhr()) {
> + var request = this.xhr()
> + request.open('GET', file, false)
> + request.send(null)
> + if (request.readyState == 4 &&
> + (request.status == 0 ||
> + request.status.toString().charAt(0) == 2))
> + return request.responseText
> + }
> + else
> + error("failed to load `" + file + "'")
> + },
> +
> + /**
> + * Load, pre-process, and evaluate a file.
> + *
> + * @param {string} file
> + * @param {JSpec}
> + * @api public
> + */
> +
> + exec : function(file) {
> + if (any(hook('executing', file), haveStopped)) return this
> + eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
> + return this
> + }
> + }
> +
> + // --- Node.js support
> +
> + if (typeof GLOBAL === 'object' && typeof exports === 'object')
> + quit = process.exit,
> + print = require('sys').puts,
> + readFile = require('fs').readFileSync
> +
> + // --- Utility functions
> +
> + var main = this,
> + find = JSpec.any,
> + utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
> + error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/)
> + while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift())
> + if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
> +
> + // --- Matchers
> +
> + addMatchers({
> + equal : "===",
> + eql : "equal(actual, expected)",
> + be : "alias equal",
> + be_greater_than : ">",
> + be_less_than : "<",
> + be_at_least : ">=",
> + be_at_most : "<=",
> + be_a : "actual.constructor == expected",
> + be_an : "alias be_a",
> + be_an_instance_of : "actual instanceof expected",
> + be_null : "actual == null",
> + be_true : "actual == true",
> + be_false : "actual == false",
> + be_undefined : "typeof actual == 'undefined'",
> + be_type : "typeof actual == expected",
> + match : "typeof actual == 'string' ? actual.match(expected) : false",
> + respond_to : "typeof actual[expected] == 'function'",
> + have_length : "actual.length == expected",
> + be_within : "actual >= expected[0] && actual <= last(expected)",
> + have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
> +
> + receive : { defer : true, match : function(actual, method, times) {
> + proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
> + JSpec.currentSpec.assertions.push(proxy)
> + return proxy
> + }},
> +
> + be_empty : function(actual) {
> + if (actual.constructor == Object && actual.length == undefined)
> + for (var key in actual)
> + return false;
> + return !actual.length
> + },
> +
> + include : function(actual) {
> + for (state = true, i = 1; i < arguments.length; i++) {
> + arg = arguments[i]
> + switch (actual.constructor) {
> + case String:
> + case Number:
> + case RegExp:
> + case Function:
> + state = actual.toString().indexOf(arg) !== -1
> + break
> +
> + case Object:
> + state = arg in actual
> + break
> +
> + case Array:
> + state = any(actual, function(value){ return equal(value, arg) })
> + break
> + }
> + if (!state) return false
> + }
> + return true
> + },
> +
> + throw_error : { match : function(actual, expected, message) {
> + try { actual() }
> + catch (e) {
> + this.e = e
> + var assert = function(arg) {
> + switch (arg.constructor) {
> + case RegExp : return arg.test(e.message || e.toString())
> + case String : return arg == (e.message || e.toString())
> + case Function : return e instanceof arg || e.name == arg.name
> + }
> + }
> + return message ? assert(expected) && assert(message) :
> + expected ? assert(expected) :
> + true
> + }
> + }, message : function(actual, expected, negate) {
> + // TODO: refactor when actual is not in expected [0]
> + var message_for = function(i) {
> + if (expected[i] == undefined) return 'exception'
> + switch (expected[i].constructor) {
> + case RegExp : return 'exception matching ' + puts(expected[i])
> + case String : return 'exception of ' + puts(expected[i])
> + case Function : return expected[i].name || 'Error'
> + }
> + }
> + exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
> + return 'expected ' + exception + (negate ? ' not ' : '' ) +
> + ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
> + }},
> +
> + have : function(actual, length, property) {
> + return actual[property].length == length
> + },
> +
> + have_at_least : function(actual, length, property) {
> + return actual[property].length >= length
> + },
> +
> + have_at_most :function(actual, length, property) {
> + return actual[property].length <= length
> + },
> +
> + have_within : function(actual, range, property) {
> + length = actual[property].length
> + return length >= range.shift() && length <= range.pop()
> + },
> +
> + have_prop : function(actual, property, value) {
> + return actual[property] == null ||
> + actual[property] instanceof Function ? false:
> + value == null ? true:
> + does(actual[property], 'eql', value)
> + },
> +
> + have_property : function(actual, property, value) {
> + return actual[property] == null ||
> + actual[property] instanceof Function ? false:
> + value == null ? true:
> + value === actual[property]
> + }
> + })
> +
> +})()
>
> Added: couchdb/trunk/share/www/script/jspec/jspec.xhr.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jspec/jspec.xhr.js?rev=950689&view=auto
> ==============================================================================
> --- couchdb/trunk/share/www/script/jspec/jspec.xhr.js (added)
> +++ couchdb/trunk/share/www/script/jspec/jspec.xhr.js Wed Jun 2 17:45:56 2010
> @@ -0,0 +1,195 @@
> +
> +// JSpec - XHR - Copyright TJ Holowaychuk <tj...@vision-media.ca> (MIT Licensed)
> +
> +(function(){
> +
> + var lastRequest
> +
> + // --- Original XMLHttpRequest
> +
> + var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ?
> + XMLHttpRequest :
> + function(){}
> + var OriginalActiveXObject = 'ActiveXObject' in this ?
> + ActiveXObject :
> + undefined
> +
> + // --- MockXMLHttpRequest
> +
> + var MockXMLHttpRequest = function() {
> + this.requestHeaders = {}
> + }
> +
> + MockXMLHttpRequest.prototype = {
> + status: 0,
> + async: true,
> + readyState: 0,
> + responseText: '',
> + abort: function(){},
> + onreadystatechange: function(){},
> +
> + /**
> + * Return response headers hash.
> + */
> +
> + getAllResponseHeaders : function(){
> + return this.responseHeaders
> + },
> +
> + /**
> + * Return case-insensitive value for header _name_.
> + */
> +
> + getResponseHeader : function(name) {
> + return this.responseHeaders[name.toLowerCase()]
> + },
> +
> + /**
> + * Set case-insensitive _value_ for header _name_.
> + */
> +
> + setRequestHeader : function(name, value) {
> + this.requestHeaders[name.toLowerCase()] = value
> + },
> +
> + /**
> + * Open mock request.
> + */
> +
> + open : function(method, url, async, user, password) {
> + this.user = user
> + this.password = password
> + this.url = url
> + this.readyState = 1
> + this.method = method.toUpperCase()
> + if (async != undefined) this.async = async
> + if (this.async) this.onreadystatechange()
> + },
> +
> + /**
> + * Send request _data_.
> + */
> +
> + send : function(data) {
> + var self = this
> + this.data = data
> + this.readyState = 4
> + if (this.method == 'HEAD') this.responseText = null
> + this.responseHeaders['content-length'] = (this.responseText || '').length
> + if(this.async) this.onreadystatechange()
> + lastRequest = function(){
> + return self
> + }
> + }
> + }
> +
> + // --- Response status codes
> +
> + JSpec.statusCodes = {
> + 100: 'Continue',
> + 101: 'Switching Protocols',
> + 200: 'OK',
> + 201: 'Created',
> + 202: 'Accepted',
> + 203: 'Non-Authoritative Information',
> + 204: 'No Content',
> + 205: 'Reset Content',
> + 206: 'Partial Content',
> + 300: 'Multiple Choice',
> + 301: 'Moved Permanently',
> + 302: 'Found',
> + 303: 'See Other',
> + 304: 'Not Modified',
> + 305: 'Use Proxy',
> + 307: 'Temporary Redirect',
> + 400: 'Bad Request',
> + 401: 'Unauthorized',
> + 402: 'Payment Required',
> + 403: 'Forbidden',
> + 404: 'Not Found',
> + 405: 'Method Not Allowed',
> + 406: 'Not Acceptable',
> + 407: 'Proxy Authentication Required',
> + 408: 'Request Timeout',
> + 409: 'Conflict',
> + 410: 'Gone',
> + 411: 'Length Required',
> + 412: 'Precondition Failed',
> + 413: 'Request Entity Too Large',
> + 414: 'Request-URI Too Long',
> + 415: 'Unsupported Media Type',
> + 416: 'Requested Range Not Satisfiable',
> + 417: 'Expectation Failed',
> + 422: 'Unprocessable Entity',
> + 500: 'Internal Server Error',
> + 501: 'Not Implemented',
> + 502: 'Bad Gateway',
> + 503: 'Service Unavailable',
> + 504: 'Gateway Timeout',
> + 505: 'HTTP Version Not Supported'
> + }
> +
> + /**
> + * Mock XMLHttpRequest requests.
> + *
> + * mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' })
> + *
> + * @return {hash}
> + * @api public
> + */
> +
> + function mockRequest() {
> + return { and_return : function(body, type, status, headers) {
> + XMLHttpRequest = MockXMLHttpRequest
> + ActiveXObject = false
> + status = status || 200
> + headers = headers || {}
> + headers['content-type'] = type
> + JSpec.extend(XMLHttpRequest.prototype, {
> + responseText: body,
> + responseHeaders: headers,
> + status: status,
> + statusText: JSpec.statusCodes[status]
> + })
> + }}
> + }
> +
> + /**
> + * Unmock XMLHttpRequest requests.
> + *
> + * @api public
> + */
> +
> + function unmockRequest() {
> + XMLHttpRequest = OriginalXMLHttpRequest
> + ActiveXObject = OriginalActiveXObject
> + }
> +
> + JSpec.include({
> + name: 'Mock XHR',
> +
> + // --- Utilities
> +
> + utilities : {
> + mockRequest: mockRequest,
> + unmockRequest: unmockRequest
> + },
> +
> + // --- Hooks
> +
> + afterSpec : function() {
> + unmockRequest()
> + },
> +
> + // --- DSLs
> +
> + DSLs : {
> + snake : {
> + mock_request: mockRequest,
> + unmock_request: unmockRequest,
> + last_request: function(){ return lastRequest() }
> + }
> + }
> +
> + })
> +})()
> \ No newline at end of file
>
> Added: couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js
> URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js?rev=950689&view=auto
> ==============================================================================
> --- couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js (added)
> +++ couchdb/trunk/share/www/spec/couch_js_class_methods_spec.js Wed Jun 2 17:45:56 2010
> @@ -0,0 +1,389 @@
> +// Specs for couch.js lines 313-470
> +
> +describe 'CouchDB class'
> + describe 'session stuff'
> + before
> + useTestUserDb();
> + end
> +
> + after
> + useOldUserDb();
> + end
> +
> + before_each
> + userDoc = users_db.save(CouchDB.prepareUserDoc({name: "Gaius Baltar", roles: ["president"]}, "secretpass"));
> + end
> +
> + after_each
> + users_db.deleteDoc({_id : userDoc.id, _rev : userDoc.rev})
> + end
> +
> + describe '.login'
> + it 'should return ok true'
> + CouchDB.login("Gaius Baltar", "secretpass").ok.should.be_true
> + end
> +
> + it 'should return the name of the logged in user'
> + CouchDB.login("Gaius Baltar", "secretpass").name.should.eql "Gaius Baltar"
> + end
> +
> + it 'should return the roles of the logged in user'
> + CouchDB.login("Gaius Baltar", "secretpass").roles.should.eql ["president"]
> + end
> +
> + it 'should post _session'
> + CouchDB.should.receive("request", "once").with_args("POST", "/_session")
> + CouchDB.login("Gaius Baltar", "secretpass");
> + end
> +
> + it 'should create a session'
> + CouchDB.login("Gaius Baltar", "secretpass");
> + CouchDB.session().userCtx.name.should.eql "Gaius Baltar"
> + end
> + end
> +
> + describe '.logout'
> + before_each
> + CouchDB.login("Gaius Baltar", "secretpass");
> + end
> +
> + it 'should return ok true'
> + CouchDB.logout().ok.should.be_true
> + end
> +
> + it 'should delete _session'
> + CouchDB.should.receive("request", "once").with_args("DELETE", "/_session")
> + CouchDB.logout();
> + end
> +
> + it 'should result in an invalid session'
> + CouchDB.logout();
> + CouchDB.session().name.should.be_null
> + end
> + end
> +
> + describe '.session'
> + before_each
> + CouchDB.login("Gaius Baltar", "secretpass");
> + end
> +
> + it 'should return ok true'
> + CouchDB.session().ok.should.be_true
> + end
> +
> + it 'should return the users name'
> + CouchDB.session().userCtx.name.should.eql "Gaius Baltar"
> + end
> +
> + it 'should return the users roles'
> + CouchDB.session().userCtx.roles.should.eql ["president"]
> + end
> +
> + it 'should return the name of the authentication db'
> + CouchDB.session().info.authentication_db.should.eql "spec_users_db"
> + end
> +
> + it 'should return the active authentication handler'
> + CouchDB.session().info.authenticated.should.eql "cookie"
> + end
> + end
> + end
> +
> + describe 'db stuff'
> + before_each
> + db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"});
> + db.createDb();
> + end
> +
> + after_each
> + db.deleteDb();
> + end
> +
> + describe '.prepareUserDoc'
> + before_each
> + userDoc = CouchDB.prepareUserDoc({name: "Laura Roslin"}, "secretpass");
> + end
> +
> + it 'should return the users name'
> + userDoc.name.should.eql "Laura Roslin"
> + end
> +
> + it 'should prefix the id with the CouchDB user_prefix'
> + userDoc._id.should.eql "org.couchdb.user:Laura Roslin"
> + end
> +
> + it 'should return the users roles'
> + var userDocWithRoles = CouchDB.prepareUserDoc({name: "William Adama", roles: ["admiral", "commander"]}, "secretpass")
> + userDocWithRoles.roles.should.eql ["admiral", "commander"]
> + end
> +
> + it 'should return the hashed password'
> + userDoc.password_sha.length.should.be_at_least 30
> + userDoc.password_sha.should.be_a String
> + end
> + end
> +
> + describe '.allDbs'
> + it 'should get _all_dbs'
> + CouchDB.should.receive("request", "once").with_args("GET", "/_all_dbs");
> + CouchDB.allDbs();
> + end
> +
> + it 'should return an array that includes a created database'
> + temp_db = new CouchDB("temp_spec_db", {"X-Couch-Full-Commit":"false"});
> + temp_db.createDb();
> + CouchDB.allDbs().should.include("temp_spec_db");
> + temp_db.deleteDb();
> + end
> +
> + it 'should return an array that does not include a database that does not exist'
> + CouchDB.allDbs().should.not.include("not_existing_temp_spec_db");
> + end
> + end
> +
> + describe '.allDesignDocs'
> + it 'should return the total number of documents'
> + CouchDB.allDesignDocs().spec_db.total_rows.should.eql 0
> + db.save({'type':'battlestar', 'name':'galactica'});
> + CouchDB.allDesignDocs().spec_db.total_rows.should.eql 1
> + end
> +
> + it 'should return undefined when the db does not exist'
> + CouchDB.allDesignDocs().non_existing_db.should.be_undefined
> + end
> +
> + it 'should return no documents when there are no design documents'
> + CouchDB.allDesignDocs().spec_db.rows.should.eql []
> + end
> +
> + it 'should return all design documents'
> + var designDoc = {
> + "views" : {
> + "people" : {
> + "map" : "function(doc) { emit(doc._id, doc); }"
> + }
> + },
> + "_id" : "_design/spec_db"
> + };
> + db.save(designDoc);
> +
> + var allDesignDocs = CouchDB.allDesignDocs();
> + allDesignDocs.spec_db.rows[0].id.should.eql "_design/spec_db"
> + allDesignDocs.spec_db.rows[0].key.should.eql "_design/spec_db"
> + allDesignDocs.spec_db.rows[0].value.rev.length.should.be_at_least 30
> + end
> + end
> +
> + describe '.getVersion'
> + it 'should get the CouchDB version'
> + CouchDB.should.receive("request", "once").with_args("GET", "/")
> + CouchDB.getVersion();
> + end
> +
> + it 'should return the CouchDB version'
> + CouchDB.getVersion().should_match /^\d\d?\.\d\d?\.\d\d?.*/
> + end
> + end
> +
> + describe '.replicate'
> + before_each
> + db2 = new CouchDB("spec_db_2", {"X-Couch-Full-Commit":"false"});
> + db2.createDb();
> + host = window.location.protocol + "//" + window.location.host ;
> + end
> +
> + after_each
> + db2.deleteDb();
> + end
> +
> + it 'should return no_changes true when there are no changes between the dbs'
> + CouchDB.replicate(host + db.uri, host + db2.uri).no_changes.should.be_true
> + end
> +
> + it 'should return the session ID'
> + db.save({'type':'battlestar', 'name':'galactica'});
> + CouchDB.replicate(host + db.uri, host + db2.uri).session_id.length.should.be_at_least 30
> + end
> +
> + it 'should return source_last_seq'
> + db.save({'type':'battlestar', 'name':'galactica'});
> + db.save({'type':'battlestar', 'name':'pegasus'});
> +
> + CouchDB.replicate(host + db.uri, host + db2.uri).source_last_seq.should.eql 2
> + end
> +
> + it 'should return the replication history'
> + db.save({'type':'battlestar', 'name':'galactica'});
> + db.save({'type':'battlestar', 'name':'pegasus'});
> +
> + var result = CouchDB.replicate(host + db.uri, host + db2.uri);
> + result.history[0].docs_written.should.eql 2
> + result.history[0].start_last_seq.should.eql 0
> + end
> +
> + it 'should pass through replication options'
> + db.save({'type':'battlestar', 'name':'galactica'});
> + db2.deleteDb();
> + -{CouchDB.replicate(host + db.uri, host + db2.uri)}.should.throw_error
> + var result = CouchDB.replicate(host + db.uri, host + db2.uri, {"body" : {"create_target":true}});
> +
> + result.ok.should.eql true
> + result.history[0].docs_written.should.eql 1
> + db2.info().db_name.should.eql "spec_db_2"
> + end
> + end
> +
> + describe '.newXhr'
> + it 'should return a XMLHTTPRequest'
> + CouchDB.newXhr().should.have_prop 'readyState'
> + CouchDB.newXhr().should.have_prop 'responseText'
> + CouchDB.newXhr().should.have_prop 'status'
> + end
> + end
> +
> + describe '.request'
> + it 'should return a XMLHttpRequest'
> + var req = CouchDB.request("GET", '/');
> + req.should.include "readyState"
> + req.should.include "responseText"
> + req.should.include "statusText"
> + end
> +
> + it 'should pass through the options headers'
> + var xhr = CouchDB.newXhr();
> + stub(CouchDB, 'newXhr').and_return(xhr);
> +
> + xhr.should.receive("setRequestHeader", "once").with_args("X-Couch-Full-Commit", "true")
> + CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}});
> + end
> +
> + it 'should pass through the options body'
> + var xhr = CouchDB.newXhr();
> + stub(CouchDB, 'newXhr').and_return(xhr);
> +
> + xhr.should.receive("send", "once").with_args({"body_key":"body_value"})
> + CouchDB.request("GET", "/", {'body': {"body_key":"body_value"}});
> + end
> +
> + it 'should prepend the urlPrefix to the uri'
> + var oldPrefix = CouchDB.urlPrefix;
> + CouchDB.urlPrefix = "/_utils";
> +
> + var xhr = CouchDB.newXhr();
> + stub(CouchDB, 'newXhr').and_return(xhr);
> +
> + xhr.should.receive("open", "once").with_args("GET", "/_utils/", false)
> + CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}});
> +
> + CouchDB.urlPrefix = oldPrefix;
> + end
> + end
> +
> + describe '.requestStats'
> + it 'should get the stats for specified module and key'
> + var stats = CouchDB.requestStats('couchdb', 'open_databases', null);
> + stats.description.should.eql 'number of open databases'
> + stats.current.should.be_a Number
> + end
> +
> + it 'should add flush true to the request when there is a test argument'
> + CouchDB.should.receive("request", "once").with_args("GET", "/_stats/httpd/requests?flush=true")
> + CouchDB.requestStats('httpd', 'requests', 'test');
> + end
> +
> + it 'should still work when there is a test argument'
> + var stats = CouchDB.requestStats('httpd_status_codes', '200', 'test');
> + stats.description.should.eql 'number of HTTP 200 OK responses'
> + stats.sum.should.be_a Number
> + end
> + end
> +
> + describe '.newUuids'
> + after_each
> + CouchDB.uuids_cache = [];
> + end
> +
> + it 'should return the specified amount of uuids'
> + var uuids = CouchDB.newUuids(45);
> + uuids.should.have_length 45
> + end
> +
> + it 'should return an array with uuids'
> + var uuids = CouchDB.newUuids(1);
> + uuids[0].should.be_a String
> + uuids[0].should.have_length 32
> + end
> +
> + it 'should leave the uuids_cache with 100 uuids when theres no buffer size specified'
> + CouchDB.newUuids(23);
> + CouchDB.uuids_cache.should.have_length 100
> + end
> +
> + it 'should leave the uuids_cache with the specified buffer size'
> + CouchDB.newUuids(23, 150);
> + CouchDB.uuids_cache.should.have_length 150
> + end
> +
> + it 'should get the uuids from the uuids_cache when there are enough uuids in there'
> + CouchDB.newUuids(10);
> + CouchDB.newUuids(25);
> + CouchDB.uuids_cache.should.have_length 75
> + end
> +
> + it 'should create new uuids and add as many as specified to the uuids_cache when there are not enough uuids in the cache'
> + CouchDB.newUuids(10);
> + CouchDB.newUuids(125, 60);
> + CouchDB.uuids_cache.should.have_length 160
> + end
> + end
> +
> + describe '.maybeThrowError'
> + it 'should throw an error when the request has status 404'
> + var req = CouchDB.request("GET", "/nonexisting_db");
> + -{CouchDB.maybeThrowError(req)}.should.throw_error
> + end
> +
> + it 'should throw an error when the request has status 412'
> + var req = CouchDB.request("PUT", "/spec_db");
> + -{CouchDB.maybeThrowError(req)}.should.throw_error
> + end
> +
> + it 'should throw an error when the request has status 405'
> + var req = CouchDB.request("DELETE", "/_utils");
> + -{CouchDB.maybeThrowError(req)}.should.throw_error
> + end
> +
> + it 'should throw the responseText of the request'
> + var req = CouchDB.request("GET", "/nonexisting_db");
> + try {
> + CouchDB.maybeThrowError(req)
> + } catch(e) {
> + e.error.should.eql JSON.parse(req.responseText).error
> + e.reason.should.eql JSON.parse(req.responseText).reason
> + }
> + end
> +
> + it 'should throw an unknown error when the responseText is invalid json'
> + mock_request().and_return("invalid json...", "application/json", 404, {})
> + try {
> + CouchDB.maybeThrowError(CouchDB.newXhr())
> + } catch(e) {
> + e.error.should.eql "unknown"
> + e.reason.should.eql "invalid json..."
> + }
> + end
> + end
> +
> + describe '.params'
> + it 'should turn a json object into a http params string'
> + var params = CouchDB.params({"president":"laura", "cag":"lee"})
> + params.should.eql "president=laura&cag=lee"
> + end
> +
> + it 'should return a blank string when the object is empty'
> + var params = CouchDB.params({})
> + params.should.eql ""
> + end
> + end
> + end
> +end
> \ No newline at end of file
>
>