summaryrefslogtreecommitdiff
path: root/web-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'web-ui/src')
-rw-r--r--web-ui/src/account_recovery/account_recovery.html14
-rw-r--r--web-ui/src/account_recovery/account_recovery.js32
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js54
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss22
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js38
-rw-r--r--web-ui/src/account_recovery/backup_account_step/backup_account_step.js40
-rw-r--r--web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js27
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.js111
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.scss22
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.spec.js161
-rw-r--r--web-ui/src/account_recovery/page.js95
-rw-r--r--web-ui/src/account_recovery/page.scss121
-rw-r--r--web-ui/src/account_recovery/page.spec.js91
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js59
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss39
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js55
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.js43
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.spec.js72
-rw-r--r--web-ui/src/backup_account/confirmation/confirmation.js11
-rw-r--r--web-ui/src/backup_account/confirmation/confirmation.spec.js4
-rw-r--r--web-ui/src/backup_account/page.js13
-rw-r--r--web-ui/src/backup_account/page.scss14
-rw-r--r--web-ui/src/backup_account/page.spec.js30
-rw-r--r--web-ui/src/common/back_link/back_link.js42
-rw-r--r--web-ui/src/common/back_link/back_link.scss35
-rw-r--r--web-ui/src/common/back_link/back_link.spec.js41
-rw-r--r--web-ui/src/common/header/header.js12
-rw-r--r--web-ui/src/common/header/header.spec.js13
-rw-r--r--web-ui/src/common/link_button/link_button.js58
-rw-r--r--web-ui/src/common/link_button/link_button.scss49
-rw-r--r--web-ui/src/common/link_button/link_button.spec.js20
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.js65
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.scss22
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.spec.js31
-rw-r--r--web-ui/src/common/submit_button/submit_button.js3
-rw-r--r--web-ui/src/common/util.js21
-rw-r--r--web-ui/src/common/util.spec.js35
-rw-r--r--web-ui/src/login/opensans.css20
38 files changed, 1545 insertions, 90 deletions
diff --git a/web-ui/src/account_recovery/account_recovery.html b/web-ui/src/account_recovery/account_recovery.html
new file mode 100644
index 00000000..35054455
--- /dev/null
+++ b/web-ui/src/account_recovery/account_recovery.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="icon" type="image/png" href="public/images/favicon.png" />
+ <meta charset="utf-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
+ <title>Pixelated</title>
+ </head>
+ <body>
+ <div id="root"/>
+ <script type="text/javascript" src="/public/account_recovery.js"></script>
+ </body>
+</html>
diff --git a/web-ui/src/account_recovery/account_recovery.js b/web-ui/src/account_recovery/account_recovery.js
new file mode 100644
index 00000000..19b7c19c
--- /dev/null
+++ b/web-ui/src/account_recovery/account_recovery.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { render } from 'react-dom';
+import a11y from 'react-a11y';
+
+import App from 'src/common/app';
+import PageWrapper from './page';
+
+require('es6-promise').polyfill();
+
+if (process.env.NODE_ENV === 'development') a11y(React);
+
+render(
+ <App child={<PageWrapper />} />,
+ document.getElementById('root')
+);
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js
new file mode 100644
index 00000000..5b9da350
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+
+import './admin_recovery_code_form.scss';
+
+export const AdminRecoveryCodeForm = ({ t, next }) => (
+ <form className='account-recovery-form admin-code' onSubmit={next}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_1.svg'
+ alt={t('account-recovery.admin-form.image-description')}
+ />
+ <h1>{t('account-recovery.admin-form.title')}</h1>
+ <img
+ className='admin-code-image'
+ src='/public/images/account-recovery/admins_contact.svg'
+ alt=''
+ />
+ <ul>
+ <li>{t('account-recovery.admin-form.tip1')}</li>
+ <li>{t('account-recovery.admin-form.tip2')}</li>
+ <li>{t('account-recovery.admin-form.tip3')}</li>
+ </ul>
+ <InputField name='admin-code' label={t('account-recovery.admin-form.input-label')} />
+ <SubmitButton buttonText={t('account-recovery.button-next')} />
+ </form>
+);
+
+AdminRecoveryCodeForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(AdminRecoveryCodeForm);
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss
new file mode 100644
index 00000000..6dcbcc67
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.admin-code-image {
+ margin: 1em 0;
+ align-self: center;
+ height: 2.7em;
+}
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js
new file mode 100644
index 00000000..73c4c1e0
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js
@@ -0,0 +1,38 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import { AdminRecoveryCodeForm } from './admin_recovery_code_form';
+
+describe('AdminRecoveryCodeForm', () => {
+ let adminRecoveryCodeForm;
+ let mockNext;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ mockNext = expect.createSpy();
+ adminRecoveryCodeForm = shallow(
+ <AdminRecoveryCodeForm t={mockTranslations} next={mockNext} />
+ );
+ });
+
+ it('renders title for admin recovery code', () => {
+ expect(adminRecoveryCodeForm.find('h1').text()).toEqual('account-recovery.admin-form.title');
+ });
+
+ it('renders tips for retrieving recovery code', () => {
+ expect(adminRecoveryCodeForm.find('li').length).toEqual(3);
+ });
+
+ it('renders input field for admin code', () => {
+ expect(adminRecoveryCodeForm.find('InputField').props().name).toEqual('admin-code');
+ });
+
+ it('renders button for next step', () => {
+ expect(adminRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('submits form to next step', () => {
+ adminRecoveryCodeForm.find('form').simulate('submit');
+ expect(mockNext).toHaveBeenCalled();
+ });
+});
diff --git a/web-ui/src/account_recovery/backup_account_step/backup_account_step.js b/web-ui/src/account_recovery/backup_account_step/backup_account_step.js
new file mode 100644
index 00000000..891b45ec
--- /dev/null
+++ b/web-ui/src/account_recovery/backup_account_step/backup_account_step.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import { translate } from 'react-i18next';
+import LinkButton from 'src/common/link_button/link_button';
+
+export const BackupAccountStep = ({ t }) => (
+ <div className='account-recovery-form backup-account'>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_4.svg'
+ alt={t('account-recovery.backup-account-step.image-description')}
+ />
+ <h1>{t('account-recovery.backup-account-step.title')}</h1>
+ <LinkButton
+ buttonText={t('account-recovery.backup-account-step.buttonText')}
+ href='/backup-account'
+ />
+ </div>
+);
+
+BackupAccountStep.propTypes = {
+ t: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(BackupAccountStep);
diff --git a/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js b/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js
new file mode 100644
index 00000000..38a5e560
--- /dev/null
+++ b/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js
@@ -0,0 +1,27 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import LinkButton from 'src/common/link_button/link_button';
+import { BackupAccountStep } from './backup_account_step';
+
+describe('BackupAccountStep', () => {
+ let backupAccountStep;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ backupAccountStep = shallow(<BackupAccountStep t={mockTranslations} />);
+ });
+
+ it('renders title for backup account step', () => {
+ expect(backupAccountStep.find('h1').text()).toEqual('account-recovery.backup-account-step.title');
+ });
+
+ it('renders submit button with given href', () => {
+ expect(backupAccountStep.find(LinkButton).props().href).toEqual('/backup-account');
+ });
+
+ it('renders submit button with given button text', () => {
+ expect(backupAccountStep.find(LinkButton).props().buttonText)
+ .toEqual('account-recovery.backup-account-step.buttonText');
+ });
+});
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.js b/web-ui/src/account_recovery/new_password_form/new_password_form.js
new file mode 100644
index 00000000..5e1e72c9
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import 'isomorphic-fetch';
+import React from 'react';
+import { translate } from 'react-i18next';
+import validator from 'validator';
+
+import { submitForm } from 'src/common/util';
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
+
+import './new_password_form.scss';
+
+export class NewPasswordForm extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ submitButtonDisabled: true,
+ password: '',
+ errorPassword: '',
+ confirmPassword: '',
+ errorConfirmPassword: ''
+ };
+ }
+
+ submitHandler = (event) => {
+ event.preventDefault();
+ submitForm(event, '/account-recovery', {
+ userCode: this.props.userCode,
+ password: this.state.password,
+ confirmPassword: this.state.confirmPassword
+ }).then(() => this.props.next());
+ }
+
+ handleChangePassword = (event) => {
+ this.setState({ password: event.target.value });
+ this.validatePassword(event.target.value, this.state.confirmPassword);
+ };
+
+ handleChangeConfirmPassword = (event) => {
+ this.setState({ confirmPassword: event.target.value });
+ this.validatePassword(this.state.password, event.target.value);
+ };
+
+ validatePassword = (password, confirmPassword) => {
+ const emptyPassword = validator.isEmpty(password);
+ const validPassword = validator.isLength(password, { min: 8, max: 9999 });
+ const emptyConfirmPassword = validator.isEmpty(confirmPassword);
+ const validConfirmPassword = confirmPassword === password;
+
+ const t = this.props.t;
+
+ this.setState({
+ errorPassword: !emptyPassword && !validPassword ? t('account-recovery.new-password-form.error.invalid-password') : '',
+ errorConfirmPassword: !emptyConfirmPassword && !validConfirmPassword ? t('account-recovery.new-password-form.error.invalid-confirm-password') : '',
+ submitButtonDisabled: !validPassword || !validConfirmPassword
+ });
+ };
+
+ render() {
+ const { t, previous } = this.props;
+ return (
+ <form className='account-recovery-form new-password' onSubmit={this.submitHandler}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_3.svg'
+ alt={t('account-recovery.new-password-form.image-description')}
+ />
+ <h1>{t('account-recovery.new-password-form.title')}</h1>
+ <InputField
+ type='password' name='new-password' value={this.state.password}
+ label={t('account-recovery.new-password-form.input-label1')}
+ errorText={this.state.errorPassword} onChange={this.handleChangePassword}
+ />
+ <InputField
+ type='password' name='confirm-password' value={this.state.confirmPassword}
+ label={t('account-recovery.new-password-form.input-label2')}
+ errorText={this.state.errorConfirmPassword} onChange={this.handleChangeConfirmPassword}
+ />
+ <SubmitButton buttonText={t('account-recovery.button-next')} disabled={this.state.submitButtonDisabled} />
+ <BackLink text={t('account-recovery.back')} onClick={previous} />
+ </form>
+ );
+ }
+}
+
+NewPasswordForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired,
+ previous: React.PropTypes.func.isRequired,
+ userCode: React.PropTypes.string.isRequired
+};
+
+export default translate('', { wait: true })(NewPasswordForm);
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.scss b/web-ui/src/account_recovery/new_password_form/new_password_form.scss
new file mode 100644
index 00000000..77623262
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.new-password {
+ .input-field-group:first-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js
new file mode 100644
index 00000000..c29487a7
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js
@@ -0,0 +1,161 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import fetchMock from 'fetch-mock';
+import { NewPasswordForm } from './new_password_form';
+
+describe('NewPasswordForm', () => {
+ let newPasswordForm;
+ let mockPrevious;
+ let mockNext;
+ let mockTranslations;
+
+ beforeEach(() => {
+ mockTranslations = key => key;
+ mockPrevious = expect.createSpy();
+ newPasswordForm = shallow(
+ <NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' />
+ );
+ });
+
+ it('renders title for new password form', () => {
+ expect(newPasswordForm.find('h1').text()).toEqual('account-recovery.new-password-form.title');
+ });
+
+ it('renders input for new password', () => {
+ expect(newPasswordForm.find('InputField').at(0).props().type).toEqual('password');
+ expect(newPasswordForm.find('InputField').at(0).props().label).toEqual('account-recovery.new-password-form.input-label1');
+ });
+
+ it('renders input to confirm new password', () => {
+ expect(newPasswordForm.find('InputField').at(1).props().type).toEqual('password');
+ expect(newPasswordForm.find('InputField').at(1).props().label).toEqual('account-recovery.new-password-form.input-label2');
+ });
+
+ it('renders submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('returns to previous step on link click', () => {
+ newPasswordForm.find('BackLink').simulate('click');
+ expect(mockPrevious).toHaveBeenCalled();
+ });
+
+ describe('Submit', () => {
+ beforeEach((done) => {
+ mockNext = expect.createSpy().andCall(() => done());
+ newPasswordForm = shallow(
+ <NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' next={mockNext} />
+ );
+ fetchMock.post('/account-recovery', 200);
+ newPasswordForm.find('InputField[name="new-password"]').simulate('change', { target: { value: '123' } });
+ newPasswordForm.find('InputField[name="confirm-password"]').simulate('change', { target: { value: '456' } });
+ newPasswordForm.find('form').simulate('submit', { preventDefault: expect.createSpy() });
+ });
+
+ it('posts to account recovery', () => {
+ expect(fetchMock.called('/account-recovery')).toBe(true, 'POST was not called');
+ });
+
+ it('sends user code as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"userCode":"def234"');
+ });
+
+ it('sends password as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"password":"123"');
+ });
+
+ it('sends confirm password as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"confirmPassword":"456"');
+ });
+ });
+
+ describe('Password validation', () => {
+ let newPasswordFormInstance;
+
+ beforeEach(() => {
+ newPasswordFormInstance = newPasswordForm.instance();
+ });
+
+ it('verifies initial state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+
+ context('with valid fields', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('12345678', '12345678');
+ });
+
+ it('does not set error in state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('enables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(false);
+ });
+ });
+
+ context('with invalid password', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('1234', '');
+ });
+
+ it('sets password error in state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('account-recovery.new-password-form.error.invalid-password');
+ });
+
+ it('disables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ context('with invalid confirm password', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('12345678', '1234');
+ });
+
+ it('sets confirm password error in state', () => {
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('account-recovery.new-password-form.error.invalid-confirm-password');
+ });
+
+ it('disables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ context('with empty fields', () => {
+ it('does not set error in state if both empty', () => {
+ newPasswordFormInstance.validatePassword('', '');
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('does not set confirm password error in state if empty', () => {
+ newPasswordFormInstance.validatePassword('12345678', '');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('sets confirm password error in state if not empty', () => {
+ newPasswordFormInstance.validatePassword('', '12345678');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('account-recovery.new-password-form.error.invalid-confirm-password');
+ });
+
+ it('disables submit button if empty confirm password', () => {
+ newPasswordFormInstance.validatePassword('12345678', '');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+
+ it('disables submit button if empty password', () => {
+ newPasswordFormInstance.validatePassword('', '12345678');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ it('calls next handler on success', () => {
+ expect(mockNext).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/web-ui/src/account_recovery/page.js b/web-ui/src/account_recovery/page.js
new file mode 100644
index 00000000..94927a16
--- /dev/null
+++ b/web-ui/src/account_recovery/page.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+import DocumentTitle from 'react-document-title';
+import Header from 'src/common/header/header';
+import AdminRecoveryCodeForm from 'src/account_recovery/admin_recovery_code_form/admin_recovery_code_form';
+import UserRecoveryCodeForm from 'src/account_recovery/user_recovery_code_form/user_recovery_code_form';
+import NewPasswordForm from 'src/account_recovery/new_password_form/new_password_form';
+import BackupAccountStep from 'src/account_recovery/backup_account_step/backup_account_step';
+import Footer from 'src/common/footer/footer';
+
+import 'font-awesome/scss/font-awesome.scss';
+import './page.scss';
+
+
+export class Page extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = { step: 0, userCode: '' };
+ }
+
+ nextStep = (event) => {
+ if (event) {
+ event.preventDefault();
+ }
+ this.setState({ step: this.state.step + 1 });
+ }
+
+ previousStep = () => {
+ this.setState({ step: this.state.step - 1 });
+ }
+
+ saveUserCode = (event) => {
+ this.setState({ userCode: event.target.value });
+ }
+
+ steps = () => ({
+ 0: <AdminRecoveryCodeForm next={this.nextStep} />,
+ 1:
+ (<UserRecoveryCodeForm
+ previous={this.previousStep}
+ next={this.nextStep}
+ saveCode={this.saveUserCode}
+ />),
+ 2:
+ (<NewPasswordForm
+ previous={this.previousStep}
+ userCode={this.state.userCode}
+ next={this.nextStep}
+ />),
+ 3: <BackupAccountStep />
+ })
+
+ mainContent = () => this.steps()[this.state.step];
+
+ render() {
+ const t = this.props.t;
+ return (
+ <DocumentTitle title={t('account-recovery.page-title')}>
+ <div className='page'>
+ <Header />
+ <section>
+ <div className='container'>
+ {this.mainContent()}
+ </div>
+ </section>
+ <Footer />
+ </div>
+ </DocumentTitle>
+ );
+ }
+}
+
+Page.propTypes = {
+ t: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(Page);
diff --git a/web-ui/src/account_recovery/page.scss b/web-ui/src/account_recovery/page.scss
new file mode 100644
index 00000000..378bf63c
--- /dev/null
+++ b/web-ui/src/account_recovery/page.scss
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/vendor/reset";
+@import "~scss/base/colors";
+@import "~scss/base/fonts";
+
+html, body, #root {
+ height: 100%;
+}
+
+body, #root {
+ min-height: 100%;
+}
+
+a {
+ text-decoration: none;
+}
+
+.container {
+ background: $white;
+ margin: 3% auto;
+ box-shadow: 0 2px 3px 0 $shadow;
+ width: 84%;
+ padding: 6% 5%;
+}
+
+.page {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ background: $dark_blue; /* For browsers that do not support gradients */
+ background: -webkit-linear-gradient(left top, $dark_blue, $middle_blue); /* For Safari 5.1 to 6.0 */
+ background: -o-linear-gradient(bottom right, $dark_blue, $middle_blue); /* For Opera 11.1 to 12.0 */
+ background: -moz-linear-gradient(bottom right, $dark_blue, $middle_blue); /* For Firefox 3.6 to 15 */
+ background: linear-gradient(to bottom right, $dark_blue, $middle_blue); /* Standard syntax */
+ color: $dark_grey_text;
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+section {
+ flex: 1 0 auto;
+}
+
+h1 {
+ font-size: 1.3em;
+ font-weight: 600;
+ text-align: center;
+}
+
+p {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.account-recovery-form {
+ display: flex;
+ flex-direction: column;
+
+ .account-recovery-progress {
+ align-self: center;
+ width: 100%;
+ }
+
+ .input-field-group {
+ margin-top: 0;
+ }
+}
+
+@media only screen and (min-width : 500px) {
+ body {
+ font-size: 1.3em;
+ }
+
+ .account-recovery-form {
+ align-items: center;
+
+ .account-recovery-progress {
+ margin-bottom: 1.4em;
+ width: 80%;
+ }
+
+ h1 {
+ width: 80%;
+ }
+ }
+}
+
+@media only screen and (min-width : 960px) {
+ .container {
+ width: 60%;
+ max-width: 700px;
+ padding: 3em;
+ }
+
+ .account-recovery-form {
+ h1 {
+ max-width: 80%;
+ width: auto;
+ }
+
+ .input-field-group, .submit-button {
+ width: 60%;
+ align-self: center;
+ }
+ }
+}
diff --git a/web-ui/src/account_recovery/page.spec.js b/web-ui/src/account_recovery/page.spec.js
new file mode 100644
index 00000000..8e4ccc33
--- /dev/null
+++ b/web-ui/src/account_recovery/page.spec.js
@@ -0,0 +1,91 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+
+import { Page } from 'src/account_recovery/page';
+import Header from 'src/common/header/header';
+import Footer from 'src/common/footer/footer';
+
+import AdminRecoveryCodeFormWrapper from './admin_recovery_code_form/admin_recovery_code_form';
+import UserRecoveryCodeFormWrapper from './user_recovery_code_form/user_recovery_code_form';
+import NewPasswordFormWrapper from './new_password_form/new_password_form';
+import BackupAccountStepWrapper from './backup_account_step/backup_account_step';
+
+describe('Account Recovery Page', () => {
+ let page;
+ let pageInstance;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ page = shallow(<Page t={mockTranslations} />);
+ pageInstance = page.instance();
+ });
+
+ it('renders account recovery page title', () => {
+ expect(page.props().title).toEqual('account-recovery.page-title');
+ });
+
+ it('renders header without logout button', () => {
+ expect(page.find(Header).props().renderLogout).toEqual(false);
+ });
+
+ it('renders footer', () => {
+ expect(page.find(Footer).length).toEqual(1);
+ });
+
+ it('saves user code', () => {
+ pageInstance.saveUserCode({ target: { value: '123' } });
+ expect(pageInstance.state.userCode).toEqual('123');
+ });
+
+ it('prevents default event before next', () => {
+ const eventSpy = expect.createSpy();
+ pageInstance.nextStep({ preventDefault: eventSpy });
+
+ expect(eventSpy).toHaveBeenCalled();
+ });
+
+ context('main content', () => {
+ it('renders admin recovery code form as default form', () => {
+ expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(0);
+ expect(page.find(NewPasswordFormWrapper).length).toEqual(0);
+ });
+
+ it('renders user recovery code form when admin code submitted', () => {
+ pageInstance.nextStep();
+
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('returns to admin code form on user code form back link', () => {
+ pageInstance.nextStep();
+ pageInstance.previousStep();
+
+ expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('renders new password form when user code submitted', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+
+ expect(page.find(NewPasswordFormWrapper).length).toEqual(1);
+ });
+
+ it('returns to user code form on new password form back link', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+ pageInstance.previousStep();
+
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('renders backup account form after submitting new password', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+
+ expect(page.find(BackupAccountStepWrapper).length).toEqual(1);
+ });
+ });
+});
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js
new file mode 100644
index 00000000..c39c894d
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
+
+import './user_recovery_code_form.scss';
+
+export const UserRecoveryCodeForm = ({ t, previous, next, saveCode }) => (
+ <form className='account-recovery-form user-code' onSubmit={next}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_2.svg'
+ alt={t('account-recovery.user-form.image-description')}
+ />
+ <h1>{t('account-recovery.user-form.title')}</h1>
+ <div className='user-code-form-content'>
+ <img
+ className='user-code-image'
+ src='/public/images/account-recovery/codes.svg'
+ alt=''
+ />
+ <p>{t('account-recovery.user-form.description')}</p>
+ </div>
+ <InputField
+ name='user-code' label={t('account-recovery.user-form.input-label')}
+ onChange={saveCode}
+ />
+ <SubmitButton buttonText={t('account-recovery.button-next')} />
+ <BackLink text={t('account-recovery.back')} onClick={previous} />
+ </form>
+);
+
+UserRecoveryCodeForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ previous: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired,
+ saveCode: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(UserRecoveryCodeForm);
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss
new file mode 100644
index 00000000..55f5621b
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.user-code-form-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.user-code-image {
+ margin: 1em 0;
+ align-self: center;
+ height: 4em;
+}
+
+@media only screen and (min-width : 500px) {
+ .user-code-form-content {
+ flex-direction: row;
+ width: 80%;
+ }
+
+ .user-code-image {
+ margin-right: 1.6em;
+ }
+}
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js
new file mode 100644
index 00000000..386c3a19
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js
@@ -0,0 +1,55 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import { UserRecoveryCodeForm } from './user_recovery_code_form';
+
+describe('UserRecoveryCodeForm', () => {
+ let userRecoveryCodeForm;
+ let mockNext;
+ let mockPrevious;
+ let mockSaveCode;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ mockNext = expect.createSpy();
+ mockPrevious = expect.createSpy();
+ mockSaveCode = expect.createSpy();
+ userRecoveryCodeForm = shallow(
+ <UserRecoveryCodeForm
+ t={mockTranslations} next={mockNext}
+ previous={mockPrevious} saveCode={mockSaveCode}
+ />
+ );
+ });
+
+ it('renders title for user recovery code', () => {
+ expect(userRecoveryCodeForm.find('h1').text()).toEqual('account-recovery.user-form.title');
+ });
+
+ it('renders description', () => {
+ expect(userRecoveryCodeForm.find('p').text()).toEqual('account-recovery.user-form.description');
+ });
+
+ it('renders input for user code', () => {
+ expect(userRecoveryCodeForm.find('InputField').props().label).toEqual('account-recovery.user-form.input-label');
+ });
+
+ it('renders submit button', () => {
+ expect(userRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('submits form to next step', () => {
+ userRecoveryCodeForm.find('form').simulate('submit');
+ expect(mockNext).toHaveBeenCalled();
+ });
+
+ it('returns to previous step on link click', () => {
+ userRecoveryCodeForm.find('BackLink').simulate('click');
+ expect(mockPrevious).toHaveBeenCalled();
+ });
+
+ it('saves code on input change', () => {
+ userRecoveryCodeForm.find('InputField').simulate('change', '123');
+ expect(mockSaveCode).toHaveBeenCalledWith('123');
+ });
+});
diff --git a/web-ui/src/backup_account/backup_email/backup_email.js b/web-ui/src/backup_account/backup_email/backup_email.js
index 09863950..ac64f02e 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.js
@@ -18,10 +18,12 @@
import 'isomorphic-fetch';
import React from 'react';
import { translate } from 'react-i18next';
+import validator from 'validator';
+
+import { submitForm } from 'src/common/util';
import SubmitButton from 'src/common/submit_button/submit_button';
import InputField from 'src/common/input_field/input_field';
-import validator from 'validator';
-import browser from 'helpers/browser';
+import BackLink from 'src/common/back_link/back_link';
import './backup_email.scss';
@@ -29,7 +31,7 @@ export class BackupEmail extends React.Component {
constructor(props) {
super(props);
- this.state = { error: '', submitButtonDisabled: true };
+ this.state = { error: '', submitButtonDisabled: true, backupEmail: '' };
}
validateEmail = (event) => {
@@ -40,21 +42,20 @@ export class BackupEmail extends React.Component {
error: !emptyEmail && !validEmail ? t('backup-account.backup-email.error.invalid-email') : '',
submitButtonDisabled: !validEmail || emptyEmail
});
- }
+ };
submitHandler = (event) => {
- event.preventDefault();
+ submitForm(event, '/backup-account', {
+ backupEmail: this.state.backupEmail
+ }).then((response) => {
+ if (response.ok) this.props.onSubmit('success');
+ else this.props.onSubmit('error');
+ });
+ };
- fetch('/backup-account', {
- credentials: 'same-origin',
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- csrftoken: [browser.getCookie('XSRF-TOKEN')]
- })
- }).then(() => this.props.onSubmit('success'));
+ handleChange = (event) => {
+ this.setState({ backupEmail: event.target.value });
+ this.validateEmail(event);
}
render() {
@@ -70,14 +71,12 @@ export class BackupEmail extends React.Component {
<h1>{t('backup-account.backup-email.title')}</h1>
<p>{t('backup-account.backup-email.paragraph1')}</p>
<p>{t('backup-account.backup-email.paragraph2')}</p>
- <InputField name='email' label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.validateEmail} />
+ <InputField name='email' value={this.state.backupEmail} label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.handleChange} />
<SubmitButton buttonText={t('backup-account.backup-email.button')} disabled={this.state.submitButtonDisabled} />
- <div className='link-content'>
- <a href='/' className='link'>
- <i className='fa fa-angle-left' aria-hidden='true' />
- <span>{t('back-to-inbox')}</span>
- </a>
- </div>
+ <BackLink
+ href='/'
+ text={t('back-to-inbox')}
+ />
</form>
</div>
);
diff --git a/web-ui/src/backup_account/backup_email/backup_email.spec.js b/web-ui/src/backup_account/backup_email/backup_email.spec.js
index 48199738..a34afa06 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.spec.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.spec.js
@@ -3,12 +3,12 @@ import expect from 'expect';
import React from 'react';
import fetchMock from 'fetch-mock';
import { BackupEmail } from 'src/backup_account/backup_email/backup_email';
-import browser from 'helpers/browser';
describe('BackupEmail', () => {
let backupEmail;
let mockOnSubmit;
let mockTranslations;
+ let backupEmailInstance;
beforeEach(() => {
mockOnSubmit = expect.createSpy();
@@ -30,8 +30,6 @@ describe('BackupEmail', () => {
});
describe('Email validation', () => {
- let backupEmailInstance;
-
beforeEach(() => {
backupEmailInstance = backupEmail.instance();
});
@@ -84,42 +82,64 @@ describe('BackupEmail', () => {
});
});
+ describe('Email changing handler', () => {
+ beforeEach(() => {
+ backupEmailInstance = backupEmail.instance();
+ });
+
+ it('sets user backup email in the state', () => {
+ backupEmailInstance.handleChange({ target: { value: 'test@test.com' } });
+ expect(backupEmailInstance.state.backupEmail).toEqual('test@test.com');
+ });
+ });
+
describe('Submit', () => {
let preventDefaultSpy;
- beforeEach((done) => {
- mockOnSubmit = expect.createSpy().andCall(() => done());
+ beforeEach(() => {
preventDefaultSpy = expect.createSpy();
- expect.spyOn(browser, 'getCookie').andReturn('abc123');
+ });
- backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
+ context('on success', () => {
+ beforeEach((done) => {
+ mockOnSubmit = expect.createSpy().andCall(() => done());
- fetchMock.post('/backup-account', 204);
- backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
- });
+ fetchMock.post('/backup-account', 204);
+ backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
- it('posts backup email', () => {
- expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called');
- });
+ backupEmail.find('InputField').simulate('change', { target: { value: 'test@test.com' } });
+ backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
+ });
- it('sends csrftoken as content', () => {
- expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]');
- });
+ it('posts backup email', () => {
+ expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called');
+ });
- it('sends content-type header', () => {
- expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json');
- });
+ it('sends user email as content', () => {
+ expect(fetchMock.lastOptions('/backup-account').body).toContain('"backupEmail":"test@test.com"');
+ });
- it('sends same origin headers', () => {
- expect(fetchMock.lastOptions('/backup-account').credentials).toEqual('same-origin');
+ it('calls onSubmit from props with success', () => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('success');
+ });
});
- it('prevents default call to refresh page', () => {
- expect(preventDefaultSpy).toHaveBeenCalled();
- });
+ context('on error', () => {
+ beforeEach((done) => {
+ mockOnSubmit = expect.createSpy().andCall(() => done());
+
+ fetchMock.post('/backup-account', 500);
+ backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
+ backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
+ });
- it('calls onSubmit from props when success', () => {
- expect(mockOnSubmit).toHaveBeenCalledWith('success');
+ it('calls onSubmit from props with error', () => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('error');
+ });
});
});
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
});
diff --git a/web-ui/src/backup_account/confirmation/confirmation.js b/web-ui/src/backup_account/confirmation/confirmation.js
index 41637dab..49b0d19c 100644
--- a/web-ui/src/backup_account/confirmation/confirmation.js
+++ b/web-ui/src/backup_account/confirmation/confirmation.js
@@ -18,6 +18,7 @@
import React from 'react';
import { translate } from 'react-i18next';
import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
import './confirmation.scss';
@@ -29,12 +30,10 @@ export const Confirmation = ({ t }) => (
<form action='/'>
<SubmitButton buttonText={t('backup-account.confirmation.button')} type='submit' />
</form>
- <div className='link-content'>
- <a href='/backup-account' className='link'>
- <i className='fa fa-angle-left' aria-hidden='true' />
- <span>{t('backup-account.confirmation.retry-button')}</span>
- </a>
- </div>
+ <BackLink
+ href='/backup-account'
+ text={t('backup-account.confirmation.retry-button')}
+ />
</div>
);
diff --git a/web-ui/src/backup_account/confirmation/confirmation.spec.js b/web-ui/src/backup_account/confirmation/confirmation.spec.js
index 291d156d..7a6f38ca 100644
--- a/web-ui/src/backup_account/confirmation/confirmation.spec.js
+++ b/web-ui/src/backup_account/confirmation/confirmation.spec.js
@@ -20,10 +20,10 @@ describe('Confirmation', () => {
});
it('renders confirmation retry button', () => {
- expect(page.find('a').text()).toEqual('backup-account.confirmation.retry-button');
+ expect(page.find('BackLink').props().text).toEqual('backup-account.confirmation.retry-button');
});
it('retries button redirects to backup account', () => {
- expect(page.find('a').props().href).toEqual('/backup-account');
+ expect(page.find('BackLink').props().href).toEqual('/backup-account');
});
});
diff --git a/web-ui/src/backup_account/page.js b/web-ui/src/backup_account/page.js
index 49e4b316..e7663205 100644
--- a/web-ui/src/backup_account/page.js
+++ b/web-ui/src/backup_account/page.js
@@ -22,6 +22,7 @@ import Footer from 'src/common/footer/footer';
import Header from 'src/common/header/header';
import BackupEmail from 'src/backup_account/backup_email/backup_email';
import Confirmation from 'src/backup_account/confirmation/confirmation';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
import 'font-awesome/scss/font-awesome.scss';
import './page.scss';
@@ -36,22 +37,30 @@ export class Page extends React.Component {
saveBackupEmail = (status) => {
this.setState({ status });
- }
+ };
mainContent = () => {
if (this.state.status === 'success') return <Confirmation />;
return <BackupEmail onSubmit={this.saveBackupEmail} />;
};
+ showSnackbarOnError = (t) => {
+ if (this.state.status === 'error') {
+ return <SnackbarNotification message={t('backup-account.error.submit-error')} isError />;
+ }
+ return undefined; // To satisfy eslint error - consistent-return
+ };
+
render() {
const t = this.props.t;
return (
<DocumentTitle title={t('backup-account.page-title')}>
<div className='page'>
- <Header />
+ <Header renderLogout />
<section>
{this.mainContent()}
</section>
+ {this.showSnackbarOnError(t)}
<Footer />
</div>
</DocumentTitle>
diff --git a/web-ui/src/backup_account/page.scss b/web-ui/src/backup_account/page.scss
index 71e3f074..d4f1f887 100644
--- a/web-ui/src/backup_account/page.scss
+++ b/web-ui/src/backup_account/page.scss
@@ -64,20 +64,6 @@ p {
margin-bottom: 0.5em;
}
-.link {
- color: $dark_blue;
- font-style: italic;
- font-size: 0.8em;
-
- .fa {
- font-size: 1.6em;
- position: relative;
- top: 3px;
- margin-right: 0.3em;
- }
-
-}
-
@media only screen and (min-width : 500px) {
body {
font-size: 1.3em;
diff --git a/web-ui/src/backup_account/page.spec.js b/web-ui/src/backup_account/page.spec.js
index bd7bb884..8c014ee4 100644
--- a/web-ui/src/backup_account/page.spec.js
+++ b/web-ui/src/backup_account/page.spec.js
@@ -4,6 +4,8 @@ import React from 'react';
import { Page } from 'src/backup_account/page';
import BackupEmail from 'src/backup_account/backup_email/backup_email';
import Confirmation from 'src/backup_account/confirmation/confirmation';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
+import Header from 'src/common/header/header';
describe('BackupAccount', () => {
let page;
@@ -17,6 +19,10 @@ describe('BackupAccount', () => {
expect(page.props().title).toEqual('backup-account.page-title');
});
+ it('renders header with logout button', () => {
+ expect(page.find(Header).props().renderLogout).toEqual(true);
+ });
+
describe('save backup email', () => {
let pageInstance;
@@ -41,5 +47,29 @@ describe('BackupAccount', () => {
pageInstance.saveBackupEmail('success');
expect(page.find(Confirmation).length).toEqual(1);
});
+
+ context('on submit error', () => {
+ beforeEach(() => {
+ pageInstance.saveBackupEmail('error');
+ });
+
+ it('returns snackbar component on error', () => {
+ const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
+ expect(snackbar).toEqual(<SnackbarNotification message='backup-account.error.submit-error' isError />);
+ });
+
+ it('returns nothing when there is no error', () => {
+ pageInstance.saveBackupEmail('success');
+ const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
+ expect(snackbar).toEqual(undefined);
+ });
+
+ it('renders snackbar notification on error', () => {
+ const snackbar = page.find(SnackbarNotification);
+ expect(snackbar).toExist();
+ expect(snackbar.props().message).toEqual('backup-account.error.submit-error');
+ expect(snackbar.props().isError).toEqual(true);
+ });
+ });
});
});
diff --git a/web-ui/src/common/back_link/back_link.js b/web-ui/src/common/back_link/back_link.js
new file mode 100644
index 00000000..bb5ffbea
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+
+import './back_link.scss';
+
+const icon = <i className='fa fa-angle-left' aria-hidden='true' />;
+
+const button = (text, options) => (
+ <button className='link' {...options}>{icon}<span>{text}</span></button>
+);
+
+const link = (text, options) => (
+ <a className='link' {...options}>{icon}<span>{text}</span></a>
+);
+
+const BackLink = ({ text, ...other }) => (
+ <div className='link-content'>
+ { other.href ? link(text, other) : button(text, other) }
+ </div>
+);
+
+BackLink.propTypes = {
+ text: React.PropTypes.string.isRequired
+};
+
+export default BackLink;
diff --git a/web-ui/src/common/back_link/back_link.scss b/web-ui/src/common/back_link/back_link.scss
new file mode 100644
index 00000000..5541d9d9
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.scss
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/base/colors";
+
+.link {
+ color: $dark_blue;
+ font-style: italic;
+ font-size: 0.8em;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+
+ .fa {
+ font-size: 1.6em;
+ position: relative;
+ top: 3px;
+ margin-right: 0.3em;
+ }
+}
diff --git a/web-ui/src/common/back_link/back_link.spec.js b/web-ui/src/common/back_link/back_link.spec.js
new file mode 100644
index 00000000..5f49a6f9
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.spec.js
@@ -0,0 +1,41 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import BackLink from 'src/common/back_link/back_link';
+
+describe('BackLink', () => {
+ context('as link', () => {
+ let backLink;
+
+ beforeEach(() => {
+ backLink = shallow(<BackLink text='Back to inbox' href='/' />);
+ });
+
+ it('renders link with text', () => {
+ expect(backLink.find('a').text()).toEqual('Back to inbox');
+ });
+
+ it('adds link action', () => {
+ expect(backLink.find('a').props().href).toEqual('/');
+ });
+ });
+
+ context('as button', () => {
+ let backLink;
+ let mockClick;
+
+ beforeEach(() => {
+ mockClick = expect.createSpy();
+ backLink = shallow(<BackLink text='Back to inbox' onClick={mockClick} />);
+ });
+
+ it('renders button with text', () => {
+ expect(backLink.find('button').text()).toEqual('Back to inbox');
+ });
+
+ it('adds button click event', () => {
+ backLink.find('button').simulate('click');
+ expect(mockClick).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/web-ui/src/common/header/header.js b/web-ui/src/common/header/header.js
index 715d54c6..3ad924c0 100644
--- a/web-ui/src/common/header/header.js
+++ b/web-ui/src/common/header/header.js
@@ -19,7 +19,7 @@ import React from 'react';
import Logout from 'src/common/logout/logout';
import './header.scss';
-export const Header = () => (
+export const Header = ({ renderLogout }) => (
<header className='header-wrapper'>
<div className='header-content'>
<a href='/'>
@@ -30,10 +30,18 @@ export const Header = () => (
/>
</a>
<div className='header-icons'>
- <Logout />
+ { renderLogout ? <Logout /> : <div /> }
</div>
</div>
</header>
);
+Header.propTypes = {
+ renderLogout: React.PropTypes.bool
+};
+
+Header.defaultProps = {
+ renderLogout: false
+};
+
export default Header;
diff --git a/web-ui/src/common/header/header.spec.js b/web-ui/src/common/header/header.spec.js
index 81a952c7..0c11713b 100644
--- a/web-ui/src/common/header/header.spec.js
+++ b/web-ui/src/common/header/header.spec.js
@@ -11,11 +11,16 @@ describe('Header', () => {
header = shallow(<Header />);
});
- it('renders the header containing the logout button', () => {
- expect(header.find('header').find(Logout)).toExist();
- });
-
it('renders the header pixelated logo', () => {
expect(header.find('header').find('img').props().alt).toEqual('Pixelated');
});
+
+ it('renders the header containing the logout button when renderLogout is true', () => {
+ header = shallow(<Header renderLogout />);
+ expect(header.find('header').find(Logout).length).toEqual(1);
+ });
+
+ it('hides logout button when renderLogout is false', () => {
+ expect(header.find('header').find(Logout).length).toEqual(0);
+ });
});
diff --git a/web-ui/src/common/link_button/link_button.js b/web-ui/src/common/link_button/link_button.js
new file mode 100644
index 00000000..e7fd80b0
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import FlatButton from 'material-ui/FlatButton';
+
+import './link_button.scss';
+
+const labelStyle = {
+ textTransform: 'none',
+ color: 'inherit',
+ fontSize: 'inherit',
+ width: '100%',
+ padding: '0'
+};
+
+const linkButtonStyle = {
+ color: 'inherit',
+ borderRadius: '0',
+ minHeight: '36px',
+ height: 'auto',
+ lineHeight: '20px',
+ padding: '12px 0'
+};
+
+const LinkButton = ({ buttonText, href }) => (
+ <div className='link-button'>
+ <FlatButton
+ href={href}
+ containerElement='a'
+ label={buttonText}
+ labelStyle={labelStyle}
+ hoverColor={'#ff9c00'}
+ style={linkButtonStyle}
+ />
+ </div>
+);
+
+LinkButton.propTypes = {
+ buttonText: React.PropTypes.string.isRequired,
+ href: React.PropTypes.string.isRequired
+};
+
+export default LinkButton;
diff --git a/web-ui/src/common/link_button/link_button.scss b/web-ui/src/common/link_button/link_button.scss
new file mode 100644
index 00000000..3b4a534c
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.scss
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/base/colors";
+
+.link-button > a {
+ width: 100%;
+}
+
+.link-button {
+ width: 100%;
+ margin-top: 2em;
+ border: 2px solid $light_orange;
+ border-radius: 2px;
+ font-size: 1em;
+ color: $light_orange;
+ &:hover {
+ color: $white;
+ }
+}
+
+@media only screen and (min-width : 500px) {
+ .link-button {
+ width: 70%;
+ align-self: center;
+ }
+}
+
+@media only screen and (min-width : 960px) {
+ .link-button {
+ width: 300px;
+ margin-bottom: 1em;
+ font-size: 0.8em;
+ }
+}
diff --git a/web-ui/src/common/link_button/link_button.spec.js b/web-ui/src/common/link_button/link_button.spec.js
new file mode 100644
index 00000000..945afffe
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.spec.js
@@ -0,0 +1,20 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import LinkButton from 'src/common/link_button/link_button';
+
+describe('LinkButton', () => {
+ let linkButton;
+
+ beforeEach(() => {
+ linkButton = shallow(<LinkButton buttonText='Go To Link' href='/some-link' />);
+ });
+
+ it('renders link button with given button text', () => {
+ expect(linkButton.find('FlatButton').props().label).toEqual('Go To Link');
+ });
+
+ it('renders link button with given href', () => {
+ expect(linkButton.find('FlatButton').props().href).toEqual('/some-link');
+ });
+});
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.js b/web-ui/src/common/snackbar_notification/snackbar_notification.js
new file mode 100644
index 00000000..8a7e5094
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import Snackbar from 'material-ui/Snackbar';
+import { red500, blue500 } from 'material-ui/styles/colors';
+
+import './snackbar_notification.scss';
+
+const notificationStyle = () => ({
+ top: 0,
+ left: 'auto',
+ bottom: 'auto',
+ alignSelf: 'center',
+ transform: 'translate3d(0, 0px, 0)'
+});
+
+const contentStyle = {
+ textAlign: 'center'
+};
+
+const getStyleByType = (isError) => {
+ const style = { height: 'auto' };
+ style.backgroundColor = isError ? red500 : blue500;
+ return style;
+};
+
+const SnackbarNotification = ({ message, isError = false, autoHideDuration = 5000 }) => (
+ <Snackbar
+ id='snackbar'
+ open
+ bodyStyle={getStyleByType(isError)}
+ message={message}
+ autoHideDuration={autoHideDuration}
+ contentStyle={contentStyle}
+ style={notificationStyle()}
+ />
+);
+
+SnackbarNotification.propTypes = {
+ message: React.PropTypes.string.isRequired,
+ isError: React.PropTypes.bool,
+ autoHideDuration: React.PropTypes.number
+};
+
+SnackbarNotification.defaultProps = {
+ isError: false,
+ autoHideDuration: 5000
+};
+
+export default SnackbarNotification;
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.scss b/web-ui/src/common/snackbar_notification/snackbar_notification.scss
new file mode 100644
index 00000000..e37ba4ae
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//TODO: Refer to Issue - https://github.com/callemall/material-ui/issues/3860
+#snackbar > div {
+ line-height: 20px !important;
+ padding: 24px !important;
+}
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js
new file mode 100644
index 00000000..24b79e71
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js
@@ -0,0 +1,31 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
+import Snackbar from 'material-ui/Snackbar';
+import { red500 } from 'material-ui/styles/colors';
+
+describe('SnackbarNotification', () => {
+ let snackbarNotification;
+
+ beforeEach(() => {
+ snackbarNotification = shallow(<SnackbarNotification message={'Error Message'} isError />);
+ });
+
+ it('renders snackbar with error message', () => {
+ expect(snackbarNotification.find(Snackbar).props().message).toEqual('Error Message');
+ });
+
+ it('renders snackbar with open as true', () => {
+ expect(snackbarNotification.find(Snackbar).props().open).toEqual(true);
+ });
+
+ it('renders snackbar with error body style', () => {
+ expect(snackbarNotification.find(Snackbar).props().bodyStyle)
+ .toEqual({ height: 'auto', backgroundColor: red500 });
+ });
+
+ it('renders snackbar with default auto-hide duration', () => {
+ expect(snackbarNotification.find(Snackbar).props().autoHideDuration).toEqual(5000);
+ });
+});
diff --git a/web-ui/src/common/submit_button/submit_button.js b/web-ui/src/common/submit_button/submit_button.js
index 1224c7bd..f77a5596 100644
--- a/web-ui/src/common/submit_button/submit_button.js
+++ b/web-ui/src/common/submit_button/submit_button.js
@@ -30,7 +30,7 @@ const buttonStyle = {
height: '48px'
};
-const SubmitButton = ({ buttonText, disabled = false }) => (
+const SubmitButton = ({ buttonText, disabled = false, ...other }) => (
<div className='submit-button'>
<RaisedButton
type='submit'
@@ -41,6 +41,7 @@ const SubmitButton = ({ buttonText, disabled = false }) => (
overlayStyle={buttonStyle}
fullWidth
primary
+ {...other}
/>
</div>
);
diff --git a/web-ui/src/common/util.js b/web-ui/src/common/util.js
index effb3d9c..c70a8444 100644
--- a/web-ui/src/common/util.js
+++ b/web-ui/src/common/util.js
@@ -1,8 +1,27 @@
+import browser from 'helpers/browser';
+
export const hasQueryParameter = (param) => {
const decodedUri = decodeURIComponent(window.location.search.substring(1));
return !(decodedUri.split('&').indexOf(param) < 0);
};
+export const submitForm = (event, url, body = {}) => {
+ event.preventDefault();
+
+ return fetch(url, {
+ credentials: 'same-origin',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ csrftoken: [browser.getCookie('XSRF-TOKEN')],
+ ...body
+ })
+ });
+};
+
export default {
- hasQueryParameter
+ hasQueryParameter,
+ submitForm
};
diff --git a/web-ui/src/common/util.spec.js b/web-ui/src/common/util.spec.js
index 805d9dd5..a79859a0 100644
--- a/web-ui/src/common/util.spec.js
+++ b/web-ui/src/common/util.spec.js
@@ -1,4 +1,7 @@
import expect from 'expect';
+import fetchMock from 'fetch-mock';
+
+import browser from 'helpers/browser';
import Util from 'src/common/util';
describe('Utils', () => {
@@ -17,4 +20,36 @@ describe('Utils', () => {
expect(Util.hasQueryParameter('error')).toBe(false);
});
});
+
+ describe('submitForm', () => {
+ const event = {};
+
+ beforeEach(() => {
+ event.preventDefault = expect.createSpy();
+ expect.spyOn(browser, 'getCookie').andReturn('abc123');
+
+ fetchMock.post('/some-url', 200);
+ Util.submitForm(event, '/some-url', { userCode: '123' });
+ });
+
+ it('sends csrftoken as content', () => {
+ expect(fetchMock.lastOptions('/some-url').body).toContain('"csrftoken":["abc123"]');
+ });
+
+ it('sends body as content', () => {
+ expect(fetchMock.lastOptions('/some-url').body).toContain('"userCode":"123"');
+ });
+
+ it('sends content-type header', () => {
+ expect(fetchMock.lastOptions('/some-url').headers['Content-Type']).toEqual('application/json');
+ });
+
+ it('sends same origin headers', () => {
+ expect(fetchMock.lastOptions('/some-url').credentials).toEqual('same-origin');
+ });
+
+ it('prevents default call to refresh page', () => {
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+ });
});
diff --git a/web-ui/src/login/opensans.css b/web-ui/src/login/opensans.css
index 8795bdf7..74f98822 100644
--- a/web-ui/src/login/opensans.css
+++ b/web-ui/src/login/opensans.css
@@ -2,68 +2,68 @@
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
- src: local("Open Sans Light"), local("OpenSans-Light"), url("/assets/fonts/OpenSans-Light.woff") format("woff");
+ src: local("Open Sans Light"), local("OpenSans-Light"), url("fonts/OpenSans-Light.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
- src: local("Open Sans"), local("OpenSans"), url("/assets/fonts/OpenSans.woff") format("woff");
+ src: local("Open Sans"), local("OpenSans"), url("fonts/OpenSans.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
- src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("/assets/fonts/OpenSans-Semibold.woff") format("woff");
+ src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("fonts/OpenSans-Semibold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
- src: local("Open Sans Bold"), local("OpenSans-Bold"), url("/assets/fonts/OpenSans-Bold.woff") format("woff");
+ src: local("Open Sans Bold"), local("OpenSans-Bold"), url("fonts/OpenSans-Bold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
- src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("/assets/fonts/OpenSans-Extrabold.woff") format("woff");
+ src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("fonts/OpenSans-Extrabold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
- src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("/assets/fonts/OpenSansLight-Italic.woff") format("woff");
+ src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("fonts/OpenSansLight-Italic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
- src: local("Open Sans Italic"), local("OpenSans-Italic"), url("/assets/fonts/OpenSans-Italic.woff") format("woff");
+ src: local("Open Sans Italic"), local("OpenSans-Italic"), url("fonts/OpenSans-Italic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
- src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("/assets/fonts/OpenSans-SemiboldItalic.woff ") format("woff");
+ src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("fonts/OpenSans-SemiboldItalic.woff ") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
- src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("/assets/fonts/OpenSans-BoldItalic.woff") format("woff");
+ src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("fonts/OpenSans-BoldItalic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
- src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("/assets/fonts/OpenSans-ExtraboldItalic.woff") format("woff");
+ src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("fonts/OpenSans-ExtraboldItalic.woff") format("woff");
}