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 2017/08/14 07:59:16 UTC

[couchdb-fauxton] branch master updated: Fix sidebar expand/collapse when design doc name has special chars (#948)

This is an automated email from the ASF dual-hosted git repository.

garren pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new fad82ab  Fix sidebar expand/collapse when design doc name has special chars (#948)
fad82ab is described below

commit fad82abcf5a31b9e4d14d27b1cb8b83602fdbc26
Author: Antonio Maranhao <30...@users.noreply.github.com>
AuthorDate: Mon Aug 14 03:59:14 2017 -0400

    Fix sidebar expand/collapse when design doc name has special chars (#948)
    
    Fix for sidebar when a design doc's name contains special characters
---
 app/addons/documents/routes-index-editor.js        |  5 +-
 .../sidebar/__tests__/sidebar.components.test.js   | 76 ++++++++++++++++++++++
 app/addons/documents/sidebar/sidebar.js            | 18 ++---
 .../tests/nightwatch/checkSidebarBehavior.js       | 29 +++++----
 app/helpers.js                                     |  4 ++
 .../custom-commands/createDocument.js              |  6 +-
 test/nightwatch_tests/custom-commands/helper.js    |  2 +-
 7 files changed, 115 insertions(+), 25 deletions(-)

diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js
index ea2fdf6..3c6fbd3 100644
--- a/app/addons/documents/routes-index-editor.js
+++ b/app/addons/documents/routes-index-editor.js
@@ -53,7 +53,6 @@ const IndexEditorAndResults = BaseRoute.extend({
     const params = this.createParams(),
           urlParams = params.urlParams,
           docParams = params.docParams,
-          decodeDdoc = decodeURIComponent(ddoc),
           store = IndexResultsStores.indexResultsStore;
 
     // if the user is simply switching the layout style (i.e. metadata, json, or table),
@@ -68,7 +67,7 @@ const IndexEditorAndResults = BaseRoute.extend({
     viewName = viewName.replace(/\?.*$/, '');
     this.indexedDocs = new Documents.IndexCollection(null, {
       database: this.database,
-      design: decodeDdoc,
+      design: ddoc,
       view: viewName,
       params: docParams,
       paging: {
@@ -89,7 +88,7 @@ const IndexEditorAndResults = BaseRoute.extend({
       newView: false,
       database: this.database,
       designDocs: this.designDocs,
-      designDocId: '_design/' + decodeDdoc
+      designDocId: '_design/' + ddoc
     });
 
     SidebarActions.selectNavItem('designDoc', {
diff --git a/app/addons/documents/sidebar/__tests__/sidebar.components.test.js b/app/addons/documents/sidebar/__tests__/sidebar.components.test.js
new file mode 100644
index 0000000..f7fc09e
--- /dev/null
+++ b/app/addons/documents/sidebar/__tests__/sidebar.components.test.js
@@ -0,0 +1,76 @@
+// 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
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import utils from '../../../../../test/mocha/testUtils';
+import FauxtonAPI from "../../../../core/api";
+import React from "react";
+import ReactDOM from "react-dom";
+import sinon from "sinon";
+import { mount } from 'enzyme';
+import Components from "../sidebar";
+
+const { assert, restore} = utils;
+
+
+describe('DesignDoc', () => {
+  const database = { id: 'test-db' };
+  const selectedNavInfo = {
+    navItem: 'all-docs',
+    designDocName: '',
+    designDocSection: '',
+    indexName: ''
+  };
+
+  afterEach(function () {
+    restore(FauxtonAPI.urls);
+  });
+
+  it('confirm URLs are properly encoded when design doc name has special chars', function () {
+    sinon.stub(FauxtonAPI, 'urls').callsFake((a, b, c, d) => {
+      if (a === 'designDocs') {
+        return '#/database/MOCK/_design/' + encodeURIComponent(c) + '/' + encodeURIComponent(d);
+      }
+      return '' + (a || '') + '/' + (b || '') + '/' + (c || '') + '/' + (d || '');
+    });
+    const wrapper = mount(<Components.DesignDoc
+      database={database}
+      toggle={sinon.stub()}
+      sidebarListTypes={[]}
+      isExpanded={true}
+      designDocName={'doc-$-#-.1'}
+      selectedNavInfo={selectedNavInfo}
+      toggledSections={{}}
+      designDoc={{}} />);
+
+    assert.include(wrapper.find('a.icon .fonticon-plus-circled').at(1).props()['href'], '/doc-%24-%23-.1');
+    assert.include(wrapper.find('a.toggle-view .accordion-header').props()['href'], '/doc-%24-%23-.1');
+  });
+
+  it('check toggle() works when design doc name has special characters', function () {
+    sinon.stub(FauxtonAPI, 'urls');
+
+    const toggleStub = sinon.stub();
+    const wrapper = mount(<Components.DesignDoc
+      database={database}
+      toggle={toggleStub}
+      sidebarListTypes={[]}
+      isExpanded={true}
+      designDocName={'id#1'}
+      selectedNavInfo={{}}
+      toggledSections={{}}
+      designDoc={{}} />);
+
+    // NOTE: wrapper.find doesn't work special chars so we use class name instead
+    wrapper.find('div.accordion-list-item').simulate('click', {preventDefault: sinon.stub()});
+    assert.ok(toggleStub.calledOnce);
+  });
+});
diff --git a/app/addons/documents/sidebar/sidebar.js b/app/addons/documents/sidebar/sidebar.js
index 9898fa1..79daa6e 100644
--- a/app/addons/documents/sidebar/sidebar.js
+++ b/app/addons/documents/sidebar/sidebar.js
@@ -11,6 +11,7 @@
 // the License.
 
 import app from "../../../app";
+import Helpers from "../../../helpers";
 import FauxtonAPI from "../../../core/api";
 import React from "react";
 import ReactDOM from "react-dom";
@@ -148,14 +149,14 @@ var IndexSection = React.createClass({
     var sortedItems = this.props.items.sort();
 
     return _.map(sortedItems, function (indexName, index) {
-      var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', encodeURIComponent(this.props.database.id), this.props.designDocName);
+      var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', encodeURIComponent(this.props.database.id), encodeURIComponent(this.props.designDocName));
       var className = (this.props.selectedIndex === indexName) ? 'active' : '';
 
       return (
         <li className={className} key={index}>
           <a
             id={this.props.designDocName + '_' + indexName}
-            href={"#/" + href + indexName}
+            href={"#/" + href + encodeURIComponent(indexName)}
             className="toggle-view">
             {indexName}
           </a>
@@ -257,7 +258,8 @@ var DesignDoc = React.createClass({
     sidebarListTypes: React.PropTypes.array.isRequired,
     isExpanded: React.PropTypes.bool.isRequired,
     selectedNavInfo: React.PropTypes.object.isRequired,
-    toggledSections: React.PropTypes.object.isRequired
+    toggledSections: React.PropTypes.object.isRequired,
+    designDocName:  React.PropTypes.string.isRequired
   },
 
   getInitialState: function () {
@@ -319,7 +321,7 @@ var DesignDoc = React.createClass({
     e.preventDefault();
     var newToggleState = !this.props.isExpanded;
     var state = newToggleState ? 'show' : 'hide';
-    $(ReactDOM.findDOMNode(this)).find('#' + this.props.designDocName).collapse(state);
+    $(ReactDOM.findDOMNode(this)).find('#' + Helpers.escapeJQuerySelector(this.props.designDocName)).collapse(state);
     this.props.toggle(this.props.designDocName);
   },
 
@@ -330,13 +332,13 @@ var DesignDoc = React.createClass({
     var addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) {
       menuLinks.push({
         title: link.title,
-        url: '#' + newUrlPrefix + '/' + link.url + '/' + designDocName,
+        url: '#' + newUrlPrefix + '/' + link.url + '/' + encodeURIComponent(designDocName),
         icon: 'fonticon-plus-circled'
       });
       return menuLinks;
     }, [{
       title: 'New View',
-      url: '#' + FauxtonAPI.urls('new', 'addView', encodeURIComponent(this.props.database.id), designDocName),
+      url: '#' + FauxtonAPI.urls('new', 'addView', encodeURIComponent(this.props.database.id), encodeURIComponent(designDocName)),
       icon: 'fonticon-plus-circled'
     }]);
 
@@ -356,7 +358,7 @@ var DesignDoc = React.createClass({
       toggleBodyClassNames += ' in';
     }
     var designDocName = this.props.designDocName;
-    var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', encodeURIComponent(this.props.database.id), designDocName);
+    var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName);
     var metadataRowClass = (this.props.selectedNavInfo.designDocSection === 'metadata') ? 'active' : '';
 
     return (
@@ -394,7 +396,7 @@ var DesignDocList = React.createClass({
 
   designDocList: function () {
     return _.map(this.props.designDocs, function (designDoc, key) {
-      var ddName = designDoc.safeId;
+      var ddName = decodeURIComponent(designDoc.safeId);
 
       // only pass down the selected nav info and toggle info if they're relevant for this particular design doc
       var expanded = false,
diff --git a/app/addons/documents/tests/nightwatch/checkSidebarBehavior.js b/app/addons/documents/tests/nightwatch/checkSidebarBehavior.js
index 4329c73..680eee7 100644
--- a/app/addons/documents/tests/nightwatch/checkSidebarBehavior.js
+++ b/app/addons/documents/tests/nightwatch/checkSidebarBehavior.js
@@ -14,24 +14,29 @@
 
 module.exports = {
 
-  'Checks if design docs that have a dot symbol in the id show up in the UI': function (client) {
-    var waitTime = 10000,
+  'Checks if design docs that have special chars in the ID show up in the UI and are clickable': function (client) {
+    const waitTime = 10000,
         newDatabaseName = client.globals.testDatabaseName,
         baseUrl = client.globals.test_settings.launch_url;
-
+    const docNormal = 'ddoc_normal';
+    const docSpecialChars = 'ddoc_with.$pecialcharacters()+-';
+    const docSpecialCharsEncoded = 'ddoc_with.%24pecialcharacters()%2B-';
     client
       .loginToGUI()
-      .createDocument('_design/ddoc_normal', newDatabaseName)
-      .createDocument('_design/ddoc.with.specialcharacters', newDatabaseName)
+      .createDocument('_design/' + docNormal, newDatabaseName)
+      .createDocument('_design/' + docSpecialChars, newDatabaseName)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
       .waitForElementPresent('.nav-list', waitTime, false)
-      .assert.hidden('a[href="#/database/' + newDatabaseName + '/_design/ddoc_normal/_info"]')
-      .assert.hidden('a[href="#/database/' + newDatabaseName + '/_design/ddoc.with.specialcharacters/_info"]')
-
-      .clickWhenVisible('#nav-header-ddoc_normal')
-      .assert.visible('a[href="#/database/' + newDatabaseName + '/_design/ddoc_normal/_info"]')
-      .clickWhenVisible('[title="_design/ddoc.with.specialcharacters"]')
-      .assert.visible('a[href="#/database/' + newDatabaseName + '/_design/ddoc.with.specialcharacters/_info"]')
+      // Verify 'Metadata' subitem is not visible
+      .assert.hidden('a[href="#/database/' + newDatabaseName + '/_design/' + docNormal + '/_info"]')
+      .assert.hidden('a[href="#/database/' + newDatabaseName + '/_design/' + docSpecialCharsEncoded + '/_info"]')
+      // Click sidebar items and verify they expand
+      .clickWhenVisible('#nav-header-' + docNormal)
+      .assert.visible('a[href="#/database/' + newDatabaseName + '/_design/' + docNormal + '/_info"]')
+      .clickWhenVisible('span[title="_design/' + docSpecialChars + '"]')
+      .assert.visible('a[href="#/database/' + newDatabaseName + '/_design/' + docSpecialCharsEncoded + '/_info"]')
+      // Verify display name is not encoded
+      .assert.containsText('span[title="_design/' + docSpecialChars + '"]', docSpecialChars)
     .end();
   }
 };
diff --git a/app/helpers.js b/app/helpers.js
index 2991bcf..f242722 100644
--- a/app/helpers.js
+++ b/app/helpers.js
@@ -57,4 +57,8 @@ Helpers.getDateFromNow = function (timestamp) {
   return moment(timestamp, 'X').fromNow();
 };
 
+Helpers.escapeJQuerySelector = function (selector) {
+  return selector && selector.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&");
+};
+
 export default Helpers;
diff --git a/test/nightwatch_tests/custom-commands/createDocument.js b/test/nightwatch_tests/custom-commands/createDocument.js
index ffa9fa6..bd39afb 100644
--- a/test/nightwatch_tests/custom-commands/createDocument.js
+++ b/test/nightwatch_tests/custom-commands/createDocument.js
@@ -47,7 +47,11 @@ CreateDocument.prototype.command = function (documentName, databaseName, docCont
     if (!databaseName) {
       databaseName = helpers.testDatabaseName;
     }
-
+    if (documentName.startsWith('_design/')) {
+      documentName = '_design/' + encodeURIComponent(documentName.substr(8));
+    } else {
+      documentName = encodeURIComponent(documentName);
+    }
     const url = [couchUrl, databaseName, documentName].join('/');
 
     checkForDocumentCreated(url, helpers.maxWaitTime, () => {
diff --git a/test/nightwatch_tests/custom-commands/helper.js b/test/nightwatch_tests/custom-commands/helper.js
index 93a1516..64f3abc 100644
--- a/test/nightwatch_tests/custom-commands/helper.js
+++ b/test/nightwatch_tests/custom-commands/helper.js
@@ -3,7 +3,7 @@ const request = require('request');
 exports.checkForDocumentCreated = function checkForDocumentCreated (url, timeout, cb) {
 
   const timeOutId = setTimeout(() => {
-    throw new Error('timeout waiting for doc to appear');
+    throw new Error('timeout waiting for doc to appear (' + url + ')');
   }, timeout);
 
   const intervalId = setInterval(() => {

-- 
To stop receiving notification emails like this one, please contact
['"commits@couchdb.apache.org" <co...@couchdb.apache.org>'].