From ff925b06f19363bea6fc9c2ecd3f91e599bfce7b Mon Sep 17 00:00:00 2001 From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:39:49 +0500 Subject: [PATCH] mubbsharanwar/unenrollment process improvement (#704) Co-authored-by: Deborah Kaplan --- .../components/ConfirmPane.jsx | 6 +++++ .../components/ConfirmPane.test.jsx | 1 + .../components/FinishedPane.jsx | 11 +++++---- .../components/FinishedPane.test.jsx | 20 +++------------- .../components/ReasonPane.jsx | 11 +++++---- .../components/ReasonPane.test.jsx | 6 ++--- .../components/messages.js | 24 +++++++++---------- .../UnenrollConfirmModal/constants.js | 8 ++++++- .../UnenrollConfirmModal/hooks/index.js | 2 +- .../UnenrollConfirmModal/hooks/reasons.js | 16 ++++--------- .../hooks/reasons.test.js | 18 ++------------ src/containers/UnenrollConfirmModal/index.jsx | 6 ++--- .../UnenrollConfirmModal/index.test.jsx | 15 +++++------- 13 files changed, 62 insertions(+), 82 deletions(-) diff --git a/src/containers/UnenrollConfirmModal/components/ConfirmPane.jsx b/src/containers/UnenrollConfirmModal/components/ConfirmPane.jsx index eea4869..723976d 100644 --- a/src/containers/UnenrollConfirmModal/components/ConfirmPane.jsx +++ b/src/containers/UnenrollConfirmModal/components/ConfirmPane.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { reduxHooks } from 'hooks'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -10,13 +11,17 @@ import { import messages from './messages'; export const ConfirmPane = ({ + cardId, handleClose, handleConfirm, }) => { const { formatMessage } = useIntl(); + const { courseName } = reduxHooks.useCardCourseData(cardId); + const courseTitle = “{courseName}”; return ( <>

{formatMessage(messages.confirmHeader)}

+

{formatMessage(messages.confirmText, { courseTitle })}

@@ -29,7 +32,7 @@ export const FinishedPane = ({ }; FinishedPane.propTypes = { handleClose: PropTypes.func.isRequired, - gaveReason: PropTypes.bool.isRequired, + cardId: PropTypes.string.isRequired, }; export default FinishedPane; diff --git a/src/containers/UnenrollConfirmModal/components/FinishedPane.test.jsx b/src/containers/UnenrollConfirmModal/components/FinishedPane.test.jsx index f639e0e..02851da 100644 --- a/src/containers/UnenrollConfirmModal/components/FinishedPane.test.jsx +++ b/src/containers/UnenrollConfirmModal/components/FinishedPane.test.jsx @@ -6,7 +6,7 @@ import { FinishedPane } from './FinishedPane'; import messages from './messages'; const props = { - gaveReason: true, + cardId: 'cardId', handleClose: jest.fn().mockName('props.handleClose'), }; @@ -25,22 +25,8 @@ describe('UnenrollConfirmModal FinishedPane', () => { expect(returnButton).toBeInTheDocument(); }); it('Gave reason, display thanks message', () => { - const thanksMsg = screen.getByText((text) => text.includes('Thank you')); - expect(thanksMsg).toBeInTheDocument(); - expect(thanksMsg.innerHTML).toContain(formatMessage(messages.finishThanksText)); - }); - }); - describe('Did not give reason', () => { - it('Does not display thanks message', () => { - const customProps = { - gaveReason: false, - handleClose: jest.fn().mockName('props.handleClose'), - }; - render(); - const thanksMsg = screen.queryByText((text) => text.includes('Thank you')); - expect(thanksMsg).toBeNull(); - const finishMsg = screen.getByText(formatMessage(messages.finishText)); - expect(finishMsg).toBeInTheDocument(); + const finishSuccessMessage = screen.getByText((text) => text.includes('Unenrollment Successful')); + expect(finishSuccessMessage).toBeInTheDocument(); }); }); }); diff --git a/src/containers/UnenrollConfirmModal/components/ReasonPane.jsx b/src/containers/UnenrollConfirmModal/components/ReasonPane.jsx index ac106a2..2d0481a 100644 --- a/src/containers/UnenrollConfirmModal/components/ReasonPane.jsx +++ b/src/containers/UnenrollConfirmModal/components/ReasonPane.jsx @@ -13,6 +13,7 @@ import messages from './messages'; export const ReasonPane = ({ reason, + handleClose, }) => { const { formatMessage } = useIntl(); const option = (key) => ( @@ -27,6 +28,7 @@ export const ReasonPane = ({ name="unenrollReason" onChange={reason.selectOption} value={reason.selected} + defaultValue={constants.reasonKeys.preferNotToSay} > {constants.order.map(option)} @@ -35,12 +37,13 @@ export const ReasonPane = ({ placeholder={formatMessage(constants.messages.customPlaceholder)} /> + {option(constants.reasonKeys.preferNotToSay)} - - @@ -50,7 +53,6 @@ export const ReasonPane = ({ ReasonPane.propTypes = { reason: PropTypes.shape({ value: PropTypes.string, - handleSkip: PropTypes.func, hasReason: PropTypes.bool, selectOption: PropTypes.func, customOption: PropTypes.shape({ @@ -60,6 +62,7 @@ ReasonPane.propTypes = { selected: PropTypes.string, handleSubmit: PropTypes.func.isRequired, }).isRequired, + handleClose: PropTypes.func.isRequired, }; export default ReasonPane; diff --git a/src/containers/UnenrollConfirmModal/components/ReasonPane.test.jsx b/src/containers/UnenrollConfirmModal/components/ReasonPane.test.jsx index ba34ebd..86af7ac 100644 --- a/src/containers/UnenrollConfirmModal/components/ReasonPane.test.jsx +++ b/src/containers/UnenrollConfirmModal/components/ReasonPane.test.jsx @@ -28,11 +28,11 @@ describe('UnenrollConfirmModal ReasonPane', () => { render(); const radioButtons = screen.getAllByRole('radio'); expect(radioButtons).toBeDefined(); - expect(radioButtons.length).toBe(10); + expect(radioButtons.length).toBe(11); }); - it('render skip button', () => { + it('render cancel button', () => { render(); - const skipButton = screen.getByRole('button', { name: formatMessage(messages.reasonSkip) }); + const skipButton = screen.getByRole('button', { name: formatMessage(messages.confirmCancel) }); expect(skipButton).toBeInTheDocument(); }); it('render submit button', () => { diff --git a/src/containers/UnenrollConfirmModal/components/messages.js b/src/containers/UnenrollConfirmModal/components/messages.js index 22cc01c..67fe758 100644 --- a/src/containers/UnenrollConfirmModal/components/messages.js +++ b/src/containers/UnenrollConfirmModal/components/messages.js @@ -5,12 +5,17 @@ const messages = defineMessages({ confirmHeader: { id: 'learner-dash.unenrollConfirm.confirm.header', description: 'Header for confirm unenroll modal', - defaultMessage: 'Unenroll from course?', + defaultMessage: 'Confirm Unenrollment', + }, + confirmText: { + id: 'learner-dash.unenrollConfirm.confirm.text', + description: 'Text for confirm unenroll modal', + defaultMessage: 'Are you sure you want to unenroll from the course {courseTitle} ?', }, confirmCancel: { id: 'learner-dash.unenrollConfirm.confirm.cancel', description: 'Cancel action for confirm unenroll modal', - defaultMessage: 'Never mind', + defaultMessage: 'Cancel', }, confirmUnenroll: { id: 'learner-dash.unenrollConfirm.confirm.unenroll', @@ -20,7 +25,7 @@ const messages = defineMessages({ reasonHeading: { id: 'learner-dash.unenrollConfirm.confirm.reason.heading', description: 'Heading for unenroll reason modal', - defaultMessage: 'What\'s your main reason for unenrolling?', + defaultMessage: 'Why are you unenrolling?', }, reasonSkip: { id: 'learner-dash.unenrollConfirm.confirm.reason.skip', @@ -30,27 +35,22 @@ const messages = defineMessages({ reasonSubmit: { id: 'learner-dash.unenrollConfirm.confirm.reason.submit', description: 'Submit action for unenroll reason modal', - defaultMessage: 'Submit reason', + defaultMessage: 'Unenroll', }, finishHeading: { id: 'learner-dash.unenrollConfirm.confirm.finish.heading', description: 'Heading for unenroll finish modal', - defaultMessage: 'You are unenrolled', - }, - finishThanksText: { - id: 'learner-dash.unenrollConfirm.confirm.finish.thanks-text', - description: 'Thank you message on unenroll modal for providing a reason', - defaultMessage: 'Thank you for sharing your reason for unenrolling. ', + defaultMessage: 'Unenrollment Successful', }, finishText: { id: 'learner-dash.unenrollConfirm.confirm.finish.text', description: 'Text for unenroll finish modal', - defaultMessage: 'This course will be removed from your dashboard.', + defaultMessage: 'You have been unenrolled from the course {courseTitle}', }, finishReturn: { id: 'learner-dash.unenrollConfirm.confirm.finish.return', description: 'Return action for unenroll finish modal', - defaultMessage: 'Return to dashboard', + defaultMessage: 'Ok', }, }); diff --git a/src/containers/UnenrollConfirmModal/constants.js b/src/containers/UnenrollConfirmModal/constants.js index f7870c1..442ad57 100644 --- a/src/containers/UnenrollConfirmModal/constants.js +++ b/src/containers/UnenrollConfirmModal/constants.js @@ -13,18 +13,19 @@ export const reasonKeys = StrictDict({ quality: 'quality', easy: 'easy', custom: 'custom', + preferNotToSay: 'prefer-not-to-say', }); export const order = [ reasonKeys.prereqs, reasonKeys.difficulty, + reasonKeys.easy, reasonKeys.goals, reasonKeys.broken, reasonKeys.time, reasonKeys.browse, reasonKeys.support, reasonKeys.quality, - reasonKeys.easy, ]; const messages = defineMessages({ @@ -78,6 +79,11 @@ const messages = defineMessages({ description: 'Unenroll custom reason option placeholder text', defaultMessage: 'Other', }, + [reasonKeys.preferNotToSay]: { + id: 'learner-dash.unenrollConfirm.reasons.prefer-not-to-say', + description: 'Unenroll reason option - prefer not to say', + defaultMessage: 'I prefer not to say', + }, }); export default { diff --git a/src/containers/UnenrollConfirmModal/hooks/index.js b/src/containers/UnenrollConfirmModal/hooks/index.js index 3f0a219..1db0992 100644 --- a/src/containers/UnenrollConfirmModal/hooks/index.js +++ b/src/containers/UnenrollConfirmModal/hooks/index.js @@ -24,7 +24,7 @@ export const useUnenrollData = ({ closeModal, cardId }) => { let modalState; if (isConfirmed) { - modalState = (reason.isSubmitted || reason.isSkipped) + modalState = (reason.isSubmitted) ? modalStates.finished : modalStates.reason; } else { modalState = modalStates.confirm; diff --git a/src/containers/UnenrollConfirmModal/hooks/reasons.js b/src/containers/UnenrollConfirmModal/hooks/reasons.js index a80d3b4..9df005b 100644 --- a/src/containers/UnenrollConfirmModal/hooks/reasons.js +++ b/src/containers/UnenrollConfirmModal/hooks/reasons.js @@ -9,10 +9,10 @@ import { StrictDict } from 'utils'; import track from 'tracking'; import * as module from './reasons'; +import constants from '../constants'; export const state = StrictDict({ customOption: (val) => React.useState(val), // eslint-disable-line - isSkipped: (val) => React.useState(val), // eslint-disable-line selectedReason: (val) => React.useState(val), // eslint-disable-line isSubmitted: (val) => React.useState(val), //eslint-disable-line }); @@ -21,12 +21,12 @@ export const useUnenrollReasons = ({ cardId, }) => { // The selected option element from the menu - const [selectedReason, setSelectedReason] = module.state.selectedReason(null); + const [selectedReason, setSelectedReason] = module.state.selectedReason( + constants.reasonKeys.preferNotToSay, + ); // Custom option element entry value const [customOption, setCustomOption] = module.state.customOption(''); - // Did the user choose to skip selecting a reason? - const [isSkipped, setIsSkipped] = module.state.isSkipped(false); // Did the user submit an unenrollment reason const [isSubmitted, setIsSubmitted] = module.state.isSubmitted(false); @@ -47,15 +47,9 @@ export const useUnenrollReasons = ({ const handleClear = () => { setSelectedReason(null); setCustomOption(''); - setIsSkipped(false); setIsSubmitted(false); }; - const handleSkip = () => { - setIsSkipped(true); - unenrollFromCourse(); - }; - const handleSubmit = (e) => { handleTrackReasons(e); setIsSubmitted(true); @@ -68,10 +62,8 @@ export const useUnenrollReasons = ({ return { customOption: { value: customOption, onChange: handleCustomOptionChange }, handleClear, - handleSkip, handleSubmit, hasReason, - isSkipped, isSubmitted, selectOption: handleSelectOption, submittedReason, diff --git a/src/containers/UnenrollConfirmModal/hooks/reasons.test.js b/src/containers/UnenrollConfirmModal/hooks/reasons.test.js index 3347192..0fc9cbc 100644 --- a/src/containers/UnenrollConfirmModal/hooks/reasons.test.js +++ b/src/containers/UnenrollConfirmModal/hooks/reasons.test.js @@ -7,6 +7,7 @@ import { } from 'hooks'; import * as hooks from './reasons'; +import constants from '../constants'; jest.mock('hooks', () => ({ apiHooks: { @@ -39,7 +40,6 @@ const loadHook = (isEntitlement = false) => { describe('UnenrollConfirmModal reasons hooks', () => { describe('state fields', () => { state.testGetter(state.keys.customOption); - state.testGetter(state.keys.isSkipped); state.testGetter(state.keys.isSubmitted); state.testGetter(state.keys.selectedReason); }); @@ -55,14 +55,11 @@ describe('UnenrollConfirmModal reasons hooks', () => { describe('behavior', () => { describe('state fields', () => { it('initializes selectedReason with null', () => { - state.expectInitializedWith(state.keys.selectedReason, null); + state.expectInitializedWith(state.keys.selectedReason, constants.reasonKeys.preferNotToSay); }); it('initializes customOption with empty string', () => { state.expectInitializedWith(state.keys.customOption, ''); }); - it('initializes isSkipped with false', () => { - state.expectInitializedWith(state.keys.isSkipped, false); - }); it('initializes isSubmitted with false', () => { state.expectInitializedWith(state.keys.isSubmitted, false); }); @@ -140,15 +137,9 @@ describe('UnenrollConfirmModal reasons hooks', () => { out.handleClear(); expect(state.setState.selectedReason).toHaveBeenCalledWith(null); expect(state.setState.customOption).toHaveBeenCalledWith(''); - expect(state.setState.isSkipped).toHaveBeenCalledWith(false); expect(state.setState.isSubmitted).toHaveBeenCalledWith(false); }); }); - test('handleSkip sets isSkipped and isSubmitted, and unenrolls w/out a reason', () => { - out.handleSkip(); - expect(state.setState.isSkipped).toHaveBeenCalledWith(true); - expect(unenrollFromCourse).toHaveBeenCalledWith(); - }); describe('handleSubmit', () => { it('tracks reason event and calls unenroll action', () => { state.mockVal(state.keys.selectedReason, testValue); @@ -160,11 +151,6 @@ describe('UnenrollConfirmModal reasons hooks', () => { expect(unenrollFromCourse).toHaveBeenCalledWith(); }); }); - test('isSkipped returns state value', () => { - state.mockVal(state.keys.isSkipped, testValue); - loadHook(); - expect(out.isSkipped).toEqual(testValue); - }); test('isSubmitted returns state value', () => { state.mockVal(state.keys.isSubmitted, testValue); loadHook(); diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx index df44276..fb756a1 100644 --- a/src/containers/UnenrollConfirmModal/index.jsx +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -38,13 +38,13 @@ export const UnenrollConfirmModal = ({ style={{ textAlign: 'start' }} > {(modalState === modalStates.confirm) && ( - + )} {(modalState === modalStates.finished) && ( - + )} {(modalState === modalStates.reason) && ( - + )} diff --git a/src/containers/UnenrollConfirmModal/index.test.jsx b/src/containers/UnenrollConfirmModal/index.test.jsx index 8e95c12..f4d3260 100644 --- a/src/containers/UnenrollConfirmModal/index.test.jsx +++ b/src/containers/UnenrollConfirmModal/index.test.jsx @@ -17,7 +17,6 @@ describe('UnenrollConfirmModal component', () => { const hookProps = { confirm: jest.fn().mockName('hooks.confirm'), reason: { - isSkipped: false, reasonProps: 'other', }, close: jest.fn().mockName('hooks.close'), @@ -49,22 +48,20 @@ describe('UnenrollConfirmModal component', () => { render(); const finishHeading = screen.getByText(formatMessage(messages.finishHeading)); expect(finishHeading).toBeInTheDocument(); - const thanksMsg = screen.getByText((text) => text.includes('Thank you')); - expect(thanksMsg).toBeInTheDocument(); - expect(thanksMsg.innerHTML).toContain(formatMessage(messages.finishThanksText)); + const finishMsg = screen.getByText((text) => text.includes('You have been unenrolled from the course')); + expect(finishMsg).toBeInTheDocument(); }); - it('modalStates.finished, reason skipped', () => { + it('modalStates.finished, cancel unenrollment', () => { hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished, - reason: { isSkipped: true }, }); render(); const finishHeading = screen.getByText(formatMessage(messages.finishHeading)); expect(finishHeading).toBeInTheDocument(); - const thanksMsg = screen.queryByText((text) => text.includes('Thank you')); - expect(thanksMsg).toBeNull(); - const finishMsg = screen.getByText(formatMessage(messages.finishText)); + const okButton = screen.queryByText((text) => text.includes('Ok')); + expect(okButton).toBeInTheDocument(); + const finishMsg = screen.queryByText('You have been unenrolled from the course'); expect(finishMsg).toBeInTheDocument(); }); it('modalStates.reason, should display correct component with no shadow', () => {