diff --git a/src/_style.scss b/src/_style.scss index 56c44d94..3399c0ad 100644 --- a/src/_style.scss +++ b/src/_style.scss @@ -15,10 +15,22 @@ $apple-black: #000000; $apple-focus-black: $apple-black; $accent-a-light: #c9f2f5; +.main-content { + min-width: 464px !important; +} + .register-button-width { width: 12rem; } +.login-button-width { + width: 6rem; +} + +.tpa-skeleton { + margin-bottom: 0.75rem; +} + .nav-tabs { a { color: $primary !important; @@ -121,10 +133,6 @@ $accent-a-light: #c9f2f5; } } -.tpa-container { - margin: 0 !important; -} - .font-container { background-color: $font-blue; color: $white; @@ -229,24 +237,16 @@ $accent-a-light: #c9f2f5; } } -.field-link { - font-weight: normal; - display: block; - color: $primary; - margin-bottom: 5px; - margin-top: 5px; - border: none; - padding: 0; - background: transparent; - box-shadow: none; - text-transform: initial; - letter-spacing: normal; - text-decoration: none; - text-shadow: none; +.institute-icon { + @extend .mr-1; + @extend .text-gray; - &:focus, - &:hover { - color: $primary; + display: inline-block; + height: 18px; + width: 18px; + + svg { + display: inline-block; } } @@ -279,6 +279,10 @@ select.form-control { @extend .sr-only; } +.font-weight-500 { + font-weight: 500 !important; +} + .mw-420 { max-width: 420px; } @@ -307,13 +311,6 @@ select.form-control { padding-top: 10px; } -@media (min-width: 576px) { - .reset-password-container { - width: 420px; - max-width: 420px; - } -} - @media (min-width: 1024px) { .mw-500 { width: 500px; @@ -326,7 +323,18 @@ select.form-control { } } -@media (max-width: 450px) { +@media (min-width: 463px) { + .reset-password-container { + width: 420px; + max-width: 420px; + } + + .tpa-skeleton { + min-width: 464px !important; + } +} + +@media (max-width: 464px) { .section-heading-line { position: relative; text-align: center; @@ -338,10 +346,14 @@ select.form-control { } .btn-social { - width: 100%; + min-width: 100%; margin-bottom: 0.75rem; margin-right: 0 !important; } + + .main-content { + min-width: 100vw !important; + } } .large-screen-container { diff --git a/src/common-components/ConfirmationAlert.jsx b/src/common-components/ConfirmationAlert.jsx index 5a202373..fa8207c1 100644 --- a/src/common-components/ConfirmationAlert.jsx +++ b/src/common-components/ConfirmationAlert.jsx @@ -17,9 +17,12 @@ const ConfirmationAlert = (props) => { {email} }} + values={{ + strongEmail: {email}, + platformName: getConfig().SITE_NAME, + }} />

{intl.formatMessage(messages['forgot.password.confirmation.info'])}

diff --git a/src/common-components/LargeScreenRightLayout.jsx b/src/common-components/LargeScreenRightLayout.jsx index 0ec2d562..43ccad9e 100644 --- a/src/common-components/LargeScreenRightLayout.jsx +++ b/src/common-components/LargeScreenRightLayout.jsx @@ -7,7 +7,7 @@ const LargeScreenRightLayout = (props) => { const { children } = props; return ( - + { children } ); diff --git a/src/common-components/Logistration.jsx b/src/common-components/Logistration.jsx index eca48d28..99d43158 100644 --- a/src/common-components/Logistration.jsx +++ b/src/common-components/Logistration.jsx @@ -23,7 +23,7 @@ const Logistration = (props) => { {intl.formatMessage(messages['logistration.login'])} -
+
{selectedPage === LOGIN_PAGE ? : }
diff --git a/src/common-components/PasswordField.jsx b/src/common-components/PasswordField.jsx index c76fd5a1..08d32985 100644 --- a/src/common-components/PasswordField.jsx +++ b/src/common-components/PasswordField.jsx @@ -18,8 +18,8 @@ const PasswordField = (props) => { const [showTooltip, setShowTooltip] = useState(false); const handleBlur = (e) => { - props.handleBlur(e); - setShowTooltip(false); + if (props.handleBlur) { props.handleBlur(e); } + setShowTooltip(props.showRequirements && false); }; const HideButton = ( @@ -51,7 +51,7 @@ const PasswordField = (props) => { setShowTooltip(true)} + handleFocus={() => setShowTooltip(props.showRequirements && true)} handleBlur={handleBlur} type={isPasswordHidden ? 'password' : 'text'} trailingElement={isPasswordHidden ? ShowButton : HideButton} @@ -64,6 +64,7 @@ PasswordField.defaultProps = { errorMessage: '', handleBlur: null, handleChange: () => {}, + showRequirements: true, }; PasswordField.propTypes = { @@ -73,6 +74,7 @@ PasswordField.propTypes = { handleChange: PropTypes.func, intl: intlShape.isRequired, name: PropTypes.string.isRequired, + showRequirements: PropTypes.bool, value: PropTypes.string.isRequired, }; diff --git a/src/common-components/tests/Logistration.test.jsx b/src/common-components/tests/Logistration.test.jsx new file mode 100644 index 00000000..6a1bcf16 --- /dev/null +++ b/src/common-components/tests/Logistration.test.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import configureStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; + +import * as analytics from '@edx/frontend-platform/analytics'; +import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; + +import Logistration from '../Logistration'; +import { LOGIN_PAGE } from '../../data/constants'; + +jest.mock('@edx/frontend-platform/analytics'); +analytics.sendPageEvent = jest.fn(); + +const mockStore = configureStore(); +const IntlLogistration = injectIntl(Logistration); + +describe('Logistration', () => { + let store = {}; + + const reduxWrapper = children => ( + + + {children} + + + ); + + it('should render registration page', () => { + configure({ + loggingService: { logError: jest.fn() }, + config: { + ENVIRONMENT: 'production', + LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', + }, + messages: { 'es-419': {}, de: {}, 'en-us': {} }, + }); + + store = mockStore({ + register: { + registrationResult: { success: false, redirectUrl: '' }, + registrationError: null, + }, + commonComponents: { + thirdPartyAuthApiStatus: null, + }, + }); + const logistration = mount(reduxWrapper()); + + expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy(); + }); + + it('should render login page', () => { + store = mockStore({ + login: { + loginResult: { success: false, redirectUrl: '' }, + }, + commonComponents: { + thirdPartyAuthApiStatus: null, + }, + }); + + const props = { selectedPage: LOGIN_PAGE }; + const logistration = mount(reduxWrapper()); + + expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy(); + }); +}); diff --git a/src/login/LoginFailure.jsx b/src/login/LoginFailure.jsx index 1b876fe3..d4f77472 100644 --- a/src/login/LoginFailure.jsx +++ b/src/login/LoginFailure.jsx @@ -81,7 +81,7 @@ const LoginFailureMessage = (props) => { case INVALID_FORM: errorList = ( <> - {context.email &&
  • {context.email}
  • } + {context.emailOrUsername &&
  • {context.emailOrUsername}
  • } {context.password &&
  • {context.password}
  • } ); diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index ffe9d1f1..f1960016 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,37 +1,39 @@ import React from 'react'; -import Skeleton from 'react-loading-skeleton'; + +import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; +import Skeleton from 'react-loading-skeleton'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { - Form, Hyperlink, StatefulButton, + Form, Hyperlink, Icon, StatefulButton, } from '@edx/paragon'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Institution } from '@edx/paragon/icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import AccountActivationMessage from './AccountActivationMessage'; -import ConfirmationAlert from '../common-components/ConfirmationAlert'; import { loginRequest, loginRequestFailure } from './data/actions'; import { INVALID_FORM } from './data/constants'; -import { getThirdPartyAuthContext } from '../common-components/data/actions'; import { loginErrorSelector, loginRequestSelector } from './data/selectors'; -import { thirdPartyAuthContextSelector } from '../common-components/data/selectors'; -import LoginHelpLinks from './LoginHelpLinks'; import LoginFailureMessage from './LoginFailure'; -import EnterpriseSSO from '../common-components/EnterpriseSSO'; import messages from './messages'; + import { RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton, - InstitutionLogistration, AuthnValidationFormGroup, + InstitutionLogistration, FormGroup, PasswordField, } from '../common-components'; +import ConfirmationAlert from '../common-components/ConfirmationAlert'; +import { getThirdPartyAuthContext } from '../common-components/data/actions'; +import { thirdPartyAuthContextSelector } from '../common-components/data/selectors'; +import EnterpriseSSO from '../common-components/EnterpriseSSO'; import { - DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, PENDING_STATE, VALID_EMAIL_REGEX, + DEFAULT_STATE, ENTERPRISE_LOGIN_URL, PENDING_STATE, RESET_PAGE, VALID_EMAIL_REGEX, } from '../data/constants'; -import { forgotPasswordResultSelector } from '../forgot-password'; import { getTpaHint, getTpaProvider, @@ -39,8 +41,8 @@ import { setSurveyCookie, getActivationStatus, getAllPossibleQueryParam, - updatePathWithQueryParams, } from '../data/utils'; +import { forgotPasswordResultSelector } from '../forgot-password'; class LoginPage extends React.Component { constructor(props, context) { @@ -49,9 +51,9 @@ class LoginPage extends React.Component { sendPageEvent('login_and_registration', 'login'); this.state = { password: '', - email: '', + emailOrUsername: '', errors: { - email: '', + emailOrUsername: '', password: '', }, institutionLogin: false, @@ -84,20 +86,20 @@ class LoginPage extends React.Component { e.preventDefault(); this.setState({ isSubmitted: true }); - const { email, password } = this.state; - const emailValidationError = this.validateEmail(email); + const { emailOrUsername, password } = this.state; + const emailValidationError = this.validateEmail(emailOrUsername); const passwordValidationError = this.validatePassword(password); if (emailValidationError !== '' || passwordValidationError !== '') { this.props.loginRequestFailure({ errorCode: INVALID_FORM, - context: { email: emailValidationError, password: passwordValidationError }, + context: { emailOrUsername: emailValidationError, password: passwordValidationError }, }); return; } const payload = { - email, password, ...this.queryParams, + email: emailOrUsername, password, ...this.queryParams, }; this.props.loginRequest(payload); } @@ -107,16 +109,16 @@ class LoginPage extends React.Component { const regex = new RegExp(VALID_EMAIL_REGEX, 'i'); if (email === '') { - errors.email = this.props.intl.formatMessage(messages['email.validation.message']); + errors.emailOrUsername = this.props.intl.formatMessage(messages['email.validation.message']); } else if (email.length < 3) { - errors.email = this.props.intl.formatMessage(messages['email.format.validation.less.chars.message']); + errors.emailOrUsername = this.props.intl.formatMessage(messages['email.format.validation.less.chars.message']); } else if (!regex.test(email)) { - errors.email = this.props.intl.formatMessage(messages['email.format.validation.message']); + errors.emailOrUsername = this.props.intl.formatMessage(messages['email.format.validation.message']); } else { - errors.email = ''; + errors.emailOrUsername = ''; } this.setState({ errors }); - return errors.email; + return errors.emailOrUsername; } validatePassword(password) { @@ -127,10 +129,6 @@ class LoginPage extends React.Component { return errors.password; } - handleCreateAccountLinkClickEvent() { - sendTrackEvent('edx.bi.register_form.toggled', { category: 'user-engagement' }); - } - renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) { let thirdPartyComponent = null; if ((providers.length || secondaryProviders.length) && !currentProvider) { @@ -141,13 +139,13 @@ class LoginPage extends React.Component { secondaryProviders={secondaryProviders} buttonTitle={intl.formatMessage(messages['institution.login.button'])} /> -
    +
    ); } else if (thirdPartyAuthApiStatus === PENDING_STATE) { - thirdPartyComponent = ; + thirdPartyComponent = ; } return thirdPartyComponent; } @@ -160,7 +158,6 @@ class LoginPage extends React.Component { submitState, intl, ) { - const { email, errors, password } = this.state; const activationMsgType = getActivationStatus(); if (this.state.institutionLogin) { return ( @@ -189,88 +186,63 @@ class LoginPage extends React.Component { redirectUrl={this.props.loginResult.redirectUrl} finishAuthUrl={thirdPartyAuthContext.finishAuthUrl} /> -
    -
    -
    - {thirdPartyAuthContext.currentProvider - && ( - - )} - {this.props.loginError ? : null} - {submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null} - {activationMsgType && } - {this.props.forgotPassword.status === 'complete' && !this.props.loginError ? ( - - ) : null} -

    - {intl.formatMessage(messages['first.time.here'])} - - {intl.formatMessage(messages['create.an.account'])}. - -

    -
    -

    - {intl.formatMessage(messages['sign.in.heading'])} -

    -
    - this.setState({ email: e.target.value, isSubmitted: false })} - inputFieldStyle="border-gray-600" - /> - this.setState({ password: e.target.value, isSubmitted: false })} - inputFieldStyle="border-gray-600" - /> - - - {intl.formatMessage(messages['enterprise.login.link.text'])} - - }} - onClick={this.handleSubmit} - onMouseDown={(e) => e.preventDefault()} - /> - - {(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE) - && !currentProvider ? ( -
    -
    - {intl.formatMessage(messages['or.sign.in.with'])} -
    - ) : null} - {this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)} +
    + {thirdPartyAuthContext.currentProvider + && ( + + )} + {this.props.loginError ? : null} + {submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null} + {activationMsgType && } + {this.props.forgotPassword.status === 'complete' && !this.props.loginError ? ( + + ) : null} +
    + this.setState({ emailOrUsername: e.target.value, isSubmitted: false })} + errorMessage={this.state.errors.emailOrUsername} + floatingLabel={intl.formatMessage(messages['login.user.identity.label'])} + /> + this.setState({ password: e.target.value, isSubmitted: false })} + errorMessage={this.state.errors.password} + floatingLabel={intl.formatMessage(messages['login.password.label'])} + /> + , + }} + onClick={this.handleSubmit} + onMouseDown={(e) => e.preventDefault()} + /> + + {intl.formatMessage(messages['forgot.password'])} + +
    + {intl.formatMessage(messages['login.other.options.heading'])}
    -
    + + + {intl.formatMessage(messages['enterprise.login.btn.text'])} + + {this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)} +
    ); diff --git a/src/login/messages.jsx b/src/login/messages.jsx index d2eb4dfa..3e613c3a 100644 --- a/src/login/messages.jsx +++ b/src/login/messages.jsx @@ -6,11 +6,27 @@ const messages = defineMessages({ defaultMessage: 'Login | {siteName}', description: 'login page title', }, + // Login labels + 'login.user.identity.label': { + id: 'login.user.identity.label', + defaultMessage: 'Username or email', + description: 'Label for user identity field to enter either username or email to login', + }, + 'login.password.label': { + id: 'login.password.label', + defaultMessage: 'Password', + description: 'Label for password field', + }, 'sign.in.button': { id: 'sign.in.button', defaultMessage: 'Sign in', description: 'Button label that appears on login page', }, + 'sign.in.btn.pending.state': { + id: 'sign.in.btn.pending.state', + defaultMessage: 'Loading', + description: 'Title of icon that appears when button is in pending state', + }, 'need.help.signing.in.collapsible.menu': { id: 'need.help.signing.in.collapsible.menu', defaultMessage: 'Need help signing in?', @@ -21,6 +37,11 @@ const messages = defineMessages({ defaultMessage: 'Forgot my password', description: 'Forgot password link', }, + 'forgot.password': { + id: 'forgot.password', + defaultMessage: 'Forgot password', + description: 'Button text for forgot password', + }, 'other.sign.in.issues': { id: 'other.sign.in.issues', defaultMessage: 'Other sign in issues', @@ -56,10 +77,10 @@ const messages = defineMessages({ defaultMessage: 'Create an account', description: 'Message on button to return to register page', }, - 'or.sign.in.with': { - id: 'or.sign.in.with', - defaultMessage: 'or sign in with', - description: 'gives hint about other sign in options', + 'login.other.options.heading': { + id: 'login.other.options.heading', + defaultMessage: 'Or sign in with:', + description: 'Text that appears above other sign in options like social auth buttons', }, 'non.compliant.password.title': { id: 'non.compliant.password.title', @@ -71,19 +92,14 @@ const messages = defineMessages({ defaultMessage: 'First time here?', description: 'A question that appears before sign up link', }, - 'email.label': { - id: 'email.label', - defaultMessage: 'Email', - description: 'Label that appears above email field', - }, 'email.help.message': { id: 'email.help.message', defaultMessage: 'The email address you used to register with edX.', description: 'Message that appears below email field on login page', }, - 'enterprise.login.link.text': { - id: 'enterprise.login.link.text', - defaultMessage: 'Sign in with your company or school', + 'enterprise.login.btn.text': { + id: 'enterprise.login.btn.text', + defaultMessage: 'Company or school credentials', description: 'Company or school login link text.', }, 'email.format.validation.message': { @@ -106,11 +122,6 @@ const messages = defineMessages({ defaultMessage: 'Please enter your password.', description: 'Validation message that appears when password is empty', }, - 'password.label': { - id: 'password.label', - defaultMessage: 'Password', - description: 'Text that appears above password field or as a placeholder', - }, 'register.link': { id: 'register.link', defaultMessage: 'Create an account', diff --git a/src/login/tests/LoginFailure.test.jsx b/src/login/tests/LoginFailure.test.jsx index fc406320..8743e549 100644 --- a/src/login/tests/LoginFailure.test.jsx +++ b/src/login/tests/LoginFailure.test.jsx @@ -102,7 +102,7 @@ describe('LoginFailureMessage', () => { props = { loginError: { errorCode: INVALID_FORM, - context: { email: 'Please enter your email.', password: 'Please enter your password.' }, + context: { emailOrUsername: 'Please enter your email.', password: 'Please enter your password.' }, }, }; diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index bffff36e..061f682b 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -1,17 +1,20 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import renderer from 'react-test-renderer'; + import { mount } from 'enzyme'; import configureStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import renderer from 'react-test-renderer'; import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner'; import { getConfig, mergeConfig } from '@edx/frontend-platform'; import * as analytics from '@edx/frontend-platform/analytics'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; +import { loginRequest, loginRequestFailure } from '../data/actions'; import LoginFailureMessage from '../LoginFailure'; import LoginPage from '../LoginPage'; -import { loginRequest, loginRequestFailure } from '../data/actions'; + import { RenderInstitutionButton } from '../../common-components'; import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants'; @@ -28,9 +31,18 @@ describe('LoginPage', () => { mergeConfig({ USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME, }); + let props = {}; + let store = {}; + + const reduxWrapper = children => ( + + + {children} + + + ); const initialState = { - forgotPassword: { status: null }, login: { loginResult: { success: false, redirectUrl: '' }, }, @@ -45,9 +57,6 @@ describe('LoginPage', () => { }, }; - let props = {}; - let store = {}; - const secondaryProviders = { id: 'saml-test', name: 'Test University', @@ -56,7 +65,7 @@ describe('LoginPage', () => { skipHintedLogin: false, }; - const appleProvider = { + const ssoProvider = { id: 'oa2-apple-id', name: 'Apple', iconClass: null, @@ -64,12 +73,6 @@ describe('LoginPage', () => { loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard', }; - const reduxWrapper = children => ( - - {children} - - ); - beforeEach(() => { store = mockStore(initialState); props = { @@ -77,97 +80,43 @@ describe('LoginPage', () => { }; }); - it('should match default section snapshot', () => { - const tree = renderer.create(reduxWrapper()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should match pending button state snapshot', () => { - store = mockStore({ - ...initialState, - login: { - ...initialState.login, - submitState: PENDING_STATE, - }, - }); - - const tree = renderer.create(reduxWrapper()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should match forget password alert message snapshot', () => { - store = mockStore({ - ...initialState, - forgotPassword: { status: 'complete', email: 'test@example.com' }, - }); - - const tree = renderer.create(reduxWrapper()).toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should match TPA provider snapshot', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [appleProvider], - }, - }, - }); - - const tree = renderer.create(reduxWrapper()).toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should show error message', () => { - store = mockStore({ - ...initialState, - login: { - ...initialState.login, - loginError: { value: 'Email or password is incorrect.' }, - }, - }); - - const tree = renderer.create(reduxWrapper()).toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it('should show account activation message', () => { - delete window.location; - window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=info' }; - - const expectedMessage = 'This account has already been activated.'; + // ******** test login form submission ******** + it('should submit form for valid input', () => { + store.dispatch = jest.fn(store.dispatch); const loginPage = mount(reduxWrapper()); - expect(loginPage.find('#account-activation-message').find('div').text()).toEqual(expectedMessage); + + loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com' } }); + loginPage.find('input#password').simulate('change', { target: { value: 'password' } }); + loginPage.find('button.btn-brand').simulate('click'); + + expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email: 'test@example.com', password: 'password' })); }); - it('should display login help button', () => { - const root = mount(reduxWrapper()); - expect(root.find('button.field-link').first().text()).toEqual('Need help signing in?'); + it('should not dispatch loginRequest on empty form submission', () => { + store.dispatch = jest.fn(store.dispatch); + const loginPage = mount(reduxWrapper()); + + loginPage.find('button.btn-brand').simulate('click'); + expect(store.dispatch).not.toHaveBeenCalledWith(loginRequest({})); }); - it('updates the error state for empty email input on form submission', () => { - const errorState = { email: 'Please enter your email.', password: '' }; + // ******** test login form validations ******** + + it('should match state on empty form submission', () => { + const errorState = { emailOrUsername: 'Please enter your email.', password: 'Please enter your password.' }; store.dispatch = jest.fn(store.dispatch); const loginPage = (mount(reduxWrapper())).find('LoginPage'); - - loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } }); loginPage.find('button.btn-brand').simulate('click'); + // Check that loginRequestFailure was dispatched and state is updated expect(loginPage.state('errors')).toEqual(errorState); - expect(store.dispatch).toHaveBeenCalledWith( - loginRequestFailure({ errorCode: 'invalid-form', context: errorState }), - ); + expect(store.dispatch).toHaveBeenCalledWith(loginRequestFailure({ errorCode: 'invalid-form', context: errorState })); }); - it('updates the error state for invalid email; less than 3 characters on form submission', () => { - const errorState = { email: 'Email must have at least 3 characters.', password: '' }; + it('should match state for invalid email (less than 3 characters), on form submission', () => { + const errorState = { emailOrUsername: 'Email must have at least 3 characters.', password: '' }; store.dispatch = jest.fn(store.dispatch); const loginPage = (mount(reduxWrapper())).find('LoginPage'); @@ -177,13 +126,10 @@ describe('LoginPage', () => { loginPage.find('button.btn-brand').simulate('click'); expect(loginPage.state('errors')).toEqual(errorState); - expect(store.dispatch).toHaveBeenCalledWith( - loginRequestFailure({ errorCode: 'invalid-form', context: errorState }), - ); }); - it('updates the error state for invalid email format validation on form submission', () => { - const errorState = { email: 'The email address you\'ve provided isn\'t formatted correctly.', password: '' }; + it('should match the state for invalid email format on form submission', () => { + const errorState = { emailOrUsername: 'The email address you\'ve provided isn\'t formatted correctly.', password: '' }; store.dispatch = jest.fn(store.dispatch); const loginPage = (mount(reduxWrapper())).find('LoginPage'); @@ -195,127 +141,102 @@ describe('LoginPage', () => { expect(loginPage.state('errors')).toEqual(errorState); }); - it('updates the error state for invalid password', () => { - const errorState = { email: '', password: 'Please enter your password.' }; - store.dispatch = jest.fn(store.dispatch); + // ******** test form buttons and links ******** - const loginPage = (mount(reduxWrapper())).find('LoginPage'); - - loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com', name: 'email' } }); - loginPage.find('button.btn-brand').simulate('click'); - - expect(loginPage.state('errors')).toEqual(errorState); - expect(store.dispatch).toHaveBeenCalledWith( - loginRequestFailure({ errorCode: 'invalid-form', context: errorState }), - ); + it('should match default button state', () => { + const loginPage = mount(reduxWrapper()); + expect(loginPage.find('button[type="submit"] span').first().text()).toEqual('Sign in'); }); - it('submits login request for valid email and password values', () => { - store.dispatch = jest.fn(store.dispatch); - const loginPage = (mount(reduxWrapper())).find('LoginPage'); - loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com' } }); - loginPage.find('input#password').simulate('change', { target: { value: 'password' } }); - loginPage.find('button.btn-brand').simulate('click'); - - expect(store.dispatch).toHaveBeenCalledWith( - loginRequest({ email: 'test@example.com', password: 'password' }), - ); - }); - - it('should match url after redirection', () => { - const dasboardUrl = 'http://test.com/testing-dashboard/'; + it('should match pending button state', () => { store = mockStore({ ...initialState, login: { ...initialState.login, - loginResult: { - success: true, - redirectUrl: dasboardUrl, - }, + submitState: PENDING_STATE, }, }); - delete window.location; - window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); - expect(window.location.href).toBe(dasboardUrl); - }); - - it('should match url after TPA redirection', () => { - const authCompleteUrl = '/auth/complete/google-oauth2/'; - store = mockStore({ - ...initialState, - login: { - ...initialState.login, - loginResult: { - success: true, - redirectUrl: '', - }, - }, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - finishAuthUrl: authCompleteUrl, - }, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); - expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); - }); - - it('should redirect to enterprise selection page', () => { - const authCompleteUrl = '/auth/complete/google-oauth2/'; - const enterpriseSelectionPage = 'http://localhost:18000/enterprise/select/active/?success_url='.concat(authCompleteUrl); - store = mockStore({ - ...initialState, - login: { - ...initialState.login, - loginResult: { - success: true, - redirectUrl: enterpriseSelectionPage, - }, - }, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - finishAuthUrl: authCompleteUrl, - }, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); - expect(window.location.href).toBe(enterpriseSelectionPage); - }); - - it('should redirect to social auth provider url', () => { - const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard'; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...appleProvider, - loginUrl, - }], - }, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL }; const loginPage = mount(reduxWrapper()); + const button = loginPage.find('button[type="submit"] span').first(); - loginPage.find('button#oa2-apple-id').simulate('click'); - expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl); + // test pending state icon and that pending state icon has title associated with it + expect(button.find('svg').prop('className')).toEqual(expect.stringContaining('fa-spinner')); + expect(button.find('svg').find('title').text()).toEqual('Loading'); + }); + + it('should show forgot password link', () => { + const loginPage = mount(reduxWrapper()); + expect(loginPage.find('a#forgot-password').text()).toEqual('Forgot password'); + }); + + it('should show single sign on provider button', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + }, + }); + + const loginPage = mount(reduxWrapper()); + expect(loginPage.find(`button#${ssoProvider.id}`).length).toEqual(1); + }); + + it('should not display institution login option when no secondary providers are present', () => { + const root = mount(reduxWrapper()); + expect(root.text().includes('Use my university info')).toBe(false); + }); + + it('should display institution login option when secondary providers are present', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + + const loginPage = mount(reduxWrapper()); + expect(loginPage.text().includes('Use my university info')).toBe(true); + + // on clicking "Use my university info" button, it should display institution login page + loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); + expect(loginPage.text().includes('Test University')).toBe(true); + }); + + // ******** test alert messages ******** + + it('should match login error message', () => { + const errorMessage = 'Email or password is incorrect.'; + store = mockStore({ + ...initialState, + login: { + ...initialState.login, + loginError: { value: errorMessage }, + }, + }); + + const loginPage = mount(reduxWrapper()); + expect(loginPage.find('#login-failure-alert').first().text()).toEqual(`We couldn't sign you in.${errorMessage}`); + }); + + it('should match account activation message', () => { + const activationMessage = 'Success! You have activated your account.' + + 'You will now receive email updates and alerts from us related ' + + 'to the courses you are enrolled in. Sign in to continue.'; + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=success' }; + + const loginPage = mount(reduxWrapper()); + expect(loginPage.find('div#account-activation-message').text()).toEqual(activationMessage); }); it('should match third party auth alert', () => { @@ -338,154 +259,119 @@ describe('LoginPage', () => { expect(loginPage.find('#tpa-alert').find('span').text()).toEqual(expectedMessage); }); - it('should display institution login button', () => { + it('should match forget password confirmation message', () => { store = mockStore({ ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], + forgotPassword: { status: 'complete', email: 'test@example.com' }, + }); + + const confirmationMessage = 'Check your email' + + 'You entered test@example.com. If this email address is associated with your edX account, ' + + 'we will send a message with password recovery instructions to this email address.If you do not ' + + 'receive a password reset message after 1 minute, verify that you entered the correct email address, ' + + 'or check your spam folder.If you need further assistance, contact technical support.'; + + const loginPage = mount(reduxWrapper()); + expect(loginPage.find('#confirmation-alert').first().text()).toEqual(confirmationMessage); + }); + + // ******** test redirection ******** + + it('should redirect to url returned by login endpoint', () => { + const dasboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard'; + store = mockStore({ + ...initialState, + login: { + ...initialState.login, + loginResult: { + success: true, + redirectUrl: dasboardUrl, }, }, }); - const root = mount(reduxWrapper()); - expect(root.text().includes('Use my university info')).toBe(true); + + delete window.location; + window.location = { href: getConfig().BASE_URL }; + renderer.create(reduxWrapper()); + expect(window.location.href).toBe(dasboardUrl); }); - it('should not display institution login button', () => { - const root = mount(reduxWrapper()); - expect(root.text().includes('Use my university info')).toBe(false); - }); - - it('should display institution login page', () => { + it('should redirect to finishAuthUrl upon successful login via SSO', () => { + const authCompleteUrl = '/auth/complete/google-oauth2/'; store = mockStore({ ...initialState, + login: { + ...initialState.login, + loginResult: { + success: true, + redirectUrl: '', + }, + }, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], + finishAuthUrl: authCompleteUrl, }, }, }); - const loginPage = mount(reduxWrapper()); - loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); - expect(loginPage.text().includes('Test University')).toBe(true); + + delete window.location; + window.location = { href: getConfig().BASE_URL }; + + renderer.create(reduxWrapper()); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); }); - it('send tracking event when create account link is clicked', () => { - const loginPage = mount(reduxWrapper()); - - loginPage.find('a[href*="/register"]').simulate('click'); - loginPage.update(); - expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' }); - }); - - it('send page event when login page is rendered', () => { - mount(reduxWrapper()); - expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); - }); - - it('send tracking and page events when institutional button is clicked', () => { + it('should redirect to social auth provider url on SSO button click', () => { + const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard'; store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], + providers: [{ + ...ssoProvider, + loginUrl, + }], }, }, }); - const loginPage = mount(reduxWrapper()); - loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); - expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); - expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); - }); - it('check cookie rendered', () => { - const loginPage = mount(reduxWrapper()); - expect(loginPage.find()).toBeTruthy(); - }); + delete window.location; + window.location = { href: getConfig().BASE_URL }; - it('form only be scrollable on submission', () => { const loginPage = mount(reduxWrapper()); - loginPage.find('input#password').simulate('change', { target: { value: 'test@example.com', name: 'password' } }); - loginPage.find('button.btn-brand').simulate('click'); - - expect(loginPage.find()).toBeTruthy(); - expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true); + loginPage.find('button#oa2-apple-id').simulate('click'); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl); }); - it('should render tpa button for tpa_hint id in primary provider', () => { - const expectedMessage = `Sign in using ${appleProvider.name}`; + // ******** test hinted third party auth ******** + + it('should render tpa button for tpa_hint id matching one of the primary providers', () => { store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [appleProvider], + providers: [ssoProvider], }, thirdPartyAuthApiStatus: COMPLETE_STATE, }, }); delete window.location; - window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?next=/dashboard&tpa_hint=${appleProvider.id}` }; - appleProvider.iconImage = null; + window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; + ssoProvider.iconImage = null; const loginPage = mount(reduxWrapper()); - expect(loginPage.find(`button#${appleProvider.id}`).find('span').text()).toEqual(expectedMessage); + expect(loginPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(`Sign in using ${ssoProvider.name}`); }); - it('should render regular tpa button for invalid tpa_hint value', () => { - const expectedMessage = `${appleProvider.name}`; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [appleProvider], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?next=/dashboard&tpa_hint=invalid' }; - appleProvider.iconImage = null; - - const loginPage = mount(reduxWrapper()); - expect(loginPage.find(`button#${appleProvider.id}`).find('span#provider-name').text()).toEqual(expectedMessage); - }); - - it('should render tpa button for tpa_hint id in secondary provider', () => { - const expectedMessage = `Sign in using ${secondaryProviders.name}`; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - delete window.location; - window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; - secondaryProviders.iconImage = null; - - const loginPage = mount(reduxWrapper()); - expect(loginPage.find(`button#${secondaryProviders.id}`).find('span').text()).toEqual(expectedMessage); - }); - - it('should redirect to idp page if skipHinetedLogin is true', () => { - secondaryProviders.skipHintedLogin = true; + it('should render tpa button for tpa_hint id matching one of the secondary providers', () => { store = mockStore({ ...initialState, commonComponents: { @@ -506,6 +392,67 @@ describe('LoginPage', () => { expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl); }); + it('should render regular tpa button for invalid tpa_hint value', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + thirdPartyAuthApiStatus: COMPLETE_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?next=/dashboard&tpa_hint=invalid' }; + ssoProvider.iconImage = null; + + const loginPage = mount(reduxWrapper()); + expect(loginPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(`${ssoProvider.name}`); + }); + + // ******** miscellaneous tests ******** + + it('should render cookie banner', () => { + const loginPage = mount(reduxWrapper()); + expect(loginPage.find()).toBeTruthy(); + }); + + it('should send page event when login page is rendered', () => { + mount(reduxWrapper()); + expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); + }); + + it('send tracking and page events when institutional button is clicked', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + const loginPage = mount(reduxWrapper()); + loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); + + expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); + expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); + }); + + it('tests that form is only scrollable on form submission', () => { + const loginPage = mount(reduxWrapper()); + + loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } }); + loginPage.find('button.btn-brand').simulate('click'); + + expect(loginPage.find()).toBeTruthy(); + expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true); + }); + it('should set login survey cookie', () => { store = mockStore({ ...initialState, diff --git a/src/login/tests/__snapshots__/LoginPage.test.jsx.snap b/src/login/tests/__snapshots__/LoginPage.test.jsx.snap deleted file mode 100644 index 226f0a54..00000000 --- a/src/login/tests/__snapshots__/LoginPage.test.jsx.snap +++ /dev/null @@ -1,889 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LoginPage should match TPA provider snapshot 1`] = ` -
    -
    -
    -

    - First time here? - - Create an account - . - -

    -
    -

    - Sign in -

    -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    - - Sign in with your company or school - - - -
    -
    - or sign in with -
    -
    - -
    -
    -
    -
    -`; - -exports[`LoginPage should match default section snapshot 1`] = ` -
    -
    -
    -

    - First time here? - - Create an account - . - -

    -
    -

    - Sign in -

    -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    - - Sign in with your company or school - - - -
    -
    -
    -`; - -exports[`LoginPage should match forget password alert message snapshot 1`] = ` -
    -
    -
    - -

    - First time here? - - Create an account - . - -

    -
    -

    - Sign in -

    -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    - - Sign in with your company or school - - - -
    -
    -
    -`; - -exports[`LoginPage should match pending button state snapshot 1`] = ` -
    -
    -
    -

    - First time here? - - Create an account - . - -

    -
    -

    - Sign in -

    -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    - - Sign in with your company or school - - - -
    -
    -
    -`; - -exports[`LoginPage should show error message 1`] = ` -
    -
    -
    - -

    - First time here? - - Create an account - . - -

    -
    -

    - Sign in -

    -
    -
    - - - -
    -
    - - - -
    - -
    -
    -
    - - Sign in with your company or school - - - -
    -
    -
    -`; diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 496f8bb9..477edb82 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -401,13 +401,13 @@ class RegistrationPage extends React.Component { secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders} buttonTitle={intl.formatMessage(messages['register.institution.login.button'])} /> -
    +
    ); } else if (thirdPartyAuthApiStatus === PENDING_STATE) { - thirdPartyComponent = ; + thirdPartyComponent = ; } return thirdPartyComponent; } @@ -571,11 +571,7 @@ class RegistrationPage extends React.Component { pending: '', }} icons={{ - pending: ( - - {intl.formatMessage(messages['create.an.account.btn.pending.state'])} - - ), + pending: , }} onClick={this.handleSubmit} onMouseDown={(e) => e.preventDefault()} diff --git a/src/register/messages.jsx b/src/register/messages.jsx index e582d67d..cb5bdf1d 100644 --- a/src/register/messages.jsx +++ b/src/register/messages.jsx @@ -76,7 +76,7 @@ const messages = defineMessages({ 'create.an.account.btn.pending.state': { id: 'create.an.account.btn.pending.state', defaultMessage: 'Loading', - description: 'Message that appears for screen readers only when button is in pending state', + description: 'Title of icon that appears when button is in pending state', }, 'register.rate.limit.reached.message': { id: 'register.rate.limit.reached.message', diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index 41ed8a92..d14c88e1 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -387,12 +387,12 @@ describe('RegistrationPage', () => { const registrationPage = mount(reduxWrapper()); const button = registrationPage.find('button[type="submit"] span').first(); - // submit button has no text when it is loading - expect(button.text()).toEqual(''); + // test pending state icon and that pending state icon has title associated with it expect(button.find('svg').prop('className')).toEqual(expect.stringContaining('fa-spinner')); + expect(button.find('svg').find('title').text()).toEqual('Loading'); }); - it('should match single sign on provider button', () => { + it('should show single sign on provider button', () => { store = mockStore({ ...initialState, commonComponents: { @@ -405,7 +405,7 @@ describe('RegistrationPage', () => { }); const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('button#oa2-apple-id').length).toEqual(1); + expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1); }); it('should display institution register button', () => { @@ -547,7 +547,7 @@ describe('RegistrationPage', () => { // ******** test hinted third party auth ******** - it('should render tpa button for tpa_hint id in primary provider', () => { + it('should render tpa button for tpa_hint id matching one of the primary providers', () => { const expectedMessage = `Sign in using ${ssoProvider.name}`; store = mockStore({ ...initialState, @@ -569,7 +569,7 @@ describe('RegistrationPage', () => { expect(registerPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(expectedMessage); }); - it('should render tpa button for tpa_hint id in secondary provider', () => { + it('should render tpa button for tpa_hint id matching one of the secondary providers', () => { store = mockStore({ ...initialState, commonComponents: { @@ -632,12 +632,12 @@ describe('RegistrationPage', () => { expect(shouldUpdate).toBe(false); }); - it('check cookie rendered', () => { + it('should render cookie banner', () => { const registerPage = mount(reduxWrapper()); expect(registerPage.find()).toBeTruthy(); }); - it('send page event when register page is rendered', () => { + it('should send page event when register page is rendered', () => { mount(reduxWrapper()); expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); });