fix: update logic for unenrollment (#36)

This commit is contained in:
leangseu-edx
2022-10-07 11:08:00 -04:00
committed by GitHub
parent b327e49f14
commit ab2bf89e25
10 changed files with 66 additions and 75 deletions

View File

@@ -6,7 +6,6 @@ import { ReasonPane } from './ReasonPane';
describe('UnenrollConfirmModal ReasonPane', () => {
const props = {
reason: {
value: 'props.reason.value',
skip: jest.fn().mockName('props.reason.skip'),
selectOption: jest.fn().mockName('props.reason.selectOption'),
customOption: {

View File

@@ -11,7 +11,6 @@ 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
submittedReason: (val) => React.useState(val), // eslint-disable-line
});
export const modalStates = StrictDict({
@@ -20,27 +19,25 @@ export const modalStates = StrictDict({
finished: 'finished',
});
export const useUnenrollReasons = () => {
export const useUnenrollReasons = ({
dispatch,
cardId,
}) => {
const [selectedReason, setSelectedReason] = module.state.selectedReason(null);
const [submittedReason, setSubmittedReason] = module.state.submittedReason(null);
const [isSkipped, setIsSkipped] = module.state.isSkipped(false);
const [customOption, setCustomOption] = module.state.customOption('');
return {
clear: React.useCallback(() => {
setSelectedReason(null);
setSubmittedReason(null);
setCustomOption('');
setIsSkipped(false);
}, [
setSelectedReason,
setSubmittedReason,
setCustomOption,
setIsSkipped,
]),
value: submittedReason,
customOption: {
value: customOption,
onChange: useValueCallback(setCustomOption),
@@ -52,23 +49,23 @@ export const useUnenrollReasons = () => {
isSkipped,
skip: React.useCallback(() => setIsSkipped(true), [setIsSkipped]),
isSubmitted: submittedReason !== null || isSkipped,
isSubmitted: isSkipped,
submit: React.useCallback(() => {
if (selectedReason === 'custom') {
setSubmittedReason(customOption);
} else {
setSubmittedReason(selectedReason);
}
}, [setSubmittedReason, customOption, selectedReason]),
const submittedReason = selectedReason === 'custom' ? customOption : selectedReason;
dispatch(thunkActions.app.unenrollFromCourse(cardId, submittedReason));
}, [cardId, customOption, dispatch, selectedReason]),
};
};
export const useUnenrollData = ({ closeModal, dispatch }) => {
export const useUnenrollData = ({ closeModal, dispatch, cardId }) => {
const [isConfirmed, setIsConfirmed] = module.state.confirmed(false);
const confirm = React.useCallback(() => setIsConfirmed(true), [setIsConfirmed]);
const reason = module.useUnenrollReasons();
const reason = module.useUnenrollReasons({
dispatch,
cardId,
});
const close = React.useCallback(() => {
closeModal();

View File

@@ -15,21 +15,25 @@ jest.mock('data/redux/thunkActions/app', () => ({
const state = new MockUseState(hooks);
const testValue = 'test-value';
let out;
let hook;
describe('UnenrollConfirmModal hooks', () => {
const dispatch = jest.fn();
const closeModal = jest.fn();
const cardId = 'test-card-id';
const createUseUnenrollReasons = () => hooks.useUnenrollReasons({ dispatch, cardId });
const createUseUnenrollData = () => hooks.useUnenrollData({ closeModal, dispatch, cardId });
describe('state fields', () => {
state.testGetter(state.keys.confirmed);
state.testGetter(state.keys.customOption);
state.testGetter(state.keys.isSkipped);
state.testGetter(state.keys.selectedReason);
state.testGetter(state.keys.submittedReason);
});
describe('useUnenrollReasons', () => {
beforeEach(() => {
hook = hooks.useUnenrollReasons;
state.mock();
out = hook();
out = createUseUnenrollReasons();
});
afterEach(() => {
state.restore();
@@ -39,38 +43,32 @@ describe('UnenrollConfirmModal hooks', () => {
const { cb, prereqs } = out.clear.useCallback;
expect(prereqs).toEqual([
state.setState.selectedReason,
state.setState.submittedReason,
state.setState.customOption,
state.setState.isSkipped,
]);
cb();
expect(state.setState.selectedReason).toHaveBeenCalledWith(null);
expect(state.setState.submittedReason).toHaveBeenCalledWith(null);
expect(state.setState.customOption).toHaveBeenCalledWith('');
expect(state.setState.isSkipped).toHaveBeenCalledWith(false);
});
});
test('value returns submitted reason', () => {
state.mockVal(state.keys.submittedReason, testValue);
expect(hook().value).toEqual(testValue);
});
test('customOption.value returns custom option', () => {
state.mockVal(state.keys.customOption, testValue);
expect(hook().customOption.value).toEqual(testValue);
expect(createUseUnenrollReasons().customOption.value).toEqual(testValue);
});
test('customOption.onChange returns valueCallback for setCustomOption', () => {
expect(out.customOption.onChange).toEqual(useValueCallback(state.setState.customOption));
});
test('selected returns selectedReason', () => {
state.mockVal(state.keys.selectedReason, testValue);
expect(hook().selected).toEqual(testValue);
expect(createUseUnenrollReasons().selected).toEqual(testValue);
});
test('selectedOption returns valueCallback for setSelectedReason', () => {
expect(out.selectOption).toEqual(useValueCallback(state.setState.selectedReason));
});
test('isSkipped returns state value', () => {
state.mockVal(state.keys.isSkipped, testValue);
expect(hook().isSkipped).toEqual(testValue);
expect(createUseUnenrollReasons().isSkipped).toEqual(testValue);
});
test('skip returns callback that sets isSkipped to true', () => {
const { cb, prereqs } = out.skip.useCallback;
@@ -82,44 +80,25 @@ describe('UnenrollConfirmModal hooks', () => {
it('returns false if submittedReason is null and not isSkipped', () => {
expect(out.isSubmitted).toEqual(false);
});
it('returns true if submittedReason is not null', () => {
state.mockVal(state.keys.submittedReason, testValue);
expect(hook().isSubmitted).toEqual(true);
});
it('returns true if isSkipped', () => {
state.mockVal(state.keys.isSkipped, true);
expect(hook().isSubmitted).toEqual(true);
expect(createUseUnenrollReasons().isSubmitted).toEqual(true);
});
});
describe('submit', () => {
it('sets customOption as submittedReason if selectedReason is custom', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue);
hook().submit.useCallback.cb();
expect(state.setState.submittedReason).toHaveBeenCalledWith(testValue);
});
it('sets selectedReason as submittedReason if selectedReason is not custom', () => {
state.mockVal(state.keys.selectedReason, testValue);
state.mockVal(state.keys.customOption, 'customValue');
hook().submit.useCallback.cb();
expect(state.setState.submittedReason).toHaveBeenCalledWith(testValue);
});
it('depends on customOption and selectedReason', () => {
const customValue = 'custom-value';
state.mockVal(state.keys.selectedReason, testValue);
state.mockVal(state.keys.customOption, customValue);
const { prereqs } = hook().submit.useCallback;
const { prereqs } = createUseUnenrollReasons().submit.useCallback;
expect(prereqs).toContain(testValue);
expect(prereqs).toContain(customValue);
});
});
});
describe('modalHooks', () => {
const closeModal = jest.fn();
const dispatch = jest.fn();
let mockReason;
beforeEach(() => {
hook = hooks.useUnenrollData;
mockReason = {
isSubmitted: false,
clear: jest.fn(),
@@ -127,7 +106,7 @@ describe('UnenrollConfirmModal hooks', () => {
state.mock();
state.mockVal(state.keys.confirmed, testValue);
hooks.useUnenrollReasons = jest.fn(() => mockReason);
out = hook({ closeModal, dispatch });
out = createUseUnenrollData();
});
afterEach(() => {
state.restore();
@@ -184,17 +163,17 @@ describe('UnenrollConfirmModal hooks', () => {
it('returns modalStates.finished if confirmed and submitted', () => {
state.mockVal(state.keys.confirmed, true);
hooks.useUnenrollReasons = jest.fn(() => ({ ...mockReason, isSubmitted: true }));
out = hook({ closeModal, dispatch });
out = createUseUnenrollData();
expect(out.modalState).toEqual(hooks.modalStates.finished);
});
it('returns modalStates.reason if confirmed and not submitted', () => {
state.mockVal(state.keys.confirmed, true);
out = hook({ closeModal, dispatch });
out = createUseUnenrollData();
expect(out.modalState).toEqual(hooks.modalStates.reason);
});
it('returns modalStates.confirm if not confirmed', () => {
state.mockVal(state.keys.confirmed, false);
out = hook({ closeModal, dispatch });
out = createUseUnenrollData();
expect(out.modalState).toEqual(hooks.modalStates.confirm);
});
});

View File

@@ -18,6 +18,7 @@ import { useUnenrollData, modalStates } from './hooks';
export const UnenrollConfirmModal = ({
closeModal,
show,
cardId,
}) => {
const dispatch = useDispatch();
const {
@@ -26,7 +27,7 @@ export const UnenrollConfirmModal = ({
closeAndRefresh,
close,
modalState,
} = useUnenrollData({ dispatch, closeModal });
} = useUnenrollData({ dispatch, closeModal, cardId });
const showFullscreen = modalState === modalStates.reason;
return (
<ModalDialog
@@ -56,6 +57,7 @@ export const UnenrollConfirmModal = ({
UnenrollConfirmModal.propTypes = {
closeModal: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
cardId: PropTypes.string.isRequired,
};
export default UnenrollConfirmModal;

View File

@@ -28,20 +28,25 @@ describe('UnenrollConfirmModal component', () => {
closeAndRefresh: jest.fn().mockName('hooks.closeAndRefresh'),
modalState: hooks.modalStates.confirm,
};
const closeModal = jest.fn().mockName('props.closeModal');
const show = true;
const closeModal = jest.fn().mockName('closeModal');
const cardId = 'cardId';
const props = {
closeModal,
show: true,
cardId,
};
test('hooks called with dispatch and closeModal props', () => {
hooks.useUnenrollData.mockReturnValueOnce(hookProps);
shallow(<UnenrollConfirmModal {...{ closeModal, show }} />);
expect(hooks.useUnenrollData).toHaveBeenCalledWith({ dispatch, closeModal });
shallow(<UnenrollConfirmModal {...props} />);
expect(hooks.useUnenrollData).toHaveBeenCalledWith({ dispatch, closeModal, cardId });
});
test('snapshot: modalStates.confirm', () => {
hooks.useUnenrollData.mockReturnValueOnce(hookProps);
expect(shallow(<UnenrollConfirmModal {...{ closeModal, show }} />)).toMatchSnapshot();
expect(shallow(<UnenrollConfirmModal {...props} />)).toMatchSnapshot();
});
test('snapshot: modalStates.finished, reason given', () => {
hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished });
expect(shallow(<UnenrollConfirmModal {...{ closeModal, show }} />)).toMatchSnapshot();
expect(shallow(<UnenrollConfirmModal {...props} />)).toMatchSnapshot();
});
test('snapshot: modalStates.finished, reason skipped', () => {
hooks.useUnenrollData.mockReturnValueOnce({
@@ -49,10 +54,10 @@ describe('UnenrollConfirmModal component', () => {
modalState: hooks.modalStates.finished,
isSkipped: true,
});
expect(shallow(<UnenrollConfirmModal {...{ closeModal, show }} />)).toMatchSnapshot();
expect(shallow(<UnenrollConfirmModal {...props} />)).toMatchSnapshot();
});
test('snapshot: modalStates.reason, should be fullscreen with no shadow', () => {
hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.reason });
expect(shallow(<UnenrollConfirmModal {...{ closeModal, show }} />)).toMatchSnapshot();
expect(shallow(<UnenrollConfirmModal {...props} />)).toMatchSnapshot();
});
});

View File

@@ -67,7 +67,8 @@ export const leaveEntitlementSession = (cardId) => (dispatch, getState) => {
return dispatch(requests.leaveEntitlementSession({ uuid }));
};
export const unenrollFromCourse = (courseId, reason) => (dispatch) => {
export const unenrollFromCourse = (cardId, reason) => (dispatch, getState) => {
const { courseId } = selectors.app.courseCard.courseRun(getState(), cardId);
handleEvent(eventNames.unenrollReason, {
category: 'user-engagement',
displayName: 'v1',

View File

@@ -34,10 +34,10 @@ const updateEmailSettings = ({ courseId, enable }) => post(stringifyUrl(
{ [apiKeys.courseId]: courseId, ...(enable && enableEmailsAction) },
));
const unenrollFromCourse = ({ courseId }) => post(stringifyUrl(
urls.courseUnenroll,
{ [apiKeys.courseId]: courseId, ...unenrollmentAction },
));
const unenrollFromCourse = ({ courseId }) => post(stringifyUrl(urls.courseUnenroll), {
[apiKeys.courseId]: courseId,
...unenrollmentAction,
});
export default {
initializeList,

View File

@@ -90,8 +90,7 @@ describe('lms api methods', () => {
).toEqual(
utils.post(utils.stringifyUrl(
urls.courseUnenroll,
{ [apiKeys.courseId]: testCourseId, ...unenrollmentAction },
)),
), { [apiKeys.courseId]: testCourseId, ...unenrollmentAction }),
);
});
});

View File

@@ -10,10 +10,11 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
/**
* post(url, data)
* simple wrapper providing an authenticated Http client post action
* queryString.stringify is used to convert the object to query string with = and &
* @param {string} url - target url
* @param {object|string} data - post payload
* @param {object|string} body - post payload
*/
export const post = (...args) => getAuthenticatedHttpClient().post(...args);
export const post = (url, body) => getAuthenticatedHttpClient().post(url, queryString.stringify(body));
export const client = getAuthenticatedHttpClient;

View File

@@ -4,6 +4,7 @@ import * as utils from './utils';
jest.mock('query-string', () => ({
stringifyUrl: jest.fn((url, options) => ({ url, options })),
stringify: jest.fn((data) => data),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
@@ -22,8 +23,15 @@ describe('lms service utils', () => {
it('forwards arguments to authenticatedHttpClient().post', () => {
const post = jest.fn((...args) => ({ post: args }));
getAuthenticatedHttpClient.mockReturnValue({ post });
const args = ['some', 'args', 'for', 'the', 'test'];
expect(utils.post(...args)).toEqual(post(...args));
const url = 'some url';
const body = {
some: 'body',
for: 'the',
test: 'yay',
};
const expectedUrl = utils.post(url, body);
expect(queryString.stringify).toHaveBeenCalledWith(body);
expect(expectedUrl).toEqual(post(url, body));
});
});
describe('stringifyUrl', () => {