From 3381d9c222867e45f022816e2ff238a36f1cfff1 Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 7 Jul 2016 16:16:38 -0300 Subject: [PATCH 1/5] Ivan - OS-48 - Add basic login logout logic --- client/src/actions/common-actions.js | 4 ++- client/src/app/App.js | 14 ++++++-- .../app/main/dashboard/dashboard-layout.js | 10 ++++++ .../main-home/main-home-page-login-widget.js | 29 +++++++++++++-- .../src/app/main/main-home/main-home-page.js | 9 +++++ client/src/app/main/main-layout-header.js | 30 ++++++++++++++-- .../app/main/main-signup/main-signup-page.js | 8 +++++ client/src/core-components/form.js | 33 +++++++++++++---- client/src/data/fixtures/user-fixtures.js | 10 ++++++ client/src/lib-app/api-call.js | 10 ++++-- client/src/lib-app/session-store.js | 35 ++++++++++++++++++ client/src/stores/common-store.js | 14 ++++++++ client/src/stores/user-store.js | 36 ++++++++++++++++--- 13 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 client/src/lib-app/session-store.js diff --git a/client/src/actions/common-actions.js b/client/src/actions/common-actions.js index e15290ef..8112df05 100644 --- a/client/src/actions/common-actions.js +++ b/client/src/actions/common-actions.js @@ -1,7 +1,9 @@ import Reflux from 'reflux'; let CommonActions = Reflux.createActions([ - 'changeLanguage' + 'changeLanguage', + 'logged', + 'loggedOut' ]); export default CommonActions; \ No newline at end of file diff --git a/client/src/app/App.js b/client/src/app/App.js index cb34adc4..b0364d48 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -8,6 +8,10 @@ import CommonStore from 'stores/common-store'; let App = React.createClass({ + contextTypes: { + router: React.PropTypes.object + }, + mixins: [Reflux.listenTo(CommonStore, 'onCommonStoreChanged')], render() { @@ -19,8 +23,14 @@ let App = React.createClass({ }, onCommonStoreChanged(change) { - if (change === 'i18n') { - this.forceUpdate(); + let handle = { + 'i18n': () => {this.forceUpdate()}, + 'logged': () => {this.context.router.push('/app/dashboard')}, + 'loggedOut': () => {this.context.router.push('/app')} + }; + + if (handle[change]) { + handle[change](); } } }); diff --git a/client/src/app/main/dashboard/dashboard-layout.js b/client/src/app/main/dashboard/dashboard-layout.js index 29f3fc69..d8bfa4bd 100644 --- a/client/src/app/main/dashboard/dashboard-layout.js +++ b/client/src/app/main/dashboard/dashboard-layout.js @@ -1,8 +1,18 @@ import React from 'react'; + +import UserStore from 'stores/user-store'; +import CommonActions from 'actions/common-actions'; + import DashboardMenu from 'app/main/dashboard/dashboard-menu'; const DashboardLayout = React.createClass({ + componentWillMount() { + if (!UserStore.isLoggedIn()) { + CommonActions.loggedOut(); + } + }, + render() { return (
diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index fce0d34e..5a98f840 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -1,4 +1,6 @@ -const React = require( 'react'); +const React = require('react'); +const Reflux = require('reflux'); +const _ = require('lodash'); const classNames = require('classnames'); const UserActions = require('actions/user-actions'); @@ -12,10 +14,13 @@ const Widget = require('core-components/widget'); const WidgetTransition = require('core-components/widget-transition'); let MainHomePageLoginWidget = React.createClass({ + + mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')], getInitialState() { return { - sideToShow: 'front' + sideToShow: 'front', + loginFormErrors: {} }; }, @@ -31,7 +36,7 @@ let MainHomePageLoginWidget = React.createClass({ renderLogin() { return ( -
+
@@ -71,6 +76,12 @@ let MainHomePageLoginWidget = React.createClass({ UserActions.login(formState); }, + handleLoginFormErrorsValidation(errors) { + this.setState({ + loginFormErrors: errors + }); + }, + handleForgetPasswordClick() { this.setState({ sideToShow: 'back' @@ -81,6 +92,18 @@ let MainHomePageLoginWidget = React.createClass({ this.setState({ sideToShow: 'front' }); + }, + + onUserStoreChanged(event) { + if (event === 'LOGIN_FAIL') { + this.setState({ + loginFormErrors: { + password: 'Password does not match' + } + }, function () { + this.refs.loginForm.refs.password.focus() + }.bind(this)); + } } }); diff --git a/client/src/app/main/main-home/main-home-page.js b/client/src/app/main/main-home/main-home-page.js index aa7a2de7..eb3218cf 100644 --- a/client/src/app/main/main-home/main-home-page.js +++ b/client/src/app/main/main-home/main-home-page.js @@ -3,8 +3,17 @@ const React = require( 'react'); const MainHomePageLoginWidget = require('app/main/main-home/main-home-page-login-widget'); const MainHomePagePortal = require('app/main/main-home/main-home-page-portal'); +const CommonActions = require('actions/common-actions'); +const UserStore = require('stores/user-store'); + const MainHomePage = React.createClass({ + componentWillMount() { + if (UserStore.isLoggedIn()) { + CommonActions.logged(); + } + }, + render() { return (
diff --git a/client/src/app/main/main-layout-header.js b/client/src/app/main/main-layout-header.js index 12b60c08..b1c33fdc 100644 --- a/client/src/app/main/main-layout-header.js +++ b/client/src/app/main/main-layout-header.js @@ -2,6 +2,8 @@ import React from 'react'; import i18n from 'lib-app/i18n'; import CommonActions from 'actions/common-actions'; +import UserActions from 'actions/user-actions'; +import UserStore from 'stores/user-store'; import Button from 'core-components/button'; import DropDown from 'core-components/drop-down'; @@ -22,13 +24,31 @@ let MainLayoutHeader = React.createClass({ render() { return (
+ {this.renderAccessLinks()} + +
+ ); + }, + + renderAccessLinks() { + let result; + if (UserStore.isLoggedIn()) { + result = ( +
+ Welcome, pepito + +
+ ); + } else { + result = (
- -
- ); + ); + } + + return result; }, getLanguageList() { @@ -44,6 +64,10 @@ let MainLayoutHeader = React.createClass({ let language = languageList[event.index]; CommonActions.changeLanguage(codeLanguages[language]); + }, + + logout() { + UserActions.logout(); } }); diff --git a/client/src/app/main/main-signup/main-signup-page.js b/client/src/app/main/main-signup/main-signup-page.js index 316ab717..23b2e76e 100644 --- a/client/src/app/main/main-signup/main-signup-page.js +++ b/client/src/app/main/main-signup/main-signup-page.js @@ -11,8 +11,16 @@ import Input from 'core-components/input'; import Widget from 'core-components/widget'; import WidgetTransition from 'core-components/widget-transition'; +const CommonActions = require('actions/common-actions'); + let MainSignUpPageWidget = React.createClass({ + componentDidMount() { + if (UserStore.isLoggedIn()) { + CommonActions.logged(); + } + }, + render() { return (
diff --git a/client/src/core-components/form.js b/client/src/core-components/form.js index b05508b2..dc97b2f5 100644 --- a/client/src/core-components/form.js +++ b/client/src/core-components/form.js @@ -10,6 +10,12 @@ const Checkbox = require('core-components/checkbox'); const Form = React.createClass({ + propTypes: { + errors: React.PropTypes.func, + onValidateErrors: React.PropTypes.func, + onSubmit: React.PropTypes.func + }, + getInitialState() { return { form: {}, @@ -47,7 +53,7 @@ const Form = React.createClass({ additionalProps = { ref: fieldName, value: this.state.form[fieldName] || props.value, - error: this.state.errors[fieldName], + error: this.getFieldError(fieldName), onChange: this.handleFieldChange.bind(this, fieldName, type), onBlur: this.validateField.bind(this, fieldName) } @@ -56,6 +62,15 @@ const Form = React.createClass({ return additionalProps; }, + getFieldError(fieldName) { + let error = this.state.errors[fieldName]; + + if (this.props.errors) { + error = this.props.errors[fieldName] + } + return error; + }, + getFirstErrorField() { let fieldName = _.findKey(this.state.errors); let fieldNode; @@ -119,9 +134,7 @@ const Form = React.createClass({ event.preventDefault(); if (this.hasFormErrors()) { - this.setState({ - errors: this.getAllFieldErrors() - }, this.focusFirstErrorField); + this.updateErrors(this.getAllFieldErrors(), this.focusFirstErrorField); } else if (this.props.onSubmit) { this.props.onSubmit(this.state.form); } @@ -150,9 +163,17 @@ const Form = React.createClass({ }, validateField(fieldName) { + this.updateErrors(this.getErrorsWithValidatedField(fieldName)); + }, + + updateErrors(errors, callback) { this.setState({ - errors: this.getErrorsWithValidatedField(fieldName) - }); + errors + }, callback); + + if (this.props.onValidateErrors) { + this.props.onValidateErrors(errors); + } }, focusFirstErrorField() { diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index cba6af7e..f98981f2 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -22,5 +22,15 @@ module.exports = [ return response; } + }, + { + path: 'user/logout', + time: 1000, + response: function () { + return { + status: 'success', + data: {} + }; + } } ]; diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index a2885b08..06abd137 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -12,8 +12,14 @@ function processData (data) { } module.exports = { - call: function (path, data, callback) { - APIUtils.post(root + path, processData(data)).then(callback); + call: function ({path, data, onSuccess, onFail}) { + APIUtils.post(root + path, processData(data)).then(function (result) { + if (result.status === 'success') { + onSuccess && onSuccess(result); + } else { + onFail && onFail(result); + } + }); }, setConfig: function (userId, token) { SessionStorage.setItem('userId', userId); diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js new file mode 100644 index 00000000..f6a05b77 --- /dev/null +++ b/client/src/lib-app/session-store.js @@ -0,0 +1,35 @@ +import SessionStorage from 'sessionstorage'; + + +class SessionStore { + static initialize() { + if (!SessionStorage.getItem('language')) { + SessionStorage.setItem('language', 'english'); + } + } + + static createSession(userId, token) { + SessionStorage.setItem('userId', userId); + SessionStorage.setItem('token', token); + } + + static getSessionData() { + return { + userId: SessionStorage.getItem('userId'), + token: SessionStorage.getItem('token') + }; + } + + static isLoggedIn() { + return !!SessionStorage.getItem('userId'); + } + + static closeSession() { + SessionStorage.removeItem('userId'); + SessionStorage.removeItem('token'); + } +} + +SessionStore.initialize(); + +export default SessionStore; \ No newline at end of file diff --git a/client/src/stores/common-store.js b/client/src/stores/common-store.js index b24f1fd8..9dcd271d 100644 --- a/client/src/stores/common-store.js +++ b/client/src/stores/common-store.js @@ -8,11 +8,25 @@ let CommonStore = Reflux.createStore({ this.language = 'us'; this.listenTo(CommonActions.changeLanguage, this.changeLanguage); + this.listenTo(CommonActions.logged, this.logged); + this.listenTo(CommonActions.loggedOut, this.loggedOut); }, changeLanguage(lang) { this.language = lang; this.trigger('i18n'); + }, + + logged() { + this.trigger('logged'); + }, + + loggedOut() { + this.trigger('loggedOut'); + }, + + isLogged() { + return this.logged() } }); diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js index 200c2ed7..61086059 100644 --- a/client/src/stores/user-store.js +++ b/client/src/stores/user-store.js @@ -1,7 +1,9 @@ const Reflux = require('reflux'); const API = require('lib-app/api-call'); +const SessionStore = require('lib-app/session-store'); const UserActions = require('actions/user-actions'); +const CommonActions = require('actions/common-actions'); const UserStore = Reflux.createStore({ @@ -15,11 +17,37 @@ const UserStore = Reflux.createStore({ }, loginUser(loginData) { - API.call('user/login', loginData, result => { - console.log(result); - - API.setConfig(result.userId, result.token); + API.call({ + path: 'user/login', + data: loginData, + onSuccess: this.handleLoginSuccess, + onFail: this.handleLoginFail }); + }, + + logoutUser() { + API.call({ + path: 'user/logout', + onSuccess: function () { + SessionStore.closeSession(); + this.trigger('LOGOUT'); + CommonActions.loggedOut(); + }.bind(this) + }); + }, + + isLoggedIn() { + return SessionStore.isLoggedIn(); + }, + + handleLoginSuccess(result) { + SessionStore.createSession(result.data.userId, result.data.token); + this.trigger('LOGIN_SUCCESS'); + CommonActions.logged(); + }, + + handleLoginFail(result) { + this.trigger('LOGIN_FAIL'); } }); From 88f2735f9e70441863d8a6ce244d5e52481869e6 Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 7 Jul 2016 16:34:10 -0300 Subject: [PATCH 2/5] Ivan - OS-48 - Remove console errors of invalid attributes --- client/src/app/App.js | 1 - .../core-components/__tests__/form-test.js | 6 ++--- client/src/core-components/button.js | 24 +++++++++++++++---- client/src/core-components/checkbox.js | 6 ++++- client/src/core-components/form.js | 5 +++- client/src/core-components/input.js | 7 +++++- client/src/core-components/menu.js | 6 ++++- client/src/lib-test/react-mock.js | 2 +- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/client/src/app/App.js b/client/src/app/App.js index b0364d48..927d3d56 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -3,7 +3,6 @@ import Reflux from 'reflux'; import {ListenerMixin} from 'reflux'; import {RouteHandler} from 'react-router'; -import CommonActions from 'actions/common-actions'; import CommonStore from 'stores/common-store'; let App = React.createClass({ diff --git a/client/src/core-components/__tests__/form-test.js b/client/src/core-components/__tests__/form-test.js index ac73aca7..073ecc34 100644 --- a/client/src/core-components/__tests__/form-test.js +++ b/client/src/core-components/__tests__/form-test.js @@ -90,13 +90,13 @@ describe('Form component', function () { expect(fields[0].props.error).to.equal(undefined); expect(fields[0].props.error).to.equal(undefined); - TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[0])); + fields[0].props.onBlur(); expect(fields[0].props.error).to.equal('MOCK_ERROR'); - TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[1])); + fields[1].props.onBlur(); expect(fields[1].props.error).to.equal('MOCK_ERROR_2'); - TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[2])); + fields[2].props.onBlur(); expect(fields[2].props.error).to.equal(undefined); }); }); diff --git a/client/src/core-components/button.js b/client/src/core-components/button.js index c0ccec07..f2cfbf5e 100644 --- a/client/src/core-components/button.js +++ b/client/src/core-components/button.js @@ -1,6 +1,10 @@ -var React = require('react'); -var classNames = require('classnames'); -var callback = require('lib-core/callback'); +// VENDOR LIBS +import React from 'react'; +import _ from 'lodash'; +import classNames from 'classnames'; + +// CORE LIBS +import callback from 'lib-core/callback'; let Button = React.createClass({ @@ -30,12 +34,24 @@ let Button = React.createClass({ render() { return ( - ); }, + getProps() { + let props = _.clone(this.props); + + props.onClick = callback(this.handleClick, this.props.onClick); + props.className = this.getClass(); + + delete props.route; + delete props.type; + + return props; + }, + getClass() { let classes = { 'button': true diff --git a/client/src/core-components/checkbox.js b/client/src/core-components/checkbox.js index 48f775a2..bd36f4fd 100644 --- a/client/src/core-components/checkbox.js +++ b/client/src/core-components/checkbox.js @@ -46,7 +46,11 @@ let CheckBox = React.createClass({ props.className = 'checkbox--box'; props.checked = this.getValue(); props.onChange = callback(this.handleChange, this.props.onChange); - props.value = null; + + delete props.alignment; + delete props.error; + delete props.label; + delete props.value; return props; }, diff --git a/client/src/core-components/form.js b/client/src/core-components/form.js index dc97b2f5..33bb0710 100644 --- a/client/src/core-components/form.js +++ b/client/src/core-components/form.js @@ -11,7 +11,7 @@ const Checkbox = require('core-components/checkbox'); const Form = React.createClass({ propTypes: { - errors: React.PropTypes.func, + errors: React.PropTypes.object, onValidateErrors: React.PropTypes.func, onSubmit: React.PropTypes.func }, @@ -40,6 +40,9 @@ const Form = React.createClass({ let props = _.clone(this.props); props.onSubmit = this.handleSubmit; + + delete props.errors; + delete props.onValidateErrors; return props; }, diff --git a/client/src/core-components/input.js b/client/src/core-components/input.js index 66b497d0..42e6d919 100644 --- a/client/src/core-components/input.js +++ b/client/src/core-components/input.js @@ -57,11 +57,16 @@ const Input = React.createClass({ getInputProps() { let props = _.clone(this.props); - props.required = null; props['aria-required'] = this.props.required; props.type = (this.props.password) ? 'password' : 'text'; props.ref = 'nativeInput'; + delete props.required; + delete props.validation; + delete props.inputType; + delete props.error; + delete props.password; + return props; }, diff --git a/client/src/core-components/menu.js b/client/src/core-components/menu.js index 7f73faa2..7654726f 100644 --- a/client/src/core-components/menu.js +++ b/client/src/core-components/menu.js @@ -48,7 +48,11 @@ const Menu = React.createClass({ var props = _.clone(this.props); props.className = this.getClass(); - props.type = null; + + delete props.items; + delete props.onItemClick; + delete props.selectedIndex; + delete props.type; return props; }, diff --git a/client/src/lib-test/react-mock.js b/client/src/lib-test/react-mock.js index a5cd8142..ca0ea959 100644 --- a/client/src/lib-test/react-mock.js +++ b/client/src/lib-test/react-mock.js @@ -4,7 +4,7 @@ const _ = require('lodash'); module.exports = function (options) { return React.createClass(_.extend({ render() { - return
; + return
; } }, options)); }; \ No newline at end of file From e401edef1099a58ee930fc633413cd0f8d83629c Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 8 Jul 2016 00:15:44 -0300 Subject: [PATCH 3/5] Ivan - OS-48 - Add UnitTest and necessary mocks for login/logout logic [skip ci] --- client/package.json | 2 +- .../actions/__mocks__/common-actions-mock.js | 5 + .../actions/__mocks__/user-actions-mock.js | 5 + client/src/app/App.js | 2 - client/src/app/__tests__/App-test.js | 44 ++++++++ .../__tests__/dashboard-layout-test.js | 32 ++++++ .../main-home-page-login-widget-test.js | 75 +++++++++++++ .../__tests__/main-home-page-test.js | 31 ++++++ .../main-home/main-home-page-login-widget.js | 11 +- .../__tests__/main-signup-page-test.js | 32 ++++++ .../core-components/__tests__/form-test.js | 66 ++++++++++- client/src/core-components/form.js | 1 - client/src/lib-app/__mocks__/api-call-mock.js | 3 + .../lib-app/__mocks__/session-store-mock.js | 6 + client/src/lib-test/preprocessor.js | 11 ++ client/src/lib-test/react-mock.js | 2 +- .../src/stores/__mocks__/common-store-mock.js | 6 + .../src/stores/__mocks__/user-store-mock.js | 6 + .../src/stores/__tests__/user-store-test.js | 105 ++++++++++++++++++ client/src/stores/common-store.js | 4 - client/src/stores/user-store.js | 3 +- 21 files changed, 434 insertions(+), 18 deletions(-) create mode 100644 client/src/actions/__mocks__/common-actions-mock.js create mode 100644 client/src/actions/__mocks__/user-actions-mock.js create mode 100644 client/src/app/__tests__/App-test.js create mode 100644 client/src/app/main/dashboard/__tests__/dashboard-layout-test.js create mode 100644 client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js create mode 100644 client/src/app/main/main-home/__tests__/main-home-page-test.js create mode 100644 client/src/app/main/main-signup/__tests__/main-signup-page-test.js create mode 100644 client/src/lib-app/__mocks__/api-call-mock.js create mode 100644 client/src/lib-app/__mocks__/session-store-mock.js create mode 100644 client/src/stores/__mocks__/common-store-mock.js create mode 100644 client/src/stores/__mocks__/user-store-mock.js create mode 100644 client/src/stores/__tests__/user-store-test.js diff --git a/client/package.json b/client/package.json index 07a0990f..d6e54b3b 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "npm": "^2.1.x" }, "scripts": { - "test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --compilers js:babel-core/register --recursive src/**/__tests__/*-test.js" + "test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --compilers js:babel-core/register --recursive src/**/**/__tests__/*-test.js" }, "devDependencies": { "babel-core": "^5.8.22", diff --git a/client/src/actions/__mocks__/common-actions-mock.js b/client/src/actions/__mocks__/common-actions-mock.js new file mode 100644 index 00000000..1027e3bf --- /dev/null +++ b/client/src/actions/__mocks__/common-actions-mock.js @@ -0,0 +1,5 @@ +export default { + changeLanguage: stub(), + logged: stub(), + loggedOut: stub() +}; \ No newline at end of file diff --git a/client/src/actions/__mocks__/user-actions-mock.js b/client/src/actions/__mocks__/user-actions-mock.js new file mode 100644 index 00000000..b8a568db --- /dev/null +++ b/client/src/actions/__mocks__/user-actions-mock.js @@ -0,0 +1,5 @@ +export default { + checkLoginStatus: stub(), + login: stub(), + logout: stub() +}; \ No newline at end of file diff --git a/client/src/app/App.js b/client/src/app/App.js index 927d3d56..91b2ce24 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -1,7 +1,5 @@ import React from 'react'; import Reflux from 'reflux'; -import {ListenerMixin} from 'reflux'; -import {RouteHandler} from 'react-router'; import CommonStore from 'stores/common-store'; diff --git a/client/src/app/__tests__/App-test.js b/client/src/app/__tests__/App-test.js new file mode 100644 index 00000000..4bb2680c --- /dev/null +++ b/client/src/app/__tests__/App-test.js @@ -0,0 +1,44 @@ +const CommonStore = require('stores/__mocks__/common-store-mock'); + +const App = requireUnit('app/App', { + 'store/common-store': CommonStore +}); + +describe('App component', function () { + describe('when reacting to CommonStore', function () { + let app; + + beforeEach(function () { + console.log(App); + app = TestUtils.renderIntoDocument( + MOCK_CHILD + ); + + app.context = { + router: { + push: stub() + } + }; + + spy(app, 'forceUpdate'); + }); + + it('should update with i18n', function () { + app.forceUpdate.reset(); + app.onCommonStoreChanged('i18n'); + expect(app.forceUpdate).to.have.been.called; + }); + + it('should redirect when logged in', function () { + app.context.router.push.reset(); + app.onCommonStoreChanged('logged'); + expect(app.context.router.push).to.have.been.calledWith('/app/dashboard'); + }); + + it('should redirect when logged out', function () { + app.context.router.push.reset(); + app.onCommonStoreChanged('loggedOut'); + expect(app.context.router.push).to.have.been.calledWith('/app'); + }); + }); +}); diff --git a/client/src/app/main/dashboard/__tests__/dashboard-layout-test.js b/client/src/app/main/dashboard/__tests__/dashboard-layout-test.js new file mode 100644 index 00000000..29cfac8c --- /dev/null +++ b/client/src/app/main/dashboard/__tests__/dashboard-layout-test.js @@ -0,0 +1,32 @@ +const CommonActions = require('actions/__mocks__/common-actions-mock'); +const UserStore = require('stores/__mocks__/user-store-mock'); + +const DashboardLayout = requireUnit('app/main/dashboard/dashboard-layout', { + 'actions/common-actions': CommonActions, + 'stores/user-store': UserStore, + 'app/main/dashboard/dashboard-menu': ReactMock() +}); + + +describe('Dashboard page', function () { + + afterEach(function () { + UserStore.isLoggedIn.returns(false); + }); + + it('should trigger common action if user is not logged', function () { + CommonActions.loggedOut.reset(); + UserStore.isLoggedIn.returns(false); + + TestUtils.renderIntoDocument(); + expect(CommonActions.loggedOut).to.have.been.called; + }); + + it('should not trigger common action user if is logged', function () { + CommonActions.loggedOut.reset(); + UserStore.isLoggedIn.returns(true); + + TestUtils.renderIntoDocument(); + expect(CommonActions.loggedOut).to.not.have.been.called; + }); +}); \ No newline at end of file diff --git a/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js new file mode 100644 index 00000000..e34d5965 --- /dev/null +++ b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js @@ -0,0 +1,75 @@ +const UserActions = require('actions/__mocks__/user-actions-mock'); +const UserStore = require('stores/__mocks__/user-store-mock'); + +const Button = ReactMock(); +const Input = ReactMock(); +const Form = ReactMock(); +const Checkbox = ReactMock(); +const Widget = ReactMock(); +const WidgetTransition = ReactMock(); + +const MainHomePageLoginWidget = requireUnit('app/main/main-home/main-home-page-login-widget', { + 'core-components/button': Button, + 'core-components/input': Input, + 'core-components/form': Form, + 'core-components/checkbox': Checkbox, + 'core-components/widget': Widget, + 'core-components/widget-transition': WidgetTransition, + 'actions/user-actions': UserActions, + 'stores/user-store': UserStore +}); + + +describe('Login/Recover Widget', function () { + describe('Login Form', function () { + let loginWidget, loginForm, widgetTransition, inputs, checkbox, component, + forgotPasswordButton; + + beforeEach(function () { + component = TestUtils.renderIntoDocument( + + ); + widgetTransition = TestUtils.scryRenderedComponentsWithType(component, WidgetTransition)[0]; + loginWidget = TestUtils.scryRenderedComponentsWithType(component, Widget)[0]; + loginForm = TestUtils.scryRenderedComponentsWithType(component, Form)[0]; + inputs = TestUtils.scryRenderedComponentsWithType(component, Input); + checkbox = TestUtils.scryRenderedComponentsWithType(component, Checkbox)[0]; + forgotPasswordButton = TestUtils.scryRenderedComponentsWithType(component, Button)[1]; + + component.refs.loginForm = { + refs: { + password: { + focus: stub() + } + } + }; + }); + + it('should control form errors by prop', function () { + expect(loginForm.props.errors).to.deep.equal({}); + loginForm.props.onValidateErrors({email: 'MOCK_ERROR'}); + expect(loginForm.props.errors).to.deep.equal({email: 'MOCK_ERROR'}); + }); + + it('should trigger login action when submitted', function () { + let mockSubmitData = {email: 'MOCK_VALUE', password: 'MOCK_VALUE'}; + + UserActions.login.reset(); + loginForm.props.onSubmit(mockSubmitData); + expect(UserActions.login).to.have.been.calledWith(mockSubmitData); + }); + + it('should add error if login fails', function () { + component.refs.loginForm.refs.password.focus.reset(); + component.onUserStoreChanged('LOGIN_FAIL'); + expect(loginForm.props.errors).to.deep.equal({password: 'Password does not match'}); + expect(component.refs.loginForm.refs.password.focus).to.have.been.called; + }); + + it('should show back side if \'Forgot your password?\' link is clicked', function () { + expect(widgetTransition.props.sideToShow).to.equal('front'); + forgotPasswordButton.props.onClick(); + expect(widgetTransition.props.sideToShow).to.equal('back'); + }); + }); +}); \ No newline at end of file diff --git a/client/src/app/main/main-home/__tests__/main-home-page-test.js b/client/src/app/main/main-home/__tests__/main-home-page-test.js new file mode 100644 index 00000000..736b0ed4 --- /dev/null +++ b/client/src/app/main/main-home/__tests__/main-home-page-test.js @@ -0,0 +1,31 @@ +const CommonActions = require('actions/__mocks__/common-actions-mock'); +const UserStore = require('stores/__mocks__/user-store-mock'); + +const MainHomePage = requireUnit('app/main/main-home/main-home-page', { + 'actions/common-actions': CommonActions, + 'stores/user-store': UserStore +}); + + +describe('Main home page', function () { + + afterEach(function () { + UserStore.isLoggedIn.returns(false); + }); + + it('should trigger common action if user is currently logged', function () { + CommonActions.logged.reset(); + UserStore.isLoggedIn.returns(true); + + TestUtils.renderIntoDocument(); + expect(CommonActions.logged).to.have.been.called; + }); + + it('should not trigger common action user if is not logged', function () { + CommonActions.logged.reset(); + UserStore.isLoggedIn.returns(false); + + TestUtils.renderIntoDocument(); + expect(CommonActions.logged).to.not.have.been.called; + }); +}); \ No newline at end of file diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index 5a98f840..9b5f90ee 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -46,7 +46,7 @@ let MainHomePageLoginWidget = React.createClass({
- @@ -56,7 +56,7 @@ let MainHomePageLoginWidget = React.createClass({ renderPasswordRecovery() { return ( -
+
@@ -72,17 +72,20 @@ let MainHomePageLoginWidget = React.createClass({ }, handleLoginFormSubmit(formState) { - console.log(formState); UserActions.login(formState); }, + handleForgotPasswordSubmit() { + + }, + handleLoginFormErrorsValidation(errors) { this.setState({ loginFormErrors: errors }); }, - handleForgetPasswordClick() { + handleForgotPasswordClick() { this.setState({ sideToShow: 'back' }); diff --git a/client/src/app/main/main-signup/__tests__/main-signup-page-test.js b/client/src/app/main/main-signup/__tests__/main-signup-page-test.js new file mode 100644 index 00000000..0c267a6e --- /dev/null +++ b/client/src/app/main/main-signup/__tests__/main-signup-page-test.js @@ -0,0 +1,32 @@ +const CommonActions = require('actions/__mocks__/common-actions-mock'); +const UserStore = require('stores/__mocks__/user-store-mock'); + +const MainSignupPage = requireUnit('app/main/main-signup/main-signup-page', { + 'actions/common-actions': CommonActions, + 'stores/user-store': UserStore, + 'react-google-recaptcha': ReactMock() +}); + + +describe('Signup page', function () { + + afterEach(function () { + UserStore.isLoggedIn.returns(false); + }); + + it('should trigger common action if user is currently logged', function () { + CommonActions.logged.reset(); + UserStore.isLoggedIn.returns(true); + + TestUtils.renderIntoDocument(); + expect(CommonActions.logged).to.have.been.called; + }); + + it('should not trigger common action user if is not logged', function () { + CommonActions.logged.reset(); + UserStore.isLoggedIn.returns(false); + + TestUtils.renderIntoDocument(); + expect(CommonActions.logged).to.not.have.been.called; + }); +}); \ No newline at end of file diff --git a/client/src/core-components/__tests__/form-test.js b/client/src/core-components/__tests__/form-test.js index 073ecc34..a6962ffc 100644 --- a/client/src/core-components/__tests__/form-test.js +++ b/client/src/core-components/__tests__/form-test.js @@ -2,7 +2,7 @@ const ValidationFactoryMock = require('lib-app/__mocks__/validations/validation-factory-mock'); const Input = ReactMock(); -// COMPONENTS +// COMPONENT const Form = requireUnit('core-components/form', { 'lib-app/validations/validations-factory': ValidationFactoryMock, 'core-components/input': Input @@ -11,9 +11,9 @@ const Form = requireUnit('core-components/form', { describe('Form component', function () { let form, fields, onSubmit = stub(); - function renderForm() { + function renderForm(props = {}) { form = TestUtils.renderIntoDocument( - +
@@ -101,6 +101,66 @@ describe('Form component', function () { }); }); + describe('when using controlled errors', function () { + let onValidateErrors; + + beforeEach(function () { + onValidateErrors = stub(); + + ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR'); + ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2'); + + renderForm({ + errors: {first: 'MOCK_ERROR_CONTROLLED'}, + onValidateErrors: onValidateErrors + }); + }); + afterEach(resetStubs); + + it('should pass the errors to inputs', function () { + expect(fields[0].props.error).to.equal('MOCK_ERROR_CONTROLLED'); + expect(fields[1].props.error).to.equal(undefined); + }); + + it('should prioritize prop error over state error', function () { + fields[1].props.onBlur(); + expect(fields[1].props.error).to.equal(undefined); + }); + + it('should call onValidateErrors when state changes', function () { + fields[1].props.onBlur(); + expect(onValidateErrors).to.have.been.calledWith({second: 'MOCK_ERROR_2'}); + + }); + + it('should still working if the error prop changes', function () { + function setErrorsOrRender(errors = {}) { + form = reRenderIntoDocument( + +
+ + +
+ + + ); + fields = TestUtils.scryRenderedComponentsWithType(form, Input); + } + + setErrorsOrRender(); + expect(fields[0].props.error).to.equal(undefined); + expect(fields[1].props.error).to.equal(undefined); + + setErrorsOrRender({second: 'MOCK_ERROR_CONTROLLED_2'}); + expect(fields[0].props.error).to.equal(undefined); + expect(fields[1].props.error).to.equal('MOCK_ERROR_CONTROLLED_2'); + + setErrorsOrRender({first: 'MOCK_ERROR_CONTROLLED', second: 'MOCK_ERROR_CONTROLLED_2'}); + expect(fields[0].props.error).to.equal('MOCK_ERROR_CONTROLLED'); + expect(fields[1].props.error).to.equal('MOCK_ERROR_CONTROLLED_2'); + }); + }); + describe('when submitting the form', function () { beforeEach(renderForm); afterEach(resetStubs); diff --git a/client/src/core-components/form.js b/client/src/core-components/form.js index 33bb0710..530edca8 100644 --- a/client/src/core-components/form.js +++ b/client/src/core-components/form.js @@ -1,5 +1,4 @@ const React = require('react'); -const ReactDOM = require('react-dom'); const _ = require('lodash'); const {reactDFS, renderChildrenWithProps} = require('lib-core/react-dfs'); diff --git a/client/src/lib-app/__mocks__/api-call-mock.js b/client/src/lib-app/__mocks__/api-call-mock.js new file mode 100644 index 00000000..3fcd2003 --- /dev/null +++ b/client/src/lib-app/__mocks__/api-call-mock.js @@ -0,0 +1,3 @@ +export default { + call: stub() +}; \ No newline at end of file diff --git a/client/src/lib-app/__mocks__/session-store-mock.js b/client/src/lib-app/__mocks__/session-store-mock.js new file mode 100644 index 00000000..5ceed298 --- /dev/null +++ b/client/src/lib-app/__mocks__/session-store-mock.js @@ -0,0 +1,6 @@ +export default { + createSession: stub(), + getSessionData: stub().returns({}), + isLoggedIn: stub().returns(false), + closeSession: stub() +}; \ No newline at end of file diff --git a/client/src/lib-test/preprocessor.js b/client/src/lib-test/preprocessor.js index 78afdb67..3af8576b 100644 --- a/client/src/lib-test/preprocessor.js +++ b/client/src/lib-test/preprocessor.js @@ -19,3 +19,14 @@ global.TestUtils = require('react-addons-test-utils'); global.requireUnit = function (path, mocks) { return proxyquire(process.cwd() + '/src/' + path + '.js', mocks) }; +global.reRenderIntoDocument = (function () { + let div; + + return function (jsx) { + if (!div) { + div = document.createElement('div') + } + + return ReactDOM.render(jsx, div); + } +})(); diff --git a/client/src/lib-test/react-mock.js b/client/src/lib-test/react-mock.js index ca0ea959..ec6091ab 100644 --- a/client/src/lib-test/react-mock.js +++ b/client/src/lib-test/react-mock.js @@ -4,7 +4,7 @@ const _ = require('lodash'); module.exports = function (options) { return React.createClass(_.extend({ render() { - return
; + return
{this.props.children}
; } }, options)); }; \ No newline at end of file diff --git a/client/src/stores/__mocks__/common-store-mock.js b/client/src/stores/__mocks__/common-store-mock.js new file mode 100644 index 00000000..1a488e3d --- /dev/null +++ b/client/src/stores/__mocks__/common-store-mock.js @@ -0,0 +1,6 @@ +export default { + changeLanguage: stub(), + logged: stub(), + loggedOut: stub(), + listen: stub() +}; \ No newline at end of file diff --git a/client/src/stores/__mocks__/user-store-mock.js b/client/src/stores/__mocks__/user-store-mock.js new file mode 100644 index 00000000..abba4ba6 --- /dev/null +++ b/client/src/stores/__mocks__/user-store-mock.js @@ -0,0 +1,6 @@ +export default { + loginUser: stub(), + logoutUser: stub(), + isLoggedIn: stub().returns(false), + listen: stub() +}; \ No newline at end of file diff --git a/client/src/stores/__tests__/user-store-test.js b/client/src/stores/__tests__/user-store-test.js new file mode 100644 index 00000000..5ccc6790 --- /dev/null +++ b/client/src/stores/__tests__/user-store-test.js @@ -0,0 +1,105 @@ +// MOCKS +const CommonActions = require('actions/__mocks__/common-actions-mock'); +const SessionStore = require('lib-app/__mocks__/session-store-mock'); +const API = require('lib-app/__mocks__/api-call-mock'); +const UserActions = { + checkLoginStatus: {listen: stub()}, + login: {listen: stub()}, + logout: {listen: stub()} +}; + +const UserStore = requireUnit('stores/user-store', { + 'actions/user-actions': UserActions, + 'actions/common-actions': CommonActions, + 'lib-app/session-store': SessionStore, + 'lib-app/api-call': API +}); + +describe('UserStore', function () { + describe('when login user', function () { + it('should call /user/login api path', function () { + let mockLoginData = {email: 'mock', password: 'mock'}; + + UserStore.loginUser(mockLoginData); + expect(API.call).to.have.been.calledWith({ + path: 'user/login', + data: mockLoginData, + onSuccess: sinon.match.func, + onFail: sinon.match.func + }); + }); + + it('should create session, trigger success event and inform common action when having a successful login', function () { + let mockLoginData = {email: 'mock', password: 'mock'}; + let mockSuccessData = { + status: 'success', + data: { + userId: 12, + token: 'RANDOM_TOKEN' + } + }; + + spy(UserStore, 'trigger'); + CommonActions.logged.reset(); + SessionStore.createSession.reset(); + API.call = ({onSuccess}) => {onSuccess(mockSuccessData)}; + + UserStore.loginUser(mockLoginData); + + expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN'); + expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS'); + expect(CommonActions.logged).to.have.been.called; + UserStore.trigger.restore(); + }); + + it('should trigger fail event if login fails', function () { + let mockLoginData = {email: 'mock', password: 'mock'}; + let mockSuccessData = { + status: 'success', + data: { + userId: 12, + token: 'RANDOM_TOKEN' + } + }; + + spy(UserStore, 'trigger'); + API.call = ({onFail}) => {onFail(mockSuccessData)}; + + UserStore.loginUser(mockLoginData); + + expect(UserStore.trigger).to.have.been.calledWith('LOGIN_FAIL'); + UserStore.trigger.restore(); + }); + }); + + describe('when login out', function () { + + it('should call /user/logout api path', function () { + API.call = stub(); + + UserStore.logoutUser(); + expect(API.call).to.have.been.calledWith({ + path: 'user/logout', + onSuccess: sinon.match.func + }); + }); + + it('should delete session, trigger LOGOUT event and inform common action of logout', function () { + API.call = ({onSuccess}) => {onSuccess()}; + spy(UserStore, 'trigger'); + + UserStore.logoutUser(); + expect(SessionStore.closeSession).to.have.been.called; + expect(UserStore.trigger).to.have.been.calledWith('LOGOUT'); + expect(CommonActions.loggedOut).to.have.been.called; + UserStore.trigger.restore() + }) + }); + + it ('should inform is the user is logged based on SessionStores\' info', function () { + SessionStore.isLoggedIn.returns(true); + expect(UserStore.isLoggedIn()).to.equal(true); + SessionStore.isLoggedIn.returns(false); + expect(UserStore.isLoggedIn()).to.equal(false); + }); +}); diff --git a/client/src/stores/common-store.js b/client/src/stores/common-store.js index 9dcd271d..b4a82de7 100644 --- a/client/src/stores/common-store.js +++ b/client/src/stores/common-store.js @@ -23,10 +23,6 @@ let CommonStore = Reflux.createStore({ loggedOut() { this.trigger('loggedOut'); - }, - - isLogged() { - return this.logged() } }); diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js index 61086059..93118c36 100644 --- a/client/src/stores/user-store.js +++ b/client/src/stores/user-store.js @@ -9,7 +9,6 @@ const UserStore = Reflux.createStore({ init() { this.user = null; - this.hasBeenChecked = false; this.listenTo(UserActions.checkLoginStatus, this.checkLoginStatus); this.listenTo(UserActions.login, this.loginUser); @@ -46,7 +45,7 @@ const UserStore = Reflux.createStore({ CommonActions.logged(); }, - handleLoginFail(result) { + handleLoginFail() { this.trigger('LOGIN_FAIL'); } }); From 64d2a9d70a951c1f18701e63b042248e6b985aab Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 8 Jul 2016 03:56:52 -0300 Subject: [PATCH 4/5] Ivan - OS-48 - Add UnitTest and necessary mocks for login/logout logic [skip ci] --- client/src/app/__tests__/App-test.js | 1 - client/src/lib-app/api-call.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/client/src/app/__tests__/App-test.js b/client/src/app/__tests__/App-test.js index 4bb2680c..6bc02936 100644 --- a/client/src/app/__tests__/App-test.js +++ b/client/src/app/__tests__/App-test.js @@ -9,7 +9,6 @@ describe('App component', function () { let app; beforeEach(function () { - console.log(App); app = TestUtils.renderIntoDocument( MOCK_CHILD ); diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index 06abd137..90c308ce 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -20,9 +20,5 @@ module.exports = { onFail && onFail(result); } }); - }, - setConfig: function (userId, token) { - SessionStorage.setItem('userId', userId); - SessionStorage.setItem('token', token); } }; \ No newline at end of file From f7fde84d412c5193222156b9bd10562243073576 Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 8 Jul 2016 04:00:51 -0300 Subject: [PATCH 5/5] Ivan - OS-48 - Use SessionStore in api-call [skip ci] --- client/src/lib-app/api-call.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index 90c308ce..2db55bb8 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -1,14 +1,11 @@ const _ = require('lodash'); const APIUtils = require('lib-core/APIUtils'); -const SessionStorage = require('sessionstorage'); +const SessionStore = require('lib-app/session-store'); const root = 'http://localhost:3000/api/'; function processData (data) { - return _.extend({ - userId: SessionStorage.getItem('userId'), - token: SessionStorage.getItem('token') - }, data); + return _.extend(SessionStore.getSessionData(), data); } module.exports = {