mubbsharanwar/unenrollment process improvement (#704)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
This commit is contained in:
@@ -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 = <span className="font-italic">“{courseName}”</span>;
|
||||
return (
|
||||
<>
|
||||
<h4>{formatMessage(messages.confirmHeader)}</h4>
|
||||
<p className="py-2">{formatMessage(messages.confirmText, { courseTitle })}</p>
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={handleClose}>
|
||||
{formatMessage(messages.confirmCancel)}
|
||||
@@ -31,6 +36,7 @@ export const ConfirmPane = ({
|
||||
ConfirmPane.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
handleConfirm: PropTypes.func.isRequired,
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ConfirmPane;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ConfirmPane } from './ConfirmPane';
|
||||
import messages from './messages';
|
||||
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
handleClose: jest.fn().mockName('props.handleClose'),
|
||||
handleConfirm: jest.fn().mockName('props.handleConfirm'),
|
||||
};
|
||||
|
||||
@@ -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,16 +11,18 @@ import {
|
||||
import messages from './messages';
|
||||
|
||||
export const FinishedPane = ({
|
||||
gaveReason,
|
||||
cardId,
|
||||
handleClose,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const courseTitle = <span className="font-italic">“{courseName}”</span>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{formatMessage(messages.finishHeading)}</h4>
|
||||
<p>
|
||||
{gaveReason && formatMessage(messages.finishThanksText)}
|
||||
{formatMessage(messages.finishText)}
|
||||
{formatMessage(messages.finishText, { courseTitle })}
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button onClick={handleClose}>{formatMessage(messages.finishReturn)}</Button>
|
||||
@@ -29,7 +32,7 @@ export const FinishedPane = ({
|
||||
};
|
||||
FinishedPane.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
gaveReason: PropTypes.bool.isRequired,
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FinishedPane;
|
||||
|
||||
@@ -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(<IntlProvider locale="en"><FinishedPane {...customProps} /></IntlProvider>);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)}
|
||||
<Form.Radio value={constants.reasonKeys.custom}>
|
||||
@@ -35,12 +37,13 @@ export const ReasonPane = ({
|
||||
placeholder={formatMessage(constants.messages.customPlaceholder)}
|
||||
/>
|
||||
</Form.Radio>
|
||||
{option(constants.reasonKeys.preferNotToSay)}
|
||||
</Form.RadioSet>
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={reason.handleSkip}>
|
||||
{formatMessage(messages.reasonSkip)}
|
||||
<Button variant="tertiary" onClick={handleClose}>
|
||||
{formatMessage(messages.confirmCancel)}
|
||||
</Button>
|
||||
<Button disabled={!reason.hasReason} onClick={reason.handleSubmit}>
|
||||
<Button onClick={reason.handleSubmit}>
|
||||
{formatMessage(messages.reasonSubmit)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
@@ -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;
|
||||
|
||||
@@ -28,11 +28,11 @@ describe('UnenrollConfirmModal ReasonPane', () => {
|
||||
render(<IntlProvider locale="en"><ReasonPane {...props} /></IntlProvider>);
|
||||
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(<IntlProvider locale="en"><ReasonPane {...props} hasReason={false} /></IntlProvider>);
|
||||
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', () => {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -38,13 +38,13 @@ export const UnenrollConfirmModal = ({
|
||||
style={{ textAlign: 'start' }}
|
||||
>
|
||||
{(modalState === modalStates.confirm) && (
|
||||
<ConfirmPane handleClose={close} handleConfirm={confirm} />
|
||||
<ConfirmPane handleClose={close} handleConfirm={confirm} cardId={cardId} />
|
||||
)}
|
||||
{(modalState === modalStates.finished) && (
|
||||
<FinishedPane handleClose={closeAndRefresh} gaveReason={!reason.isSkipped} />
|
||||
<FinishedPane handleClose={closeAndRefresh} cardId={cardId} />
|
||||
)}
|
||||
{(modalState === modalStates.reason) && (
|
||||
<ReasonPane reason={reason} />
|
||||
<ReasonPane reason={reason} handleClose={close} />
|
||||
)}
|
||||
</div>
|
||||
</ModalDialog>
|
||||
|
||||
@@ -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(<IntlProvider><UnenrollConfirmModal {...props} /></IntlProvider>);
|
||||
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(<IntlProvider><UnenrollConfirmModal {...props} /></IntlProvider>);
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user