Redesign login page (#252)

- Updated the form fields on login page to use latest paragon components
- Updated tests (removed snapshot tests)

VAN-406
This commit is contained in:
Zainab Amir
2021-04-22 13:28:08 +05:00
committed by Waheed Ahmed
parent 379c8c097f
commit 349d67ab96
15 changed files with 502 additions and 1379 deletions

View File

@@ -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 {

View File

@@ -17,9 +17,12 @@ const ConfirmationAlert = (props) => {
<FormattedMessage
id="forgot.password.confirmation.message"
defaultMessage="You entered {strongEmail}. If this email address is associated with your
edX account, we will send a message with password recovery instructions to this email address."
{platformName} account, we will send a message with password recovery instructions to this email address."
description="Forgot password confirmation message"
values={{ strongEmail: <strong className="data-hj-suppress">{email}</strong> }}
values={{
strongEmail: <strong className="data-hj-suppress">{email}</strong>,
platformName: getConfig().SITE_NAME,
}}
/>
</p>
<p>{intl.formatMessage(messages['forgot.password.confirmation.info'])}</p>

View File

@@ -7,7 +7,7 @@ const LargeScreenRightLayout = (props) => {
const { children } = props;
return (
<Col xs={6} className="min-vh-100 d-flex align-items-center justify-content-center">
<Col xs={6} className="min-vh-100 d-flex justify-content-center mt-5">
{ children }
</Col>
);

View File

@@ -23,7 +23,7 @@ const Logistration = (props) => {
{intl.formatMessage(messages['logistration.login'])}
</Link>
</span>
<div className="p-2">
<div id="main-content" className="p-2 main-content">
{selectedPage === LOGIN_PAGE ? <LoginPage /> : <RegistrationPage />}
</div>
</div>

View File

@@ -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) => {
<OverlayTrigger key="tooltip" placement="left" overlay={tooltip} show={showTooltip}>
<FormGroup
{...props}
handleFocus={() => 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,
};

View File

@@ -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 => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
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(<IntlLogistration />));
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(<IntlLogistration {...props} />));
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
});
});

View File

@@ -81,7 +81,7 @@ const LoginFailureMessage = (props) => {
case INVALID_FORM:
errorList = (
<>
{context.email && <li key={`${INVALID_FORM}-email`}>{context.email}</li>}
{context.emailOrUsername && <li key={`${INVALID_FORM}-email`}>{context.emailOrUsername}</li>}
{context.password && <li key={`${INVALID_FORM}-password`}>{context.password}</li>}
</>
);

View File

@@ -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'])}
/>
<div className="row tpa-container">
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} />
</div>
</>
);
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
thirdPartyComponent = <Skeleton height={36} />;
thirdPartyComponent = <Skeleton className="tpa-skeleton mb-3" height={30} count={2} />;
} 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}
/>
<div className="d-flex justify-content-center m-4">
<div className="d-flex flex-column">
<div className="mw-500">
{thirdPartyAuthContext.currentProvider
&& (
<ThirdPartyAuthAlert
currentProvider={thirdPartyAuthContext.currentProvider}
platformName={thirdPartyAuthContext.platformName}
/>
)}
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
{this.props.forgotPassword.status === 'complete' && !this.props.loginError ? (
<ConfirmationAlert email={this.props.forgotPassword.email} />
) : null}
<p>
{intl.formatMessage(messages['first.time.here'])}
<Hyperlink
className="ml-1"
destination={updatePathWithQueryParams(REGISTER_PAGE)}
onClick={this.handleCreateAccountLinkClickEvent}
>
{intl.formatMessage(messages['create.an.account'])}.
</Hyperlink>
</p>
<hr className="mt-0 border-gray-200" />
<h1 className="text-left mt-2 mb-3 h3">
{intl.formatMessage(messages['sign.in.heading'])}
</h1>
<Form className="m-0">
<AuthnValidationFormGroup
label={intl.formatMessage(messages['email.label'])}
for="email"
name="email"
type="email"
invalid={errors.email !== ''}
ariaInvalid={errors.email !== ''}
invalidMessage={errors.email}
value={email}
helpText={intl.formatMessage(messages['email.help.message'])}
onChange={(e) => this.setState({ email: e.target.value, isSubmitted: false })}
inputFieldStyle="border-gray-600"
/>
<AuthnValidationFormGroup
label={intl.formatMessage(messages['password.label'])}
for="password"
name="password"
type="password"
invalid={errors.password !== ''}
ariaInvalid={errors.password !== ''}
invalidMessage={errors.password}
value={password}
onChange={(e) => this.setState({ password: e.target.value, isSubmitted: false })}
inputFieldStyle="border-gray-600"
/>
<LoginHelpLinks page={LOGIN_PAGE} />
<Hyperlink className="field-link mt-0 mb-3 small" destination={this.getEnterPriseLoginURL()}>
{intl.formatMessage(messages['enterprise.login.link.text'])}
</Hyperlink>
<StatefulButton
type="submit"
variant="brand"
state={submitState}
labels={{
default: intl.formatMessage(messages['sign.in.button']),
}}
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
onClick={this.handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
</Form>
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
&& !currentProvider ? (
<div className="mb-3">
<hr className="mt-3 mb-3 border-gray-200" />
{intl.formatMessage(messages['or.sign.in.with'])}
</div>
) : null}
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
<div className="mw-xs mt-3">
{thirdPartyAuthContext.currentProvider
&& (
<ThirdPartyAuthAlert
currentProvider={thirdPartyAuthContext.currentProvider}
platformName={thirdPartyAuthContext.platformName}
/>
)}
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
{this.props.forgotPassword.status === 'complete' && !this.props.loginError ? (
<ConfirmationAlert email={this.props.forgotPassword.email} />
) : null}
<Form className="test">
<FormGroup
name="email"
value={this.state.emailOrUsername}
handleChange={(e) => this.setState({ emailOrUsername: e.target.value, isSubmitted: false })}
errorMessage={this.state.errors.emailOrUsername}
floatingLabel={intl.formatMessage(messages['login.user.identity.label'])}
/>
<PasswordField
name="password"
value={this.state.password}
showRequirements={false}
handleChange={(e) => this.setState({ password: e.target.value, isSubmitted: false })}
errorMessage={this.state.errors.password}
floatingLabel={intl.formatMessage(messages['login.password.label'])}
/>
<StatefulButton
type="submit"
variant="brand"
className="login-button-width"
state={submitState}
labels={{
default: intl.formatMessage(messages['sign.in.button']),
pending: '',
}}
icons={{
pending: <FontAwesomeIcon title={intl.formatMessage(messages['sign.in.btn.pending.state'])} icon={faSpinner} spin />,
}}
onClick={this.handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
<Link id="forgot-password" className="btn btn-link font-weight-500 text-body" to={RESET_PAGE}>
{intl.formatMessage(messages['forgot.password'])}
</Link>
<div className="mt-4 mb-3 h4">
{intl.formatMessage(messages['login.other.options.heading'])}
</div>
</div>
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={this.getEnterPriseLoginURL()}>
<Icon src={Institution} className="institute-icon" />
{intl.formatMessage(messages['enterprise.login.btn.text'])}
</Hyperlink>
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
</Form>
</div>
</>
);

View File

@@ -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',

View File

@@ -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.' },
},
};

View File

@@ -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 => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
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 => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
beforeEach(() => {
store = mockStore(initialState);
props = {
@@ -77,97 +80,43 @@ describe('LoginPage', () => {
};
});
it('should match default section snapshot', () => {
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />))
.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(<IntlLoginPage {...props} />))
.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(<IntlLoginPage {...props} />)).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(<IntlLoginPage {...props} />)).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(<IntlLoginPage {...props} />)).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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />))).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(<IntlLoginPage {...props} />))).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(<IntlLoginPage {...props} />))).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(<IntlLoginPage {...props} />))).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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />))).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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
expect(root.text().includes('Use my university info')).toBe(true);
delete window.location;
window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(dasboardUrl);
});
it('should not display institution login button', () => {
const root = mount(reduxWrapper(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
it('send tracking event when create account link is clicked', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
expect(loginPage.find(<CookiePolicyBanner />)).toBeTruthy();
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
it('form only be scrollable on submission', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
loginPage.find('input#password').simulate('change', { target: { value: 'test@example.com', name: 'password' } });
loginPage.find('button.btn-brand').simulate('click');
expect(loginPage.find(<IntlLoginFailureMessage />)).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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
expect(loginPage.find(<CookiePolicyBanner />)).toBeTruthy();
});
it('should send page event when login page is rendered', () => {
mount(reduxWrapper(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
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(<IntlLoginPage {...props} />));
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
loginPage.find('button.btn-brand').simulate('click');
expect(loginPage.find(<IntlLoginFailureMessage />)).toBeTruthy();
expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true);
});
it('should set login survey cookie', () => {
store = mockStore({
...initialState,

View File

@@ -1,889 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LoginPage should match TPA provider snapshot 1`] = `
<div
className="d-flex justify-content-center m-4"
>
<div
className="d-flex flex-column"
>
<div
className="mw-500"
>
<p>
First time here?
<a
className="ml-1"
href="/register"
onClick={[Function]}
target="_self"
>
Create an account
.
</a>
</p>
<hr
className="mt-0 border-gray-200"
/>
<h1
className="text-left mt-2 mb-3 h3"
>
Sign in
</h1>
<form
className="m-0"
>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Email
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="email"
value=""
/>
<span />
</div>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Password
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="password"
value=""
/>
<span />
</div>
<button
className="mt-2 field-link small"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
data-icon="caret-right"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 192 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
fill="currentColor"
style={Object {}}
/>
</svg>
Need help signing in?
</button>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
/>
</div>
<a
className="field-link mt-0 mb-3 small"
href="http://localhost:18000/enterprise/login"
onClick={[Function]}
target="_self"
>
Sign in with your company or school
</a>
<button
aria-disabled={false}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span>
Sign in
</span>
</span>
</button>
</form>
<div
className="mb-3"
>
<hr
className="mt-3 mb-3 border-gray-200"
/>
or sign in with
</div>
<div
className="row tpa-container"
>
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
id="oa2-apple-id"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
>
<img
alt="icon Apple"
className="icon-image"
src="https://edx.devstack.lms/logo.png"
/>
</div>
<span
aria-hidden="true"
className="mr-auto pl-2"
id="provider-name"
>
Apple
</span>
<span
className="sr-only"
>
Sign in with Apple
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`LoginPage should match default section snapshot 1`] = `
<div
className="d-flex justify-content-center m-4"
>
<div
className="d-flex flex-column"
>
<div
className="mw-500"
>
<p>
First time here?
<a
className="ml-1"
href="/register"
onClick={[Function]}
target="_self"
>
Create an account
.
</a>
</p>
<hr
className="mt-0 border-gray-200"
/>
<h1
className="text-left mt-2 mb-3 h3"
>
Sign in
</h1>
<form
className="m-0"
>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Email
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="email"
value=""
/>
<span />
</div>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Password
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="password"
value=""
/>
<span />
</div>
<button
className="mt-2 field-link small"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
data-icon="caret-right"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 192 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
fill="currentColor"
style={Object {}}
/>
</svg>
Need help signing in?
</button>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
/>
</div>
<a
className="field-link mt-0 mb-3 small"
href="http://localhost:18000/enterprise/login"
onClick={[Function]}
target="_self"
>
Sign in with your company or school
</a>
<button
aria-disabled={false}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span>
Sign in
</span>
</span>
</button>
</form>
</div>
</div>
</div>
`;
exports[`LoginPage should match forget password alert message snapshot 1`] = `
<div
className="d-flex justify-content-center m-4"
>
<div
className="d-flex flex-column"
>
<div
className="mw-500"
>
<div
className="fade alert alert-success show"
id="confirmation-alert"
role="alert"
>
<div
className="alert-heading h4"
>
Check your email
</div>
<p>
<span>
You entered
<strong
className="data-hj-suppress"
>
test@example.com
</strong>
. If this email address is associated with your edX account, we will send a message with password recovery instructions to this email address.
</span>
</p>
<p>
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.
</p>
<p>
<span>
If you need further assistance,
<a
className="alert-link"
href="#"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
>
contact technical support
</a>
.
</span>
</p>
</div>
<p>
First time here?
<a
className="ml-1"
href="/register"
onClick={[Function]}
target="_self"
>
Create an account
.
</a>
</p>
<hr
className="mt-0 border-gray-200"
/>
<h1
className="text-left mt-2 mb-3 h3"
>
Sign in
</h1>
<form
className="m-0"
>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Email
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="email"
value=""
/>
<span />
</div>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Password
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="password"
value=""
/>
<span />
</div>
<button
className="mt-2 field-link small"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
data-icon="caret-right"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 192 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
fill="currentColor"
style={Object {}}
/>
</svg>
Need help signing in?
</button>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
/>
</div>
<a
className="field-link mt-0 mb-3 small"
href="http://localhost:18000/enterprise/login"
onClick={[Function]}
target="_self"
>
Sign in with your company or school
</a>
<button
aria-disabled={false}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span>
Sign in
</span>
</span>
</button>
</form>
</div>
</div>
</div>
`;
exports[`LoginPage should match pending button state snapshot 1`] = `
<div
className="d-flex justify-content-center m-4"
>
<div
className="d-flex flex-column"
>
<div
className="mw-500"
>
<p>
First time here?
<a
className="ml-1"
href="/register"
onClick={[Function]}
target="_self"
>
Create an account
.
</a>
</p>
<hr
className="mt-0 border-gray-200"
/>
<h1
className="text-left mt-2 mb-3 h3"
>
Sign in
</h1>
<form
className="m-0"
>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Email
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="email"
value=""
/>
<span />
</div>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Password
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="password"
value=""
/>
<span />
</div>
<button
className="mt-2 field-link small"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
data-icon="caret-right"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 192 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
fill="currentColor"
style={Object {}}
/>
</svg>
Need help signing in?
</button>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
/>
</div>
<a
className="field-link mt-0 mb-3 small"
href="http://localhost:18000/enterprise/login"
onClick={[Function]}
target="_self"
>
Sign in with your company or school
</a>
<button
aria-disabled={true}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-pending disabled btn btn-brand"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span
className="pgn__stateful-btn-icon"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-spinner fa-w-16 fa-spin "
data-icon="spinner"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"
fill="currentColor"
style={Object {}}
/>
</svg>
</span>
<span>
Sign in
</span>
</span>
</button>
</form>
</div>
</div>
</div>
`;
exports[`LoginPage should show error message 1`] = `
<div
className="d-flex justify-content-center m-4"
>
<div
className="d-flex flex-column"
>
<div
className="mw-500"
>
<div
className="fade alert alert-danger show"
id="login-failure-alert"
role="alert"
>
<div
className="alert-heading h4"
>
We couldn't sign you in.
</div>
<ul>
<li>
Email or password is incorrect.
</li>
</ul>
</div>
<p>
First time here?
<a
className="ml-1"
href="/register"
onClick={[Function]}
target="_self"
>
Create an account
.
</a>
</p>
<hr
className="mt-0 border-gray-200"
/>
<h1
className="text-left mt-2 mb-3 h3"
>
Sign in
</h1>
<form
className="m-0"
>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Email
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="email"
value=""
/>
<span />
</div>
<div
className="form-group"
>
<label
className="pgn__form-label pt-10 focus-out"
>
Password
</label>
<input
aria-describedby=""
aria-invalid={false}
autoComplete="on"
className="form-control border-gray-600"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
required={true}
type="password"
value=""
/>
<span />
</div>
<button
className="mt-2 field-link small"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
data-icon="caret-right"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 192 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
fill="currentColor"
style={Object {}}
/>
</svg>
Need help signing in?
</button>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
/>
</div>
<a
className="field-link mt-0 mb-3 small"
href="http://localhost:18000/enterprise/login"
onClick={[Function]}
target="_self"
>
Sign in with your company or school
</a>
<button
aria-disabled={false}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span>
Sign in
</span>
</span>
</button>
</form>
</div>
</div>
</div>
`;

View File

@@ -401,13 +401,13 @@ class RegistrationPage extends React.Component {
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
/>
<div className="row tpa-container">
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
</div>
</>
);
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
thirdPartyComponent = <Skeleton height={36} count={2} />;
thirdPartyComponent = <Skeleton className="tpa-skeleton" height={36} count={2} />;
}
return thirdPartyComponent;
}
@@ -571,11 +571,7 @@ class RegistrationPage extends React.Component {
pending: '',
}}
icons={{
pending: (
<FontAwesomeIcon icon={faSpinner} spin>
<span className="sr-only">{intl.formatMessage(messages['create.an.account.btn.pending.state'])}</span>
</FontAwesomeIcon>
),
pending: <FontAwesomeIcon title={intl.formatMessage(messages['create.an.account.btn.pending.state'])} icon={faSpinner} spin />,
}}
onClick={this.handleSubmit}
onMouseDown={(e) => e.preventDefault()}

View File

@@ -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',

View File

@@ -387,12 +387,12 @@ describe('RegistrationPage', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
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(<IntlRegistrationPage {...props} />));
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(<IntlRegistrationPage {...props} />));
expect(registerPage.find(<CookiePolicyBanner />)).toBeTruthy();
});
it('send page event when register page is rendered', () => {
it('should send page event when register page is rendered', () => {
mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});