fix: select session workflow (#59)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -13,8 +12,7 @@ export const SelectSessionButton = ({ cardId }) => {
|
||||
const { canChange, hasSessions } = hooks.useCardEntitlementData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
@@ -12,7 +11,6 @@ import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ cardId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
isEntitlement,
|
||||
hasSessions,
|
||||
@@ -22,7 +20,7 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
isExpired,
|
||||
} = appHooks.useCardEntitlementData(cardId);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
@@ -11,7 +10,7 @@ jest.mock('data/redux', () => ({
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useUpdateSelectSessionModalCallback: jest.fn(
|
||||
(_, cardId) => jest.fn().mockName(`updateSelectSessionModalCallback(${cardId})`),
|
||||
(cardId) => jest.fn().mockName(`updateSelectSessionModalCallback(${cardId})`),
|
||||
),
|
||||
},
|
||||
}));
|
||||
@@ -36,13 +35,11 @@ const render = (overrides = {}) => {
|
||||
el = shallow(<EntitlementBanner cardId={cardId} />);
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
describe('EntitlementBanner', () => {
|
||||
test('initializes data with course number from entitlement', () => {
|
||||
render();
|
||||
expect(appHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(dispatch, cardId);
|
||||
expect(appHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
test('no display if not an entitlement', () => {
|
||||
render({ entitlement: { isEntitlement: false } });
|
||||
|
||||
@@ -37,7 +37,7 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useCardDetailsData = ({ dispatch, cardId }) => {
|
||||
export const useCardDetailsData = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const providerName = appHooks.useCardProviderData(cardId).name;
|
||||
const { courseNumber } = appHooks.useCardCourseData(cardId);
|
||||
@@ -47,7 +47,7 @@ export const useCardDetailsData = ({ dispatch, cardId }) => {
|
||||
canChange,
|
||||
} = appHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
|
||||
return {
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
@@ -8,7 +7,6 @@ import useCardDetailsData from './hooks';
|
||||
import './index.scss';
|
||||
|
||||
export const CourseCardDetails = ({ cardId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
providerName,
|
||||
accessMessage,
|
||||
@@ -18,7 +16,7 @@ export const CourseCardDetails = ({ cardId }) => {
|
||||
openSessionModal,
|
||||
courseNumber,
|
||||
changeOrLeaveSessionMessage,
|
||||
} = useCardDetailsData({ cardId, dispatch });
|
||||
} = useCardDetailsData({ cardId });
|
||||
|
||||
return (
|
||||
<span className="small" data-testid="CourseCardDetails">
|
||||
|
||||
@@ -71,17 +71,17 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
)}
|
||||
*/}
|
||||
{twitter.isEnabled && (
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
@@ -15,16 +15,19 @@ export const state = StrictDict({
|
||||
});
|
||||
|
||||
export const useSelectSessionModalData = () => {
|
||||
const dispatch = useDispatch();
|
||||
const selectedCardId = appHooks.useSelectSessionModalData().cardId;
|
||||
const {
|
||||
availableSessions,
|
||||
isFulfilled,
|
||||
uuid,
|
||||
} = appHooks.useCardEntitlementData(selectedCardId);
|
||||
const { title: courseTitle } = appHooks.useCardCourseData(selectedCardId);
|
||||
const { courseId } = appHooks.useCardCourseRunData(selectedCardId) || {};
|
||||
const { isEnrolled } = appHooks.useCardEnrollmentData(selectedCardId);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const [selectedSession, setSelectedSession] = module.state.selectedSession(null);
|
||||
|
||||
const [selectedSession, setSelectedSession] = module.state.selectedSession(courseId || null);
|
||||
|
||||
let header;
|
||||
let hint;
|
||||
@@ -37,20 +40,24 @@ export const useSelectSessionModalData = () => {
|
||||
});
|
||||
hint = formatMessage(messages.selectSessionHint);
|
||||
}
|
||||
const updateCallback = appHooks.useUpdateSelectSessionModalCallback;
|
||||
const updateCardIdCallback = appHooks.useUpdateSelectSessionModalCallback;
|
||||
const closeSessionModal = updateCardIdCallback(null);
|
||||
|
||||
const handleSelection = ({ target: { value } }) => setSelectedSession(value);
|
||||
const handleSubmit = () => {
|
||||
if (selectedSession === LEAVE_OPTION) {
|
||||
return dispatch(thunkActions.app.leaveEntitlementSession({ uuid }));
|
||||
dispatch(thunkActions.app.leaveEntitlementSession(selectedCardId));
|
||||
} else if (isEnrolled) {
|
||||
dispatch(thunkActions.app.switchEntitlementEnrollment(selectedCardId, selectedSession));
|
||||
} else {
|
||||
dispatch(thunkActions.app.newEntitlementEnrollment(selectedCardId, selectedSession));
|
||||
}
|
||||
return dispatch(thunkActions.app.switchEntitlementEnrollment({ uuid, courseId: selectedSession }));
|
||||
closeSessionModal();
|
||||
};
|
||||
|
||||
return {
|
||||
showModal: selectedCardId != null,
|
||||
closeSessionModal: updateCallback(dispatch, null),
|
||||
openSessionModal: (cardId) => updateCallback(dispatch, cardId),
|
||||
closeSessionModal,
|
||||
showLeaveOption: isFulfilled,
|
||||
availableSessions,
|
||||
hint,
|
||||
|
||||
@@ -11,10 +11,12 @@ import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useSelectSessionModalData: jest.fn(),
|
||||
useUpdateSelectSessionModalCallback: jest.fn((...args) => ({
|
||||
useUpdateSelectSessionModalCallback: jest.fn((...args) => () => ({
|
||||
updateSelectSession: args,
|
||||
})),
|
||||
},
|
||||
@@ -25,20 +27,18 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
thunkActions: {
|
||||
app: {
|
||||
leaveEntitlementSession: jest.fn(),
|
||||
switchEntitlementEnrollment: jest.fn(),
|
||||
switchEntitlementEnrollment: jest.fn((...args) => ({ switchEntitlementEnrollment: args })),
|
||||
leaveEntitlementSession: jest.fn((...args) => ({ leaveEntitlementSession: args })),
|
||||
newEntitlementEnrollment: jest.fn((...args) => ({ newEntitlementEnrollment: args })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const selectedCardId = 'test-selected-card-id';
|
||||
const courseTitle = 'course-title: brown fox';
|
||||
const uuid = 'test-uuid';
|
||||
|
||||
const selectSessionData = {
|
||||
cardId: selectedCardId,
|
||||
};
|
||||
|
||||
const entitlementData = {
|
||||
availableSessions: [
|
||||
{ startDate: '1/2/2000', endDate: '1/2/2020', cardId: 'session-id-1' },
|
||||
@@ -49,15 +49,14 @@ const entitlementData = {
|
||||
uuid,
|
||||
};
|
||||
|
||||
const cardCourseData = {
|
||||
title: 'course-title: brown fox',
|
||||
};
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const testValue = 'test-value';
|
||||
|
||||
const courseId = 'test-course-id';
|
||||
appHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
||||
|
||||
describe('SelectSessionModal hooks', () => {
|
||||
let out;
|
||||
|
||||
@@ -68,19 +67,18 @@ describe('SelectSessionModal hooks', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('useSelectSessionModalData', () => {
|
||||
const runHook = ({ selectSession = {}, entitlement = {}, course = {} }) => {
|
||||
appHooks.useSelectSessionModalData.mockReturnValueOnce({
|
||||
...selectSessionData,
|
||||
...selectSession,
|
||||
});
|
||||
appHooks.useCardEntitlementData.mockReturnValueOnce({
|
||||
...entitlementData,
|
||||
...entitlement,
|
||||
});
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...cardCourseData,
|
||||
...course,
|
||||
});
|
||||
const runHook = ({
|
||||
course = {},
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
entitlement = {},
|
||||
selectSession = {},
|
||||
}) => {
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({ title: courseTitle, ...course });
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({ courseId, ...courseRun });
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, ...enrollment });
|
||||
appHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
||||
appHooks.useSelectSessionModalData.mockReturnValueOnce({ cardId: selectedCardId, ...selectSession });
|
||||
out = hooks.useSelectSessionModalData();
|
||||
};
|
||||
beforeEach(() => {
|
||||
@@ -97,8 +95,14 @@ describe('SelectSessionModal hooks', () => {
|
||||
});
|
||||
|
||||
describe('output', () => {
|
||||
test('selected session defaults to null', () => {
|
||||
expect(out.selectedSession).toEqual(null);
|
||||
describe('selectedSession', () => {
|
||||
it('defaults to current courseId if enrolled', () => {
|
||||
expect(out.selectedSession).toEqual(courseId);
|
||||
});
|
||||
it('defaults to null if not enrolled', () => {
|
||||
runHook({ enrollment: { isEnrolled: false }, courseRun: { courseId: undefined } });
|
||||
expect(out.selectedSession).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('handleSelection', () => {
|
||||
it('sets selected session with event target value', () => {
|
||||
@@ -107,19 +111,35 @@ describe('SelectSessionModal hooks', () => {
|
||||
});
|
||||
});
|
||||
describe('handleSubmit', () => {
|
||||
it('dispatches updateEntitlementSession with selected card ID and session', () => {
|
||||
state.mockVal(state.keys.selectedSession, testValue);
|
||||
runHook({});
|
||||
expect(out.handleSubmit()).toEqual(dispatch(
|
||||
thunkActions.app.switchEntitlementEnrollment({ courseId: testValue, uuid }),
|
||||
));
|
||||
describe('if LEAVE_OPTION is selected', () => {
|
||||
it('dispatches leaveEntitlementSession', () => {
|
||||
state.mockVal(state.keys.selectedSession, LEAVE_OPTION);
|
||||
runHook({});
|
||||
out.handleSubmit();
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
thunkActions.app.leaveEntitlementSession(selectedCardId),
|
||||
);
|
||||
});
|
||||
});
|
||||
it('dispatches leaveEntitlementSession if LEAVE_OPTION is selected', () => {
|
||||
state.mockVal(state.keys.selectedSession, LEAVE_OPTION);
|
||||
runHook({});
|
||||
expect(out.handleSubmit()).toEqual(dispatch(
|
||||
thunkActions.app.leaveEntitlementSession({ uuid }),
|
||||
));
|
||||
describe('if not enrolled in a session yet', () => {
|
||||
it('dispatches newEntitlementEnrollment with selected card ID and session', () => {
|
||||
state.mockVal(state.keys.selectedSession, testValue);
|
||||
runHook({});
|
||||
out.handleSubmit();
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
thunkActions.app.newEntitlementEnrollment(selectedCardId, testValue),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('if enrolled in a session already, selecting a new session', () => {
|
||||
it('dispatches swtichEntitlementEnrollment with selected card ID and session', () => {
|
||||
state.mockVal(state.keys.selectedSession, testValue);
|
||||
runHook({ enrollment: { isEnrolled: true } });
|
||||
out.handleSubmit();
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
thunkActions.app.switchEntitlementEnrollment(selectedCardId, testValue),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
test('showModal returns true if selectedCardId is not null or undefined', () => {
|
||||
@@ -130,10 +150,7 @@ describe('SelectSessionModal hooks', () => {
|
||||
expect(out.showModal).toEqual(false);
|
||||
});
|
||||
test('displays change or leave header and hint if fulfilled', () => {
|
||||
expect(out.header).toEqual(formatMessage(
|
||||
messages.selectSessionHeader,
|
||||
{ courseTitle: cardCourseData.title },
|
||||
));
|
||||
expect(out.header).toEqual(formatMessage(messages.selectSessionHeader, { courseTitle }));
|
||||
expect(out.hint).toEqual(formatMessage(messages.selectSessionHint));
|
||||
});
|
||||
test('displays select session header (w/ courseTitle) and hint if unfulfilled', () => {
|
||||
@@ -142,8 +159,8 @@ describe('SelectSessionModal hooks', () => {
|
||||
expect(out.hint).toEqual(formatMessage(messages.changeOrLeaveHint));
|
||||
});
|
||||
test('closeSessionModal returns update callback wth dispatch and null card id', () => {
|
||||
expect(out.closeSessionModal).toEqual(
|
||||
appHooks.useUpdateSelectSessionModalCallback(dispatch, null),
|
||||
expect(out.closeSessionModal()).toEqual(
|
||||
appHooks.useUpdateSelectSessionModalCallback(null)(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { actions as appActions } from './app/reducer';
|
||||
import appSelectors from './app/selectors';
|
||||
@@ -58,9 +58,10 @@ export const useCardSocialSettingsData = (cardId) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => dispatch(
|
||||
appActions.updateSelectSessionModal(cardId),
|
||||
);
|
||||
export const useUpdateSelectSessionModalCallback = (cardId) => {
|
||||
const dispatch = useDispatch();
|
||||
return () => dispatch(appActions.updateSelectSessionModal(cardId));
|
||||
};
|
||||
|
||||
export const useMasqueradeData = () => useSelector(requestSelectors.masquerade);
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ export const newEntitlementEnrollment = (cardId, selection) => (dispatch, getSta
|
||||
fromCourseRun: null,
|
||||
toCourseRun: selection,
|
||||
});
|
||||
return dispatch(requests.newEntitlementEnrollment({ uuid, courseId: selection }));
|
||||
dispatch(requests.newEntitlementEnrollment({ uuid, courseId: selection }));
|
||||
dispatch(initialize());
|
||||
};
|
||||
|
||||
export const switchEntitlementEnrollment = (cardId, selection) => (dispatch, getState) => {
|
||||
@@ -48,7 +49,8 @@ export const switchEntitlementEnrollment = (cardId, selection) => (dispatch, get
|
||||
fromCourseRun: courseId,
|
||||
toCourseRun: selection,
|
||||
});
|
||||
return dispatch(requests.switchEntitlementEnrollment({ uuid, courseId: selection }));
|
||||
dispatch(requests.switchEntitlementEnrollment({ uuid, courseId: selection }));
|
||||
dispatch(initialize());
|
||||
};
|
||||
|
||||
export const leaveEntitlementSession = (cardId) => (dispatch, getState) => {
|
||||
@@ -58,7 +60,8 @@ export const leaveEntitlementSession = (cardId) => (dispatch, getState) => {
|
||||
fromCourseRun: courseId,
|
||||
toCourseRun: null,
|
||||
});
|
||||
return dispatch(requests.leaveEntitlementSession({ uuid }));
|
||||
dispatch(requests.leaveEntitlementSession({ uuid }));
|
||||
dispatch(initialize());
|
||||
};
|
||||
|
||||
export const unenrollFromCourse = (cardId, reason) => (dispatch, getState) => {
|
||||
|
||||
Reference in New Issue
Block a user