You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2014/03/03 17:58:38 UTC

[26/34] couchdb commit: updated refs/heads/paginate-api-options to b63c791

Add new pagination algorithmn


Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/f796b38e
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/f796b38e
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/f796b38e

Branch: refs/heads/paginate-api-options
Commit: f796b38ef2474bdfed5303a627136ee3ad8ea8d2
Parents: 4e10385
Author: Garren Smith <ga...@gmail.com>
Authored: Wed Feb 19 15:23:52 2014 +0200
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Mar 3 11:18:42 2014 +0200

----------------------------------------------------------------------
 src/fauxton/app/addons/documents/resources.js   |  54 ++
 src/fauxton/app/addons/documents/routes.js      | 105 +--
 .../app/addons/documents/tests/resourcesSpec.js |  27 -
 src/fauxton/app/addons/documents/views.js       | 133 +--
 src/fauxton/app/addons/fauxton/base.js          |  14 +-
 src/fauxton/app/addons/fauxton/components.js    |  38 +-
 .../app/addons/fauxton/tests/paginateSpec.js    |  18 -
 src/fauxton/test/mocha/chai.js                  | 804 ++++++++++++++-----
 src/fauxton/test/test.config.underscore         |   4 +
 9 files changed, 765 insertions(+), 432 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/documents/resources.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/documents/resources.js b/src/fauxton/app/addons/documents/resources.js
index c2f9a70..6112bab 100644
--- a/src/fauxton/app/addons/documents/resources.js
+++ b/src/fauxton/app/addons/documents/resources.js
@@ -18,6 +18,51 @@ define([
 function(app, FauxtonAPI) {
   var Documents = FauxtonAPI.addon();
 
+  Documents.paginate = {
+    next: function (docs, currentParams, perPage, _isAllDocs) {
+      var params = {limit: perPage, skip: 1},
+          doc = _.last(docs),
+          docId = '',
+          lastId = '',
+          isView = !!!_isAllDocs,
+          key;
+
+      if (currentParams.keys) {
+        throw "Cannot paginate _all_docs with keys";
+      }
+
+      if (_.isUndefined(doc)) {
+        throw "Require docs to paginate";
+      }
+
+      params = _.reduce(['reduce', 'keys', 'endkey', 'descending', 'inclusive_end'], function (params, p) {
+        if (_.has(currentParams, p)) {
+          params[p] = currentParams[p]; 
+        }
+        return params;
+      }, params);
+
+      lastId = doc.id || doc._id;
+
+      if (isView) {
+        key = doc.key;
+        docId = lastId;
+      } else {
+        docId = key = lastId;
+      }
+
+      if (isView && !currentParams.keys) {
+        params.startkey_docid = docId; 
+        params.startkey = key;
+      } else if (currentParams.startkey) {
+        params.startkey = key;
+      } else {
+        params.startkey_docid = docId; 
+      }
+      return params;
+    }
+ };
+
   Documents.Doc = FauxtonAPI.Model.extend({
     idAttribute: "_id",
     documentation: function(){
@@ -274,6 +319,7 @@ function(app, FauxtonAPI) {
 
   Documents.AllDocs = FauxtonAPI.Collection.extend({
     model: Documents.Doc,
+    isAllDocs: true,
     documentation: function(){
       return "docs";
     },
@@ -330,6 +376,10 @@ function(app, FauxtonAPI) {
       this.params.limit = limit;
     },
 
+    updateParams: function (params) {
+      this.params = params;
+    },
+
     nextPage: function (num, lastId) {
       if (!lastId) {
         var doc = this.last();
@@ -478,6 +528,10 @@ function(app, FauxtonAPI) {
       return this.url('app');
     },
 
+    updateParams: function (params) {
+      this.params = params;
+    },
+
     updateLimit: function (limit) {
       if (this.params.startkey_docid && this.params.startkey) {
         //we are paginating so set limit + 1

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/documents/routes.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/documents/routes.js b/src/fauxton/app/addons/documents/routes.js
index 0d143de..5ce66b1 100644
--- a/src/fauxton/app/addons/documents/routes.js
+++ b/src/fauxton/app/addons/documents/routes.js
@@ -160,8 +160,8 @@ function(app, FauxtonAPI, Documents, Databases) {
     },
 
     initialize: function (route, masterLayout, options) {
-      var docOptions = app.getParams();
-      docOptions.include_docs = true;
+      var docParams = app.getParams();
+      docParams.include_docs = true;
 
       this.databaseName = options[0];
 
@@ -186,23 +186,32 @@ function(app, FauxtonAPI, Documents, Databases) {
       return this.data.designDocs.fetch();
     },
 
+    createParams: function (options) {
+      var urlParams = app.getParams(options);
+      return {
+        urlParams: urlParams,
+        docParams: _.extend(_.clone(urlParams), {limit: 20})
+      };
+    },
+
+    /*
+    * docParams are the options collection uses to fetch from the server 
+    * urlParams are what are shown in the url and to the user
+    * They are not the same when paginating
+    */
     allDocs: function(databaseName, options) {
-      var docOptions = app.getParams(options),
-          docLimit;
+      var params = this.createParams(options),
+          urlParams = params.urlParams,
+          docParams = params.docParams;
 
       if (this.eventAllDocs) {
         this.eventAllDocs = false;
         return;
       }
 
-      if (docOptions.limit) {
-        docLimit = docOptions.limit;
-      }
-
-      docOptions.limit = 20; //default per page
-      this.data.database.buildAllDocs(docOptions);
+      this.data.database.buildAllDocs(docParams);
 
-      if (docOptions.startkey && docOptions.startkey.indexOf('_design') > -1) {
+      if (docParams.startkey && docParams.startkey.indexOf('_design') > -1) {
         this.sidebar.setSelectedTab('design-docs');
       } else {
         this.sidebar.setSelectedTab('all-docs');
@@ -218,12 +227,14 @@ function(app, FauxtonAPI, Documents, Databases) {
       this.setView("#dashboard-upper-content", new Documents.Views.AllDocsLayout({
         database: this.data.database,
         collection: this.data.database.allDocs,
-        params: docOptions
+        params: urlParams,
+        docParams: docParams
       }));
 
       this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
         collection: this.data.database.allDocs,
-        docLimit: parseInt(docLimit, 10)
+        docParams: docParams,
+        params: urlParams
       }));
 
       this.crumbs = [
@@ -313,43 +324,34 @@ function(app, FauxtonAPI, Documents, Databases) {
 
     updateAllDocsFromView: function (event) {
       var view = event.view,
-          docOptions = app.getParams(),
+          params = this.createParams(),
+          urlParams = params.urlParams,
+          docParams = params.docParams,
           ddoc = event.ddoc,
-          docLimit;
+          collection;
 
-      if (docOptions.limit) {
-        docLimit = docOptions.limit;
-      }
+      docParams.limit = this.documentsView.perPage();
 
-      docOptions.limit = this.documentsView.perPage();
-      this.documentsView && this.documentsView.remove();
+      this.documentsView.forceRender();
 
       if (event.allDocs) {
         this.eventAllDocs = true; // this is horrible. But I cannot get the trigger not to fire the route!
-        this.data.database.buildAllDocs(docOptions);
-        this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
-          collection: this.data.database.allDocs,
-          docLimit: parseInt(docLimit, 10)
-        }));
-        return;
-      }
+        this.data.database.buildAllDocs(docParams);
+        collection = this.data.database.allDocs;
 
-      this.data.indexedDocs = new Documents.IndexCollection(null, {
-        database: this.data.database,
-        design: ddoc,
-        view: view,
-        params: app.getParams()
-      });
+      } else {
+        collection = this.data.indexedDocs = new Documents.IndexCollection(null, {
+          database: this.data.database,
+          design: ddoc,
+          view: view,
+          params: docParams
+        });
 
-      this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
-        database: this.data.database,
-        collection: this.data.indexedDocs,
-        nestedView: Documents.Views.Row,
-        viewList: true,
-        docLimit: parseInt(docLimit, 10)
-      }));
+        this.apiUrl = [this.data.indexedDocs.url("apiurl"), "docs"];
+      }
 
-      this.apiUrl = [this.data.indexedDocs.url("apiurl"), "docs"];
+      this.documentsView.setCollection(collection);
+      this.documentsView.setParams(docParams, urlParams);
     },
 
     updateAllDocsFromPreview: function (event) {
@@ -373,24 +375,23 @@ function(app, FauxtonAPI, Documents, Databases) {
     },
 
     perPageChange: function (perPage) {
+      this.perPage = perPage;
       this.documentsView.collection.updateLimit(perPage);
       this.documentsView.forceRender();
     },
 
     paginate: function (options) {
+      var params = options.params,
+          urlParams = app.getParams(),
+          collection = this.documentsView.collection;
+
       this.documentsView.forceRender();
 
       if (options.direction === 'next') {
-        this.documentsView.collection.skipFirstItem = true;
-        this.documentsView.collection.nextPage(options.perPage);
-      } else {
-        if (options.params && options.params.startkey) {
-          this.documentsView.collection.skipFirstItem = true;
-        } else {
-          this.documentsView.collection.skipFirstItem = false;
-        }
-        this.documentsView.collection.previousPage(options.perPage, options.params);
+        params = Documents.paginate.next(collection.map(function (item) { return item.toJSON(); }), collection.params, options.perPage, !!collection.isAllDocs);
       }
+      
+      collection.updateParams(params);
     },
 
     reloadDesignDocs: function (event) {
@@ -421,9 +422,9 @@ function(app, FauxtonAPI, Documents, Databases) {
       this.databaseName = options[0];
       this.database = new Databases.Model({id: this.databaseName});
 
-      var docOptions = app.getParams();
+      var docParams = app.getParams();
 
-      this.database.buildChanges(docOptions);
+      this.database.buildChanges(docParams);
 
       this.setView("#tabs", new Documents.Views.Tabs({
         collection: this.designDocs,

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/documents/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/documents/tests/resourcesSpec.js b/src/fauxton/app/addons/documents/tests/resourcesSpec.js
index 380a4e4..62506e6 100644
--- a/src/fauxton/app/addons/documents/tests/resourcesSpec.js
+++ b/src/fauxton/app/addons/documents/tests/resourcesSpec.js
@@ -32,20 +32,6 @@ define([
 
     });
 
-    it('Should return urlNext', function () {
-      var url = collection.urlNextPage(20);
-
-      assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=21&reduce=false&startkey_docid=myId2&startkey=');
-
-    });
-
-    it('Should return urlPrevious', function () {
-      var url = collection.urlPreviousPage(20, {limit: 21, reduce: false,  startkey_docid: "myId1",startkey:"myId1"} );
-
-      assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=20&reduce=false&startkey_docid=myId1&startkey=myId1');
-
-    });
-
   });
 
   describe('AllDocs', function () {
@@ -65,19 +51,6 @@ define([
 
     });
 
-    it('Should return urlNext', function () {
-      var url = collection.urlNextPage(20);
-
-      assert.equal(url, 'database/databaseId/_all_docs?limit=21&startkey_docid=%22myId2%22&startkey=%22myId2%22');
-
-    });
-
-     it('Should return urlPrevious', function () {
-      var url = collection.urlPreviousPage(20, {limit: 21, startkey_docid: "myId1",startkey:"myId1"} );
-      assert.equal(url, 'database/databaseId/_all_docs?limit=20&startkey_docid=myId1&startkey=myId1');
-    });
-
-
   });
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/documents/views.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/documents/views.js b/src/fauxton/app/addons/documents/views.js
index c5ad1bb..dea0bc7 100644
--- a/src/fauxton/app/addons/documents/views.js
+++ b/src/fauxton/app/addons/documents/views.js
@@ -415,8 +415,9 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
     initialize: function (options) {
       this.newView = options.newView || false;
       this.pagination = options.pagination;
+      _.bindAll(this);
       
-      this._perPage = 20;
+      this._perPage = options.perPageDefault || 20;
 
       this.listenTo(this.collection, 'totalRows:decrement', this.render);
     },
@@ -466,6 +467,10 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
 
     perPage: function () {
       return this._perPage;
+    },
+
+    setCollection: function (collection) {
+      this.collection = collection;
     }
 
   });
@@ -484,20 +489,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
 
     toggleQuery: function (event) {
       $('#dashboard-content').scrollTop(0);
-      console.log('hi');
       this.$('#query').toggle('slow');
     },
 
     beforeRender: function () {
-      this.eventer = _.extend({}, Backbone.Events);
-
       this.advancedOptions = this.insertView('#query', new Views.AdvancedOptions({
         updateViewFn: this.updateAllDocs,
         previewFn: this.previewView,
         hasReduce: false,
         showPreview: false,
         database: this.database,
-        eventer: this.eventer
       }));
     },
 
@@ -505,7 +506,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       if (this.params) {
         this.advancedOptions.updateFromParams(this.params);
       }
-
     },
 
     updateAllDocs: function (event, paramInfo) {
@@ -574,13 +574,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       this.rows = {};
       this.viewList = !! options.viewList;
       this.database = options.database;
+
       if (options.ddocInfo) {
         this.designDocs = options.ddocInfo.designDocs;
         this.ddocID = options.ddocInfo.id;
       }
       this.newView = options.newView || false;
-      this.docLimit = options.docLimit;
+      this.docParams = options.docParams;
+      this.params = options.params || {};
       this.expandDocs = true;
+      this.perPageDefault = options.perPageDefault || 20;
     },
 
     establish: function() {
@@ -666,13 +669,13 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       this.pagination = new Components.IndexPagination({
         collection: this.collection,
         scrollToSelector: '#dashboard-content',
-        docLimit: this.docLimit
+        docLimit: this.params.limit
       });
     },
 
     cleanup: function () {
-      this.pagination.remove();
-      this.allDocsNumber.remove();
+      this.pagination && this.pagination.remove();
+      this.allDocsNumber && this.allDocsNumber.remove();
       _.each(this.rows, function (row) {row.remove();});
     },
 
@@ -682,13 +685,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
         this.addPagination();
       }
 
-      this.insertView('#documents-pagination', this.pagination);
+      if (!this.params.keys) { //cannot paginate with keys
+        this.insertView('#documents-pagination', this.pagination);
+      }
 
       if (!this.allDocsNumber) {
         this.allDocsNumber = new Views.AllDocsNumber({
           collection: this.collection,
           newView: this.newView,
-          pagination: this.pagination
+          pagination: this.pagination,
+          perPageDefault: this.perPageDefault
         });
       }
 
@@ -703,6 +709,21 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       }, this);
     },
 
+    setCollection: function (collection) {
+      this.collection = collection;
+      this.pagination.setCollection(collection);
+      this.allDocsNumber.setCollection(collection);
+    },
+
+    setParams: function (docParams, urlParams) {
+      this.docParams = docParams;
+      this.params = urlParams;
+
+      if (this.params.limit) {
+        this.pagination.docLimit = this.params.limit;
+      }
+    },
+
     afterRender: function(){
       prettyPrint();
     },
@@ -1007,73 +1028,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
     }
   });
 
-  Views.AdvancedOptionsMenu = FauxtonAPI.View.extend({
-    template: 'addons/documents/templates/advanced_options_menu',
-    tagName: "div",
-    className: "controls-group advanced-options-menu",
-    events: {
-      "click input": "updateRows",
-      'change #group-level': 'updateRows',
-      'click #query-nav': 'toggleMenu'
-    },
-
-    initialize: function (options) {
-      this.hasReduce = options.hasReduce;
-      this.eventer = options.eventer;
-    },
-
-    toggleMenu: function (event) {
-      this.$('.checkbox').toggle();
-    },
-
-    updateRows: function (event) {
-      var $groupLevel = this.$('#group-level-label'),
-          params = {
-            include_docs: false,
-            reduce: false,
-            group_level: 0
-          };
-
-      if (this.$('#include-docs-views').prop('checked')) {
-        params.include_docs = true;
-      }
-
-      if (this.hasReduce && this.$('#reduce').prop('checked')) {
-        params.reduce = true;
-        params.group_level = this.$('#group-level option:selected').val();
-        $groupLevel.show();
-      } else {
-        $groupLevel.hide();
-      }
-      this.eventer.trigger('options:param_update', params);
-    },
-
-    updateFromParams: function (params) {
-      if (params.reduce) {
-        var $reduce = this.$('#reduce');
-        $reduce.prop("checked", true);
-        this.$('#group-level-label').show();
-        this.$('option[value="' + params.group_level + '"]').prop('selected', true);
-
-      } else if (params.include_docs) {
-        var $include_docs = this.$('#include-docs-views');
-        $include_docs.prop("checked", true);
-      }
-    },
-
-    serialize: function () {
-      return {
-        hasReduce: this.hasReduce
-      };
-    },
-
-    setHasReduce: function (hasReduce) {
-      this.hasReduce = hasReduce;
-    }
-
-  });
-
-
   Views.AdvancedOptions = FauxtonAPI.View.extend({
     template: "addons/documents/templates/advanced_options",
     className: "advanced-options well",
@@ -1084,7 +1038,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       this.viewName = options.viewName;
       this.updateViewFn = options.updateViewFn;
       this.previewFn = options.previewFn;
-      this.eventer = options.eventer;
 
       if (typeof(options.hasReduce) === 'undefined') {
         this.hasReduce = true;
@@ -1097,8 +1050,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       } else {
         this.showPreview = options.showPreview;
       }
-
-      this.eventer && this.listenTo(this.eventer, 'options:param_update', this.optionsParamsUpdate);
     },
 
     events: {
@@ -1108,24 +1059,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
       "click button.preview": "previewView"
     },
 
-    optionsParamsUpdate: function (params) {
-       var $form = this.$el.find("form.view-query-update");
-
-       if (!params.group_level) {
-        this.$("select[name='group_level']").attr('disabled', 'disabled');
-       }
-
-       if (params.reduce && params.group_level) {
-        $form.find("select[name='group_level']").val(params.group_level).removeAttr('disabled');
-        delete params.group_level;
-       } 
-
-      _.each(params, function(val, key) {
-        $form.find("input[name='"+key+"']").prop('checked', val);
-      });
-      this.$('form.view-query-update').submit();
-    },
-
     beforeRender: function () {
       if (this.viewName && this.ddocName) {
         var buttonViews = FauxtonAPI.getExtensions('advancedOptions:ViewButton');

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/fauxton/base.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/fauxton/base.js b/src/fauxton/app/addons/fauxton/base.js
index 35babb5..a6e462a 100644
--- a/src/fauxton/app/addons/fauxton/base.js
+++ b/src/fauxton/app/addons/fauxton/base.js
@@ -45,7 +45,6 @@ function(app, FauxtonAPI, resizeColumns) {
     }
   });
 
-
   Fauxton.initialize = function () {
     app.footer = new Fauxton.Footer({el: "#footer-content"}),
     app.navBar = new Fauxton.NavBar();
@@ -93,7 +92,7 @@ function(app, FauxtonAPI, resizeColumns) {
     });
   };
 
-  Fauxton.Breadcrumbs = Backbone.View.extend({
+  Fauxton.Breadcrumbs = FauxtonAPI.View.extend({
     template: "templates/fauxton/breadcrumbs",
 
     serialize: function() {
@@ -114,10 +113,7 @@ function(app, FauxtonAPI, resizeColumns) {
     }
   });
 
-  // TODO: this View should extend from FauxtonApi.View.
-  // Chicken and egg problem, api.js extends fauxton/base.js.
-  // Need to sort the loading order.
-  Fauxton.Footer = Backbone.View.extend({
+  Fauxton.Footer = FauxtonAPI.View.extend({
     template: "templates/fauxton/footer",
 
     initialize: function() {
@@ -135,7 +131,7 @@ function(app, FauxtonAPI, resizeColumns) {
     }
   });
 
-  Fauxton.NavBar = Backbone.View.extend({
+  Fauxton.NavBar = FauxtonAPI.View.extend({
     className:"navbar",
     template: "templates/fauxton/nav_bar",
     // TODO: can we generate this list from the router?
@@ -260,7 +256,7 @@ function(app, FauxtonAPI, resizeColumns) {
     // TODO: ADD ACTIVE CLASS
   });
 
-  Fauxton.ApiBar = Backbone.View.extend({
+  Fauxton.ApiBar = FauxtonAPI.View.extend({
     template: "templates/fauxton/api_bar",
     endpoint: '_all_docs',
 
@@ -304,7 +300,7 @@ function(app, FauxtonAPI, resizeColumns) {
 
   });
 
-  Fauxton.Notification = Backbone.View.extend({
+  Fauxton.Notification = FauxtonAPI.View.extend({
     fadeTimer: 5000,
 
     initialize: function(options) {

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/fauxton/components.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/fauxton/components.js b/src/fauxton/app/addons/fauxton/components.js
index f8032fe..a9f4ef4 100644
--- a/src/fauxton/app/addons/fauxton/components.js
+++ b/src/fauxton/app/addons/fauxton/components.js
@@ -71,14 +71,21 @@ function(app, FauxtonAPI, ace, spin) {
       this.nextUrlfn = options.nextUrlfn;
       this.scrollToSelector = options.scrollToSelector;
       _.bindAll(this);
+      this.docLimit = options.docLimit || 1000000;
+      this.perPage = 20;
+      this.setDefaults();
+    },
+
+    setDefaults: function () {
       this._pageNumber = [];
       this._pageStart = 1;
-      this.perPage = 20;
-      this.docLimit = options.docLimit || 1000000;
       this.paramsHistory = [];
+      this.enabled = true;
     },
 
     canShowPreviousfn: function () {
+      if (!this.enabled) { return this.enabled; }
+
       if (this._pageStart === 1) {
         return false;
       }
@@ -86,6 +93,8 @@ function(app, FauxtonAPI, ace, spin) {
     },
 
     canShowNextfn: function () {
+      if (!this.enabled) { return this.enabled; }
+
       if (this.collection.length < (this.perPage -1)) {
         return false;
       }
@@ -94,6 +103,10 @@ function(app, FauxtonAPI, ace, spin) {
         return false;
       }
 
+      if (this.collection.viewMeta && this.collection.viewMeta.total_rows <= this.pageStart() + this.perPage) {
+        return false;
+      }
+
       return true;
     },
 
@@ -103,11 +116,12 @@ function(app, FauxtonAPI, ace, spin) {
       if (!this.canShowPreviousfn()) { return; }
 
       this.decPageNumber();
+      var params = this.paramsHistory.pop();
 
       FauxtonAPI.triggerRouteEvent('paginate', {
        direction: 'previous',
        perPage: this.perPage,
-       params: this.paramsHistory.pop()
+       params: params
       });
     },
 
@@ -179,12 +193,22 @@ function(app, FauxtonAPI, ace, spin) {
     },
 
     pageEnd: function () {
-      if (this.collection.length < this.perPage) {
-        return this.page() + this.collection.length;
-      }
+      return this.page() + this.collection.length;
+    },
 
-      return this.page() + this.perPage;
+    disable: function () {
+      this.enabled = false;
+    },
+
+    enable: function () {
+      this.enabled = true;
+    },
+
+    setCollection: function (collection) {
+      this.collection = collection;
+      this.setDefaults();
     }
+
   });
 
   //TODO allow more of the typeahead options.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/app/addons/fauxton/tests/paginateSpec.js
----------------------------------------------------------------------
diff --git a/src/fauxton/app/addons/fauxton/tests/paginateSpec.js b/src/fauxton/app/addons/fauxton/tests/paginateSpec.js
index 535e26f..8fc409a 100644
--- a/src/fauxton/app/addons/fauxton/tests/paginateSpec.js
+++ b/src/fauxton/app/addons/fauxton/tests/paginateSpec.js
@@ -58,15 +58,6 @@ define([
         //FauxtonAPI.navigate.restore && FauxtonAPI.navigate.restore(); 
       });
 
-      it('Should navigate', function () {
-        var navigateMock = sinon.spy(FauxtonAPI, 'navigate');
-
-        paginate.$('a#next').click();
-
-        assert.ok(navigateMock.calledOnce);
-        FauxtonAPI.navigate.restore();
-      });
-
       it('Should trigger routeEvent', function () {
         var navigateMock = sinon.spy(FauxtonAPI, 'triggerRouteEvent');
 
@@ -81,15 +72,6 @@ define([
 
     describe('#previous', function () {
 
-      it('Should navigate', function () {
-        var navigateMock = sinon.spy(FauxtonAPI, 'navigate');
-
-        paginate.$('a#previous').click();
-
-        assert.ok(navigateMock.calledOnce);
-        FauxtonAPI.navigate.restore();
-      });
-
       it('Should trigger routeEvent', function () {
         var navigateMock = sinon.spy(FauxtonAPI, 'triggerRouteEvent');
 

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/test/mocha/chai.js
----------------------------------------------------------------------
diff --git a/src/fauxton/test/mocha/chai.js b/src/fauxton/test/mocha/chai.js
index 2a67f98..9dd7b0a 100644
--- a/src/fauxton/test/mocha/chai.js
+++ b/src/fauxton/test/mocha/chai.js
@@ -27,10 +27,14 @@ function require(path, parent, orig) {
   // perform real require()
   // by invoking the module's
   // registered function
-  if (!module.exports) {
-    module.exports = {};
-    module.client = module.component = true;
-    module.call(this, module.exports, require.relative(resolved), module);
+  if (!module._resolving && !module.exports) {
+    var mod = {};
+    mod.exports = {};
+    mod.client = mod.component = true;
+    module._resolving = true;
+    module.call(this, mod.exports, require.relative(resolved), mod);
+    delete module._resolving;
+    module.exports = mod.exports;
   }
 
   return module.exports;
@@ -309,6 +313,411 @@ AssertionError.prototype.toJSON = function (stack) {
 };
 
 });
+require.register("chaijs-type-detect/lib/type.js", function(exports, require, module){
+/*!
+ * type-detect
+ * Copyright(c) 2013 jake luer <ja...@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Primary Exports
+ */
+
+var exports = module.exports = getType;
+
+/*!
+ * Detectable javascript natives
+ */
+
+var natives = {
+    '[object Array]': 'array'
+  , '[object RegExp]': 'regexp'
+  , '[object Function]': 'function'
+  , '[object Arguments]': 'arguments'
+  , '[object Date]': 'date'
+};
+
+/**
+ * ### typeOf (obj)
+ *
+ * Use several different techniques to determine
+ * the type of object being tested.
+ *
+ *
+ * @param {Mixed} object
+ * @return {String} object type
+ * @api public
+ */
+
+function getType (obj) {
+  var str = Object.prototype.toString.call(obj);
+  if (natives[str]) return natives[str];
+  if (obj === null) return 'null';
+  if (obj === undefined) return 'undefined';
+  if (obj === Object(obj)) return 'object';
+  return typeof obj;
+}
+
+exports.Library = Library;
+
+/**
+ * ### Library
+ *
+ * Create a repository for custom type detection.
+ *
+ * ```js
+ * var lib = new type.Library;
+ * ```
+ *
+ */
+
+function Library () {
+  this.tests = {};
+}
+
+/**
+ * #### .of (obj)
+ *
+ * Expose replacement `typeof` detection to the library.
+ *
+ * ```js
+ * if ('string' === lib.of('hello world')) {
+ *   // ...
+ * }
+ * ```
+ *
+ * @param {Mixed} object to test
+ * @return {String} type
+ */
+
+Library.prototype.of = getType;
+
+/**
+ * #### .define (type, test)
+ *
+ * Add a test to for the `.test()` assertion.
+ *
+ * Can be defined as a regular expression:
+ *
+ * ```js
+ * lib.define('int', /^[0-9]+$/);
+ * ```
+ *
+ * ... or as a function:
+ *
+ * ```js
+ * lib.define('bln', function (obj) {
+ *   if ('boolean' === lib.of(obj)) return true;
+ *   var blns = [ 'yes', 'no', 'true', 'false', 1, 0 ];
+ *   if ('string' === lib.of(obj)) obj = obj.toLowerCase();
+ *   return !! ~blns.indexOf(obj);
+ * });
+ * ```
+ *
+ * @param {String} type
+ * @param {RegExp|Function} test
+ * @api public
+ */
+
+Library.prototype.define = function (type, test) {
+  if (arguments.length === 1) return this.tests[type];
+  this.tests[type] = test;
+  return this;
+};
+
+/**
+ * #### .test (obj, test)
+ *
+ * Assert that an object is of type. Will first
+ * check natives, and if that does not pass it will
+ * use the user defined custom tests.
+ *
+ * ```js
+ * assert(lib.test('1', 'int'));
+ * assert(lib.test('yes', 'bln'));
+ * ```
+ *
+ * @param {Mixed} object
+ * @param {String} type
+ * @return {Boolean} result
+ * @api public
+ */
+
+Library.prototype.test = function (obj, type) {
+  if (type === getType(obj)) return true;
+  var test = this.tests[type];
+
+  if (test && 'regexp' === getType(test)) {
+    return test.test(obj);
+  } else if (test && 'function' === getType(test)) {
+    return test(obj);
+  } else {
+    throw new ReferenceError('Type test "' + type + '" not defined or invalid.');
+  }
+};
+
+});
+require.register("chaijs-deep-eql/lib/eql.js", function(exports, require, module){
+/*!
+ * deep-eql
+ * Copyright(c) 2013 Jake Luer <ja...@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var type = require('type-detect');
+
+/*!
+ * Buffer.isBuffer browser shim
+ */
+
+var Buffer;
+try { Buffer = require('buffer').Buffer; }
+catch(ex) {
+  Buffer = {};
+  Buffer.isBuffer = function() { return false; }
+}
+
+/*!
+ * Primary Export
+ */
+
+module.exports = deepEqual;
+
+/**
+ * Assert super-strict (egal) equality between
+ * two objects of any type.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @param {Array} memoised (optional)
+ * @return {Boolean} equal match
+ */
+
+function deepEqual(a, b, m) {
+  if (sameValue(a, b)) {
+    return true;
+  } else if ('date' === type(a)) {
+    return dateEqual(a, b);
+  } else if ('regexp' === type(a)) {
+    return regexpEqual(a, b);
+  } else if (Buffer.isBuffer(a)) {
+    return bufferEqual(a, b);
+  } else if ('arguments' === type(a)) {
+    return argumentsEqual(a, b, m);
+  } else if (!typeEqual(a, b)) {
+    return false;
+  } else if (('object' !== type(a) && 'object' !== type(b))
+  && ('array' !== type(a) && 'array' !== type(b))) {
+    return sameValue(a, b);
+  } else {
+    return objectEqual(a, b, m);
+  }
+}
+
+/*!
+ * Strict (egal) equality test. Ensures that NaN always
+ * equals NaN and `-0` does not equal `+0`.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} equal match
+ */
+
+function sameValue(a, b) {
+  if (a === b) return a !== 0 || 1 / a === 1 / b;
+  return a !== a && b !== b;
+}
+
+/*!
+ * Compare the types of two given objects and
+ * return if they are equal. Note that an Array
+ * has a type of `array` (not `object`) and arguments
+ * have a type of `arguments` (not `array`/`object`).
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function typeEqual(a, b) {
+  return type(a) === type(b);
+}
+
+/*!
+ * Compare two Date objects by asserting that
+ * the time values are equal using `saveValue`.
+ *
+ * @param {Date} a
+ * @param {Date} b
+ * @return {Boolean} result
+ */
+
+function dateEqual(a, b) {
+  if ('date' !== type(b)) return false;
+  return sameValue(a.getTime(), b.getTime());
+}
+
+/*!
+ * Compare two regular expressions by converting them
+ * to string and checking for `sameValue`.
+ *
+ * @param {RegExp} a
+ * @param {RegExp} b
+ * @return {Boolean} result
+ */
+
+function regexpEqual(a, b) {
+  if ('regexp' !== type(b)) return false;
+  return sameValue(a.toString(), b.toString());
+}
+
+/*!
+ * Assert deep equality of two `arguments` objects.
+ * Unfortunately, these must be sliced to arrays
+ * prior to test to ensure no bad behavior.
+ *
+ * @param {Arguments} a
+ * @param {Arguments} b
+ * @param {Array} memoize (optional)
+ * @return {Boolean} result
+ */
+
+function argumentsEqual(a, b, m) {
+  if ('arguments' !== type(b)) return false;
+  a = [].slice.call(a);
+  b = [].slice.call(b);
+  return deepEqual(a, b, m);
+}
+
+/*!
+ * Get enumerable properties of a given object.
+ *
+ * @param {Object} a
+ * @return {Array} property names
+ */
+
+function enumerable(a) {
+  var res = [];
+  for (var key in a) res.push(key);
+  return res;
+}
+
+/*!
+ * Simple equality for flat iterable objects
+ * such as Arrays or Node.js buffers.
+ *
+ * @param {Iterable} a
+ * @param {Iterable} b
+ * @return {Boolean} result
+ */
+
+function iterableEqual(a, b) {
+  if (a.length !==  b.length) return false;
+
+  var i = 0;
+  var match = true;
+
+  for (; i < a.length; i++) {
+    if (a[i] !== b[i]) {
+      match = false;
+      break;
+    }
+  }
+
+  return match;
+}
+
+/*!
+ * Extension to `iterableEqual` specifically
+ * for Node.js Buffers.
+ *
+ * @param {Buffer} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function bufferEqual(a, b) {
+  if (!Buffer.isBuffer(b)) return false;
+  return iterableEqual(a, b);
+}
+
+/*!
+ * Block for `objectEqual` ensuring non-existing
+ * values don't get in.
+ *
+ * @param {Mixed} object
+ * @return {Boolean} result
+ */
+
+function isValue(a) {
+  return a !== null && a !== undefined;
+}
+
+/*!
+ * Recursively check the equality of two objects.
+ * Once basic sameness has been established it will
+ * defer to `deepEqual` for each enumerable key
+ * in the object.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function objectEqual(a, b, m) {
+  if (!isValue(a) || !isValue(b)) {
+    return false;
+  }
+
+  if (a.prototype !== b.prototype) {
+    return false;
+  }
+
+  var i;
+  if (m) {
+    for (i = 0; i < m.length; i++) {
+      if ((m[i][0] === a && m[i][1] === b)
+      ||  (m[i][0] === b && m[i][1] === a)) {
+        return true;
+      }
+    }
+  } else {
+    m = [];
+  }
+
+  try {
+    var ka = enumerable(a);
+    var kb = enumerable(b);
+  } catch (ex) {
+    return false;
+  }
+
+  ka.sort();
+  kb.sort();
+
+  if (!iterableEqual(ka, kb)) {
+    return false;
+  }
+
+  m.push([ a, b ]);
+
+  var key;
+  for (i = ka.length - 1; i >= 0; i--) {
+    key = ka[i];
+    if (!deepEqual(a[key], b[key], m)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+});
 require.register("chai/index.js", function(exports, require, module){
 module.exports = require('./lib/chai');
 
@@ -316,7 +725,7 @@ module.exports = require('./lib/chai');
 require.register("chai/lib/chai.js", function(exports, require, module){
 /*!
  * chai
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -327,7 +736,7 @@ var used = []
  * Chai version
  */
 
-exports.version = '1.7.2';
+exports.version = '1.8.1';
 
 /*!
  * Assertion Error
@@ -400,7 +809,7 @@ require.register("chai/lib/chai/assertion.js", function(exports, require, module
 /*!
  * chai
  * http://chaijs.com
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -480,6 +889,10 @@ module.exports = function (_chai, util) {
     util.overwriteMethod(this.prototype, name, fn);
   };
 
+  Assertion.overwriteChainableMethod = function (name, fn, chainingBehavior) {
+    util.overwriteChainableMethod(this.prototype, name, fn, chainingBehavior);
+  };
+
   /*!
    * ### .assert(expression, message, negateMessage, expected, actual)
    *
@@ -533,7 +946,7 @@ require.register("chai/lib/chai/core/assertions.js", function(exports, require,
 /*!
  * chai
  * http://chaijs.com
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -545,7 +958,7 @@ module.exports = function (chai, _) {
   /**
    * ### Language Chains
    *
-   * The following are provide as chainable getters to
+   * The following are provided as chainable getters to
    * improve the readability of your assertions. They
    * do not provide an testing capability unless they
    * have been overwritten by a plugin.
@@ -558,6 +971,7 @@ module.exports = function (chai, _) {
    * - is
    * - that
    * - and
+   * - has
    * - have
    * - with
    * - at
@@ -569,7 +983,7 @@ module.exports = function (chai, _) {
    */
 
   [ 'to', 'be', 'been'
-  , 'is', 'and', 'have'
+  , 'is', 'and', 'has', 'have'
   , 'with', 'that', 'at'
   , 'of', 'same' ].forEach(function (chain) {
     Assertion.addProperty(chain, function () {
@@ -677,9 +1091,21 @@ module.exports = function (chai, _) {
 
   function include (val, msg) {
     if (msg) flag(this, 'message', msg);
-    var obj = flag(this, 'object')
+    var obj = flag(this, 'object');
+
+    if (_.type(val) === 'object') {
+      if (!flag(this, 'negate')) {
+        for (var k in val) new Assertion(obj).property(k, val[k]);
+        return;
+      }
+      var subset = {}
+      for (var k in val) subset[k] = obj[k]
+      var expected = _.eql(subset, val);
+    } else {
+      var expected = obj && ~obj.indexOf(val)
+    }
     this.assert(
-        ~obj.indexOf(val)
+        expected
       , 'expected #{this} to include ' + _.inspect(val)
       , 'expected #{this} to not include ' + _.inspect(val));
   }
@@ -776,8 +1202,8 @@ module.exports = function (chai, _) {
    *
    * Asserts that the target is `undefined`.
    *
-   *      expect(undefined).to.be.undefined;
-   *      expect(null).to.not.be.undefined;
+   *     expect(undefined).to.be.undefined;
+   *     expect(null).to.not.be.undefined;
    *
    * @name undefined
    * @api public
@@ -1534,6 +1960,7 @@ module.exports = function (chai, _) {
    * @param {String|RegExp} expected error message
    * @param {String} message _optional_
    * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+   * @returns error for chaining (null if no error)
    * @api public
    */
 
@@ -1558,7 +1985,10 @@ module.exports = function (chai, _) {
       constructor = null;
       errMsg = null;
     } else if (typeof constructor === 'function') {
-      name = (new constructor()).name;
+      name = constructor.prototype.name || constructor.name;
+      if (name === 'Error' && constructor !== Error) {
+        name = (new constructor()).name;
+      }
     } else {
       constructor = null;
     }
@@ -1572,12 +2002,14 @@ module.exports = function (chai, _) {
             err === desiredError
           , 'expected #{this} to throw #{exp} but #{act} was thrown'
           , 'expected #{this} to not throw #{exp}'
-          , desiredError
-          , err
+          , (desiredError instanceof Error ? desiredError.toString() : desiredError)
+          , (err instanceof Error ? err.toString() : err)
         );
 
+        flag(this, 'object', err);
         return this;
       }
+
       // next, check constructor
       if (constructor) {
         this.assert(
@@ -1585,11 +2017,15 @@ module.exports = function (chai, _) {
           , 'expected #{this} to throw #{exp} but #{act} was thrown'
           , 'expected #{this} to not throw #{exp} but #{act} was thrown'
           , name
-          , err
+          , (err instanceof Error ? err.toString() : err)
         );
 
-        if (!errMsg) return this;
+        if (!errMsg) {
+          flag(this, 'object', err);
+          return this;
+        }
       }
+
       // next, check message
       var message = 'object' === _.type(err) && "message" in err
         ? err.message
@@ -1604,6 +2040,7 @@ module.exports = function (chai, _) {
           , message
         );
 
+        flag(this, 'object', err);
         return this;
       } else if ((message != null) && errMsg && 'string' === typeof errMsg) {
         this.assert(
@@ -1614,6 +2051,7 @@ module.exports = function (chai, _) {
           , message
         );
 
+        flag(this, 'object', err);
         return this;
       } else {
         thrown = true;
@@ -1636,9 +2074,11 @@ module.exports = function (chai, _) {
         thrown === true
       , 'expected #{this} to throw ' + expectedThrown + actuallyGot
       , 'expected #{this} to not throw ' + expectedThrown + actuallyGot
-      , desiredError
-      , thrownError
+      , (desiredError instanceof Error ? desiredError.toString() : desiredError)
+      , (thrownError instanceof Error ? thrownError.toString() : thrownError)
     );
+
+    flag(this, 'object', thrownError);
   };
 
   Assertion.addMethod('throw', assertThrows);
@@ -1657,8 +2097,8 @@ module.exports = function (chai, _) {
    * To check if a constructor will respond to a static function,
    * set the `itself` flag.
    *
-   *    Klass.baz = function(){};
-   *    expect(Klass).itself.to.respondTo('baz');
+   *     Klass.baz = function(){};
+   *     expect(Klass).itself.to.respondTo('baz');
    *
    * @name respondTo
    * @param {String} method
@@ -1686,12 +2126,12 @@ module.exports = function (chai, _) {
    *
    * Sets the `itself` flag, later used by the `respondTo` assertion.
    *
-   *    function Foo() {}
-   *    Foo.bar = function() {}
-   *    Foo.prototype.baz = function() {}
+   *     function Foo() {}
+   *     Foo.bar = function() {}
+   *     Foo.prototype.baz = function() {}
    *
-   *    expect(Foo).itself.to.respondTo('bar');
-   *    expect(Foo).itself.not.to.respondTo('baz');
+   *     expect(Foo).itself.to.respondTo('bar');
+   *     expect(Foo).itself.not.to.respondTo('baz');
    *
    * @name itself
    * @api public
@@ -1805,7 +2245,7 @@ module.exports = function (chai, _) {
 require.register("chai/lib/chai/interface/assert.js", function(exports, require, module){
 /*!
  * chai
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -1860,13 +2300,12 @@ module.exports = function (chai, util) {
    */
 
   assert.fail = function (actual, expected, message, operator) {
-    throw new chai.AssertionError({
+    message = message || 'assert.fail()';
+    throw new chai.AssertionError(message, {
         actual: actual
       , expected: expected
-      , message: message
       , operator: operator
-      , stackStartFunction: assert.fail
-    });
+    }, assert.fail);
   };
 
   /**
@@ -2462,19 +2901,7 @@ module.exports = function (chai, util) {
    */
 
   assert.include = function (exp, inc, msg) {
-    var obj = new Assertion(exp, msg);
-
-    if (Array.isArray(exp)) {
-      obj.to.include(inc);
-    } else if ('string' === typeof exp) {
-      obj.to.contain.string(inc);
-    } else {
-      throw new chai.AssertionError(
-          'expected an array or string'
-        , null
-        , assert.include
-      );
-    }
+    new Assertion(exp, msg).include(inc);
   };
 
   /**
@@ -2494,19 +2921,7 @@ module.exports = function (chai, util) {
    */
 
   assert.notInclude = function (exp, inc, msg) {
-    var obj = new Assertion(exp, msg);
-
-    if (Array.isArray(exp)) {
-      obj.to.not.include(inc);
-    } else if ('string' === typeof exp) {
-      obj.to.not.contain.string(inc);
-    } else {
-      throw new chai.AssertionError(
-          'expected an array or string'
-        , null
-        , assert.notInclude
-      );
-    }
+    new Assertion(exp, msg).not.include(inc);
   };
 
   /**
@@ -2750,7 +3165,8 @@ module.exports = function (chai, util) {
       errt = null;
     }
 
-    new Assertion(fn, msg).to.Throw(errt, errs);
+    var assertErr = new Assertion(fn, msg).to.Throw(errt, errs);
+    return flag(assertErr, 'object');
   };
 
   /**
@@ -2888,7 +3304,7 @@ module.exports = function (chai, util) {
 require.register("chai/lib/chai/interface/expect.js", function(exports, require, module){
 /*!
  * chai
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -2903,7 +3319,7 @@ module.exports = function (chai, util) {
 require.register("chai/lib/chai/interface/should.js", function(exports, require, module){
 /*!
  * chai
- * Copyright(c) 2011-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2011-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -2982,7 +3398,7 @@ module.exports = function (chai, util) {
 require.register("chai/lib/chai/utils/addChainableMethod.js", function(exports, require, module){
 /*!
  * Chai - addChainingMethod utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3037,15 +3453,27 @@ var call  = Function.prototype.call,
  */
 
 module.exports = function (ctx, name, method, chainingBehavior) {
-  if (typeof chainingBehavior !== 'function')
+  if (typeof chainingBehavior !== 'function') {
     chainingBehavior = function () { };
+  }
+
+  var chainableBehavior = {
+      method: method
+    , chainingBehavior: chainingBehavior
+  };
+
+  // save the methods so we can overwrite them later, if we need to.
+  if (!ctx.__methods) {
+    ctx.__methods = {};
+  }
+  ctx.__methods[name] = chainableBehavior;
 
   Object.defineProperty(ctx, name,
     { get: function () {
-        chainingBehavior.call(this);
+        chainableBehavior.chainingBehavior.call(this);
 
         var assert = function () {
-          var result = method.apply(this, arguments);
+          var result = chainableBehavior.method.apply(this, arguments);
           return result === undefined ? this : result;
         };
 
@@ -3079,7 +3507,7 @@ module.exports = function (ctx, name, method, chainingBehavior) {
 require.register("chai/lib/chai/utils/addMethod.js", function(exports, require, module){
 /*!
  * Chai - addMethod utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3119,7 +3547,7 @@ module.exports = function (ctx, name, method) {
 require.register("chai/lib/chai/utils/addProperty.js", function(exports, require, module){
 /*!
  * Chai - addProperty utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3159,142 +3587,10 @@ module.exports = function (ctx, name, getter) {
 };
 
 });
-require.register("chai/lib/chai/utils/eql.js", function(exports, require, module){
-// This is (almost) directly from Node.js assert
-// https://github.com/joyent/node/blob/f8c335d0caf47f16d31413f89aa28eda3878e3aa/lib/assert.js
-
-module.exports = _deepEqual;
-
-var getEnumerableProperties = require('./getEnumerableProperties');
-
-// for the browser
-var Buffer;
-try {
-  Buffer = require('buffer').Buffer;
-} catch (ex) {
-  Buffer = {
-    isBuffer: function () { return false; }
-  };
-}
-
-function _deepEqual(actual, expected, memos) {
-
-  // 7.1. All identical values are equivalent, as determined by ===.
-  if (actual === expected) {
-    return true;
-
-  } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) {
-    if (actual.length != expected.length) return false;
-
-    for (var i = 0; i < actual.length; i++) {
-      if (actual[i] !== expected[i]) return false;
-    }
-
-    return true;
-
-  // 7.2. If the expected value is a Date object, the actual value is
-  // equivalent if it is also a Date object that refers to the same time.
-  } else if (expected instanceof Date) {
-    if (!(actual instanceof Date)) return false;
-    return actual.getTime() === expected.getTime();
-
-  // 7.3. Other pairs that do not both pass typeof value == 'object',
-  // equivalence is determined by ==.
-  } else if (typeof actual != 'object' && typeof expected != 'object') {
-    return actual === expected;
-
-  } else if (expected instanceof RegExp) {
-    if (!(actual instanceof RegExp)) return false;
-    return actual.toString() === expected.toString();
-
-  // 7.4. For all other Object pairs, including Array objects, equivalence is
-  // determined by having the same number of owned properties (as verified
-  // with Object.prototype.hasOwnProperty.call), the same set of keys
-  // (although not necessarily the same order), equivalent values for every
-  // corresponding key, and an identical 'prototype' property. Note: this
-  // accounts for both named and indexed properties on Arrays.
-  } else {
-    return objEquiv(actual, expected, memos);
-  }
-}
-
-function isUndefinedOrNull(value) {
-  return value === null || value === undefined;
-}
-
-function isArguments(object) {
-  return Object.prototype.toString.call(object) == '[object Arguments]';
-}
-
-function objEquiv(a, b, memos) {
-  if (isUndefinedOrNull(a) || isUndefinedOrNull(b))
-    return false;
-
-  // an identical 'prototype' property.
-  if (a.prototype !== b.prototype) return false;
-
-  // check if we have already compared a and b
-  var i;
-  if (memos) {
-    for(i = 0; i < memos.length; i++) {
-      if ((memos[i][0] === a && memos[i][1] === b) ||
-          (memos[i][0] === b && memos[i][1] === a))
-        return true;
-    }
-  } else {
-    memos = [];
-  }
-
-  //~~~I've managed to break Object.keys through screwy arguments passing.
-  //   Converting to array solves the problem.
-  if (isArguments(a)) {
-    if (!isArguments(b)) {
-      return false;
-    }
-    a = pSlice.call(a);
-    b = pSlice.call(b);
-    return _deepEqual(a, b, memos);
-  }
-  try {
-    var ka = getEnumerableProperties(a),
-        kb = getEnumerableProperties(b),
-        key;
-  } catch (e) {//happens when one is a string literal and the other isn't
-    return false;
-  }
-
-  // having the same number of owned properties (keys incorporates
-  // hasOwnProperty)
-  if (ka.length != kb.length)
-    return false;
-
-  //the same set of keys (although not necessarily the same order),
-  ka.sort();
-  kb.sort();
-  //~~~cheap key test
-  for (i = ka.length - 1; i >= 0; i--) {
-    if (ka[i] != kb[i])
-      return false;
-  }
-
-  // remember objects we have compared to guard against circular references
-  memos.push([ a, b ]);
-
-  //equivalent values for every corresponding key, and
-  //~~~possibly expensive deep test
-  for (i = ka.length - 1; i >= 0; i--) {
-    key = ka[i];
-    if (!_deepEqual(a[key], b[key], memos)) return false;
-  }
-
-  return true;
-}
-
-});
 require.register("chai/lib/chai/utils/flag.js", function(exports, require, module){
 /*!
  * Chai - flag utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3329,7 +3625,7 @@ module.exports = function (obj, key, value) {
 require.register("chai/lib/chai/utils/getActual.js", function(exports, require, module){
 /*!
  * Chai - getActual utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3351,7 +3647,7 @@ module.exports = function (obj, args) {
 require.register("chai/lib/chai/utils/getEnumerableProperties.js", function(exports, require, module){
 /*!
  * Chai - getEnumerableProperties utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3379,7 +3675,7 @@ module.exports = function getEnumerableProperties(object) {
 require.register("chai/lib/chai/utils/getMessage.js", function(exports, require, module){
 /*!
  * Chai - message composition utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3431,7 +3727,7 @@ module.exports = function (obj, args) {
 require.register("chai/lib/chai/utils/getName.js", function(exports, require, module){
 /*!
  * Chai - getName utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3454,7 +3750,7 @@ module.exports = function (func) {
 require.register("chai/lib/chai/utils/getPathValue.js", function(exports, require, module){
 /*!
  * Chai - getPathValue utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * @see https://github.com/logicalparadox/filtr
  * MIT Licensed
  */
@@ -3559,7 +3855,7 @@ function _getPathValue (parsed, obj) {
 require.register("chai/lib/chai/utils/getProperties.js", function(exports, require, module){
 /*!
  * Chai - getProperties utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -3659,7 +3955,7 @@ exports.transferFlags = require('./transferFlags');
  * Deep equal utility
  */
 
-exports.eql = require('./eql');
+exports.eql = require('deep-eql');
 
 /*!
  * Deep path value
@@ -3703,6 +3999,12 @@ exports.overwriteMethod = require('./overwriteMethod');
 
 exports.addChainableMethod = require('./addChainableMethod');
 
+/*!
+ * Overwrite chainable method
+ */
+
+exports.overwriteChainableMethod = require('./overwriteChainableMethod');
+
 
 });
 require.register("chai/lib/chai/utils/inspect.js", function(exports, require, module){
@@ -4031,7 +4333,7 @@ function objectToString(o) {
 require.register("chai/lib/chai/utils/objDisplay.js", function(exports, require, module){
 /*!
  * Chai - flag utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4082,7 +4384,7 @@ module.exports = function (obj) {
 require.register("chai/lib/chai/utils/overwriteMethod.js", function(exports, require, module){
 /*!
  * Chai - overwriteMethod utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4136,7 +4438,7 @@ module.exports = function (ctx, name, method) {
 require.register("chai/lib/chai/utils/overwriteProperty.js", function(exports, require, module){
 /*!
  * Chai - overwriteProperty utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4190,10 +4492,66 @@ module.exports = function (ctx, name, getter) {
 };
 
 });
+require.register("chai/lib/chai/utils/overwriteChainableMethod.js", function(exports, require, module){
+/*!
+ * Chai - overwriteChainableMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### overwriteChainableMethod (ctx, name, fn)
+ *
+ * Overwites an already existing chainable method
+ * and provides access to the previous function or
+ * property.  Must return functions to be used for
+ * name.
+ *
+ *     utils.overwriteChainableMethod(chai.Assertion.prototype, 'length',
+ *       function (_super) {
+ *       }
+ *     , function (_super) {
+ *       }
+ *     );
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.overwriteChainableMethod('foo', fn, fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(myFoo).to.have.length(3);
+ *     expect(myFoo).to.have.length.above(3);
+ *
+ * @param {Object} ctx object whose method / property is to be overwritten
+ * @param {String} name of method / property to overwrite
+ * @param {Function} method function that returns a function to be used for name
+ * @param {Function} chainingBehavior function that returns a function to be used for property
+ * @name overwriteChainableMethod
+ * @api public
+ */
+
+module.exports = function (ctx, name, method, chainingBehavior) {
+  var chainableBehavior = ctx.__methods[name];
+
+  var _chainingBehavior = chainableBehavior.chainingBehavior;
+  chainableBehavior.chainingBehavior = function () {
+    var result = chainingBehavior(_chainingBehavior).call(this);
+    return result === undefined ? this : result;
+  };
+
+  var _method = chainableBehavior.method;
+  chainableBehavior.method = function () {
+    var result = method(_method).apply(this, arguments);
+    return result === undefined ? this : result;
+  };
+};
+
+});
 require.register("chai/lib/chai/utils/test.js", function(exports, require, module){
 /*!
  * Chai - test utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4222,7 +4580,7 @@ module.exports = function (obj, args) {
 require.register("chai/lib/chai/utils/transferFlags.js", function(exports, require, module){
 /*!
  * Chai - transferFlags utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4269,7 +4627,7 @@ module.exports = function (assertion, object, includeAll) {
 require.register("chai/lib/chai/utils/type.js", function(exports, require, module){
 /*!
  * Chai - type utility
- * Copyright(c) 2012-2013 Jake Luer <ja...@alogicalparadox.com>
+ * Copyright(c) 2012-2014 Jake Luer <ja...@alogicalparadox.com>
  * MIT Licensed
  */
 
@@ -4314,17 +4672,25 @@ module.exports = function (obj) {
 };
 
 });
+
+
+
+
 require.alias("chaijs-assertion-error/index.js", "chai/deps/assertion-error/index.js");
 require.alias("chaijs-assertion-error/index.js", "chai/deps/assertion-error/index.js");
 require.alias("chaijs-assertion-error/index.js", "assertion-error/index.js");
 require.alias("chaijs-assertion-error/index.js", "chaijs-assertion-error/index.js");
-
-require.alias("chai/index.js", "chai/index.js");
-
-if (typeof exports == "object") {
+require.alias("chaijs-deep-eql/lib/eql.js", "chai/deps/deep-eql/lib/eql.js");
+require.alias("chaijs-deep-eql/lib/eql.js", "chai/deps/deep-eql/index.js");
+require.alias("chaijs-deep-eql/lib/eql.js", "deep-eql/index.js");
+require.alias("chaijs-type-detect/lib/type.js", "chaijs-deep-eql/deps/type-detect/lib/type.js");
+require.alias("chaijs-type-detect/lib/type.js", "chaijs-deep-eql/deps/type-detect/index.js");
+require.alias("chaijs-type-detect/lib/type.js", "chaijs-type-detect/index.js");
+require.alias("chaijs-deep-eql/lib/eql.js", "chaijs-deep-eql/index.js");
+require.alias("chai/index.js", "chai/index.js");if (typeof exports == "object") {
   module.exports = require("chai");
 } else if (typeof define == "function" && define.amd) {
-  define(function(){ return require("chai"); });
+  define([], function(){ return require("chai"); });
 } else {
   this["chai"] = require("chai");
 }})();

http://git-wip-us.apache.org/repos/asf/couchdb/blob/f796b38e/src/fauxton/test/test.config.underscore
----------------------------------------------------------------------
diff --git a/src/fauxton/test/test.config.underscore b/src/fauxton/test/test.config.underscore
index 5cebe78..95494a4 100644
--- a/src/fauxton/test/test.config.underscore
+++ b/src/fauxton/test/test.config.underscore
@@ -7,7 +7,11 @@ require.config(
 require([
         "app",
         <% _.each(testFiles, function (test) {%>
+           <% if (test[0] === '.') { %>
            '../<%= test %>',
+           <% } else { %>
+           '<%= test %>',
+           <% }  %>
         <% }) %>
 ], function() {
   if (window.mochaPhantomJS) { mochaPhantomJS.run(); }