From 71885b6fd21efbdc49772bee11b6778abe685452 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Fri, 27 Nov 2020 15:22:41 +0500 Subject: [PATCH] Add stateful buttons on forms (#37) Add stateful button component from paragon to - LoginPage - RegistrationPage - ForgetPasswordPage - ResetPasswordPage VAN-123 --- src/data/constants.js | 4 + src/forgot-password/ForgotPasswordPage.jsx | 16 +- .../tests/ForgotPasswordPage.test.jsx | 10 + .../ForgotPasswordPage.test.jsx.snap | 161 +- src/logistration/LoginPage.jsx | 86 +- src/logistration/RegistrationPage.jsx | 28 +- src/logistration/data/reducers.js | 11 +- src/logistration/messages.jsx | 10 + src/logistration/tests/LoginPage.test.jsx | 17 + .../tests/RegistrationPage.test.jsx | 14 + .../__snapshots__/LoginPage.test.jsx.snap | 304 +++- .../RegistrationPage.test.jsx.snap | 1486 ++++++++++++++++- src/reset-password/ResetPasswordPage.jsx | 13 +- .../tests/ResetPasswordPage.test.jsx | 41 +- .../ResetPasswordPage.test.jsx.snap | 140 +- 15 files changed, 2179 insertions(+), 162 deletions(-) diff --git a/src/data/constants.js b/src/data/constants.js index 6b58337e..e8b1f5ac 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -6,3 +6,7 @@ export const DEFAULT_REDIRECT_URL = '/dashboard'; // Constants export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft']; + +// Stateful Submit Button States +export const DEFAULT_STATE = 'default'; +export const PENDING_STATE = 'pending'; diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx index 611add2b..7b4d3980 100644 --- a/src/forgot-password/ForgotPasswordPage.jsx +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -2,13 +2,14 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; -import { Button, Input, ValidationFormGroup } from '@edx/paragon'; +import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from './messages'; import { forgotPassword } from './data/actions'; import { forgotPasswordResultSelector } from './data/selectors'; import RequestInProgressAlert from './RequestInProgressAlert'; +import { LOGIN_PAGE } from '../data/constants'; import LoginHelpLinks from '../logistration/LoginHelpLinks'; const ForgotPasswordPage = (props) => { @@ -39,7 +40,7 @@ const ForgotPasswordPage = (props) => { return ( <> - {status === 'complete' ? : null} + {status === 'complete' ? : null}
@@ -79,12 +80,15 @@ const ForgotPasswordPage = (props) => {

- + />
diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/forgot-password/tests/ForgotPasswordPage.test.jsx index 22561ee3..1cb46428 100644 --- a/src/forgot-password/tests/ForgotPasswordPage.test.jsx +++ b/src/forgot-password/tests/ForgotPasswordPage.test.jsx @@ -49,6 +49,16 @@ describe('ForgotPasswordPage', () => { expect(tree).toMatchSnapshot(); }); + it('should match pending section snapshot', () => { + props = { + ...props, + status: 'pending', + }; + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('should match success section snapshot', () => { props = { ...props, diff --git a/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap index 9468f6ac..6c22eaef 100644 --- a/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap +++ b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap @@ -110,12 +110,18 @@ exports[`ForgotPasswordPage should match default section snapshot 1`] = ` @@ -260,12 +266,155 @@ exports[`ForgotPasswordPage should match forbidden section snapshot 1`] = ` + + + +`; + +exports[`ForgotPasswordPage should match pending section snapshot 1`] = ` +
+
+
+
+

+ Password assistance +

+

+ Please enter your log-in or recovery email address below and we will send you an email with instructions. +

+
+
+ + + + The email address you've provided isn't formatted correctly. + +
+
+

+ The email address you used to register with edX. +

+ +
+
+
+
+
diff --git a/src/logistration/LoginPage.jsx b/src/logistration/LoginPage.jsx index 821349a2..e187abe2 100644 --- a/src/logistration/LoginPage.jsx +++ b/src/logistration/LoginPage.jsx @@ -1,22 +1,23 @@ import React from 'react'; -import { Button, Input, ValidationFormGroup } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { forgotPasswordResultSelector } from '../forgot-password'; import ConfirmationAlert from './ConfirmationAlert'; import { getThirdPartyAuthContext, loginRequest } from './data/actions'; -import { DEFAULT_REDIRECT_URL, REGISTER_PAGE } from '../data/constants'; import { loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors'; +import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration'; import LoginHelpLinks from './LoginHelpLinks'; import LoginFailureMessage from './LoginFailure'; +import messages from './messages'; import RedirectLogistration from './RedirectLogistration'; import SocialAuthProviders from './SocialAuthProviders'; import ThirdPartyAuthAlert from './ThirdPartyAuthAlert'; -import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration'; -import messages from './messages'; + +import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, REGISTER_PAGE } from '../data/constants'; +import { forgotPasswordResultSelector } from '../forgot-password'; class LoginPage extends React.Component { constructor(props, context) { @@ -111,13 +112,13 @@ class LoginPage extends React.Component { } render() { - const { intl } = this.props; - const { currentProvider, finishAuthUrl, providers } = this.props.thirdPartyAuthContext; + const { intl, submitState, thirdPartyAuthContext } = this.props; + if (this.state.institutionLogin) { return ( @@ -128,15 +129,15 @@ class LoginPage extends React.Component {
- {currentProvider + {thirdPartyAuthContext.currentProvider && ( )} {this.props.loginError ? : null} @@ -146,15 +147,21 @@ class LoginPage extends React.Component { First time here?Create an Account.

-

{intl.formatMessage(messages['logistration.login.institution.login.sign.in'])}

- -
-

{intl.formatMessage(messages['logistration.login.institution.login.sign.in.with'])}

-
+

+ {intl.formatMessage(messages['logistration.login.institution.login.sign.in'])} +

+ {thirdPartyAuthContext.secondaryProviders.length ? ( + <> + +
+

{intl.formatMessage(messages['logistration.login.institution.login.sign.in.with'])}

+
+ + ) : null }
@@ -196,20 +203,23 @@ class LoginPage extends React.Component {
- + />
- {providers.length && !currentProvider ? ( + {thirdPartyAuthContext.providers.length && !thirdPartyAuthContext.currentProvider ? ( <>

or sign in with

- +
) : null} @@ -221,28 +231,32 @@ class LoginPage extends React.Component { } LoginPage.defaultProps = { - loginResult: null, forgotPassword: null, + loginResult: null, loginError: null, + submitState: DEFAULT_STATE, thirdPartyAuthContext: { currentProvider: null, finishAuthUrl: null, providers: [], + secondaryProviders: [], }, }; LoginPage.propTypes = { - intl: intlShape.isRequired, + forgotPassword: PropTypes.shape({ + email: PropTypes.string, + status: PropTypes.string, + }), getThirdPartyAuthContext: PropTypes.func.isRequired, + intl: intlShape.isRequired, + loginError: PropTypes.string, loginRequest: PropTypes.func.isRequired, loginResult: PropTypes.shape({ redirectUrl: PropTypes.string, success: PropTypes.bool, }), - forgotPassword: PropTypes.shape({ - email: PropTypes.string, - status: PropTypes.string, - }), + submitState: PropTypes.string, thirdPartyAuthContext: PropTypes.shape({ currentProvider: PropTypes.string, platformName: PropTypes.string, @@ -250,7 +264,6 @@ LoginPage.propTypes = { secondaryProviders: PropTypes.array, finishAuthUrl: PropTypes.string, }), - loginError: PropTypes.string, }; const mapStateToProps = state => { @@ -258,10 +271,11 @@ const mapStateToProps = state => { const loginResult = loginRequestSelector(state); const thirdPartyAuthContext = thirdPartyAuthContextSelector(state); return { + loginError: state.logistration.loginError, + submitState: state.logistration.submitState, forgotPassword, loginResult, thirdPartyAuthContext, - loginError: state.logistration.loginError, }; }; diff --git a/src/logistration/RegistrationPage.jsx b/src/logistration/RegistrationPage.jsx index 37116553..f8a351c1 100644 --- a/src/logistration/RegistrationPage.jsx +++ b/src/logistration/RegistrationPage.jsx @@ -1,9 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { - Button, Input, ValidationFormGroup, -} from '@edx/paragon'; +import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon'; import { getLocale, getCountryList, injectIntl, intlShape, } from '@edx/frontend-platform/i18n'; @@ -12,7 +10,9 @@ import { getThirdPartyAuthContext, registerNewUser } from './data/actions'; import { registrationRequestSelector, thirdPartyAuthContextSelector } from './data/selectors'; import RedirectLogistration from './RedirectLogistration'; import RegistrationFailure from './RegistrationFailure'; -import { DEFAULT_REDIRECT_URL, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; +import { + DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, +} from '../data/constants'; import SocialAuthProviders from './SocialAuthProviders'; import ThirdPartyAuthAlert from './ThirdPartyAuthAlert'; import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration'; @@ -159,7 +159,7 @@ class RegistrationPage extends React.Component { } render() { - const { intl } = this.props; + const { intl, submitState } = this.props; const { currentProvider, finishAuthUrl, providers, secondaryProviders, } = this.props.thirdPartyAuthContext; @@ -295,12 +295,15 @@ class RegistrationPage extends React.Component { /> By creating an account, you agree to the Terms of Service and Honor Code and you acknowledge that edX and each Member process your personal data in accordance with the Privacy Policy. - + />
@@ -312,6 +315,7 @@ RegistrationPage.defaultProps = { registrationResult: null, registerNewUser: null, registrationError: null, + submitState: DEFAULT_STATE, thirdPartyAuthContext: { currentProvider: null, finishAuthUrl: null, @@ -333,6 +337,7 @@ RegistrationPage.propTypes = { email: PropTypes.array, username: PropTypes.array, }), + submitState: PropTypes.string, thirdPartyAuthContext: PropTypes.shape({ currentProvider: PropTypes.string, platformName: PropTypes.string, @@ -353,8 +358,9 @@ const mapStateToProps = state => { const registrationResult = registrationRequestSelector(state); const thirdPartyAuthContext = thirdPartyAuthContextSelector(state); return { - registrationResult, registrationError: state.logistration.registrationError, + submitState: state.logistration.submitState, + registrationResult, thirdPartyAuthContext, }; }; diff --git a/src/logistration/data/reducers.js b/src/logistration/data/reducers.js index 7abc5284..013b9dd3 100644 --- a/src/logistration/data/reducers.js +++ b/src/logistration/data/reducers.js @@ -4,11 +4,13 @@ import { THIRD_PARTY_AUTH_CONTEXT, } from './actions'; +import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants'; + export const defaultState = { - registrationResult: {}, + loginError: null, loginResult: {}, registrationError: null, - loginError: null, + registrationResult: {}, }; const reducer = (state = defaultState, action) => { @@ -16,21 +18,23 @@ const reducer = (state = defaultState, action) => { case REGISTER_NEW_USER.BEGIN: return { ...state, + submitState: PENDING_STATE, }; case REGISTER_NEW_USER.SUCCESS: return { ...state, - registrationResult: action.payload, }; case REGISTER_NEW_USER.FAILURE: return { ...state, registrationError: action.payload.error, + submitState: DEFAULT_STATE, }; case LOGIN_REQUEST.BEGIN: return { ...state, + submitState: PENDING_STATE, }; case LOGIN_REQUEST.SUCCESS: return { @@ -41,6 +45,7 @@ const reducer = (state = defaultState, action) => { return { ...state, loginError: action.payload.loginError, + submitState: DEFAULT_STATE, }; case THIRD_PARTY_AUTH_CONTEXT.BEGIN: return { diff --git a/src/logistration/messages.jsx b/src/logistration/messages.jsx index eb4bd58f..3d442448 100644 --- a/src/logistration/messages.jsx +++ b/src/logistration/messages.jsx @@ -2,6 +2,16 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + 'logistration.sign.in.button': { + id: 'logistration.sign.in.button', + defaultMessage: 'Sign in', + description: 'Button label that appears on login page', + }, + 'logistration.create.account.button': { + id: 'ogistration.create.account.button', + defaultMessage: 'Create Account', + description: 'Button label that appears on register page', + }, 'logistration.need.help.signing.in.collapsible.menu': { id: 'logistration.need.help.signing.in.collapsible.menu', defaultMessage: 'Need help signing in?', diff --git a/src/logistration/tests/LoginPage.test.jsx b/src/logistration/tests/LoginPage.test.jsx index 30b26626..1a1c9031 100644 --- a/src/logistration/tests/LoginPage.test.jsx +++ b/src/logistration/tests/LoginPage.test.jsx @@ -8,6 +8,7 @@ import { getConfig } from '@edx/frontend-platform'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import LoginPage from '../LoginPage'; import { RenderInstitutionButton } from '../InstitutionLogistration'; +import { PENDING_STATE } from '../../data/constants'; const IntlLoginPage = injectIntl(LoginPage); const mockStore = configureStore(); @@ -64,6 +65,20 @@ describe('LoginPage', () => { expect(tree).toMatchSnapshot(); }); + it('should match pending button state snapshot', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + submitState: PENDING_STATE, + }, + }); + + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('should match forget password alert message snapshot', () => { props = { ...props, @@ -79,6 +94,7 @@ describe('LoginPage', () => { logistration: { ...initialState.logistration, thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, providers: [appleProvider], }, }, @@ -184,6 +200,7 @@ describe('LoginPage', () => { logistration: { ...initialState.logistration, thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, providers: [{ ...appleProvider, loginUrl, diff --git a/src/logistration/tests/RegistrationPage.test.jsx b/src/logistration/tests/RegistrationPage.test.jsx index 40716492..7d0a4c99 100644 --- a/src/logistration/tests/RegistrationPage.test.jsx +++ b/src/logistration/tests/RegistrationPage.test.jsx @@ -8,6 +8,7 @@ import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n import RegistrationPage from '../RegistrationPage'; import { RenderInstitutionButton } from '../InstitutionLogistration'; +import { PENDING_STATE } from '../../data/constants'; const IntlRegistrationPage = injectIntl(RegistrationPage); const mockStore = configureStore(); @@ -78,6 +79,19 @@ describe('./RegistrationPage.js', () => { expect(tree.toJSON()).toMatchSnapshot(); }); + it('should match pending button state snapshot', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + submitState: PENDING_STATE, + }, + }); + + const tree = renderer.create(reduxWrapper()); + expect(tree.toJSON()).toMatchSnapshot(); + }); + it('should match TPA provider snapshot', () => { store = mockStore({ ...initialState, diff --git a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap index facf4090..0791e67e 100644 --- a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap +++ b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap @@ -25,18 +25,11 @@ exports[`LoginPage should match TPA provider snapshot 1`] = `

-

Sign In -

-
-

- or sign in with -

-
+
@@ -159,12 +152,18 @@ exports[`LoginPage should match TPA provider snapshot 1`] = `

-

Sign In -

-
-

- or sign in with -

-
+
@@ -364,12 +356,18 @@ exports[`LoginPage should match default section snapshot 1`] = `
@@ -401,18 +399,11 @@ exports[`LoginPage should match forget password alert message snapshot 1`] = `

-

Sign In -

-
-

- or sign in with -

-
+
@@ -535,12 +526,197 @@ exports[`LoginPage should match forget password alert message snapshot 1`] = ` +
+ + +`; + +exports[`LoginPage should match pending button state snapshot 1`] = ` +
+
+
+

+ First time here? + + Create an Account. + +

+
+

+ Sign In +

+
+
+
+
+ + + + The email address you've provided isn't formatted correctly. + +
+
+

+ The email address you used to register with edX. +

+
+
+ + + + Please enter your password. + +
+
+ +
+
+
+
+
@@ -595,18 +771,11 @@ exports[`LoginPage should show error message on 400 1`] = `

-

Sign In -

-
-

- or sign in with -

-
+
@@ -729,12 +898,18 @@ exports[`LoginPage should show error message on 400 1`] = `
@@ -797,18 +972,11 @@ exports[`LoginPage should show error message on 400 on receiving link 1`] = `

-

Sign In -

-
-

- or sign in with -

-
+
@@ -931,12 +1099,18 @@ exports[`LoginPage should show error message on 400 on receiving link 1`] = `
diff --git a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap index 3e2d8911..18b12226 100644 --- a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap +++ b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap @@ -1464,12 +1464,18 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = ` . @@ -2899,12 +2905,1468 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = ` . + + +`; + +exports[`./RegistrationPage.js should match pending button state snapshot 1`] = ` +
+
+ + Already have an edX account? + + + Sign in. + +
+
+
+ + + + Enter a valid email address that contains at least 3 characters. + +
+
+ + + + Enter your full name. + +
+
+ + + + Username must be between 2 and 30 characters long. + +
+
+ + + + This password is too short. It must contain at least 8 characters. This password must contain at least 1 number. + +
+
+ + + + Select your country or region of residence. + +
+ + By creating an account, you agree to the + + Terms of Service and Honor Code + + and you acknowledge that edX and each Member process your personal data in accordance with the + + Privacy Policy + + . + +
@@ -4372,12 +5834,18 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = ` . diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx index 511b4e5a..73b84927 100644 --- a/src/reset-password/ResetPasswordPage.jsx +++ b/src/reset-password/ResetPasswordPage.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Button, Input, ValidationFormGroup } from '@edx/paragon'; +import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getQueryParameters } from '@edx/frontend-platform'; @@ -123,12 +123,15 @@ const ResetPasswordPage = (props) => { - + /> diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx index f6bb7b24..e761f0fd 100644 --- a/src/reset-password/tests/ResetPasswordPage.test.jsx +++ b/src/reset-password/tests/ResetPasswordPage.test.jsx @@ -53,6 +53,21 @@ describe('ResetPasswordPage', () => { expect(tree).toMatchSnapshot(); }); + it('show spinner component during token validation', () => { + props = { + ...props, + token_status: 'pending', + match: { + params: { + token: 'test-token', + }, + }, + }; + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('should match invalid token message section snapshot', () => { props = { ...props, @@ -63,6 +78,17 @@ describe('ResetPasswordPage', () => { expect(tree).toMatchSnapshot(); }); + it('should match pending reset message section snapshot', () => { + props = { + ...props, + token_status: 'valid', + status: 'pending', + }; + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('should match successful reset message section snapshot', () => { props = { ...props, @@ -139,19 +165,4 @@ describe('ResetPasswordPage', () => { expect(store.dispatch).toHaveBeenCalledWith(resetPassword(formPayload, props.token, {})); resetPage.unmount(); }); - - it('show spinner component during token validation', () => { - props = { - ...props, - token_status: 'pending', - match: { - params: { - token: 'test-token', - }, - }, - }; - const tree = renderer.create(reduxWrapper()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); }); diff --git a/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap b/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap index cb5937d7..cc7980b2 100644 --- a/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap +++ b/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap @@ -58,6 +58,122 @@ exports[`ResetPasswordPage should match invalid token message section snapshot 1 `; +exports[`ResetPasswordPage should match pending reset message section snapshot 1`] = ` +
+
+
+
+

+ Reset Your Password +

+

+ Enter and confirm your new password. +

+
+
+ + +
+
+ + + + Passwords do not match. + +
+
+
+ +
+
+
+`; + exports[`ResetPasswordPage should match reset password default section snapshot 1`] = `
@@ -345,12 +467,18 @@ Array [