mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-27 23:40:28 +05:30
Complete change password flow prototype
This commit is contained in:
parent
48a726567a
commit
e2a782f0b7
4
.babelrc
4
.babelrc
@ -4,6 +4,10 @@
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
// airbnb some how disables stage-0, so forcing it after airbnb
|
||||
"presets": ["airbnb", "stage-0"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"intl-format-cache": "^2.0.4",
|
||||
"intl-messageformat": "^1.1.0",
|
||||
"react": "^15.0.0",
|
||||
"react-dom": "^15.0.0-rc.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-helmet": "^2.3.1",
|
||||
"react-intl": "^2.0.0",
|
||||
"react-motion": "^0.4.0",
|
||||
@ -38,6 +38,7 @@
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-react-intl": "~2.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.3.13",
|
||||
"babel-preset-airbnb": "^1.1.1",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-react-hmre": "^1.0.1",
|
||||
@ -47,6 +48,7 @@
|
||||
"chokidar": "^1.2.0",
|
||||
"css-loader": "^0.23.0",
|
||||
"cssnano": "^3.4.0",
|
||||
"enzyme": "^2.2.0",
|
||||
"eslint": "^1.10.3",
|
||||
"eslint-plugin-react": "^3.13.1",
|
||||
"exports-loader": "^0.6.3",
|
||||
@ -54,6 +56,7 @@
|
||||
"file-loader": "^0.8.5",
|
||||
"html-webpack-plugin": "^1.7.0",
|
||||
"imports-loader": "^0.6.5",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "*",
|
||||
"karma-chai": "*",
|
||||
"karma-es5-shim": "*",
|
||||
@ -70,7 +73,7 @@
|
||||
"phantomjs-prebuilt": "^2.0.0",
|
||||
"postcss-loader": "^0.8.0",
|
||||
"postcss-url": "^5.1.1",
|
||||
"react-addons-test-utils": "^15.0.0",
|
||||
"react-addons-test-utils": "^15.0.2",
|
||||
"sass-loader": "^3.1.2",
|
||||
"sinon": "^1.15.3",
|
||||
"style-loader": "^0.13.0",
|
||||
|
@ -1,61 +1,87 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { LabeledInput, Button } from 'components/ui/Form';
|
||||
import FormModel from 'models/Form';
|
||||
import { Form } from 'components/ui/Form';
|
||||
|
||||
import styles from 'components/profile/profileForm.scss';
|
||||
import messages from './ChangePassword.messages';
|
||||
|
||||
export default class ChangePassword extends Component {
|
||||
displayName = 'ChangePassword';
|
||||
static displayName = 'ChangePassword';
|
||||
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
form = new FormModel();
|
||||
|
||||
render() {
|
||||
const {form} = this;
|
||||
|
||||
return (
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<Link className={styles.backButton} to="/" />
|
||||
<Form onSubmit={this.onFormSubmit}>
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<Link className={styles.backButton} to="/" />
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.changePasswordTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.changePasswordTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.changePasswordDescription} />
|
||||
<br/>
|
||||
<b>
|
||||
<Message {...messages.achievementLossWarning} />
|
||||
</b>
|
||||
</p>
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.changePasswordDescription} />
|
||||
<br/>
|
||||
<b>
|
||||
<Message {...messages.achievementLossWarning} />
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<LabeledInput {...form.bindField('newPassword')}
|
||||
type="password"
|
||||
required
|
||||
skin="light"
|
||||
label={messages.newPasswordLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.passwordRequirements} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<LabeledInput {...form.bindField('newRePassword')}
|
||||
type="password"
|
||||
required
|
||||
skin="light"
|
||||
label={messages.repeatNewPasswordLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<LabeledInput skin="light" label={messages.newPasswordLabel} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.passwordRequirements} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<LabeledInput skin="light" label={messages.repeatNewPasswordLabel} />
|
||||
</div>
|
||||
<Button color="green" block label={messages.changePasswordButton} />
|
||||
</div>
|
||||
|
||||
<Button color="green" block label={messages.changePasswordButton} />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
onFormSubmit = () => {
|
||||
this.props.onSubmit(this.form.serialize());
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import FormModel from 'models/Form';
|
||||
import { Form, Button, Input } from 'components/ui/Form';
|
||||
|
||||
import messages from './PasswordRequestForm.messages';
|
||||
|
||||
export default class PasswordRequestForm extends Component {
|
||||
static displayName = 'PasswordRequestForm';
|
||||
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
form = new FormModel();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form onSubmit={this.onSubmit}>
|
||||
<h2>
|
||||
<Message {...messages.title} />
|
||||
</h2>
|
||||
|
||||
<Input {...this.form.bindField('password')}
|
||||
type="password"
|
||||
required
|
||||
autoFocus
|
||||
color="green"
|
||||
skin="light"
|
||||
icon="key"
|
||||
placeholder={messages.pleaseEnterPassword}
|
||||
/>
|
||||
<Button color="green" label="OK" block />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.onSubmit(this.form.value('password'));
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
export default defineMessages({
|
||||
pleaseEnterPassword: {
|
||||
id: 'pleaseEnterPassword',
|
||||
defaultMessage: 'Please, enter your current password'
|
||||
},
|
||||
title: {
|
||||
id: 'title',
|
||||
defaultMessage: 'Confirm your action'
|
||||
}
|
||||
});
|
@ -85,16 +85,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.darkTextField {
|
||||
background: $black;
|
||||
border-color: lighter($black);
|
||||
}
|
||||
|
||||
.lightTextField {
|
||||
background: #fff;
|
||||
border-color: #dcd8cd;
|
||||
}
|
||||
|
||||
.textFieldIcon {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
@ -104,13 +94,31 @@
|
||||
width: 50px;
|
||||
line-height: 46px;
|
||||
text-align: center;
|
||||
border: 2px solid lighter($black);
|
||||
border: 2px solid;
|
||||
color: #444;
|
||||
cursor: default;
|
||||
|
||||
@include form-transition();
|
||||
}
|
||||
|
||||
.darkTextField {
|
||||
background: $black;
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: lighter($black);
|
||||
}
|
||||
}
|
||||
|
||||
.lightTextField {
|
||||
background: #fff;
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: #dcd8cd;
|
||||
}
|
||||
}
|
||||
|
||||
.textFieldLabel {
|
||||
font-family: $font-family-title;
|
||||
color: #666;
|
||||
|
60
src/components/ui/popup/PopupStack.jsx
Normal file
60
src/components/ui/popup/PopupStack.jsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import styles from './popup.scss';
|
||||
|
||||
export class PopupStack extends Component {
|
||||
static displayName = 'PopupStack';
|
||||
|
||||
static propTypes = {
|
||||
popups: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
props: PropTypes.oneOfType([PropTypes.object, PropTypes.func])
|
||||
})),
|
||||
pool: PropTypes.object.isRequired,
|
||||
destroy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {popups, pool} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{popups.map((popup, index) => {
|
||||
const Popup = pool[popup.type];
|
||||
|
||||
if (!Popup) {
|
||||
throw new Error(`Unknown popup type: ${popup.type}`);
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
onClose: this.onClose(popup)
|
||||
};
|
||||
const props = typeof popup.props === 'function'
|
||||
? popup.props(defaultProps)
|
||||
: {...defaultProps, ...popup.props};
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} key={popup.type + index}>
|
||||
<div className={styles.popup}>
|
||||
<Popup {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClose(popup) {
|
||||
return this.props.destroy.bind(null, popup);
|
||||
}
|
||||
}
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { destroy } from 'components/ui/popup/actions';
|
||||
|
||||
export default connect((state) => ({
|
||||
...state.popup
|
||||
}), {
|
||||
destroy
|
||||
})(PopupStack);
|
29
src/components/ui/popup/actions.js
Normal file
29
src/components/ui/popup/actions.js
Normal file
@ -0,0 +1,29 @@
|
||||
export const POPUP_REGISTER = 'POPUP_REGISTER';
|
||||
export function register(type, component) {
|
||||
return {
|
||||
type: POPUP_REGISTER,
|
||||
payload: {
|
||||
type,
|
||||
component
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const POPUP_CREATE = 'POPUP_CREATE';
|
||||
export function create(type, props = {}) {
|
||||
return {
|
||||
type: POPUP_CREATE,
|
||||
payload: {
|
||||
type,
|
||||
props
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const POPUP_DESTROY = 'POPUP_DESTROY';
|
||||
export function destroy(popup) {
|
||||
return {
|
||||
type: POPUP_DESTROY,
|
||||
payload: popup
|
||||
};
|
||||
}
|
42
src/components/ui/popup/popup.scss
Normal file
42
src/components/ui/popup/popup.scss
Normal file
@ -0,0 +1,42 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
z-index: 200;
|
||||
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
||||
|
||||
:invalid {
|
||||
button {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
44
src/components/ui/popup/reducer.js
Normal file
44
src/components/ui/popup/reducer.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { POPUP_REGISTER, POPUP_CREATE, POPUP_DESTROY } from './actions';
|
||||
|
||||
export default combineReducers({
|
||||
pool,
|
||||
popups
|
||||
});
|
||||
|
||||
function pool(pool = {}, {type, payload}) {
|
||||
if (type === POPUP_REGISTER) {
|
||||
if (!payload.type || !payload.component) {
|
||||
throw new Error('Type and component are required');
|
||||
}
|
||||
|
||||
return {
|
||||
...pool,
|
||||
[payload.type]: payload.component
|
||||
};
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
function popups(popups = [], {type, payload}) {
|
||||
switch (type) {
|
||||
case POPUP_CREATE:
|
||||
if (!payload.type) {
|
||||
throw new Error('Popup type is required');
|
||||
}
|
||||
|
||||
return popups.concat(payload);
|
||||
|
||||
case POPUP_DESTROY:
|
||||
if (!payload.type) {
|
||||
throw new Error('Popup type is required');
|
||||
}
|
||||
|
||||
return popups.filter((popup) => popup !== payload);
|
||||
|
||||
default:
|
||||
return popups;
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ export default class Form {
|
||||
return {
|
||||
name,
|
||||
ref: (el) => {
|
||||
// TODO: validate React component
|
||||
this.fields[name] = el;
|
||||
}
|
||||
};
|
||||
@ -28,6 +29,14 @@ export default class Form {
|
||||
this.fields[fieldId].focus();
|
||||
}
|
||||
|
||||
value(fieldId) {
|
||||
if (!this.fields[fieldId]) {
|
||||
throw new Error(`The field with an id ${fieldId} does not exists`);
|
||||
}
|
||||
|
||||
return this.fields[fieldId].getValue();
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return Object.keys(this.fields).reduce((acc, key) => {
|
||||
acc[key] = this.fields[key].getValue();
|
||||
|
52
src/pages/profile/ChangePasswordPage.jsx
Normal file
52
src/pages/profile/ChangePasswordPage.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import accounts from 'services/api/accounts';
|
||||
import ChangePassword from 'components/profile/changePassword/ChangePassword';
|
||||
import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm';
|
||||
|
||||
class ChangePasswordPage extends Component {
|
||||
static displayName = 'ChangePasswordPage';
|
||||
|
||||
static propTypes = {
|
||||
changePassword: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ChangePassword onSubmit={this.onSubmit} />
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit = (data) => {
|
||||
this.props.changePassword(data);
|
||||
};
|
||||
}
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { routeActions } from 'react-router-redux';
|
||||
import { register as registerPopup, create as createPopup } from 'components/ui/popup/actions';
|
||||
|
||||
function goToProfile() {
|
||||
return routeActions.push('/');
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
changePassword: (data) => {
|
||||
return (dispatch) => {
|
||||
dispatch(registerPopup('requestPassword', PasswordRequestForm));
|
||||
dispatch(createPopup('requestPassword', (props) => {
|
||||
return {
|
||||
onSubmit: (password) => {
|
||||
// TODO: hide this logic in action and do not forget to update password change time
|
||||
accounts.changePassword({
|
||||
...data,
|
||||
password
|
||||
})
|
||||
.then(props.onClose)
|
||||
.then(() => dispatch(goToProfile()));
|
||||
}
|
||||
};
|
||||
}));
|
||||
};
|
||||
}
|
||||
})(ChangePasswordPage);
|
@ -1,41 +1,51 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Userbar from 'components/userbar/Userbar';
|
||||
import PopupStack from 'components/ui/popup/PopupStack';
|
||||
|
||||
import styles from './root.scss';
|
||||
|
||||
function RootPage(props) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerContent}>
|
||||
<Link to="/" className={styles.logo}>
|
||||
Ely.by
|
||||
</Link>
|
||||
<div className={styles.userbar}>
|
||||
<Userbar {...props} onLogout={props.logout} />
|
||||
<div className={classNames(styles.root, {
|
||||
[styles.isPopupActive]: props.isPopupActive
|
||||
})}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerContent}>
|
||||
<Link to="/" className={styles.logo}>
|
||||
Ely.by
|
||||
</Link>
|
||||
<div className={styles.userbar}>
|
||||
<Userbar {...props} onLogout={props.logout} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{props.children}
|
||||
</div>
|
||||
<PopupStack />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RootPage.displayName = 'RootPage';
|
||||
RootPage.propTypes = {
|
||||
children: PropTypes.element
|
||||
children: PropTypes.element,
|
||||
logout: PropTypes.func.isRequired,
|
||||
isPopupActive: PropTypes.bool
|
||||
};
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { logout } from 'components/user/actions';
|
||||
|
||||
export default connect((state) => ({
|
||||
user: state.user
|
||||
user: state.user,
|
||||
isPopupActive: state.popup.popups.length > 0
|
||||
}), {
|
||||
logout
|
||||
})(RootPage);
|
||||
|
@ -7,6 +7,12 @@ $userBarHeight: 50px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.isPopupActive {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.4s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 756px;
|
||||
margin: 0 auto;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import auth from 'components/auth/reducer';
|
||||
import user from 'components/user/reducer';
|
||||
import popup from 'components/ui/popup/reducer';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
user
|
||||
user,
|
||||
popup
|
||||
};
|
||||
|
@ -4,7 +4,9 @@ import { Route, IndexRoute } from 'react-router';
|
||||
import RootPage from 'pages/root/RootPage';
|
||||
import IndexPage from 'pages/index/IndexPage';
|
||||
import AuthPage from 'pages/auth/AuthPage';
|
||||
|
||||
import ProfilePage from 'pages/profile/ProfilePage';
|
||||
import ProfileChangePasswordPage from 'pages/profile/ChangePasswordPage';
|
||||
|
||||
import { authenticate } from 'components/user/actions';
|
||||
|
||||
@ -18,7 +20,6 @@ import ChangePassword from 'components/auth/changePassword/ChangePassword';
|
||||
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
|
||||
import Finish from 'components/auth/finish/Finish';
|
||||
|
||||
import ProfileChangePassword from 'components/profile/changePassword/ChangePassword';
|
||||
|
||||
import authFlow from 'services/authFlow';
|
||||
|
||||
@ -53,7 +54,7 @@ export default function routesFactory(store) {
|
||||
</Route>
|
||||
|
||||
<Route path="profile" component={ProfilePage}>
|
||||
<Route path="change-password" component={ProfileChangePassword} />
|
||||
<Route path="change-password" component={ProfileChangePasswordPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ChangePassword from 'components/profile/changePassword/ChangePassword';
|
||||
|
||||
describe('<ChangePassword />', () => {
|
||||
it('renders two <LabeledInput /> components', () => {
|
||||
const component = shallow(<ChangePassword onSubmit={() => {}} />);
|
||||
|
||||
expect(component.find('LabeledInput')).to.have.length(2);
|
||||
});
|
||||
|
||||
|
||||
it('should call onSubmit if passwords entered', () => {
|
||||
const onSubmit = sinon.spy();
|
||||
const component = shallow(<ChangePassword onSubmit={onSubmit} />);
|
||||
|
||||
component.find('Form').simulate('submit');
|
||||
|
||||
sinon.assert.calledOnce(onSubmit);
|
||||
});
|
||||
});
|
118
tests/components/ui/popup/PopupStack.test.jsx
Normal file
118
tests/components/ui/popup/PopupStack.test.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { PopupStack } from 'components/ui/popup/PopupStack';
|
||||
|
||||
function DummyPopup() {}
|
||||
|
||||
describe('<PopupStack />', () => {
|
||||
it('renders all popup components', () => {
|
||||
const props = {
|
||||
pool: {
|
||||
dummy: DummyPopup
|
||||
},
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
type: 'dummy'
|
||||
},
|
||||
{
|
||||
type: 'dummy'
|
||||
}
|
||||
]
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
expect(component.find(DummyPopup)).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should pass provided props', () => {
|
||||
const expectedProps = {
|
||||
foo: 'bar'
|
||||
};
|
||||
|
||||
const props = {
|
||||
pool: {
|
||||
dummy: DummyPopup
|
||||
},
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
type: 'dummy',
|
||||
props: expectedProps
|
||||
}
|
||||
]
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
expect(component.find(DummyPopup).prop('foo')).to.be.equal(expectedProps.foo);
|
||||
});
|
||||
|
||||
it('should use props as proxy if it is function', () => {
|
||||
const expectedProps = {
|
||||
foo: 'bar'
|
||||
};
|
||||
|
||||
const props = {
|
||||
pool: {
|
||||
dummy: DummyPopup
|
||||
},
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
type: 'dummy',
|
||||
props: (props) => {
|
||||
expect(props).to.have.property('onClose');
|
||||
|
||||
return expectedProps;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
expect(component.find(DummyPopup).props()).to.be.deep.equal(expectedProps);
|
||||
});
|
||||
|
||||
it('should hide popup, when onClose called', () => {
|
||||
const props = {
|
||||
pool: {
|
||||
dummy: DummyPopup
|
||||
},
|
||||
popups: [
|
||||
{
|
||||
type: 'dummy'
|
||||
},
|
||||
{
|
||||
type: 'dummy'
|
||||
}
|
||||
],
|
||||
destroy: sinon.stub()
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
component.find(DummyPopup).last().prop('onClose')();
|
||||
|
||||
sinon.assert.calledOnce(props.destroy);
|
||||
sinon.assert.calledWith(props.destroy, sinon.match.same(props.popups[1]));
|
||||
});
|
||||
|
||||
it('throws when there is no popup component in pool', () => {
|
||||
const props = {
|
||||
pool: {
|
||||
dummy: DummyPopup
|
||||
},
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
type: 'notExists'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
shallow(<PopupStack {...props} />);
|
||||
}).to.throw('Unknown popup type: notExists');
|
||||
});
|
||||
});
|
98
tests/components/ui/popup/reducer.test.js
Normal file
98
tests/components/ui/popup/reducer.test.js
Normal file
@ -0,0 +1,98 @@
|
||||
import reducer from 'components/ui/popup/reducer';
|
||||
import {create, destroy, register} from 'components/ui/popup/actions';
|
||||
|
||||
describe('popup/reducer', () => {
|
||||
it('should have empty pool by default', () => {
|
||||
const actual = reducer(undefined, {});
|
||||
|
||||
expect(actual.pool).to.be.an('object');
|
||||
expect(actual.pool).to.be.empty;
|
||||
});
|
||||
|
||||
it('should have no popups by default', () => {
|
||||
const actual = reducer(undefined, {});
|
||||
|
||||
expect(actual.popups).to.be.an('array');
|
||||
expect(actual.popups).to.be.empty;
|
||||
});
|
||||
|
||||
describe('#register', () => {
|
||||
it('should add popup components into pool', () => {
|
||||
const actual = reducer(undefined, register('foo', function() {}));
|
||||
|
||||
expect(actual.pool.foo).to.be.a('function');
|
||||
});
|
||||
|
||||
it('throws when no type or component provided', () => {
|
||||
expect(() => reducer(undefined, register()), 'type').to.throw('Type and component are required');
|
||||
expect(() => reducer(undefined, register('foo')), 'component').to.throw('Type and component are required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
it('should create popup', () => {
|
||||
const actual = reducer(undefined, create('foo'));
|
||||
|
||||
expect(actual.popups[0]).to.be.deep.equal({
|
||||
type: 'foo',
|
||||
props: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('should store props', () => {
|
||||
const expectedProps = {foo: 'bar'};
|
||||
const actual = reducer(undefined, create('foo', expectedProps));
|
||||
|
||||
expect(actual.popups[0]).to.be.deep.equal({
|
||||
type: 'foo',
|
||||
props: expectedProps
|
||||
});
|
||||
});
|
||||
|
||||
it('should not remove existed popups', () => {
|
||||
let actual = reducer(undefined, create('foo'));
|
||||
actual = reducer(actual, create('foo2'));
|
||||
|
||||
expect(actual.popups[1]).to.be.deep.equal({
|
||||
type: 'foo2',
|
||||
props: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when no type provided', () => {
|
||||
expect(() => reducer(undefined, create())).to.throw('Popup type is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#destroy', () => {
|
||||
let state;
|
||||
let popup;
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer(undefined, register('foo', () => {}));
|
||||
state = reducer(state, create('foo'));
|
||||
popup = state.popups[0];
|
||||
});
|
||||
|
||||
it('should remove popup', () => {
|
||||
expect(state.popups).to.have.length(1);
|
||||
|
||||
state = reducer(state, destroy(popup));
|
||||
|
||||
expect(state.popups).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not remove something, that it should not', () => {
|
||||
state = reducer(state, create('foo'));
|
||||
|
||||
state = reducer(state, destroy(popup));
|
||||
|
||||
expect(state.popups).to.have.length(1);
|
||||
expect(state.popups[0]).to.not.equal(popup);
|
||||
});
|
||||
|
||||
it('throws when no type provided', () => {
|
||||
expect(() => reducer(undefined, destroy({}))).to.throw('Popup type is required');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
// require all modules ending in "_test" from the
|
||||
// current directory and all subdirectories
|
||||
var testsContext = require.context(".", true, /\.test\.js$/);
|
||||
var testsContext = require.context(".", true, /\.test\.jsx?$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
@ -31,6 +31,9 @@ var isTest = process.argv.some(function(arg) {
|
||||
});
|
||||
|
||||
process.env.NODE_ENV = isProduction ? 'production' : 'development';
|
||||
if (isTest) {
|
||||
process.env.NODE_ENV = 'test';
|
||||
}
|
||||
|
||||
const CSS_CLASS_TEMPLATE = isProduction ? '[hash:base64:5]' : '[path][name]-[local]';
|
||||
var config;
|
||||
@ -78,6 +81,14 @@ var webpackConfig = {
|
||||
extensions: ['', '.js', '.jsx']
|
||||
},
|
||||
|
||||
externals: isTest ? {
|
||||
// http://airbnb.io/enzyme/docs/guides/webpack.html
|
||||
'cheerio': 'window',
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
'react/lib/ReactContext': true,
|
||||
'react/addons': true
|
||||
} : {},
|
||||
|
||||
devServer: {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
@ -142,6 +153,10 @@ var webpackConfig = {
|
||||
{
|
||||
test: /\.(png|gif|jpg)$/,
|
||||
loader: 'url?limit=1000'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user