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:
committed by
Waheed Ahmed
parent
379c8c097f
commit
349d67ab96
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
69
src/common-components/tests/Logistration.test.jsx
Normal file
69
src/common-components/tests/Logistration.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.' },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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()}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user