summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTulio Casagrande <tuliocasagrande@gmail.com>2017-04-05 11:34:48 -0300
committerGitHub <noreply@github.com>2017-04-05 11:34:48 -0300
commit658bc283585de7692af9b4d877962b2d0f96ebe0 (patch)
tree676b981ee3e4e4643f98816731c4e0f6fd5b9293
parent770165d37909488519a76222a949d2353a3745f5 (diff)
parent8198fa48d2e42d89457940590ec4b308ee541090 (diff)
Merge pull request #1044 from pixelated/backup_account_during_recovery
Backup account at end of account recovery
-rw-r--r--web-ui/app/locales/en_US/translation.json5
-rw-r--r--web-ui/app/locales/pt_BR/translation.json5
-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.js4
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.spec.js14
-rw-r--r--web-ui/src/account_recovery/page.js22
-rw-r--r--web-ui/src/account_recovery/page.spec.js28
-rw-r--r--web-ui/src/common/link_button/link_button.js55
-rw-r--r--web-ui/src/common/link_button/link_button.scss21
-rw-r--r--web-ui/src/common/link_button/link_button.spec.js20
-rw-r--r--web-ui/src/common/submit_button/submit_button.js3
-rw-r--r--web-ui/test/integration/translations.spec.js17
13 files changed, 243 insertions, 18 deletions
diff --git a/web-ui/app/locales/en_US/translation.json b/web-ui/app/locales/en_US/translation.json
index 8607f590..160e71ff 100644
--- a/web-ui/app/locales/en_US/translation.json
+++ b/web-ui/app/locales/en_US/translation.json
@@ -103,6 +103,11 @@
"input-label1": "create new password",
"input-label2": "confirm your new password"
},
+ "backup-account-step": {
+ "image-description": "Backup Account - Step 4 of 4",
+ "title": "Wait! What if you forget your password again?",
+ "buttonText": "Set-up Backup Account"
+ },
"button-next": "Next",
"back": "Back to previous step"
},
diff --git a/web-ui/app/locales/pt_BR/translation.json b/web-ui/app/locales/pt_BR/translation.json
index c4b3d9e7..b7cac507 100644
--- a/web-ui/app/locales/pt_BR/translation.json
+++ b/web-ui/app/locales/pt_BR/translation.json
@@ -103,6 +103,11 @@
"input-label1": "digite a nova senha",
"input-label2": "confirme a nova senha"
},
+ "backup-account-step": {
+ "image-description": "E-mail de Recuperação - Passo 4 de 4",
+ "title": "Opa! E se você esquecer sua senha de novo?",
+ "buttonText": "Configurar E-mail de Recuperação"
+ },
"button-next": "Próximo",
"back": "Voltar ao passo anterior"
},
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
index 4c418900..e7f689e8 100644
--- 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
@@ -28,11 +28,12 @@ import './new_password_form.scss';
export class NewPasswordForm extends React.Component {
submitHandler = (event) => {
+ event.preventDefault();
submitForm(event, '/account-recovery', {
userCode: this.props.userCode,
password: this.state.password,
confirmation: this.state.confirmation
- });
+ }).then(() => this.props.next());
}
handlePasswordChange = (event) => {
@@ -72,6 +73,7 @@ export class NewPasswordForm extends React.Component {
NewPasswordForm.propTypes = {
t: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired,
previous: React.PropTypes.func.isRequired,
userCode: React.PropTypes.string.isRequired
};
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
index 26b8651c..b57dd42e 100644
--- 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
@@ -7,9 +7,11 @@ import { NewPasswordForm } from './new_password_form';
describe('NewPasswordForm', () => {
let newPasswordForm;
let mockPrevious;
+ let mockNext;
+ let mockTranslations;
beforeEach(() => {
- const mockTranslations = key => key;
+ mockTranslations = key => key;
mockPrevious = expect.createSpy();
newPasswordForm = shallow(
<NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' />
@@ -40,7 +42,11 @@ describe('NewPasswordForm', () => {
});
describe('Submit', () => {
- beforeEach(() => {
+ 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' } });
@@ -62,5 +68,9 @@ describe('NewPasswordForm', () => {
it('sends password confirmation as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"confirmation":"456"');
});
+
+ 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
index 2d33e2fb..94927a16 100644
--- a/web-ui/src/account_recovery/page.js
+++ b/web-ui/src/account_recovery/page.js
@@ -22,6 +22,7 @@ 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';
@@ -36,7 +37,9 @@ export class Page extends React.Component {
}
nextStep = (event) => {
- event.preventDefault();
+ if (event) {
+ event.preventDefault();
+ }
this.setState({ step: this.state.step + 1 });
}
@@ -50,10 +53,19 @@ export class Page extends React.Component {
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} />
+ 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];
diff --git a/web-ui/src/account_recovery/page.spec.js b/web-ui/src/account_recovery/page.spec.js
index 31a748be..8e4ccc33 100644
--- a/web-ui/src/account_recovery/page.spec.js
+++ b/web-ui/src/account_recovery/page.spec.js
@@ -9,6 +9,7 @@ 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;
@@ -37,6 +38,13 @@ describe('Account Recovery Page', () => {
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);
@@ -45,31 +53,39 @@ describe('Account Recovery Page', () => {
});
it('renders user recovery code form when admin code submitted', () => {
- pageInstance.nextStep({ preventDefault: () => {} });
+ pageInstance.nextStep();
expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);
});
it('returns to admin code form on user code form back link', () => {
- pageInstance.nextStep({ preventDefault: () => {} });
+ pageInstance.nextStep();
pageInstance.previousStep();
expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);
});
it('renders new password form when user code submitted', () => {
- pageInstance.nextStep({ preventDefault: () => {} });
- pageInstance.nextStep({ preventDefault: () => {} });
+ 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({ preventDefault: () => {} });
- pageInstance.nextStep({ preventDefault: () => {} });
+ 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/common/link_button/link_button.js b/web-ui/src/common/link_button/link_button.js
new file mode 100644
index 00000000..e18903f2
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.js
@@ -0,0 +1,55 @@
+/*
+ * 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 RaisedButton from 'material-ui/RaisedButton';
+
+import '../submit_button/submit_button.scss';
+import './link_button.scss';
+
+const labelStyle = {
+ textTransform: 'none',
+ fontSize: '1em',
+ lineHeight: '48px',
+ color: '#ff9c00'
+};
+
+const buttonStyle = {
+ height: '48px',
+ backgroundColor: '#fff'
+};
+
+const LinkButton = ({ buttonText, href }) => (
+ <div className='submit-button link-button'>
+ <RaisedButton
+ href={href}
+ containerElement='a'
+ label={buttonText}
+ labelStyle={labelStyle}
+ buttonStyle={buttonStyle}
+ overlayStyle={buttonStyle}
+ fullWidth
+ />
+ </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..9318e08e
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.scss
@@ -0,0 +1,21 @@
+/*
+ * 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/>.
+ */
+
+.link-button > div {
+ margin-top: 2em;
+ border: 1px solid #ff9c00;
+}
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..dd040d28
--- /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('RaisedButton').props().label).toEqual('Go To Link');
+ });
+
+ it('renders link button with given href', () => {
+ expect(linkButton.find('RaisedButton').props().href).toEqual('/some-link');
+ });
+});
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/test/integration/translations.spec.js b/web-ui/test/integration/translations.spec.js
index 6faa7483..9b5256ee 100644
--- a/web-ui/test/integration/translations.spec.js
+++ b/web-ui/test/integration/translations.spec.js
@@ -9,25 +9,36 @@ import testI18n from './i18n';
describe('Translations', () => {
context('Account Recovery Page', () => {
- it('translates all keys on first step', () => {
+ it('translates all keys on admin recovery code step', () => {
const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
});
- it('translates all keys on second step', () => {
+ it('translates all keys on user recovery code step', () => {
const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
app.find('form.admin-code').simulate('submit');
expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
});
- it('translates all keys on third step', () => {
+ it('translates all keys on new password step', () => {
const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
app.find('form.admin-code').simulate('submit');
app.find('form.user-code').simulate('submit');
expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
});
+
+ it('translates all keys on backup account step', () => {
+ const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ app.find('form.admin-code').simulate('submit');
+ app.find('form.user-code').simulate('submit');
+ app.find('input[name="new-password"]').simulate('change', {target: {value: '11'}});
+ app.find('input[name="confirm-password"]').simulate('change', {target: {value: '11'}});
+ app.find('form.new-password').simulate('submit');
+
+ expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
+ });
});
context('Backup Account Page', () => {