diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 141f891..405a7f0 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -45,6 +45,7 @@ import { GENDER_OPTIONS, COUNTRY_WITH_STATES, COPPA_COMPLIANCE_YEAR, + WORK_EXPERIENCE_OPTIONS, getStatesList, } from './data/constants'; import { fetchSiteLanguages } from './site-language'; @@ -142,6 +143,10 @@ class AccountSettingsPage extends React.Component { value: key, label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]), })), + workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({ + value: key, + label: key === '' ? this.props.intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key, + })), })); handleEditableFieldChange = (name, value) => { @@ -149,7 +154,17 @@ class AccountSettingsPage extends React.Component { }; handleSubmit = (formId, values) => { - this.props.saveSettings(formId, values); + const { formValues } = this.props; + let extendedProfileObject = {}; + + if ('extended_profile' in formValues && formValues.extended_profile.some((field) => field.field_name === formId)) { + extendedProfileObject = { + extended_profile: formValues.extended_profile.map(field => (field.field_name === formId + ? { ...field, field_value: values } + : field)), + }; + } + this.props.saveSettings(formId, values, extendedProfileObject); }; handleSubmitProfileName = (formId, values) => { @@ -469,13 +484,15 @@ class AccountSettingsPage extends React.Component { yearOfBirthOptions, educationLevelOptions, genderOptions, + workExperienceOptions, } = this.getLocalizedOptions(this.context.locale, this.props.formValues.country); // Show State field only if the country is US (could include Canada later) const showState = this.props.formValues.country === COUNTRY_WITH_STATES; - const { verifiedName } = this.props; + const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience'); + const timeZoneOptions = this.getLocalizedTimeZoneOptions( this.props.timeZoneOptions, this.props.countryTimeZoneOptions, @@ -679,6 +696,18 @@ class AccountSettingsPage extends React.Component { emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])} {...editableFieldProps} /> + {hasWorkExperience + && ( + field.field_name === 'work_experience')?.field_value} + options={workExperienceOptions} + label={this.props.intl.formatMessage(messages['account.settings.field.work.experience'])} + emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.work.experience.empty'])} + {...editableFieldProps} + /> + )} { const submitButton = screen.getByText('Choose name'); fireEvent.click(submitButton); expect(mockDispatch).toHaveBeenCalledWith({ - payload: { formId, commitValues: false }, + payload: { formId, commitValues: false, extendedProfile: {} }, type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS', }); }); diff --git a/src/account-settings/data/actions.js b/src/account-settings/data/actions.js index 89eb0c9..27d8f76 100644 --- a/src/account-settings/data/actions.js +++ b/src/account-settings/data/actions.js @@ -77,9 +77,9 @@ export const beginNameChange = (formId) => ({ }); // SAVE SETTINGS ACTIONS -export const saveSettings = (formId, commitValues) => ({ +export const saveSettings = (formId, commitValues, extendedProfile = {}) => ({ type: SAVE_SETTINGS.BASE, - payload: { formId, commitValues }, + payload: { formId, commitValues, extendedProfile }, }); export const saveSettingsBegin = () => ({ diff --git a/src/account-settings/data/constants.js b/src/account-settings/data/constants.js index 54209aa..6ba4f6f 100644 --- a/src/account-settings/data/constants.js +++ b/src/account-settings/data/constants.js @@ -34,6 +34,21 @@ export const GENDER_OPTIONS = [ 'm', 'o', ]; +export const WORK_EXPERIENCE_OPTIONS = [ + '', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10+', + +]; export const COUNTRY_WITH_STATES = 'US'; diff --git a/src/account-settings/data/sagas.js b/src/account-settings/data/sagas.js index 3c16b98..c27cbe9 100644 --- a/src/account-settings/data/sagas.js +++ b/src/account-settings/data/sagas.js @@ -83,8 +83,8 @@ export function* handleSaveSettings(action) { yield put(saveSettingsBegin()); const { username, userId } = getAuthenticatedUser(); - const { commitValues, formId } = action.payload; - const commitData = { [formId]: commitValues }; + const { commitValues, formId, extendedProfile } = action.payload; + const commitData = Object.keys(extendedProfile).length > 0 ? extendedProfile : { [formId]: commitValues }; let savedValues = null; if (formId === 'siteLanguage') { const previousSiteLanguage = getLocale(); diff --git a/src/account-settings/data/selectors.js b/src/account-settings/data/selectors.js index 4939cd9..7650aa0 100644 --- a/src/account-settings/data/selectors.js +++ b/src/account-settings/data/selectors.js @@ -156,7 +156,7 @@ function chooseFormValue(draft, committed) { return draft !== undefined ? draft : committed; } -const formValuesSelector = createSelector( +export const formValuesSelector = createSelector( valuesSelector, draftsSelector, (values, drafts) => { @@ -164,6 +164,20 @@ const formValuesSelector = createSelector( Object.entries(values).forEach(([name, value]) => { if (typeof value === 'boolean') { formValues[name] = chooseFormValue(drafts[name], value); + } else if (typeof value === 'object' && name === 'extended_profile' && value !== null) { + const extendedProfile = value.slice(); + const draftsKeys = Object.keys(drafts); + + if (draftsKeys.length !== 0) { + const draftFieldName = draftsKeys[0]; + const index = extendedProfile.findIndex((profile) => profile.field_name === draftFieldName); + + if (index !== -1) { + extendedProfile[index] = { field_name: draftFieldName, field_value: drafts[draftFieldName] }; + } + } + + formValues.extended_profile = [...extendedProfile]; } else { formValues[name] = chooseFormValue(drafts[name], value) || ''; } diff --git a/src/account-settings/data/selectors.test.js b/src/account-settings/data/selectors.test.js index 253e1f8..0cfec03 100644 --- a/src/account-settings/data/selectors.test.js +++ b/src/account-settings/data/selectors.test.js @@ -1,4 +1,4 @@ -import { profileDataManagerSelector } from './selectors'; +import { profileDataManagerSelector, formValuesSelector } from './selectors'; const testValue = 'test VALUE'; @@ -13,4 +13,60 @@ describe('profileDataManagerSelector', () => { expect(result).toEqual(state.accountSettings.profileDataManager); }); + + it('should correctly select form values', () => { + const state = { + accountSettings: { + values: { + name: 'John Doe', + age: 25, + }, + drafts: { + age: 26, + + }, + verifiedNameHistory: 'test', + confirmationValues: {}, + }, + }; + + const result = formValuesSelector(state); + + const expected = { + name: 'John Doe', + age: 26, + verified_name: '', + useVerifiedNameForCerts: false, + }; + + expect(result).toEqual(expected); + }); + + it('should correctly select form values with extended_profile', () => { + // Mock data with extended_profile field in both values and drafts + const state = { + accountSettings: { + values: { + extended_profile: [ + { field_name: 'test_field', field_value: '5' }, + ], + }, + drafts: { test_field: '6' }, + verifiedNameHistory: 'test', + confirmationValues: {}, + }, + }; + + const result = formValuesSelector(state); + + const expected = { + verified_name: '', + useVerifiedNameForCerts: false, + extended_profile: [ // Draft value should override the committed value + { field_name: 'test_field', field_value: '6' }, // Value from the committed values + ], + }; + + expect(result).toEqual(expected); + }); }); diff --git a/src/account-settings/test/AccountSettingsPage.test.jsx b/src/account-settings/test/AccountSettingsPage.test.jsx new file mode 100644 index 0000000..5acd4c5 --- /dev/null +++ b/src/account-settings/test/AccountSettingsPage.test.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import { AppContext } from '@edx/frontend-platform/react'; +import { + render, screen, fireEvent, +} from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; + +import AccountSettingsPage from '../AccountSettingsPage'; +import mockData from './mockData'; + +const mockDispatch = jest.fn(); +jest.mock('@edx/frontend-platform/analytics', () => ({ + sendTrackingLogEvent: jest.fn(), + getCountryList: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); + +jest.mock('@edx/frontend-platform/auth'); + +const IntlAccountSettingsPage = injectIntl(AccountSettingsPage); + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +describe('AccountSettingsPage', () => { + let props = {}; + let store = {}; + const appContext = { locale: 'en', authenticatedUser: { userId: 3, roles: [] } }; + const reduxWrapper = children => ( + + + + + {children} + + + + + ); + + beforeEach(() => { + store = mockStore(mockData); + props = { + loaded: true, + siteLanguage: {}, + formValues: { + username: 'test_username', + accomplishments_shared: false, + name: 'test_name', + email: 'test_email@test.com', + id: 534, + extended_profile: [ + { + field_name: 'work_experience', + field_value: '', + }, + ], + + }, + fetchSettings: jest.fn(), + }; + }); + + afterEach(() => jest.clearAllMocks()); + + it('renders AccountSettingsPage correctly with editing enabled', async () => { + const { getByText, rerender, getByLabelText } = render(reduxWrapper()); + + const workExperienceText = getByText('Work Experience'); + const workExperienceEditButton = workExperienceText.parentElement.querySelector('button'); + + expect(workExperienceEditButton).toBeInTheDocument(); + + store = mockStore({ + ...mockData, + accountSettings: { + ...mockData.accountSettings, + openFormId: 'work_experience', + }, + }); + rerender(reduxWrapper()); + + const submitButton = screen.getByText('Save'); + expect(submitButton).toBeInTheDocument(); + + const workExperienceSelect = getByLabelText('Work Experience'); + + // Use fireEvent.change to simulate changing the selected value + fireEvent.change(workExperienceSelect, { target: { value: '4' } }); + + fireEvent.click(submitButton); + }); +}); diff --git a/src/account-settings/test/mockData.js b/src/account-settings/test/mockData.js new file mode 100644 index 0000000..c17bd09 --- /dev/null +++ b/src/account-settings/test/mockData.js @@ -0,0 +1,112 @@ +const mockData = { + accountSettings: { + loading: false, + loaded: true, + loadingError: null, + data: null, + values: { + username: 'test_username', + country: 'AD', + accomplishments_shared: false, + name: 'test_name', + email: 'test_email@test.com', + id: 533, + verified_name: null, + extended_profile: [ + { + field_name: 'work_experience', + field_value: '', + }, + ], + gender: null, + + 'pref-lang': 'en', + shouldDisplayDemographicsSection: false, + demographicsOptions: false, + }, + errors: {}, + confirmationValues: {}, + drafts: {}, + saveState: null, + timeZones: [ + { + time_zone: 'Africa/Abidjan', + description: 'Africa/Abidjan (GMT, UTC+0000)', + }, + ], + countryTimeZones: [ + { + time_zone: 'Europe/Andorra', + description: 'Europe/Andorra (CET, UTC+0100)', + }, + ], + previousSiteLanguage: null, + deleteAccount: { + status: null, + errorType: null, + }, + siteLanguage: { + loading: false, + loaded: true, + loadingError: null, + siteLanguageList: [ + { + code: 'en', + name: 'English', + released: true, + }, + ], + }, + resetPassword: { + status: null, + }, + nameChange: { + saveState: null, + errors: {}, + }, + thirdPartyAuth: { + providers: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + connected: false, + accepts_logins: true, + connectUrl: 'http://localhost:18000/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings', + disconnectUrl: 'http://localhost:18000/auth/disconnect/google-oauth2/?', + }, + ], + disconnectionStatuses: {}, + errors: {}, + }, + verifiedName: null, + mostRecentVerifiedName: {}, + verifiedNameHistory: { + use_verified_name_for_certs: false, + results: [], + }, + profileDataManager: null, + }, + notificationPreferences: { + showPreferences: false, + courses: { + status: 'success', + courses: [], + pagination: { + count: 0, + currentPage: 1, + hasMore: false, + totalPages: 1, + }, + }, + preferences: { + status: 'idle', + updatePreferenceStatus: 'idle', + selectedCourse: null, + preferences: [], + apps: [], + nonEditable: {}, + }, + }, +}; + +export default mockData;