From 6e99e1e72cfd298efcc8eff3d3dee4ee3baac1d1 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Tue, 1 Mar 2022 16:00:37 +0500 Subject: [PATCH] feat: add dynamic optional fields support (#534) Added a new component that renders fields based on the field descriptions returned from backend VAN-835 --- .env | 1 + jest.config.js | 1 + src/MainApp.jsx | 9 +- src/_style.scss | 9 + .../InstitutionLogistration.jsx | 2 +- src/data/constants.js | 1 + src/field-renderer/FieldRenderer.jsx | 98 ++++++++ src/field-renderer/index.jsx | 1 + .../tests/FieldRenderer.test.jsx | 105 ++++++++ src/index.jsx | 1 + src/welcome/ProgressiveProfiling.jsx | 237 ++++++++++++++++++ src/welcome/data/actions.js | 19 ++ src/welcome/data/reducers.js | 26 +- src/welcome/data/sagas.js | 17 +- src/welcome/data/service.js | 19 +- src/welcome/index.js | 1 + .../tests/ProgressiveProfiling.test.jsx | 200 +++++++++++++++ 17 files changed, 740 insertions(+), 7 deletions(-) create mode 100644 src/field-renderer/FieldRenderer.jsx create mode 100644 src/field-renderer/index.jsx create mode 100644 src/field-renderer/tests/FieldRenderer.test.jsx create mode 100644 src/welcome/ProgressiveProfiling.jsx create mode 100644 src/welcome/tests/ProgressiveProfiling.test.jsx diff --git a/.env b/.env index 47bbe43f..5dcf5614 100644 --- a/.env +++ b/.env @@ -25,3 +25,4 @@ REGISTER_CONVERSION_COOKIE_NAME=null ENABLE_PROGRESSIVE_PROFILING='' MARKETING_EMAILS_OPT_IN='' ENABLE_COPPA_COMPLIANCE='' +SHOW_DYNAMIC_PROFILING_PAGE='' diff --git a/jest.config.js b/jest.config.js index 35fcbc69..efceff61 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,5 +8,6 @@ module.exports = createConfig('jest', { 'src/setupTest.js', 'src/i18n', 'src/index.jsx', + 'MainApp.jsx', ], }); diff --git a/src/MainApp.jsx b/src/MainApp.jsx index 3c27975f..15ca8105 100755 --- a/src/MainApp.jsx +++ b/src/MainApp.jsx @@ -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 = () => ( - + diff --git a/src/_style.scss b/src/_style.scss index 91ed9389..45c120f6 100644 --- a/src/_style.scss +++ b/src/_style.scss @@ -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; diff --git a/src/common-components/InstitutionLogistration.jsx b/src/common-components/InstitutionLogistration.jsx index 683f6185..cb840f0e 100644 --- a/src/common-components/InstitutionLogistration.jsx +++ b/src/common-components/InstitutionLogistration.jsx @@ -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, })), }; diff --git a/src/data/constants.js b/src/data/constants.js index 1b67daaf..9e634ad7 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -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]+)*' diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx new file mode 100644 index 00000000..b8d43836 --- /dev/null +++ b/src/field-renderer/FieldRenderer.jsx @@ -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 = ( + + onChangeHandler(e)} + trailingElement={} + floatingLabel={fieldData.label} + > + + {fieldData.options.map(option => ( + + ))} + + + ); + break; + } + case 'textarea': { + formField = ( + + onChangeHandler(e)} + floatingLabel={fieldData.label} + /> + + ); + break; + } + case 'text': { + formField = ( + + onChangeHandler(e)} + floatingLabel={fieldData.label} + /> + + ); + break; + } + case 'checkbox': { + formField = ( + + onChangeHandler(e)} + > + {fieldData.label} + + + ); + 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; diff --git a/src/field-renderer/index.jsx b/src/field-renderer/index.jsx new file mode 100644 index 00000000..f072b7c6 --- /dev/null +++ b/src/field-renderer/index.jsx @@ -0,0 +1 @@ +export { default } from './FieldRenderer'; diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx new file mode 100644 index 00000000..58b358b3 --- /dev/null +++ b/src/field-renderer/tests/FieldRenderer.test.jsx @@ -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(); + 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( {}} />); + 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(); + 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(); + 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(); + 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( {}} />); + expect(fieldRenderer.html()).toBeNull(); + }); +}); diff --git a/src/index.jsx b/src/index.jsx index 334d7ff7..19c8a2bd 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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, }); }, }, diff --git a/src/welcome/ProgressiveProfiling.jsx b/src/welcome/ProgressiveProfiling.jsx new file mode 100644 index 00000000..e641a4c4 --- /dev/null +++ b/src/welcome/ProgressiveProfiling.jsx @@ -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 ( + + + + ); + }); + + if (formRenderState === COMPLETE_STATE) { + return ( + <> + + + {intl.formatMessage(messages['progressive.profiling.page.title'], + { siteName: getConfig().SITE_NAME })} + + + + {props.shouldRedirect ? ( + + ) : null} +
+
+

{intl.formatMessage(messages['progressive.profiling.page.heading'])}

+
+
+ {showError ? ( + + {intl.formatMessage(messages['welcome.page.error.heading'])} +

{intl.formatMessage(messages['welcome.page.error.message'])}

+
+ ) : null} +
+ {formFields} + + (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))} + > + {intl.formatMessage(messages['optional.fields.information.link'])} + + +
+ e.preventDefault()} + /> + e.preventDefault()} + /> +
+
+
+
+ + ); + } + return ; +}; + +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)); diff --git a/src/welcome/data/actions.js b/src/welcome/data/actions.js index 21de6730..917cdf3a 100644 --- a/src/welcome/data/actions.js +++ b/src/welcome/data/actions.js @@ -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, +}); diff --git a/src/welcome/data/reducers.js b/src/welcome/data/reducers.js index e4fae7de..b5762cd1 100644 --- a/src/welcome/data/reducers.js +++ b/src/welcome/data/reducers.js @@ -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, diff --git a/src/welcome/data/sagas.js b/src/welcome/data/sagas.js index 284e24e8..4e100873 100644 --- a/src/welcome/data/sagas.js +++ b/src/welcome/data/sagas.js @@ -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); } diff --git a/src/welcome/data/service.js b/src/welcome/data/service.js index 37bb10d4..a7b5246d 100644 --- a/src/welcome/data/service.js +++ b/src/welcome/data/service.js @@ -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; +} diff --git a/src/welcome/index.js b/src/welcome/index.js index e5a37ec7..c9b32035 100644 --- a/src/welcome/index.js +++ b/src/welcome/index.js @@ -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'; diff --git a/src/welcome/tests/ProgressiveProfiling.test.jsx b/src/welcome/tests/ProgressiveProfiling.test.jsx new file mode 100644 index 00000000..66a64143 --- /dev/null +++ b/src/welcome/tests/ProgressiveProfiling.test.jsx @@ -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 => ( + + {children} + + ); + + const getProgressiveProfilingPage = async () => { + const progressiveProfilingPage = mount(reduxWrapper()); + 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); + }); +});