feat: add dynamic optional fields support (#534)

Added a new component that renders fields based on the
field descriptions returned from backend

VAN-835
This commit is contained in:
Zainab Amir
2022-03-01 16:00:37 +05:00
committed by GitHub
parent 7e82785c7b
commit 6e99e1e72c
17 changed files with 740 additions and 7 deletions

1
.env
View File

@@ -25,3 +25,4 @@ REGISTER_CONVERSION_COOKIE_NAME=null
ENABLE_PROGRESSIVE_PROFILING=''
MARKETING_EMAILS_OPT_IN=''
ENABLE_COPPA_COMPLIANCE=''
SHOW_DYNAMIC_PROFILING_PAGE=''

View File

@@ -8,5 +8,6 @@ module.exports = createConfig('jest', {
'src/setupTest.js',
'src/i18n',
'src/index.jsx',
'MainApp.jsx',
],
});

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import {
@@ -13,7 +14,7 @@ import configureStore from './data/configureStore';
import { updatePathWithQueryParams } from './data/utils';
import ForgotPasswordPage from './forgot-password';
import ResetPasswordPage from './reset-password';
import WelcomePage from './welcome';
import WelcomePage, { ProgressiveProfiling } from './welcome';
import './index.scss';
registerIcons();
@@ -28,7 +29,11 @@ const MainApp = () => (
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
<Route exact path={WELCOME_PAGE} component={WelcomePage} />
<Route
exact
path={WELCOME_PAGE}
component={(getConfig().SHOW_DYNAMIC_PROFILING_PAGE) ? ProgressiveProfiling : WelcomePage}
/>
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
<Route path="*">
<Redirect to={PAGE_NOT_FOUND} />

View File

@@ -15,6 +15,15 @@ $apple-black: #000000;
$apple-focus-black: $apple-black;
$accent-a-light: #c9f2f5;
.centered-align-spinner {
left: 0;
right: 0;
bottom: 0;
top: 0;
position: absolute;
margin: auto;
}
.main-content {
@extend .pt-4;
min-width: 464px !important;

View File

@@ -70,7 +70,7 @@ const LogistrationDefaultProps = {
};
const LogistrationProps = {
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequried,
name: PropTypes.string.isRequired,
loginUrl: PropTypes.string.isRequired,
})),
};

View File

@@ -19,6 +19,7 @@ export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
export const DEFAULT_STATE = 'default';
export const PENDING_STATE = 'pending';
export const COMPLETE_STATE = 'complete';
export const FAILURE_STATE = 'failure';
// Regex
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'

View File

@@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
const FormFieldRenderer = (props) => {
let formField = null;
const { fieldData, onChangeHandler, value } = props;
switch (fieldData.type) {
case 'select': {
if (!fieldData.options) {
return null;
}
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
as="select"
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={fieldData.label}
>
<option key="default" value="">{fieldData.label}</option>
{fieldData.options.map(option => (
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
))}
</Form.Control>
</Form.Group>
);
break;
}
case 'textarea': {
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
as="textarea"
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
/>
</Form.Group>
);
break;
}
case 'text': {
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Control
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
/>
</Form.Group>
);
break;
}
case 'checkbox': {
formField = (
<Form.Group className="mb-3">
<Form.Checkbox
id={fieldData.name}
checked={!!value}
name={fieldData.name}
value={value}
onChange={(e) => onChangeHandler(e)}
>
{fieldData.label}
</Form.Checkbox>
</Form.Group>
);
break;
}
default:
break;
}
return formField;
};
FormFieldRenderer.defaultProps = {
value: '',
};
FormFieldRenderer.propTypes = {
fieldData: PropTypes.shape({
type: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
value: PropTypes.string,
};
export default FormFieldRenderer;

View File

@@ -0,0 +1 @@
export { default } from './FieldRenderer';

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { mount } from 'enzyme';
import FieldRenderer from '../FieldRenderer';
describe('FieldRendererTests', () => {
let value = '';
const changeHandler = (e) => {
if (e.target.type === 'checkbox') {
value = e.target.checked;
} else {
value = e.target.value;
}
};
beforeEach(() => {
value = '';
});
it('should render select field type', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
options: [['1997', 1997], ['1998', 1998]],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('select#yob-field');
field.simulate('change', { target: { value: 1997 } });
expect(field.type()).toEqual('select');
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
expect(value).toEqual(1997);
});
it('should return null if no options are provided for select field', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should render textarea field', () => {
const fieldData = {
type: 'textarea',
label: 'Why do you want to join this platform?',
name: 'goals-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#goals-field').last();
field.simulate('change', { target: { value: 'These are my goals.' } });
expect(field.type()).toEqual('textarea');
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
expect(value).toEqual('These are my goals.');
});
it('should render an input field', () => {
const fieldData = {
type: 'text',
label: 'Company',
name: 'company-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#company-field').last();
field.simulate('change', { target: { value: 'ABC' } });
expect(field.type()).toEqual('input');
expect(fieldRenderer.find('label').text()).toEqual('Company');
expect(value).toEqual('ABC');
});
it('should render checkbox field', () => {
const fieldData = {
type: 'checkbox',
label: 'I agree that edX may send me marketing messages.',
name: 'marketing-emails-opt-in-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
expect(value).toEqual(true);
});
it('should return null if field type is unknown', () => {
const fieldData = {
type: 'unknown',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
});

View File

@@ -40,6 +40,7 @@ initialize({
ENABLE_PROGRESSIVE_PROFILING: process.env.ENABLE_PROGRESSIVE_PROFILING || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
SHOW_DYNAMIC_PROFILING_PAGE: process.env.SHOW_DYNAMIC_PROFILING_PAGE || false,
});
},
},

View File

@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure as configureAuth,
AxiosJwtAuthService,
ensureAuthenticatedUser,
hydrateAuthenticatedUser,
getAuthenticatedUser,
} from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getLoggingService } from '@edx/frontend-platform/logging';
import {
Alert,
Form,
StatefulButton,
Hyperlink,
Spinner,
} from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import { getFieldData, saveUserProfile } from './data/actions';
import { welcomePageSelector } from './data/selectors';
import messages from './messages';
import { RedirectLogistration } from '../common-components';
import {
DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE, COMPLETE_STATE,
} from '../data/constants';
import FormFieldRenderer from '../field-renderer';
import WelcomePageModal from './WelcomePageModal';
import BaseComponent from '../base-component';
const ProgressiveProfiling = (props) => {
const {
extendedProfile, fieldDescriptions, formRenderState, intl, submitState, showError,
} = props;
const [ready, setReady] = useState(false);
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
const [values, setValues] = useState({});
const [openDialog, setOpenDialog] = useState(false);
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
useEffect(() => {
configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() });
ensureAuthenticatedUser(DASHBOARD_URL).then(() => {
hydrateAuthenticatedUser().then(() => {
props.getFieldData();
setReady(true);
});
});
if (props.location.state && props.location.state.registrationResult) {
setRegistrationResult(props.location.state.registrationResult);
sendPageEvent('login_and_registration', 'welcome');
}
}, []);
if (!props.location.state || !props.location.state.registrationResult || formRenderState === FAILURE_STATE) {
global.location.assign(DASHBOARD_URL);
return null;
}
if (!ready) {
return null;
}
const handleSubmit = (e) => {
e.preventDefault();
const authenticatedUser = getAuthenticatedUser();
const payload = { ...values, extendedProfile: [] };
extendedProfile.forEach(fieldName => {
if (values[fieldName]) {
payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] });
}
delete payload[fieldName];
});
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
},
);
};
const handleSkip = (e) => {
e.preventDefault();
setOpenDialog(true);
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked');
};
const onChangeHandler = (e) => {
if (e.target.type === 'checkbox') {
setValues({ ...values, [e.target.name]: e.target.checked });
} else {
setValues({ ...values, [e.target.name]: e.target.value });
}
};
const formFields = Object.keys(fieldDescriptions).map((fieldName) => {
const fieldData = fieldDescriptions[fieldName];
return (
<span key={fieldData.name}>
<FormFieldRenderer
fieldData={fieldData}
value={values[fieldData.name]}
onChangeHandler={onChangeHandler}
/>
</span>
);
});
if (formRenderState === COMPLETE_STATE) {
return (
<>
<BaseComponent showWelcomeBanner>
<Helmet>
<title>{intl.formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<WelcomePageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
{props.shouldRedirect ? (
<RedirectLogistration
success
redirectUrl={registrationResult.redirectUrl}
/>
) : null}
<div className="mw-xs pp-page-content">
<div className="pp-page-heading">
<h2 className="h3 text-primary">{intl.formatMessage(messages['progressive.profiling.page.heading'])}</h2>
</div>
<hr className="border-light-700 mb-4" />
{showError ? (
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
<Alert.Heading>{intl.formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
<p>{intl.formatMessage(messages['welcome.page.error.message'])}</p>
</Alert>
) : null}
<Form>
{formFields}
<span className="progressive-profiling-support">
<Hyperlink
isInline
variant="muted"
destination={getConfig().WELCOME_PAGE_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{intl.formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>
</span>
<div className="d-flex mt-4 mb-3">
<StatefulButton
type="submit"
variant="brand"
className="login-button-width"
state={submitState}
labels={{
default: intl.formatMessage(messages['optional.fields.submit.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
<StatefulButton
className="text-gray-700 font-weight-500"
type="submit"
variant="link"
labels={{
default: intl.formatMessage(messages['optional.fields.skip.button']),
}}
onClick={handleSkip}
onMouseDown={(e) => e.preventDefault()}
/>
</div>
</Form>
</div>
</BaseComponent>
</>
);
}
return <Spinner id="loader" animation="border" variant="primary" className="centered-align-spinner" />;
};
ProgressiveProfiling.propTypes = {
extendedProfile: PropTypes.arrayOf(PropTypes.string),
fieldDescriptions: PropTypes.shape({}),
formRenderState: PropTypes.string.isRequired,
intl: intlShape.isRequired,
location: PropTypes.shape({
state: PropTypes.object,
}),
getFieldData: PropTypes.func.isRequired,
saveUserProfile: PropTypes.func.isRequired,
showError: PropTypes.bool,
shouldRedirect: PropTypes.bool,
submitState: PropTypes.string,
};
ProgressiveProfiling.defaultProps = {
extendedProfile: [],
fieldDescriptions: {},
location: { state: {} },
shouldRedirect: false,
showError: false,
submitState: DEFAULT_STATE,
};
const mapStateToProps = state => ({
extendedProfile: welcomePageSelector(state).extendedProfile,
fieldDescriptions: welcomePageSelector(state).fieldDescriptions,
formRenderState: welcomePageSelector(state).formRenderState,
shouldRedirect: welcomePageSelector(state).success,
submitState: welcomePageSelector(state).submitState,
showError: welcomePageSelector(state).showError,
});
export default connect(
mapStateToProps,
{
saveUserProfile,
getFieldData,
},
)(injectIntl(ProgressiveProfiling));

View File

@@ -1,5 +1,6 @@
import { AsyncActionType } from '../../data/utils';
export const GET_FIELDS_DATA = new AsyncActionType('FIELD_DESCRIPTION', 'GET_FIELDS_DATA');
export const SAVE_USER_PROFILE = new AsyncActionType('USER_PROFILE', 'SAVE_USER_PROFILE');
// save additional user information
@@ -19,3 +20,21 @@ export const saveUserProfileSuccess = () => ({
export const saveUserProfileFailure = () => ({
type: SAVE_USER_PROFILE.FAILURE,
});
// get field data from platform
export const getFieldData = () => ({
type: GET_FIELDS_DATA.BASE,
});
export const getFieldDataBegin = () => ({
type: GET_FIELDS_DATA.BEGIN,
});
export const getFieldDataSuccess = (data, extendedProfile) => ({
type: GET_FIELDS_DATA.SUCCESS,
payload: { data, extendedProfile },
});
export const getFieldDataFailure = () => ({
type: GET_FIELDS_DATA.FAILURE,
});

View File

@@ -1,7 +1,12 @@
import { SAVE_USER_PROFILE } from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
import { GET_FIELDS_DATA, SAVE_USER_PROFILE } from './actions';
import {
DEFAULT_STATE, PENDING_STATE, COMPLETE_STATE, FAILURE_STATE,
} from '../../data/constants';
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
success: false,
submitState: DEFAULT_STATE,
showError: false,
@@ -9,6 +14,23 @@ export const defaultState = {
const reducer = (state = defaultState, action) => {
switch (action.type) {
case GET_FIELDS_DATA.BEGIN:
return {
...state,
formRenderState: PENDING_STATE,
};
case GET_FIELDS_DATA.SUCCESS:
return {
...state,
extendedProfile: action.payload.extendedProfile,
fieldDescriptions: action.payload.data,
formRenderState: COMPLETE_STATE,
};
case GET_FIELDS_DATA.FAILURE:
return {
...state,
formRenderState: FAILURE_STATE,
};
case SAVE_USER_PROFILE.BEGIN:
return {
...state,

View File

@@ -1,13 +1,17 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import {
GET_FIELDS_DATA,
getFieldDataBegin,
getFieldDataFailure,
getFieldDataSuccess,
SAVE_USER_PROFILE,
saveUserProfileBegin,
saveUserProfileFailure,
saveUserProfileSuccess,
} from './actions';
import patchAccount from './service';
import { patchAccount, getOptionalFieldData } from './service';
export function* saveUserProfileInformation(action) {
try {
@@ -20,6 +24,17 @@ export function* saveUserProfileInformation(action) {
}
}
export function* getFieldData() {
try {
yield put(getFieldDataBegin());
const data = yield call(getOptionalFieldData);
yield put(getFieldDataSuccess(data.fields, data.extended_profile));
} catch (e) {
yield put(getFieldDataFailure());
}
}
export default function* saga() {
yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfileInformation);
yield takeEvery(GET_FIELDS_DATA.BASE, getFieldData);
}

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
export default async function patchAccount(username, commitValues) {
export async function patchAccount(username, commitValues) {
const requestConfig = {
headers: { 'Content-Type': 'application/merge-patch+json' },
};
@@ -16,3 +16,20 @@ export default async function patchAccount(username, commitValues) {
throw (error);
});
}
export async function getOptionalFieldData() {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/api/optional_fields`,
requestConfig,
)
.catch((e) => {
throw (e);
});
return data;
}

View File

@@ -1,4 +1,5 @@
export { default } from './WelcomePage';
export { default as ProgressiveProfiling } from './ProgressiveProfiling';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';

View File

@@ -0,0 +1,200 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import * as logging from '@edx/frontend-platform/logging';
import { injectIntl, IntlProvider, configure } from '@edx/frontend-platform/i18n';
import { getFieldData, saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling';
import {
COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, PENDING_STATE,
} from '../../data/constants';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
jest.mock('@edx/frontend-platform/logging');
analytics.sendTrackEvent = jest.fn();
analytics.sendPageEvent = jest.fn();
logging.getLoggingService = jest.fn();
auth.configure = jest.fn();
auth.ensureAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
auth.hydrateAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
describe('ProgressiveProfilingTests', () => {
mergeConfig({
WELCOME_PAGE_SUPPORT_LINK: 'http://localhost:1999/welcome',
});
const registrationResult = { redirectUrl: 'http://localhost:18000/dashboard', success: true };
let props = {};
let store = {};
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const initialState = {
welcomePage: {
formRenderState: COMPLETE_STATE,
},
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const getProgressiveProfilingPage = async () => {
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage {...props} />));
await act(async () => {
await Promise.resolve(progressiveProfilingPage);
await new Promise(resolve => setImmediate(resolve));
progressiveProfilingPage.update();
});
return progressiveProfilingPage;
};
beforeEach(() => {
store = mockStore(initialState);
configure({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
props = {
getFieldData: jest.fn(),
location: {
state: {
registrationResult,
},
},
};
});
it('should fire action to get form fields', async () => {
store.dispatch = jest.fn(store.dispatch);
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('button.btn-link').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(getFieldData());
});
it('should show spinner until fields are fetched', async () => {
store = mockStore({
welcomePage: {
formRenderState: PENDING_STATE,
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#loader').exists()).toBeTruthy();
});
it('should render fields returned by backend api', async () => {
store = mockStore({
welcomePage: {
...initialState.welcomePage,
fieldDescriptions: {
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
options: [['m', 'Male'], ['f', 'Female'], ['o', 'Other/Prefer Not to Say']],
},
},
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy();
});
it('should submit user profile details on form submission', async () => {
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
const formPayload = {
gender: 'm',
extended_profile: [{ field_name: 'company', field_value: 'edx' }],
};
store = mockStore({
welcomePage: {
...initialState.welcomePage,
extendedProfile: ['company'],
fieldDescriptions: {
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
options: [['m', 'Male'], ['f', 'Female'], ['o', 'Other/Prefer Not to Say']],
},
company: {
name: 'company',
type: 'text',
label: 'Company',
},
},
},
});
store.dispatch = jest.fn(store.dispatch);
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
progressiveProfilingPage.find('input#company').simulate('change', { target: { value: 'edx', name: 'company' } });
progressiveProfilingPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(saveUserProfile('abc123', formPayload));
});
it('should open modal on pressing skip for now button', async () => {
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('button.btn-link').simulate('click');
expect(progressiveProfilingPage.find('.pgn__modal-content-container').exists()).toBeTruthy();
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked');
});
it('should send analytic event for support link click', async () => {
const progressiveProfilingPage = await getProgressiveProfilingPage();
progressiveProfilingPage.find('.progressive-profiling-support a[target="_blank"]').simulate('click');
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
});
it('should show error message when patch request fails', async () => {
store = mockStore({
welcomePage: {
...initialState.welcomePage,
showError: true,
},
});
const progressiveProfilingPage = await getProgressiveProfilingPage();
expect(progressiveProfilingPage.find('#pp-page-errors').exists()).toBeTruthy();
});
it('should redirect to dashboard if no form fields are configured', async () => {
store = mockStore({
welcomePage: {
formRenderState: FAILURE_STATE,
},
});
delete window.location;
window.location = {
href: getConfig().BASE_URL,
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
};
await getProgressiveProfilingPage();
expect(window.location.href).toBe(DASHBOARD_URL);
});
});