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(/<script>window.whatever=1;<\/script>/.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(/<script>window.whatever=1;<\/script>/.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>'].