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'); } });