incubator-couchdb-dev mailing list archives

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


Mime
View raw message