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, '&amp;')
+         .replace(/"/gmi, '&quot;')
+         .replace(/>/gmi, '&gt;')
+         .replace(/</gmi, '&lt;')
+     },
+     
+     /**
+      * 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, '&amp;')
> +         .replace(/"/gmi, '&quot;')
> +         .replace(/>/gmi, '&gt;')
> +         .replace(/</gmi, '&lt;')
> +     },
> +     
> +     /**
> +      * 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
> 
>