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/09/08 08:19:25 UTC

[couchdb-fauxton] branch master updated: React motion (#979)

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 ed23b84  React motion (#979)
ed23b84 is described below

commit ed23b844d149acaa6631ef81e233031fe7f72712
Author: garren smith <ga...@gmail.com>
AuthorDate: Fri Sep 8 10:19:23 2017 +0200

    React motion (#979)
    
    Use react-motion for all animation
    
    This replaces react-addons-transitions and velocity with react-motion
    for animations.
---
 app/addons/components/assets/less/jsonlink.less    |   9 +
 app/addons/components/components/tray.js           |  61 +++-
 app/addons/databases/assets/less/databases.less    |   2 +-
 .../nightwatch/deletesDatabaseSpecialChars.js      |   1 +
 .../changes-stores.test.js}                        |  49 ++-
 app/addons/documents/__tests__/changes.test.js     | 283 +++++++++++++++
 .../designdocinfo-action.test.js}                  |  26 +-
 .../documents/assets/less/query-options.less       |  46 ---
 app/addons/documents/changes/components.js         |  93 ++++-
 .../changes/tests/changes.componentsSpec.js        | 321 -----------------
 .../tests/nightwatch/deleteDatabaseModal.js        |   2 +
 app/addons/documents/tests/nightwatch/viewClone.js |   2 -
 .../documents/tests/nightwatch/viewCreate.js       |  32 +-
 app/addons/fauxton/components.js                   |   4 +-
 .../actionsSpec.js => __tests__/actions.test.js}   |  26 +-
 .../notifications/__tests__/components.test.js     | 207 +++++++++++
 .../storesSpec.js => __tests__/stores.test.js}     |  24 +-
 app/addons/fauxton/notifications/notifications.js  | 382 ++++++++++++---------
 .../fauxton/notifications/tests/componentsSpec.js  | 188 ----------
 .../fauxton/tests/nightwatch/notificationCenter.js |   4 +-
 .../{tests/apiSpec.js => __tests__/api.test.js}    |  14 +-
 .../utilsSpec.js => __tests__/utils.test.js}       |  54 +--
 assets/less/notification-center.less               |  50 ++-
 devserver.js                                       |   1 +
 package.json                                       |   4 +-
 .../custom-commands/closeNotification.js           |   3 +-
 26 files changed, 989 insertions(+), 899 deletions(-)

diff --git a/app/addons/components/assets/less/jsonlink.less b/app/addons/components/assets/less/jsonlink.less
index 285df49..071b538 100644
--- a/app/addons/components/assets/less/jsonlink.less
+++ b/app/addons/components/assets/less/jsonlink.less
@@ -38,6 +38,15 @@
   font-weight: 500;
 }
 
+@media screen and (max-width: 1200px) {
+  .faux__jsonlink-link {
+    font-size: 10px;
+  }
+  .faux__jsonlink-link-brackets {
+    font-size: 12px;
+  }
+}
+
 .faux__doclink {
   text-align: center;
   width: 55px;
diff --git a/app/addons/components/components/tray.js b/app/addons/components/components/tray.js
index 37d0d61..8c5dfee 100644
--- a/app/addons/components/components/tray.js
+++ b/app/addons/components/components/tray.js
@@ -12,15 +12,8 @@
 
 import React from "react";
 import ReactDOM from "react-dom";
-import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 import {Overlay} from 'react-bootstrap';
-
-//With React 15.2.0 it validates props and throws a warning if props to a component are not acceptable
-//this means that with the overlay it will try and pass custom props to a div which causes the overlay to stop working
-//using a custom component gets around this.
-const OverlayWarningEater = ({children}) => {
-  return children;
-};
+import {TransitionMotion, spring} from 'react-motion';
 
 export const TrayContents = React.createClass({
   propTypes: {
@@ -35,14 +28,48 @@ export const TrayContents = React.createClass({
     container: this
   },
 
-  getChildren () {
+  getChildren (items) {
+    const {style} = items[0];
     var className = "tray show-tray " + this.props.className;
     return (
-      <div key={1} id={this.props.id} className={className}>
+      <div key={'1'} id={this.props.id} style={{opacity: style.opacity, top: style.top + 'px'}} className={className}>
         {this.props.children}
       </div>);
   },
 
+  willEnter () {
+    return {
+      opacity: spring(1),
+      top: spring(55)
+    };
+  },
+
+  willLeave () {
+    return {
+      opacity: spring(0),
+      top: spring(30)
+    };
+  },
+
+  getDefaultStyles () {
+    return [{key: '1', style: {opacity: 0, top: 30}}];
+  },
+
+  getStyles (prevStyle) {
+    if (!prevStyle) {
+      return [{
+        key: '1',
+        style: this.willEnter()
+      }];
+    }
+    return prevStyle.map(item => {
+      return {
+        key: '1',
+        style: item.style
+      };
+    });
+  },
+
   render () {
     return (
       <Overlay
@@ -54,12 +81,14 @@ export const TrayContents = React.createClass({
        target={() => ReactDOM.findDOMNode(this.refs.target)}
        onEnter={this.props.onEnter}
       >
-        <OverlayWarningEater>
-          <ReactCSSTransitionGroup transitionName="tray" transitionAppear={true} component="div" transitionAppearTimeout={500}
-            transitionEnterTimeout={500} transitionLeaveTimeout={300}>
-            {this.getChildren()}
-          </ReactCSSTransitionGroup>
-        </OverlayWarningEater>
+        <TransitionMotion
+          defaultStyles={this.getDefaultStyles()}
+          styles={this.getStyles()}
+          willLeave={this.willLeave}
+          willEnter={this.willEnter}
+        >
+        {this.getChildren}
+        </TransitionMotion>
       </Overlay>
     );
   }
diff --git a/app/addons/databases/assets/less/databases.less b/app/addons/databases/assets/less/databases.less
index a2c14fc..1ab30cc 100644
--- a/app/addons/databases/assets/less/databases.less
+++ b/app/addons/databases/assets/less/databases.less
@@ -21,7 +21,7 @@
 .new-database-tray {
   padding: 16px 20px 28px;
   &:before {
-    right: 115px;
+    right: 250px;
   }
 
   input.input-xxlarge {
diff --git a/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js b/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
index 71b253a..9527567 100644
--- a/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
+++ b/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
@@ -21,6 +21,7 @@ module.exports = {
       .createDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + encodeURIComponent(newDatabaseName) + '/_all_docs')
+      .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
       .clickWhenVisible('.faux-header__doc-header-dropdown-toggle')
       .clickWhenVisible('.faux-header__doc-header-dropdown-itemwrapper .fonticon-trash')
       .waitForElementVisible('.delete-db-modal', waitTime, false)
diff --git a/app/addons/documents/changes/tests/changes.storesSpec.js b/app/addons/documents/__tests__/changes-stores.test.js
similarity index 68%
rename from app/addons/documents/changes/tests/changes.storesSpec.js
rename to app/addons/documents/__tests__/changes-stores.test.js
index ebe4365..802a50f 100644
--- a/app/addons/documents/changes/tests/changes.storesSpec.js
+++ b/app/addons/documents/__tests__/changes-stores.test.js
@@ -11,48 +11,47 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../../../core/api";
-import Stores from "../stores";
-import utils from "../../../../../test/mocha/testUtils";
+import FauxtonAPI from "../../../core/api";
+import Stores from "../changes/stores";
+import utils from "../../../../test/mocha/testUtils";
 FauxtonAPI.router = new FauxtonAPI.Router([]);
 
-var assert = utils.assert;
+const assert = utils.assert;
 
+describe('ChangesStore', () => {
 
-describe('ChangesStore', function () {
-
-  afterEach(function () {
+  afterEach(() => {
     Stores.changesStore.reset();
   });
 
-  it('addFilter() adds item in store', function () {
-    var filter = 'My filter';
+  it('addFilter() adds item in store', () => {
+    const filter = 'My filter';
     Stores.changesStore.addFilter(filter);
-    var filters = Stores.changesStore.getFilters();
+    const filters = Stores.changesStore.getFilters();
     assert.ok(filters.length === 1);
     assert.ok(filters[0] === filter);
   });
 
-  it('removeFilter() removes item from store', function () {
-    var filter1 = 'My filter 1';
-    var filter2 = 'My filter 2';
+  it('removeFilter() removes item from store', () => {
+    const filter1 = 'My filter 1';
+    const filter2 = 'My filter 2';
     Stores.changesStore.addFilter(filter1);
     Stores.changesStore.addFilter(filter2);
     Stores.changesStore.removeFilter(filter1);
 
-    var filters = Stores.changesStore.getFilters();
+    const filters = Stores.changesStore.getFilters();
     assert.ok(filters.length === 1);
     assert.ok(filters[0] === filter2);
   });
 
-  it('hasFilter() finds item in store', function () {
-    var filter = 'My filter';
+  it('hasFilter() finds item in store', () => {
+    const filter = 'My filter';
     Stores.changesStore.addFilter(filter);
     assert.ok(Stores.changesStore.hasFilter(filter) === true);
   });
 
-  it('getDatabaseName() returns database name', function () {
-    var dbName = 'hoopoes';
+  it('getDatabaseName() returns database name', () => {
+    const dbName = 'hoopoes';
     Stores.changesStore.initChanges({ databaseName: dbName });
     assert.equal(Stores.changesStore.getDatabaseName(), dbName);
 
@@ -60,28 +59,28 @@ describe('ChangesStore', function () {
     assert.equal(Stores.changesStore.getDatabaseName(), '');
   });
 
-  it("getChanges() should return a subset if there are a lot of changes", function () {
+  it("getChanges() should return a subset if there are a lot of changes", () => {
 
     // to keep the test speedy, we override the default max value
-    var maxChanges = 10;
-    var changes = [];
+    const maxChanges = 10;
+    const changes = [];
     _.times(maxChanges + 10, function (i) {
       changes.push({ id: 'doc_' + i, seq: 1, changes: {}});
     });
     Stores.changesStore.initChanges({ databaseName: "test" });
     Stores.changesStore.setMaxChanges(maxChanges);
 
-    var seqNum = 123;
+    const seqNum = 123;
     Stores.changesStore.updateChanges(seqNum, changes);
 
-    var results = Stores.changesStore.getChanges();
+    const results = Stores.changesStore.getChanges();
     assert.equal(maxChanges, results.length);
   });
 
-  it("tracks last sequence number", function () {
+  it("tracks last sequence number", () => {
     assert.equal(null, Stores.changesStore.getLastSeqNum());
 
-    var seqNum = 123;
+    const seqNum = 123;
     Stores.changesStore.updateChanges(seqNum, []);
 
     // confirm it's been stored
diff --git a/app/addons/documents/__tests__/changes.test.js b/app/addons/documents/__tests__/changes.test.js
new file mode 100644
index 0000000..d1aa2aa
--- /dev/null
+++ b/app/addons/documents/__tests__/changes.test.js
@@ -0,0 +1,283 @@
+
+// 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 React from "react";
+import ReactDOM from "react-dom";
+import Changes from "../changes/components";
+import Stores from "../changes/stores";
+import Actions from "../changes/actions";
+import {mount} from 'enzyme';
+import utils from "../../../../test/mocha/testUtils";
+import sinon from "sinon";
+import '../base';
+
+const assert = utils.assert;
+
+
+describe('ChangesTabContent', () => {
+  let el;
+
+  beforeEach(() => {
+    el = mount(<Changes.ChangesTabContent />);
+  });
+
+  afterEach(() => {
+    Stores.changesStore.reset();
+  });
+
+  it('should add filter markup', () => {
+    const submitBtn = el.find('[type="submit"]'),
+        addItemField = el.find('.js-changes-filter-field');
+
+    addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
+    submitBtn.simulate('submit');
+
+    addItemField.simulate('change', {target: {value: 'A second filter'}});
+    submitBtn.simulate('submit');
+
+    assert.equal(2, el.find('.remove-filter').length);
+  });
+
+  it('should call addFilter action on click', () => {
+    const submitBtn = el.find('[type="submit"]'),
+      addItemField = el.find('.js-changes-filter-field');
+
+    const spy = sinon.spy(Actions, 'addFilter');
+
+    addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
+    submitBtn.simulate('submit');
+
+    assert.ok(spy.calledOnce);
+  });
+
+  it('should remove filter markup', () => {
+    const submitBtn = el.find('[type="submit"]'),
+      addItemField = el.find('.js-changes-filter-field');
+
+    addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
+    submitBtn.simulate('submit');
+
+    addItemField.simulate('change', {target: {value: 'Flibble'}});
+    submitBtn.simulate('submit');
+
+    // clicks ALL 'remove' elements
+    el.find('.remove-filter').first().simulate('click');
+    el.find('.remove-filter').simulate('click');
+
+    assert.equal(0, el.find('.remove-filter').length);
+  });
+
+  it('should call removeFilter action on click', () => {
+    const submitBtn = el.find('[type="submit"]'),
+      addItemField = el.find('.js-changes-filter-field');
+
+    const spy = sinon.spy(Actions, 'removeFilter');
+
+    addItemField.simulate('change', {target: {value: 'Flibble'}});
+    submitBtn.simulate('submit');
+    el.find('.remove-filter').simulate('click');
+
+    assert.ok(spy.calledOnce);
+  });
+
+  it('should not add empty filters', () => {
+    const submitBtn = el.find('[type="submit"]'),
+      addItemField = el.find('.js-changes-filter-field');
+
+    addItemField.simulate('change', {target: {value: ''}});
+    submitBtn.simulate('submit');
+
+    assert.equal(0, el.find('.remove-filter').length);
+  });
+
+  it('should not add tooltips by default', () => {
+    assert.equal(0, el.find('.remove-filter').length);
+  });
+
+  it('should not add the same filter twice', () => {
+    const submitBtn = el.find('[type="submit"]'),
+        addItemField = el.find('.js-changes-filter-field');
+
+    const filter = 'I am unique in the whole wide world';
+    addItemField.simulate('change', {target: {value: filter}});
+    submitBtn.simulate('submit');
+
+    addItemField.simulate('change', {target: {value: filter}});
+    submitBtn.simulate('submit');
+
+    assert.equal(1, el.find('.remove-filter').length);
+  });
+});
+
+
+describe('ChangesController', () => {
+  let  headerEl, changesEl;
+
+  const results = [
+    { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } },
+    { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } },
+    { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } },
+    { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } },
+    { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } }
+  ];
+
+  const changesResponse = {
+    last_seq: 123,
+    'results': results
+  };
+
+  beforeEach(() => {
+    Actions.initChanges({ databaseName: 'testDatabase' });
+    headerEl  = mount(<Changes.ChangesTabContent />);
+    changesEl = mount(<Changes.ChangesController />);
+    Actions.updateChanges(changesResponse);
+  });
+
+  afterEach(() => {
+    Stores.changesStore.reset();
+  });
+
+
+  it('should list the right number of changes', () => {
+    assert.equal(results.length, changesEl.find('.change-box').length);
+  });
+
+
+  it('"false"/"true" filter strings should apply to change deleted status', () => {
+    // add a filter
+    const addItemField = headerEl.find('.js-changes-filter-field');
+    const submitBtn = headerEl.find('[type="submit"]');
+    addItemField.value = 'true';
+    addItemField.simulate('change', {target: {value: 'true'}});
+    submitBtn.simulate('submit');
+
+    // confirm only the two deleted items shows up and the IDs maps to the deleted rows
+    assert.equal(2, changesEl.find('.change-box').length);
+    assert.equal('doc_3', changesEl.find('.js-doc-id').first().text());
+    assert.equal('doc_5', changesEl.find('.js-doc-id').at(1).text());
+  });
+
+
+  it('confirms that a filter affects the actual search results', () => {
+    // add a filter
+    const addItemField = headerEl.find('.js-changes-filter-field');
+    const submitBtn = headerEl.find('[type="submit"]');
+    addItemField.simulate('change', {target: {value: '6'}});
+    submitBtn.simulate('submit');
+
+    // confirm only one item shows up and the ID maps to what we'd expect
+    assert.equal(1, changesEl.find('.change-box').length);
+    assert.equal('doc_3', changesEl.find('.js-doc-id').first().text());
+  });
+
+
+  // confirms that if there are multiple filters, ALL are applied to return the subset of results that match
+  // all filters
+  it('multiple filters should all be applied to results', () => {
+    // add the filters
+    const addItemField = headerEl.find('.js-changes-filter-field');
+    const submitBtn = headerEl.find('[type="submit"]');
+
+    // *** should match doc_1, doc_2 and doc_5
+    addItemField.simulate('change', {target: {value: '1'}});
+    submitBtn.simulate('submit');
+
+    // *** should match doc_3 and doc_5
+    addItemField.simulate('change', {target: {value: 'true'}});
+    submitBtn.simulate('submit');
+
+    // confirm only one item shows up and that it's doc_5
+    assert.equal(1, changesEl.find('.change-box').length);
+    assert.equal('doc_5', changesEl.find('.js-doc-id').first().text());
+  });
+
+  it('shows a No Docs Found message if no docs', () => {
+    Stores.changesStore.reset();
+    Actions.updateChanges({ last_seq: 124, results: [] });
+    assert.ok(/There\sare\sno\sdocument\schanges/.test(changesEl.html()));
+  });
+});
+
+
+describe('ChangesController max results', () => {
+  let changesEl;
+  const maxChanges = 10;
+
+
+  beforeEach(() => {
+    const changes = [];
+    _.times(maxChanges + 10, (i) => {
+      changes.push({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } });
+    });
+
+    const response = {
+      last_seq: 1,
+      results: changes
+    };
+
+    Actions.initChanges({ databaseName: 'test' });
+
+    // to keep the test speedy, override the default value (1000)
+    Stores.changesStore.setMaxChanges(maxChanges);
+
+    Actions.updateChanges(response);
+    changesEl = mount(<Changes.ChangesController />);
+  });
+
+  afterEach(() => {
+    Stores.changesStore.reset();
+  });
+
+  it('should truncate the number of results with very large # of changes', () => {
+    // check there's no more than maxChanges results
+    assert.equal(maxChanges, changesEl.find('.change-box').length);
+  });
+
+  it('should show a message if the results are truncated', () => {
+    assert.equal(1, changesEl.find('.changes-result-limit').length);
+  });
+
+});
+
+
+describe('ChangeRow', () => {
+  const change = {
+    id: '123',
+    seq: 5,
+    deleted: false,
+    changes: { code: 'here' }
+  };
+
+  it('clicking the toggle-json button shows the code section', function () {
+    const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+
+    // confirm it's hidden by default
+    assert.equal(0, changeRow.find('.prettyprint').length);
+
+    // confirm clicking it shows the element
+    changeRow.find('button.btn').simulate('click');
+    assert.equal(1, changeRow.find('.prettyprint').length);
+  });
+
+  it('deleted docs should not be clickable', () => {
+    change.deleted = true;
+    const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+    assert.equal(0, changeRow.find('a.js-doc-link').length);
+  });
+
+  it('non-deleted docs should be clickable', () => {
+    change.deleted = false;
+    const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+    assert.equal(1, changeRow.find('a.js-doc-link').length);
+  });
+});
diff --git a/app/addons/documents/designdocinfo/tests/actionsSpec.js b/app/addons/documents/__tests__/designdocinfo-action.test.js
similarity index 61%
rename from app/addons/documents/designdocinfo/tests/actionsSpec.js
rename to app/addons/documents/__tests__/designdocinfo-action.test.js
index ac54353..0244709 100644
--- a/app/addons/documents/designdocinfo/tests/actionsSpec.js
+++ b/app/addons/documents/__tests__/designdocinfo-action.test.js
@@ -10,31 +10,31 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../../../core/api";
-import Actions from "../actions";
-import testUtils from "../../../../../test/mocha/testUtils";
+import FauxtonAPI from "../../../core/api";
+import Actions from "../designdocinfo/actions";
+import testUtils from "../../../../test/mocha/testUtils";
 import sinon from "sinon";
-var assert = testUtils.assert;
-var restore = testUtils.restore;
+const assert = testUtils.assert;
+const restore = testUtils.restore;
 
-describe('DesignDocInfo Actions', function () {
+describe('DesignDocInfo Actions', () => {
 
-  describe('fetchDesignDocInfo', function () {
+  describe('fetchDesignDocInfo', () => {
 
-    afterEach(function () {
+    afterEach(() => {
       restore(Actions.monitorDesignDoc);
     });
 
-    it('calls monitorDesignDoc on successful fetch', function () {
-      var promise = FauxtonAPI.Deferred();
+    it('calls monitorDesignDoc on successful fetch', () => {
+      const promise = FauxtonAPI.Deferred();
       promise.resolve();
-      var fakeDesignDocInfo = {
-        fetch: function () {
+      const fakeDesignDocInfo = {
+        fetch: () => {
           return promise;
         }
       };
 
-      var spy = sinon.spy(Actions, 'monitorDesignDoc');
+      const spy = sinon.spy(Actions, 'monitorDesignDoc');
 
 
       Actions.fetchDesignDocInfo({
diff --git a/app/addons/documents/assets/less/query-options.less b/app/addons/documents/assets/less/query-options.less
index 4bdcd9b..27e9cc0 100644
--- a/app/addons/documents/assets/less/query-options.less
+++ b/app/addons/documents/assets/less/query-options.less
@@ -10,52 +10,6 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-.tray-enter,
-.tray-appear {
-  opacity: 0.01;
-  animation: slidefadeIn .2s ease-in;
-  -webkit-animation: slidefadeIn .2s ease-in;
-}
-
-.tray-enter.tray-enter-active,
-.tray-appear.tray-appear-active {
-  opacity: 1;
-}
-
-@keyframes slidefadeIn {
-  from {
-    opacity: 0.01;
-    top: 30px
-  }
-
-  to {
-    opacity: 1;
-    top: 55px;
-  }
-}
-
-@keyframes slidefadeOut {
-  from {
-    opacity: 1;
-    top: 55px;
-  }
-
-  to {
-    opacity: 0.01;
-    top: 30px
-  }
-}
-
-.tray-leave {
-  opacity: 1;
-  animation: slidefadeOut .2s ease-out;
-  -webkit-animation: slidefadeOut .2s ease-out;
-
-  &.tray-leave-active {
-    opacity: 0;
-  }
-}
-
 #query-options-tray:before {
   right: 180px;
 }
diff --git a/app/addons/documents/changes/components.js b/app/addons/documents/changes/components.js
index 457e7c7..781b74b 100644
--- a/app/addons/documents/changes/components.js
+++ b/app/addons/documents/changes/components.js
@@ -17,7 +17,7 @@ import Actions from "./actions";
 import Stores from "./stores";
 import Components from "../../fauxton/components";
 import ReactComponents from "../../components/react-components";
-import ReactCSSTransitionGroup from "react-addons-css-transition-group";
+import {TransitionMotion, spring, presets} from 'react-motion';
 import "../../../../assets/js/plugins/prettify";
 import uuid from 'uuid';
 
@@ -300,6 +300,11 @@ class ChangeRow extends React.Component {
           </div>
 
           <div className="row-fluid">
+            <div className="span2">deleted</div>
+            <div className="span10">{change.deleted ? 'True' : 'False'}</div>
+          </div>
+
+          <div className="row-fluid">
             <div className="span2">changes</div>
             <div className="span10">
               <button type="button" className='btn btn-small btn-secondary' onClick={this.toggleJSON.bind(this)}>
@@ -308,26 +313,94 @@ class ChangeRow extends React.Component {
             </div>
           </div>
 
-          <ReactCSSTransitionGroup transitionName="toggle-changes-code" component="div" className="changesCodeSectionWrapper"
-            transitionEnterTimeout={500} transitionLeaveTimeout={300}>
-            {this.getChangesCode()}
-          </ReactCSSTransitionGroup>
-
-          <div className="row-fluid">
-            <div className="span2">deleted</div>
-            <div className="span10">{change.deleted ? 'True' : 'False'}</div>
-          </div>
+          <ChangesCodeTransition
+            codeVisible={this.state.codeVisible}
+            code={this.getChangeCode()}
+          />
         </div>
       </div>
     );
   }
 }
+
 ChangeRow.PropTypes = {
   change: React.PropTypes.object,
   databaseName: React.PropTypes.string.isRequired
 };
 
 
+export class ChangesCodeTransition extends React.Component {
+  willEnter () {
+    return {
+      opacity: spring(1, presets.gentle),
+      height: spring(160, presets.gentle)
+    };
+  }
+
+  willLeave () {
+    return {
+      opacity: spring(0, presets.gentle),
+      height: spring(0, presets.gentle)
+    };
+  }
+
+  getStyles (prevStyle) {
+    if (!prevStyle && this.props.codeVisible) {
+      return [{
+        key: '1',
+        style: this.willEnter()
+      }];
+    }
+
+    if (!prevStyle && !this.props.codeVisible) {
+      return [{
+        key: '1',
+        style: this.willLeave()
+      }];
+    }
+    return prevStyle.map(item => {
+      return {
+        key: '1',
+        style: item.style
+      };
+    });
+  }
+
+  getChildren (items) {
+    const code =  items.map(({style}) => {
+      if (this.props.codeVisible === false && style.opacity === 0) {
+        return null;
+      }
+      return (
+        <div key='1' style={{opacity: style.opacity, height: style.height + 'px'}}>
+          <Components.CodeFormat
+          code={this.props.code}
+          />
+        </div>
+      );
+    });
+
+    return (
+      <span>
+        {code}
+      </span>
+    );
+  }
+
+  render () {
+    return (
+      <TransitionMotion
+          styles={this.getStyles()}
+          willLeave={this.willLeave}
+          willEnter={this.willEnter}
+        >
+        {this.getChildren.bind(this)}
+      </TransitionMotion>
+    );
+  }
+}
+
+
 class ChangeID extends React.Component {
   render () {
     const { deleted, id, databaseName } = this.props;
diff --git a/app/addons/documents/changes/tests/changes.componentsSpec.js b/app/addons/documents/changes/tests/changes.componentsSpec.js
deleted file mode 100644
index dd0e3b9..0000000
--- a/app/addons/documents/changes/tests/changes.componentsSpec.js
+++ /dev/null
@@ -1,321 +0,0 @@
-
-// 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 React from "react";
-import ReactDOM from "react-dom";
-import Changes from "../components";
-import Stores from "../stores";
-import Actions from "../actions";
-import utils from "../../../../../test/mocha/testUtils";
-import TestUtils from "react-addons-test-utils";
-import sinon from "sinon";
-
-var assert = utils.assert;
-
-
-describe('ChangesTabContent', function () {
-  var container, changesFilterEl;
-
-  beforeEach(function () {
-    container = document.createElement('div');
-    changesFilterEl = TestUtils.renderIntoDocument(<Changes.ChangesTabContent />, container);
-  });
-
-  afterEach(function () {
-    Stores.changesStore.reset();
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('should add filter markup', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-        submitBtn = $el.find('[type="submit"]')[0],
-        addItemField = $el.find('.js-changes-filter-field')[0];
-
-    addItemField.value = 'I wandered lonely as a filter';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    addItemField.value = 'A second filter';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    assert.equal(2, $el.find('.remove-filter').length);
-  });
-
-  it('should call addFilter action on click', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-      submitBtn = $el.find('[type="submit"]')[0],
-      addItemField = $el.find('.js-changes-filter-field')[0];
-
-    var spy = sinon.spy(Actions, 'addFilter');
-
-    addItemField.value = 'I wandered lonely as a filter';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    assert.ok(spy.calledOnce);
-  });
-
-  it('should remove filter markup', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-      submitBtn = $el.find('[type="submit"]')[0],
-      addItemField = $el.find('.js-changes-filter-field')[0];
-
-    addItemField.value = 'Bloop';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    addItemField.value = 'Flibble';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    // clicks ALL 'remove' elements
-    TestUtils.Simulate.click($el.find('.remove-filter')[0]);
-    TestUtils.Simulate.click($el.find('.remove-filter')[0]);
-
-    assert.equal(0, $el.find('.remove-filter').length);
-  });
-
-  it('should call removeFilter action on click', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-      submitBtn = $el.find('[type="submit"]')[0],
-      addItemField = $el.find('.js-changes-filter-field')[0];
-
-    var spy = sinon.spy(Actions, 'removeFilter');
-
-    addItemField.value = 'I wandered lonely as a filter';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-    TestUtils.Simulate.click($el.find('.remove-filter')[0]);
-
-    assert.ok(spy.calledOnce);
-  });
-
-  it('should not add empty filters', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-      submitBtn = $el.find('[type="submit"]')[0],
-      addItemField = $el.find('.js-changes-filter-field')[0];
-
-    addItemField.value = '';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    assert.equal(0, $el.find('.remove-filter').length);
-  });
-
-  it('should not add tooltips by default', function () {
-    assert.equal(0, $(ReactDOM.findDOMNode(changesFilterEl)).find('.remove-filter').length);
-  });
-
-  it('should not add the same filter twice', function () {
-    var $el = $(ReactDOM.findDOMNode(changesFilterEl)),
-        submitBtn = $el.find('[type="submit"]')[0],
-        addItemField = $el.find('.js-changes-filter-field')[0];
-
-    var filter = 'I am unique in the whole wide world';
-    addItemField.value = filter;
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    addItemField.value = filter;
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    assert.equal(1, $el.find('.remove-filter').length);
-  });
-});
-
-
-describe('ChangesController', function () {
-  var container, container2, headerEl, $headerEl, changesEl, $changesEl;
-
-  var results = [
-    { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } },
-    { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } },
-    { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } },
-    { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } },
-    { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } }
-  ];
-  var changesResponse = {
-    last_seq: 123,
-    'results': results
-  };
-
-  beforeEach(function () {
-    container = document.createElement('div');
-    container2 = document.createElement('div');
-    Actions.initChanges({ databaseName: 'testDatabase' });
-    headerEl  = TestUtils.renderIntoDocument(<Changes.ChangesTabContent />, container);
-    $headerEl = $(ReactDOM.findDOMNode(headerEl));
-    changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, container2);
-    $changesEl = $(ReactDOM.findDOMNode(changesEl));
-    Actions.updateChanges(changesResponse);
-  });
-
-  afterEach(function () {
-    Stores.changesStore.reset();
-    ReactDOM.unmountComponentAtNode(container);
-    ReactDOM.unmountComponentAtNode(container2);
-  });
-
-
-  it('should list the right number of changes', function () {
-    assert.equal(results.length, $changesEl.find('.change-box').length);
-  });
-
-
-  it('"false"/"true" filter strings should apply to change deleted status', function () {
-    // add a filter
-    var addItemField = $headerEl.find('.js-changes-filter-field')[0];
-    var submitBtn = $headerEl.find('[type="submit"]')[0];
-    addItemField.value = 'true';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    // confirm only the two deleted items shows up and the IDs maps to the deleted rows
-    assert.equal(2, $changesEl.find('.change-box').length);
-    assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html());
-    assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(1)).html());
-  });
-
-
-  it('confirms that a filter affects the actual search results', function () {
-    // add a filter
-    var addItemField = $headerEl.find('.js-changes-filter-field')[0];
-    var submitBtn = $headerEl.find('[type="submit"]')[0];
-    addItemField.value = '6'; // should match doc_3's sequence ID
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    // confirm only one item shows up and the ID maps to what we'd expect
-    assert.equal(1, $changesEl.find('.change-box').length);
-    assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html());
-  });
-
-
-  // confirms that if there are multiple filters, ALL are applied to return the subset of results that match
-  // all filters
-  it('multiple filters should all be applied to results', function () {
-    // add the filters
-    var addItemField = $headerEl.find('.js-changes-filter-field')[0];
-    var submitBtn = $headerEl.find('[type="submit"]')[0];
-
-    // *** should match doc_1, doc_2 and doc_5
-    addItemField.value = '1';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    // *** should match doc_3 and doc_5
-    addItemField.value = 'true';
-    TestUtils.Simulate.change(addItemField);
-    TestUtils.Simulate.submit(submitBtn);
-
-    // confirm only one item shows up and that it's doc_5
-    assert.equal(1, $changesEl.find('.change-box').length);
-    assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(0)).html());
-  });
-
-  it('shows a No Docs Found message if no docs', function () {
-    Stores.changesStore.reset();
-    Actions.updateChanges({ last_seq: 124, results: [] });
-    assert.ok(/There\sare\sno\sdocument\schanges/.test($changesEl[0].outerHTML));
-  });
-
-});
-
-
-describe('ChangesController max results', function () {
-  var changesEl;
-  var container;
-  var maxChanges = 10;
-
-
-  beforeEach(function () {
-    container = document.createElement('div');
-    var changes = [];
-    _.times(maxChanges + 10, function (i) {
-      changes.push({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } });
-    });
-
-    var response = {
-      last_seq: 1,
-      results: changes
-    };
-
-    Actions.initChanges({ databaseName: 'test' });
-
-    // to keep the test speedy, override the default value (1000)
-    Stores.changesStore.setMaxChanges(maxChanges);
-
-    Actions.updateChanges(response);
-    changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, container);
-  });
-
-  afterEach(function () {
-    Stores.changesStore.reset();
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('should truncate the number of results with very large # of changes', function () {
-    // check there's no more than maxChanges results
-    assert.equal(maxChanges, $(ReactDOM.findDOMNode(changesEl)).find('.change-box').length);
-  });
-
-  it('should show a message if the results are truncated', function () {
-    assert.equal(1, $(ReactDOM.findDOMNode(changesEl)).find('.changes-result-limit').length);
-  });
-
-});
-
-
-describe('ChangeRow', function () {
-  var container;
-  var change = {
-    id: '123',
-    seq: 5,
-    deleted: false,
-    changes: { code: 'here' }
-  };
-
-  beforeEach(function () {
-    container = document.createElement('div');
-  });
-
-  afterEach(function () {
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-
-  it('clicking the toggle-json button shows the code section', function () {
-    var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
-
-    // confirm it's hidden by default
-    assert.equal(0, $(ReactDOM.findDOMNode(changeRow)).find('.prettyprint').length);
-
-    // confirm clicking it shows the element
-    TestUtils.Simulate.click($(ReactDOM.findDOMNode(changeRow)).find('button.btn')[0]);
-    assert.equal(1, $(ReactDOM.findDOMNode(changeRow)).find('.prettyprint').length);
-  });
-
-  it('deleted docs should not be clickable', function () {
-    change.deleted = true;
-    var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
-    assert.equal(0, $(ReactDOM.findDOMNode(changeRow)).find('a.js-doc-link').length);
-  });
-
-  it('non-deleted docs should be clickable', function () {
-    change.deleted = false;
-    var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
-    assert.equal(1, $(ReactDOM.findDOMNode(changeRow)).find('a.js-doc-link').length);
-  });
-});
diff --git a/app/addons/documents/tests/nightwatch/deleteDatabaseModal.js b/app/addons/documents/tests/nightwatch/deleteDatabaseModal.js
index e986306..98163f9 100644
--- a/app/addons/documents/tests/nightwatch/deleteDatabaseModal.js
+++ b/app/addons/documents/tests/nightwatch/deleteDatabaseModal.js
@@ -36,6 +36,7 @@ module.exports = {
     client
       .loginToGUI()
       .url(baseUrl + '/#/database/_replicator/_all_docs')
+      .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
 
       .clickWhenVisible('.faux-header__doc-header-dropdown-toggle')
       .clickWhenVisible('.faux-header__doc-header-dropdown-itemwrapper .fonticon-trash')
@@ -54,6 +55,7 @@ module.exports = {
     client
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
 
       .clickWhenVisible('.faux-header__doc-header-dropdown-toggle')
       .clickWhenVisible('.faux-header__doc-header-dropdown-itemwrapper .fonticon-trash')
diff --git a/app/addons/documents/tests/nightwatch/viewClone.js b/app/addons/documents/tests/nightwatch/viewClone.js
index 74f1793..5a17075 100644
--- a/app/addons/documents/tests/nightwatch/viewClone.js
+++ b/app/addons/documents/tests/nightwatch/viewClone.js
@@ -10,8 +10,6 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-
-
 module.exports = {
 
   'Clones a view': function (client) {
diff --git a/app/addons/documents/tests/nightwatch/viewCreate.js b/app/addons/documents/tests/nightwatch/viewCreate.js
index 64b0972..36afcf0 100644
--- a/app/addons/documents/tests/nightwatch/viewCreate.js
+++ b/app/addons/documents/tests/nightwatch/viewCreate.js
@@ -10,8 +10,6 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-
-
 module.exports = {
 
   'creates design docs with js hint errors': function (client) {
@@ -30,11 +28,10 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
-      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/test_design_doc-selenium-0')
-      .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .assert.containsText('.prettyprint', 'blerg')
+      .waitForElementPresent('.table-view-docs', waitTime, false)
+      .assert.containsText('td[title="blerg"]', 'blerg')
     .end();
   },
 
@@ -54,12 +51,10 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
-      .clickWhenVisible('.fonticon-json')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
       .checkForDocumentCreated('_design/test_design_doc-selenium-1')
-      .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .assert.containsText('.prettyprint', 'hasehase')
+      .waitForElementPresent('.table-view-docs', waitTime, false)
+      .assert.containsText('td[title="hasehase"]', 'hasehase')
     .end();
   },
 
@@ -79,14 +74,12 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
-      .clickWhenVisible('.fonticon-json')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
       .checkForDocumentCreated('_design/test_design_doc-selenium-3')
-      .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
 
       // page now automatically redirects user to results of View. Confirm the new doc is present.
-      .assert.containsText('.prettyprint', 'hasehase')
+      .waitForElementPresent('.table-view-docs', waitTime, false)
+      .assert.containsText('td[title="hasehase"]', 'hasehase')
     .end();
   },
 
@@ -106,12 +99,10 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view')
-      .clickWhenVisible('.fonticon-json')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
       .checkForDocumentCreated('_design/test_design_doc-selenium-2')
-      .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .assert.containsText('.prettyprint', 'gansgans')
+      .waitForElementPresent('.table-view-docs', waitTime, false)
+      .assert.containsText('td[title="gansgans"]', 'gansgans')
     .end();
   },
 
@@ -136,12 +127,10 @@ module.exports = {
       ')
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
-      .clickWhenVisible('.fonticon-json')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
       .checkForDocumentCreated('_design/testdesigndoc/_view/test-new-view')
-      .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .assert.containsText('.prettyprint', 'enteente')
+      .waitForElementPresent('.table-view-docs', waitTime, false)
+      .assert.containsText('td[title="enteente"]', 'enteente')
     .end();
   }
 };
@@ -155,6 +144,7 @@ function openDifferentDropdownsAndClick (client) {
     .loginToGUI()
     .populateDatabase(newDatabaseName)
     .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+    .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
     .clickWhenVisible('.faux-header__doc-header-dropdown-toggle')
     .clickWhenVisible('.faux-header__doc-header-dropdown-itemwrapper a[href*="new_view"]')
     .waitForElementPresent('.index-cancel-link', waitTime, false);
diff --git a/app/addons/fauxton/components.js b/app/addons/fauxton/components.js
index 1781001..c70325d 100644
--- a/app/addons/fauxton/components.js
+++ b/app/addons/fauxton/components.js
@@ -14,8 +14,8 @@ import FauxtonAPI from "../../core/api";
 import React from "react";
 import ReactDOM from "react-dom";
 import { Modal } from "react-bootstrap";
-import "velocity-animate/velocity";
-import "velocity-animate/velocity.ui";
+// import "velocity-animate/velocity";
+// import "velocity-animate/velocity.ui";
 
 
 // formats a block of code and pretty-prints it in the page. Currently uses the prettyPrint plugin
diff --git a/app/addons/fauxton/notifications/tests/actionsSpec.js b/app/addons/fauxton/notifications/__tests__/actions.test.js
similarity index 64%
rename from app/addons/fauxton/notifications/tests/actionsSpec.js
rename to app/addons/fauxton/notifications/__tests__/actions.test.js
index 4b8ff56..0f91e8b 100644
--- a/app/addons/fauxton/notifications/tests/actionsSpec.js
+++ b/app/addons/fauxton/notifications/__tests__/actions.test.js
@@ -15,33 +15,31 @@ import Actions from "../actions";
 import utils from "../../../../../test/mocha/testUtils";
 import React from "react";
 import ReactDOM from "react-dom";
-import TestUtils from "react-addons-test-utils";
-import "sinon";
+import {mount} from 'enzyme';
+import sinon from "sinon";
 
 const store = Stores.notificationStore;
 const {restore, assert} = utils;
 
-describe('NotificationPanel', function () {
-  var container;
-
-  beforeEach(function () {
-    container = document.createElement('div');
+describe('NotificationPanel', () => {
+  beforeEach(() => {
     store.reset();
   });
 
-  afterEach(function () {
+  afterEach(() => {
     restore(Actions.clearAllNotifications);
-    ReactDOM.unmountComponentAtNode(container);
   });
 
-  it('clear all action fires', function () {
+  it('clear all action fires', () => {
     var stub = sinon.stub(Actions, 'clearAllNotifications');
 
-    var panelEl = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel
-      notifications={[]} filter={'all'}
-      visible={true} />, container);
+    var panelEl = mount(<Views.NotificationCenterPanel
+      notifications={[]}
+      style={{x: 1}}
+      filter={'all'}
+      visible={true} />);
 
-    TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+    panelEl.find('footer input').simulate('click');
     assert.ok(stub.calledOnce);
   });
 });
diff --git a/app/addons/fauxton/notifications/__tests__/components.test.js b/app/addons/fauxton/notifications/__tests__/components.test.js
new file mode 100644
index 0000000..eb68219
--- /dev/null
+++ b/app/addons/fauxton/notifications/__tests__/components.test.js
@@ -0,0 +1,207 @@
+// 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 FauxtonAPI from "../../../../core/api";
+import Views from "../notifications";
+import Stores from "../stores";
+import utils from "../../../../../test/mocha/testUtils";
+import React from "react";
+import ReactDOM from "react-dom";
+import moment from "moment";
+import {mount} from 'enzyme';
+import "sinon";
+var assert = utils.assert;
+var store = Stores.notificationStore;
+
+
+describe('NotificationController', () => {
+
+  beforeEach(() => {
+    store.reset();
+  });
+
+  it('notifications should be escaped by default', (done) => {
+    store._notificationCenterVisible = true;
+    const component = mount(<Views.NotificationController />);
+    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>' });
+    //use timer so that controller is displayed first
+    setTimeout(() => {
+      done();
+      assert.ok(/&lt;script&gt;window.whatever=1;&lt;\/script&gt;/.test(component.html()));
+    });
+  });
+
+  it('notifications should be able to render unescaped', (done) => {
+    store._notificationCenterVisible = true;
+    const component = mount(<Views.NotificationController />);
+    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>', escape: false });
+    setTimeout(() => {
+      done();
+      assert.ok(/<script>window.whatever=1;<\/script>/.test(component.html()));
+    });
+  });
+});
+
+describe('NotificationPanelRow', () => {
+  const notifications = {
+    success: {
+      notificationId: 1,
+      type: 'success',
+      msg: 'Success!',
+      cleanMsg: 'Success!',
+      time: moment()
+    },
+    info: {
+      notificationId: 2,
+      type: 'info',
+      msg: 'Error!',
+      cleanMsg: 'Error!',
+      time: moment()
+    },
+    error: {
+      notificationId: 3,
+      type: 'error',
+      msg: 'Error!',
+      cleanMsg: 'Error!',
+      time: moment()
+    }
+  };
+
+  const style = {
+    opacity: 1,
+    height: 64
+  };
+
+  it('shows all notification types when "all" filter applied', () => {
+    const row1 = mount(<Views.NotificationPanelRow
+      style={style}
+      isVisible={true}
+      filter="all"
+      item={notifications.success}
+      />);
+
+    assert.notOk(row1.find('li').prop('aria-hidden'));
+
+    const row2 = mount(<Views.NotificationPanelRow
+      style={style}
+      isVisible={true}
+      filter="all"
+      item={notifications.error}
+      />
+    );
+    assert.notOk(row2.find('li').prop('aria-hidden'));
+
+    const row3 = mount(<Views.NotificationPanelRow
+      style={style}
+      isVisible={true}
+      filter="all"
+      item={notifications.info}/>
+    );
+    assert.notOk(row3.find('li').prop('aria-hidden'));
+  });
+
+  it('hides notification when filter doesn\'t match', () => {
+    var rowEl = mount(
+      <Views.NotificationPanelRow
+      style={style}
+      isVisible={true}
+      filter="success"
+      item={notifications.info}
+      />);
+    assert.ok(rowEl.find('li').prop('aria-hidden'));
+  });
+
+  it('shows notification when filter exact match', () => {
+    const rowEl = mount(
+      <Views.NotificationPanelRow
+      style={style}
+      isVisible={true}
+      filter="info"
+      item={notifications.info}
+      />);
+    assert.notOk(rowEl.find('li').prop('aria-hidden'));
+  });
+});
+
+
+describe('NotificationCenterPanel', () => {
+  beforeEach(() => {
+    store.reset();
+  });
+
+  it('shows all notifications by default', (done) => {
+    store.addNotification({type: 'success', msg: 'Success are okay'});
+    store.addNotification({type: 'success', msg: 'another success.'});
+    store.addNotification({type: 'info', msg: 'A single info message'});
+    store.addNotification({type: 'error', msg: 'Error #1'});
+    store.addNotification({type: 'error', msg: 'Error #2'});
+    store.addNotification({type: 'error', msg: 'Error #3'});
+
+    var panelEl = mount(
+      <Views.NotificationCenterPanel
+        style={{x: 1}}
+        visible={true}
+        filter="all"
+        notifications={store.getNotifications()}
+      />);
+
+    setTimeout(() => {
+      done();
+      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 6);
+    });
+  });
+
+  it('appropriate filters are applied - 1', (done) => {
+    store.addNotification({type: 'success', msg: 'Success are okay'});
+    store.addNotification({type: 'success', msg: 'another success.'});
+    store.addNotification({type: 'info', msg: 'A single info message'});
+    store.addNotification({type: 'error', msg: 'Error #1'});
+    store.addNotification({type: 'error', msg: 'Error #2'});
+    store.addNotification({type: 'error', msg: 'Error #3'});
+
+    var panelEl = mount(
+      <Views.NotificationCenterPanel
+        style={{x: 1}}
+        visible={true}
+        filter="success"
+        notifications={store.getNotifications()}
+      />);
+
+    // there are 2 success messages
+    setTimeout(() => {
+      done();
+      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 2);
+    });
+  });
+
+  it('appropriate filters are applied - 2', (done) => {
+    store.addNotification({type: 'success', msg: 'Success are okay'});
+    store.addNotification({type: 'success', msg: 'another success.'});
+    store.addNotification({type: 'info', msg: 'A single info message'});
+    store.addNotification({type: 'error', msg: 'Error #1'});
+    store.addNotification({type: 'error', msg: 'Error #2'});
+    store.addNotification({type: 'error', msg: 'Error #3'});
+
+    var panelEl = mount(
+      <Views.NotificationCenterPanel
+        style={{x: 1}}
+        visible={true}
+        filter="error"
+        notifications={store.getNotifications()}
+      />);
+
+    // 3 errors
+    setTimeout(() => {
+      done();
+      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 3);
+    });
+  });
+});
diff --git a/app/addons/fauxton/notifications/tests/storesSpec.js b/app/addons/fauxton/notifications/__tests__/stores.test.js
similarity index 84%
rename from app/addons/fauxton/notifications/tests/storesSpec.js
rename to app/addons/fauxton/notifications/__tests__/stores.test.js
index 47d35de..d21ca19 100644
--- a/app/addons/fauxton/notifications/tests/storesSpec.js
+++ b/app/addons/fauxton/notifications/__tests__/stores.test.js
@@ -13,22 +13,22 @@
 import utils from "../../../../../test/mocha/testUtils";
 import Stores from "../stores";
 
-var assert = utils.assert;
-var store = Stores.notificationStore;
+const assert = utils.assert;
+const store = Stores.notificationStore;
 
-describe('Notification Store', function () {
+describe('Notification Store', () => {
 
-  beforeEach(function () {
+  beforeEach(() => {
     store.reset();
   });
 
-  it("sets reasonable defaults", function () {
+  it("sets reasonable defaults", () => {
     assert.equal(store.getNotifications().length, 0);
     assert.equal(store.isNotificationCenterVisible(), false);
     assert.equal(store.getNotificationFilter(), 'all');
   });
 
-  it("confirm only known notification types get added", function () {
+  it("confirm only known notification types get added", () => {
     assert.equal(store.getNotifications().length, 0);
     store.addNotification({ type: 'success', msg: 'Success are okay' });
 
@@ -45,26 +45,26 @@ describe('Notification Store', function () {
     assert.equal(store.getNotifications().length, 3);
   });
 
-  it("clearNotification clears a specific notification", function () {
+  it("clearNotification clears a specific notification", () => {
     store.addNotification({ type: 'success', msg: 'one' });
     store.addNotification({ type: 'success', msg: 'two' });
     store.addNotification({ type: 'success', msg: 'three' });
     store.addNotification({ type: 'success', msg: 'four' });
 
-    var notifications = store.getNotifications();
+    const notifications = store.getNotifications();
     assert.equal(notifications.length, 4);
 
     // find the notification ID of the "three" message
-    var notification = _.findWhere(notifications, { msg: 'three' });
+    const notification = _.findWhere(notifications, { msg: 'three' });
     store.clearNotification(notification.notificationId);
 
     // confirm it was removed
-    var updatedNotifications = store.getNotifications();
+    const updatedNotifications = store.getNotifications();
     assert.equal(updatedNotifications.length, 3);
     assert.equal(_.findWhere(updatedNotifications, { msg: 'three' }), undefined);
   });
 
-  it("setNotificationFilter only sets for known notification types", function () {
+  it("setNotificationFilter only sets for known notification types", () => {
     store.setNotificationFilter('all');
     assert.equal(store.getNotificationFilter(), 'all');
 
@@ -81,7 +81,7 @@ describe('Notification Store', function () {
     assert.equal(store.getNotificationFilter(), 'info'); // this check it's still set to the previously set value
   });
 
-  it("clear all notifications", function () {
+  it("clear all notifications", () => {
     store.addNotification({ type: 'success', msg: 'one' });
     store.addNotification({ type: 'success', msg: 'two' });
     store.addNotification({ type: 'success', msg: 'three' });
diff --git a/app/addons/fauxton/notifications/notifications.js b/app/addons/fauxton/notifications/notifications.js
index ceccd9b..43f33e0 100644
--- a/app/addons/fauxton/notifications/notifications.js
+++ b/app/addons/fauxton/notifications/notifications.js
@@ -15,24 +15,21 @@ import ReactDOM from "react-dom";
 import Actions from "./actions";
 import Stores from "./stores";
 import Components from "../../components/react-components";
-import VelocityReact from "velocity-react";
-import "velocity-animate/velocity";
-import "velocity-animate/velocity.ui";
+import {TransitionMotion, spring, presets} from 'react-motion';
 import uuid from 'uuid';
 
 var store = Stores.notificationStore;
-var VelocityComponent = VelocityReact.VelocityComponent;
 const {Copy} = Components;
 
 // The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
 // notification center panel
 export const NotificationController = React.createClass({
 
-  getInitialState: function () {
+  getInitialState () {
     return this.getStoreState();
   },
 
-  getStoreState: function () {
+  getStoreState () {
     return {
       notificationCenterVisible: store.isNotificationCenterVisible(),
       notificationCenterFilter: store.getNotificationFilter(),
@@ -40,27 +37,66 @@ export const NotificationController = React.createClass({
     };
   },
 
-  componentDidMount: function () {
+  componentDidMount () {
     store.on('change', this.onChange, this);
   },
 
-  componentWillUnmount: function () {
+  componentWillUnmount () {
     store.off('change', this.onChange);
   },
 
-  onChange: function () {
+  onChange () {
     this.setState(this.getStoreState());
   },
 
-  render: function () {
+  getStyles () {
+    const isVisible = this.state.notificationCenterVisible;
+    let item = {
+      key: '1',
+      style: {
+        x: 320
+      }
+    };
+
+    if (isVisible) {
+      item.style = {
+        x: spring(0)
+      };
+    }
+
+    if (!isVisible) {
+      item.style = {
+        x: spring(320)
+      };
+    }
+    return [item];
+  },
+
+  getNotificationCenterPanel (items) {
+    const panel = items.map(({style}) => {
+      return <NotificationCenterPanel
+        key={'1'}
+        style={style}
+        visible={this.state.notificationCenterVisible}
+        filter={this.state.notificationCenterFilter}
+        notifications={this.state.notifications} />;
+    });
+    return (
+      <span>
+        {panel}
+      </span>
+    );
+  },
+
+  render () {
     return (
       <div>
         <GlobalNotifications
           notifications={this.state.notifications} />
-        <NotificationCenterPanel
-          visible={this.state.notificationCenterVisible}
-          filter={this.state.notificationCenterFilter}
-          notifications={this.state.notifications} />
+        <TransitionMotion
+          styles={this.getStyles}>
+          {this.getNotificationCenterPanel}
+        </TransitionMotion>
       </div>
     );
   }
@@ -72,22 +108,22 @@ var GlobalNotifications = React.createClass({
     notifications: React.PropTypes.array.isRequired
   },
 
-  componentDidMount: function () {
+  componentDidMount () {
     $(document).on('keydown.notificationClose', this.onKeyDown);
   },
 
-  componentWillUnmount: function () {
+  componentWillUnmount () {
     $(document).off('keydown.notificationClose', this.onKeyDown);
   },
 
-  onKeyDown: function (e) {
+  onKeyDown (e) {
     var code = e.keyCode || e.which;
     if (code === 27) {
       Actions.hideAllVisibleNotifications();
     }
   },
 
-  getNotifications: function () {
+  getNotifications () {
     if (!this.props.notifications.length) {
       return null;
     }
@@ -113,10 +149,69 @@ var GlobalNotifications = React.createClass({
     }, this);
   },
 
-  render: function () {
+  getchildren (items) {
+    const notifications = items.map(({key, data, style}) => {
+      const notification = data;
+      return (
+        <Notification
+          key={key}
+          style={style}
+          notificationId={notification.notificationId}
+          isHiding={notification.isHiding}
+          msg={notification.msg}
+          type={notification.type}
+          escape={notification.escape}
+          onStartHide={Actions.startHidingNotification}
+          onHideComplete={Actions.hideNotification} />
+      );
+    });
+
+    return (
+      <div>
+        {notifications}
+      </div>
+    );
+  },
+
+  getStyles (prevItems) {
+    if (!prevItems) {
+      prevItems = [];
+    }
+
+    return this.props.notifications
+      .filter(notification => notification.visible)
+      .map(notification => {
+        let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
+        let style = !item ? {opacity: 0.5, minHeight: 50} : false;
+
+        if (!style && !notification.isHiding) {
+          style = {
+            opacity: spring(1, presets.stiff),
+            minHeight: spring(64)
+          };
+        } else if (!style && notification.isHiding) {
+          style = {
+            opacity: spring(0, presets.stiff),
+            minHeight: spring(0, presets.stiff)
+          };
+        }
+
+        return {
+          key: notification.notificationId.toString(),
+          style,
+          data: notification
+        };
+      });
+  },
+
+  render () {
     return (
       <div id="global-notifications">
-        {this.getNotifications()}
+        <TransitionMotion
+          styles={this.getStyles}
+        >
+          {this.getchildren}
+        </TransitionMotion>
       </div>
     );
   }
@@ -134,57 +229,25 @@ var Notification = React.createClass({
     visibleTime: React.PropTypes.number
   },
 
-  getDefaultProps: function () {
+  getDefaultProps () {
     return {
       type: 'info',
       visibleTime: 8000,
-      escape: true,
-      slideInTime: 200,
-      slideOutTime: 200
+      escape: true
     };
   },
 
-  componentWillUnmount: function () {
+  componentWillUnmount () {
     if (this.timeout) {
       window.clearTimeout(this.timeout);
     }
   },
 
-  getInitialState: function () {
-    return {
-      animation: { opacity: 0, minHeight: 0 }
-    };
-  },
-
-  componentDidMount: function () {
-    this.setState({
-      animation: {
-        opacity: (this.props.isHiding) ? 0 : 1,
-        minHeight: (this.props.isHiding) ? 0 : ReactDOM.findDOMNode(this.refs.notification).offsetHeight
-      }
-    });
-
-    this.timeout = setTimeout(function () {
-      this.hide();
-    }.bind(this), this.props.visibleTime);
-  },
-
-  componentDidUpdate: function (prevProps) {
-    if (!prevProps.isHiding && this.props.isHiding) {
-      this.setState({
-        animation: {
-          opacity: 0,
-          minHeight: 0
-        }
-      });
-    }
-  },
-
-  getHeight: function () {
-    return $(ReactDOM.findDOMNode(this)).outerHeight(true);
+  componentDidMount () {
+    this.timeout = setTimeout(this.hide, this.props.visibleTime);
   },
 
-  hide: function (e) {
+  hide (e) {
     if (e) {
       e.preventDefault();
     }
@@ -192,58 +255,64 @@ var Notification = React.createClass({
   },
 
   // many messages contain HTML, hence the need for dangerouslySetInnerHTML
-  getMsg: function () {
+  getMsg () {
     var msg = (this.props.escape) ? _.escape(this.props.msg) : this.props.msg;
     return {
       __html: msg
     };
   },
 
-  onAnimationComplete: function () {
+  onAnimationComplete () {
     if (this.props.isHiding) {
-      this.props.onHideComplete(this.props.notificationId);
+      window.setTimeout(() => this.props.onHideComplete(this.props.notificationId));
     }
   },
 
-  render: function () {
-    var iconMap = {
+  render () {
+    const {style, notificationId} = this.props;
+    const iconMap = {
       error: 'fonticon-attention-circled',
       info: 'fonticon-info-circled',
       success: 'fonticon-ok-circled'
     };
 
+    if (style.opacity === 0 && this.props.isHiding) {
+      this.onAnimationComplete();
+    }
+
     return (
-      <VelocityComponent animation={this.state.animation}
-        runOnMount={true} duration={this.props.slideInTime} complete={this.onAnimationComplete}>
-          <div className="notification-wrapper">
-            <div className={'global-notification alert alert-' + this.props.type} ref="notification">
-              <a data-bypass href="#" onClick={this.hide}><i className="pull-right fonticon-cancel" /></a>
-              <i className={'notification-icon ' + iconMap[this.props.type]} />
-              <span dangerouslySetInnerHTML={this.getMsg()}></span>
-            </div>
-          </div>
-      </VelocityComponent>
+      <div
+        key={notificationId.toString()} className="notification-wrapper" style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}>
+        <div
+          style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}
+          className={'global-notification alert alert-' + this.props.type}
+          ref="notification">
+          <a data-bypass href="#" onClick={this.hide}><i className="pull-right fonticon-cancel" /></a>
+          <i className={'notification-icon ' + iconMap[this.props.type]} />
+          <span dangerouslySetInnerHTML={this.getMsg()}></span>
+        </div>
+      </div>
     );
   }
 });
 
 
 export const NotificationCenterButton = React.createClass({
-  getInitialState: function () {
+  getInitialState () {
     return {
       visible: true
     };
   },
 
-  hide: function () {
+  hide () {
     this.setState({ visible: false });
   },
 
-  show: function () {
+  show () {
     this.setState({ visible: true });
   },
 
-  render: function () {
+  render () {
     var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
     return (
       <div className={classes} onClick={Actions.showNotificationCenter}></div>
@@ -259,32 +328,67 @@ var NotificationCenterPanel = React.createClass({
     notifications: React.PropTypes.array.isRequired
   },
 
-  getNotifications: function () {
-    if (!this.props.notifications.length) {
-      return (
-        <li className="no-notifications">No notifications.</li>
-      );
+  getNotifications (items) {
+    let notifications;
+    if (!items.length && !this.props.notifications.length) {
+        notifications = <li className="no-notifications">
+          No notifications.
+        </li>;
+    } else {
+      notifications = items
+      .map(({key, data: notification, style}) => {
+        return (
+          <NotificationPanelRow
+            isVisible={this.props.visible}
+            item={notification}
+            filter={this.props.filter}
+            key={key}
+            style={style}
+          />
+        );
+      });
     }
 
-    return _.map(this.props.notifications, function (notification) {
-      return (
-        <NotificationPanelRow
-          isVisible={this.props.visible}
-          item={notification}
-          filter={this.props.filter}
-          key={notification.notificationId}
-        />
-      );
-    }, this);
+    return (
+      <ul className="notification-list">
+        {notifications}
+      </ul>
+    );
   },
 
-  render: function () {
-    var panelClasses = 'notification-center-panel flex-layout flex-col';
-    if (this.props.visible) {
-      panelClasses += ' visible';
+  getStyles (prevItems = []) {
+    return this.props.notifications
+    .map(notification => {
+      let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
+      let style = !item ? {opacity: 0, height: 0} : false;
+
+      if (!style && (notification.type === this.props.filter || this.props.filter === 'all')) {
+        style = {
+          opacity: spring(1, presets.stiff),
+          height: spring(61, presets.stiff)
+        };
+      } else if (notification.type !== this.props.filter) {
+        style = {
+          opacity: spring(0, presets.stiff),
+          height: spring(0, presets.stiff)
+        };
+      }
+
+      return {
+        key: notification.notificationId.toString(),
+        style,
+        data: notification
+      };
+    });
+  },
+
+  render () {
+    if (!this.props.visible && this.props.style.x === 0) {
+      // panelClasses += ' visible';
+      return null;
     }
 
-    var filterClasses = {
+    const filterClasses = {
       all: 'flex-body',
       success: 'flex-body',
       error: 'flex-body',
@@ -292,10 +396,11 @@ var NotificationCenterPanel = React.createClass({
     };
     filterClasses[this.props.filter] += ' selected';
 
-    var maskClasses = 'notification-page-mask' + ((this.props.visible) ? ' visible' : '');
+    const maskClasses = `notification-page-mask ${((this.props.visible) ? ' visible' : '')}`;
+    const panelClasses = 'notification-center-panel flex-layout flex-col visible';
     return (
       <div id="notification-center">
-        <div className={panelClasses}>
+        <div className={panelClasses} style={{transform: `translate(${this.props.style.x}px)`}}>
 
           <header className="flex-layout flex-row">
             <span className="fonticon fonticon-bell" />
@@ -321,9 +426,9 @@ var NotificationCenterPanel = React.createClass({
           </ul>
 
           <div className="flex-body">
-            <ul className="notification-list">
-              {this.getNotifications()}
-            </ul>
+            <TransitionMotion styles={this.getStyles}>
+              {this.getNotifications}
+            </TransitionMotion>
           </div>
 
           <footer>
@@ -344,78 +449,35 @@ var NotificationCenterPanel = React.createClass({
 
 var NotificationPanelRow = React.createClass({
   propTypes: {
-    item: React.PropTypes.object.isRequired,
-    filter: React.PropTypes.string.isRequired,
-    transitionSpeed: React.PropTypes.number
-  },
-
-  getDefaultProps: function () {
-    return {
-      transitionSpeed: 300
-    };
-  },
-
-  clearNotification: function () {
-    var notificationId = this.props.item.notificationId;
-    this.hide(function () {
-      Actions.clearSingleNotification(notificationId);
-    });
+    item: React.PropTypes.object.isRequired
   },
 
-  componentDidMount: function () {
-    this.setState({
-      elementHeight: this.getHeight()
-    });
+  clearNotification () {
+    const {notificationId} = this.props.item;
+    Actions.clearSingleNotification(notificationId);
   },
 
-  componentDidUpdate: function (prevProps) {
-    // in order for the nice slide effects to work we need a concrete element height to slide to and from.
-    // $.outerHeight() only works reliably on visible elements, hence this additional setState here
-    if (!prevProps.isVisible && this.props.isVisible) {
-      this.setState({
-        elementHeight: this.getHeight()
-      });
-    }
-
-    var show = true;
-    if (this.props.filter !== 'all') {
-      show = this.props.item.type === this.props.filter;
-    }
-    if (show) {
-      $(ReactDOM.findDOMNode(this)).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
-      return;
-    }
-    this.hide();
-  },
-
-  getHeight: function () {
-    return $(ReactDOM.findDOMNode(this)).outerHeight(true);
-  },
-
-  hide: function (onHidden) {
-    $(ReactDOM.findDOMNode(this)).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
-      if (onHidden) {
-        onHidden();
-      }
-    });
-  },
-
-  render: function () {
-    var iconMap = {
+  render () {
+    const iconMap = {
       success: 'fonticon-ok-circled',
       error: 'fonticon-attention-circled',
       info: 'fonticon-info-circled'
     };
 
-    var timeElapsed = this.props.item.time.fromNow();
+    const timeElapsed = this.props.item.time.fromNow();
 
     // we can safely do this because the store ensures all notifications are of known types
-    var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
-    var hidden = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? false : true;
+    const rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
+    const hidden = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? false : true;
+    const {style} = this.props;
+    const {opacity, height} = style;
+    if (opacity === 0 && height === 0) {
+      return null;
+    }
 
     // N.B. wrapper <div> needed to ensure smooth hide/show transitions
     return (
-      <li aria-hidden={hidden}>
+      <li style={{opactiy: opacity, height: height + 'px', borderBottomColor: `rgba(34, 34, 34, ${opacity})`}} aria-hidden={hidden}>
         <div className="flex-layout flex-row">
           <span className={rowIconClasses}></span>
           <div className="flex-body">
diff --git a/app/addons/fauxton/notifications/tests/componentsSpec.js b/app/addons/fauxton/notifications/tests/componentsSpec.js
deleted file mode 100644
index 8e97d77..0000000
--- a/app/addons/fauxton/notifications/tests/componentsSpec.js
+++ /dev/null
@@ -1,188 +0,0 @@
-// 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 FauxtonAPI from "../../../../core/api";
-import Views from "../notifications";
-import Stores from "../stores";
-import utils from "../../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
-import moment from "moment";
-import TestUtils from "react-addons-test-utils";
-import "sinon";
-var assert = utils.assert;
-var store = Stores.notificationStore;
-
-
-describe('NotificationController', function () {
-  var container;
-
-  beforeEach(function () {
-    container = document.createElement('div');
-    store.reset();
-  });
-
-  afterEach(function () {
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('notifications should be escaped by default', function () {
-    var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
-    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>' });
-    assert.ok(/&lt;script&gt;window.whatever=1;&lt;\/script&gt;/.test(ReactDOM.findDOMNode(component).innerHTML));
-  });
-
-  it('notifications should be able to render unescaped', function () {
-    var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
-    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>', escape: false });
-    assert.ok(/<script>window.whatever=1;<\/script>/.test(ReactDOM.findDOMNode(component).innerHTML));
-  });
-});
-
-describe('NotificationPanelRow', function () {
-  var container;
-
-  var notifications = {
-    success: {
-      notificationId: 1,
-      type: 'success',
-      msg: 'Success!',
-      time: moment()
-    },
-    info: {
-      notificationId: 2,
-      type: 'info',
-      msg: 'Error!',
-      time: moment()
-    },
-    error: {
-      notificationId: 3,
-      type: 'error',
-      msg: 'Error!',
-      time: moment()
-    }
-  };
-
-  beforeEach(function () {
-    container = document.createElement('div');
-  });
-
-  afterEach(function () {
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('shows all notification types when "all" filter applied', function () {
-    var row1 = TestUtils.renderIntoDocument(
-      <Views.NotificationPanelRow filter="all" item={notifications.success}/>,
-      container
-    );
-    assert.equal($(ReactDOM.findDOMNode(row1)).attr('aria-hidden'), 'false');
-    ReactDOM.unmountComponentAtNode(container);
-
-    var row2 = TestUtils.renderIntoDocument(
-      <Views.NotificationPanelRow filter="all" item={notifications.error}/>,
-      container
-    );
-    assert.equal($(ReactDOM.findDOMNode(row2)).attr('aria-hidden'), 'false');
-    ReactDOM.unmountComponentAtNode(container);
-
-    var row3 = TestUtils.renderIntoDocument(
-      <Views.NotificationPanelRow filter="all" item={notifications.info}/>,
-      container
-    );
-    assert.equal($(ReactDOM.findDOMNode(row3)).attr('aria-hidden'), 'false');
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('hides notification when filter doesn\'t match', function () {
-    var rowEl = TestUtils.renderIntoDocument(
-      <Views.NotificationPanelRow filter="success" item={notifications.info}/>,
-      container
-    );
-    assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'true');
-  });
-
-  it('shows notification when filter exact match', function () {
-    var rowEl = TestUtils.renderIntoDocument(
-      <Views.NotificationPanelRow filter="info" item={notifications.info}/>,
-      container
-    );
-    assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'false');
-  });
-});
-
-
-describe('NotificationCenterPanel', function () {
-  var container;
-
-  beforeEach(function () {
-    container = document.createElement('div');
-    store.reset();
-  });
-
-  afterEach(function () {
-    ReactDOM.unmountComponentAtNode(container);
-  });
-
-  it('shows all notifications by default', function () {
-    store.addNotification({type: 'success', msg: 'Success are okay'});
-    store.addNotification({type: 'success', msg: 'another success.'});
-    store.addNotification({type: 'info', msg: 'A single info message'});
-    store.addNotification({type: 'error', msg: 'Error #1'});
-    store.addNotification({type: 'error', msg: 'Error #2'});
-    store.addNotification({type: 'error', msg: 'Error #3'});
-
-    var panelEl = TestUtils.renderIntoDocument(
-      <Views.NotificationCenterPanel
-        filter="all"
-        notifications={store.getNotifications()}
-      />, container);
-
-    assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 6);
-  });
-
-  it('appropriate filters are applied - 1', function () {
-    store.addNotification({type: 'success', msg: 'Success are okay'});
-    store.addNotification({type: 'success', msg: 'another success.'});
-    store.addNotification({type: 'info', msg: 'A single info message'});
-    store.addNotification({type: 'error', msg: 'Error #1'});
-    store.addNotification({type: 'error', msg: 'Error #2'});
-    store.addNotification({type: 'error', msg: 'Error #3'});
-
-    var panelEl = TestUtils.renderIntoDocument(
-      <Views.NotificationCenterPanel
-        filter="success"
-        notifications={store.getNotifications()}
-      />, container);
-
-    // there are 2 success messages
-    assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 2);
-  });
-
-  it('appropriate filters are applied - 2', function () {
-    store.addNotification({type: 'success', msg: 'Success are okay'});
-    store.addNotification({type: 'success', msg: 'another success.'});
-    store.addNotification({type: 'info', msg: 'A single info message'});
-    store.addNotification({type: 'error', msg: 'Error #1'});
-    store.addNotification({type: 'error', msg: 'Error #2'});
-    store.addNotification({type: 'error', msg: 'Error #3'});
-
-    var panelEl = TestUtils.renderIntoDocument(
-      <Views.NotificationCenterPanel
-        filter="error"
-        notifications={store.getNotifications()}
-      />, container);
-
-    // 3 errors
-    assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 3);
-  });
-
-});
diff --git a/app/addons/fauxton/tests/nightwatch/notificationCenter.js b/app/addons/fauxton/tests/nightwatch/notificationCenter.js
index 1f17fdc..8360041 100644
--- a/app/addons/fauxton/tests/nightwatch/notificationCenter.js
+++ b/app/addons/fauxton/tests/nightwatch/notificationCenter.js
@@ -27,9 +27,9 @@ module.exports = {
       .loginToGUI()
       .waitForElementNotPresent('.notification-wrapper', waitTime, false)
       .waitForElementPresent('#notification-center-btn', waitTime, false)
-      .assert.cssClassNotPresent('.notification-center-panel', 'visible')
+      .assert.cssClassNotPresent('.notification-page-mask', 'visible')
       .clickWhenVisible('#notification-center-btn', waitTime, false)
-      .waitForElementPresent('.notification-center-panel.visible', waitTime, false)
+      .waitForElementPresent('.notification-page-mask.visible', waitTime, false)
       .waitForElementPresent('.notification-list div.flex-layout', waitTime, false)
 
       .getText('.notification-center-panel', function (result) {
diff --git a/app/core/tests/apiSpec.js b/app/core/__tests__/api.test.js
similarity index 79%
rename from app/core/tests/apiSpec.js
rename to app/core/__tests__/api.test.js
index 28e4f45..a01c9eb 100644
--- a/app/core/tests/apiSpec.js
+++ b/app/core/__tests__/api.test.js
@@ -11,15 +11,15 @@
 // the License.
 import FauxtonAPI from "../api";
 import testUtils from "../../../test/mocha/testUtils";
-var assert = testUtils.assert;
+const assert = testUtils.assert;
 
-describe('URLs', function () {
+describe('URLs', () => {
 
-  it('can register and get url', function () {
-    var testUrl = 'this_is_a_test_url';
+  it('can register and get url', () => {
+    const testUrl = 'this_is_a_test_url';
 
     FauxtonAPI.registerUrls('testURL', {
-      server: function () {
+      server: () => {
         return testUrl;
       }
     });
@@ -28,8 +28,8 @@ describe('URLs', function () {
 
   });
 
-  it('can register interceptor to change url', function () {
-    var testUrl = 'interceptor_url';
+  it('can register interceptor to change url', () => {
+    const testUrl = 'interceptor_url';
 
     FauxtonAPI.registerExtension('urls:interceptors', function (name, context) {
       if (name === 'testURL' && context === 'intercept') {
diff --git a/app/core/tests/utilsSpec.js b/app/core/__tests__/utils.test.js
similarity index 61%
rename from app/core/tests/utilsSpec.js
rename to app/core/__tests__/utils.test.js
index ada4bfb..19b5035 100644
--- a/app/core/tests/utilsSpec.js
+++ b/app/core/__tests__/utils.test.js
@@ -11,77 +11,77 @@
 // the License.
 import testUtils from "../../../test/mocha/testUtils";
 import utils from "../utils";
-var assert = testUtils.assert;
+const assert = testUtils.assert;
 
-describe('Utils', function () {
+describe('Utils', () => {
 
-  describe('getDocTypeFromId', function () {
+  describe('getDocTypeFromId', () => {
 
-    it('returns doc if id not given', function () {
-      var res = utils.getDocTypeFromId();
+    it('returns doc if id not given', () => {
+      const res = utils.getDocTypeFromId();
       assert.equal(res, 'doc');
     });
 
-    it('returns design doc for design docs', function () {
-      var res = utils.getDocTypeFromId('_design/foobar');
+    it('returns design doc for design docs', () => {
+      const res = utils.getDocTypeFromId('_design/foobar');
       assert.equal(res, 'design doc');
     });
 
-    it('returns doc for all others', function () {
-      var res = utils.getDocTypeFromId('blerg');
+    it('returns doc for all others', () => {
+      const res = utils.getDocTypeFromId('blerg');
       assert.equal(res, 'doc');
     });
   });
 
-  describe('getSafeIdForDoc', function () {
+  describe('getSafeIdForDoc', () => {
 
-    it('keeps _design/ intact', function () {
-      var res = utils.getSafeIdForDoc('_design/foo/do');
+    it('keeps _design/ intact', () => {
+      const res = utils.getSafeIdForDoc('_design/foo/do');
       assert.equal(res, '_design/foo%2Fdo');
     });
 
-    it('encodes all other', function () {
-      var res = utils.getSafeIdForDoc('_redesign/foobar');
+    it('encodes all other', () => {
+      const res = utils.getSafeIdForDoc('_redesign/foobar');
       assert.equal(res, '_redesign%2Ffoobar');
     });
   });
 
-  describe('safeURLName', function () {
+  describe('safeURLName', () => {
 
-    it('encodes special chars', function () {
+    it('encodes special chars', () => {
       assert.equal('foo-bar%2Fbaz', utils.safeURLName('foo-bar/baz'));
     });
 
-    it('encodes an encoded doc', function () {
+    it('encodes an encoded doc', () => {
       assert.equal('foo-bar%252Fbaz', utils.safeURLName('foo-bar%2Fbaz'));
     });
   });
 
-  describe('isSystemDatabase', function () {
+  describe('isSystemDatabase', () => {
 
-    it('detects system databases', function () {
+    it('detects system databases', () => {
       assert.ok(utils.isSystemDatabase('_replicator'));
     });
 
-    it('ignores other dbs', function () {
+    it('ignores other dbs', () => {
       assert.notOk(utils.isSystemDatabase('foo'));
     });
   });
 
-  describe('localStorage', function () {
+  describe('localStorage', () => {
 
-    it('Should get undefined when getting a non-existent key', function () {
+    it('Should get undefined when getting a non-existent key', () => {
       assert.isUndefined(utils.localStorageGet('qwerty'));
     });
 
-    it ('Should get value after setting it', function () {
-      var key = 'key1';
+    it ('Should get value after setting it', () => {
+      const key = 'key1';
       utils.localStorageSet(key, 1);
       assert.equal(utils.localStorageGet(key), 1);
     });
 
-    it ('Set and retrieve complex object', function () {
-      var key = 'key2',
+    it ('Set and retrieve complex object', () => {
+      const key = 'key2',
         obj = {
           one: 1,
           two: ['1', 'string', 3]
@@ -90,7 +90,7 @@ describe('Utils', function () {
       assert.deepEqual(utils.localStorageGet(key), obj);
     });
 
-    it ('stripHTML removes HTML', function () {
+    it ('stripHTML removes HTML', () => {
       [
         { html: '<span>okay</span>', text: 'okay' },
         { html: 'test <span>before</span> and after', text: 'test before and after' },
diff --git a/assets/less/notification-center.less b/assets/less/notification-center.less
index 0807e9c..caaa7ec 100644
--- a/assets/less/notification-center.less
+++ b/assets/less/notification-center.less
@@ -33,22 +33,16 @@ body #dashboard #notification-center-btn {
 
 #notification-center {
   .notification-center-panel {
-    z-index: 12;
-    .translate(306px, 0px);/* the 6px extra is for the left shadow */
+    z-index: 112;
     position: fixed;
     box-shadow: 0 6px 5px 4px rgba(0, 0, 0, 0.1);
     top: 0;
     right: 0;
     width: 300px;
     height: 100%;
-    .transition(all .3s ease-out);
     background-color: #333333;
     color: #dddddd;
 
-    &.visible {
-      .translate(0px, 0px);
-    }
-
     header {
       .flex(0 0 auto);
       padding: 16px;
@@ -204,32 +198,32 @@ body #dashboard #notification-center-btn {
     bottom: 0;
     left: 0;
     opacity: 0;
-    .transition(all .3s ease-out);
 
     &.visible {
-      z-index: 10;
+      z-index: 110;
       opacity: 0.3;
       background-color: #000000;
     }
   }
 }
 
-@-webkit-keyframes in {
-  0% { max-height: 0; }
-  100% { max-height: 1000px; }
-}
-
-@-webkit-keyframes out {
-  0% { max-height: 1000px; }
-  100% { max-height:0; }
-}
-
-@keyframes in {
-  0% { max-height: 0; }
-  100% { max-height: 1000px; }
-}
-
-@keyframes out {
-  0% { max-height: 1000px; }
-  100% { max-height:0; }
-}
+//Leaving this in for now as I'm not sure where it is used
+// @-webkit-keyframes in {
+//   0% { max-height: 0; }
+//   100% { max-height: 1000px; }
+// }
+//
+// @-webkit-keyframes out {
+//   0% { max-height: 1000px; }
+//   100% { max-height:0; }
+// }
+//
+// @keyframes in {
+//   0% { max-height: 0; }
+//   100% { max-height: 1000px; }
+// }
+//
+// @keyframes out {
+//   0% { max-height: 1000px; }
+//   100% { max-height:0; }
+// }
diff --git a/devserver.js b/devserver.js
index 332a699..98b13cb 100644
--- a/devserver.js
+++ b/devserver.js
@@ -88,6 +88,7 @@ const runWebpackServer = function () {
     overlay: true,
     hot: false,
     historyApiFallback: false,
+    disableHostCheck: true,
     stats: {
       colors: true,
     },
diff --git a/package.json b/package.json
index 786426b..45ed22b 100644
--- a/package.json
+++ b/package.json
@@ -88,9 +88,9 @@
     "pouchdb-adapter-http": "^6.1.2",
     "pouchdb-core": "^6.1.2",
     "react": "~15.4.1",
-    "react-addons-css-transition-group": "~15.4.2",
     "react-bootstrap": "^0.30.7",
     "react-dom": "~15.4.1",
+    "react-motion": "^0.5.0",
     "react-redux": "^5.0.0",
     "react-select": "1.0.0-rc.2",
     "redux": "^3.6.0",
@@ -105,8 +105,6 @@
     "url-polyfill": "git+https://github.com/webcomponents/URL.git",
     "urls": "~0.0.3",
     "uuid": "^3.0.1",
-    "velocity-animate": "^1.4.2",
-    "velocity-react": "1.2.0",
     "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
     "webpack": "~2.2.1",
     "webpack-dev-server": "~2.4.1",
diff --git a/test/nightwatch_tests/custom-commands/closeNotification.js b/test/nightwatch_tests/custom-commands/closeNotification.js
index 5c29d56..6350d54 100644
--- a/test/nightwatch_tests/custom-commands/closeNotification.js
+++ b/test/nightwatch_tests/custom-commands/closeNotification.js
@@ -18,7 +18,8 @@ exports.command = function () {
 
   client
     .waitForElementPresent(dismissSelector, helpers.maxWaitTime, false)
-    .click(dismissSelector);
+    .click(dismissSelector)
+    .waitForElementNotPresent(dismissSelector, helpers.maxWaitTime, false);
 
   return this;
 };

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