couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j..@apache.org
Subject svn commit: r950689 [1/2] - in /couchdb/trunk: ./ share/www/script/ share/www/script/jspec/ share/www/spec/
Date Wed, 02 Jun 2010 17:45:57 GMT
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



Mime
View raw message