diff --git a/src/course-home/data/apiHooks.test.tsx b/src/course-home/data/apiHooks.test.tsx index 1ae3aa186b..1278d74fb9 100644 --- a/src/course-home/data/apiHooks.test.tsx +++ b/src/course-home/data/apiHooks.test.tsx @@ -6,7 +6,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '../../setupTest'; import { ToastProvider, useToast } from '../../generic/ToastContext'; -import { useResetDeadlines, usePostEvent } from './apiHooks'; +import { useResetDeadlines, usePostEvent, useRequestCert } from './apiHooks'; const { loggingService } = initializeMockApp(); @@ -116,4 +116,30 @@ describe('course-home apiHooks', () => { await waitFor(() => expect(loggingService.logError).toHaveBeenCalled()); }); }); + + describe('useRequestCert', () => { + const certUrl = `${getConfig().LMS_BASE_URL}/courses/course-1/generate_user_cert`; + + it('POSTs to the request-cert url', async () => { + axiosMock.onPost(certUrl).reply(200); + const { wrapper } = buildWrapper(); + const { result } = renderHook(() => useRequestCert(), { wrapper }); + + await act(async () => { await result.current.mutateAsync({ courseId: 'course-1' }); }); + + expect(axiosMock.history.post[0].url).toEqual(certUrl); + }); + + it('logs the error when the POST fails', async () => { + axiosMock.onPost(certUrl).reply(500); + const { wrapper } = buildWrapper(); + const { result } = renderHook(() => useRequestCert(), { wrapper }); + + await act(async () => { + await result.current.mutateAsync({ courseId: 'course-1' }).catch(() => {}); + }); + + await waitFor(() => expect(loggingService.logError).toHaveBeenCalled()); + }); + }); }); diff --git a/src/course-home/data/apiHooks.ts b/src/course-home/data/apiHooks.ts index 79bf434890..f5d4004ecd 100644 --- a/src/course-home/data/apiHooks.ts +++ b/src/course-home/data/apiHooks.ts @@ -3,7 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { useToast, ToastContent } from '@src/generic/ToastContext'; import { - executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, postCourseDeadlines, + executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, postCourseDeadlines, postRequestCert, } from './api'; import { courseHomeQueryKeys } from './queryKeys'; @@ -60,3 +60,8 @@ export const useDatesTabData = (courseId: string) => useQuery({ queryFn: () => getDatesTabData(courseId), meta: { modelType: 'dates', courseId }, }); + +export const useRequestCert = () => useMutation({ + mutationFn: ({ courseId }: { courseId: string }) => postRequestCert(courseId), + onError: (error) => logError(error), +}); diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 48acab1e02..300d2bac51 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -7,7 +7,6 @@ import { deprecatedPostCourseGoals, postWeeklyLearningGoal, postDismissWelcomeMessage, - postRequestCert, getLiveTabIframe, } from './api'; @@ -105,10 +104,6 @@ export function dismissWelcomeMessage(courseId) { return async () => postDismissWelcomeMessage(courseId); } -export function requestCert(courseId) { - return async () => postRequestCert(courseId); -} - export async function deprecatedSaveCourseGoal(courseId, goalKey) { return deprecatedPostCourseGoals(courseId, goalKey); } diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx index dd7114be7e..7e542ae105 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx @@ -6,7 +6,6 @@ import { useIntl, } from '@edx/frontend-platform/i18n'; import { Alert, Button } from '@openedx/paragon'; -import { useDispatch } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; @@ -15,7 +14,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import certMessages from './messages'; import certStatusMessages from '../../../progress-tab/certificate-status/messages'; -import { requestCert } from '../../../data/thunks'; +import { useRequestCert } from '../../../data/apiHooks'; export const CERT_STATUS_TYPE = { EARNED_NOT_AVAILABLE: 'earned_but_not_available', @@ -26,7 +25,7 @@ export const CERT_STATUS_TYPE = { const CertificateStatusAlert = ({ payload }) => { const intl = useIntl(); - const dispatch = useDispatch(); + const requestCert = useRequestCert(); const { certificateAvailableDate, certStatus, @@ -91,7 +90,7 @@ const CertificateStatusAlert = ({ payload }) => { alertProps.buttonLink = ''; alertProps.buttonAction = () => { sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked'); - dispatch(requestCert(courseId)); + requestCert.mutate({ courseId }); }; } return alertProps; diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index 7c1b2ce343..2abfc286f1 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -7,7 +7,7 @@ import { breakpoints } from '@openedx/paragon'; import MockAdapter from 'axios-mock-adapter'; import { - fireEvent, initializeMockApp, logUnhandledRequests, render, screen, act, + fireEvent, initializeMockApp, logUnhandledRequests, render, screen, act, waitFor, } from '../../setupTest'; import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; import * as thunks from '../data/thunks'; @@ -1020,6 +1020,10 @@ describe('Progress Tab', () => { is_staff: false, certificate_status_variant: 'requesting', }); + + await waitFor(() => expect( + axiosMock.history.post.some(req => req.url.includes('generate_user_cert')), + ).toBe(true)); }); it('Displays verify identity link', async () => { diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx index bc1cd92039..e7dd2c21f9 100644 --- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx +++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; @@ -10,7 +9,7 @@ import { useContextId } from '../../../data/hooks'; import { useModel } from '../../../generic/model-store'; import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils'; import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links'; -import { requestCert } from '../../data/thunks'; +import { useRequestCert } from '../../data/apiHooks'; import messages from './messages'; import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertificateStatusSlot'; @@ -62,7 +61,7 @@ const CertificateStatus = () => { courserun_key: courseId, }; - const dispatch = useDispatch(); + const requestCert = useRequestCert(); const { administrator } = getAuthenticatedUser(); let certStatus; @@ -110,7 +109,7 @@ const CertificateStatus = () => { switch (certStatus) { case 'requesting': certCase = 'requestable'; - buttonAction = () => { dispatch(requestCert(courseId)); }; + buttonAction = () => { requestCert.mutate({ courseId }); }; body = intl.formatMessage(messages[`${certCase}Body`]); buttonText = intl.formatMessage(messages[`${certCase}Button`]); break; diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx index e7c6449595..11f93cb9b1 100644 --- a/src/courseware/course/course-exit/CourseCelebration.jsx +++ b/src/courseware/course/course-exit/CourseCelebration.jsx @@ -4,7 +4,7 @@ import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons'; import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Helmet } from 'react-helmet'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Alert, breakpoints, @@ -23,7 +23,7 @@ import certificateLocked from '../../../generic/assets/openedx_locked_certificat import { FormattedPricing } from '../../../generic/upgrade-button'; import messages from './messages'; import { useModel } from '../../../generic/model-store'; -import { requestCert } from '../../../course-home/data/thunks'; +import { useRequestCert } from '../../../course-home/data/apiHooks'; import ProgramCompletion from './ProgramCompletion'; import UpgradeFootnote from './UpgradeFootnote'; import SocialIcons from '../../social-share/SocialIcons'; @@ -38,7 +38,7 @@ const CourseCelebration = () => { const intl = useIntl(); const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth; const { courseId } = useSelector(state => state.courseware); - const dispatch = useDispatch(); + const requestCert = useRequestCert(); const { certificateData, end, @@ -153,7 +153,7 @@ const CourseCelebration = () => { variant={buttonVariant} onClick={() => { logClick(org, courseId, administrator, buttonEvent); - dispatch(requestCert(courseId)); + requestCert.mutate({ courseId }); }} > {intl.formatMessage(messages.requestCertificateButton)} diff --git a/src/courseware/course/course-exit/CourseExit.test.jsx b/src/courseware/course/course-exit/CourseExit.test.jsx index d0a212da95..f8655fcf9d 100644 --- a/src/courseware/course/course-exit/CourseExit.test.jsx +++ b/src/courseware/course/course-exit/CourseExit.test.jsx @@ -4,6 +4,7 @@ import { Factory } from 'rosie'; import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { fetchCourse } from '../../data'; import { buildSimpleCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory'; @@ -139,6 +140,15 @@ describe('Course Exit Pages', () => { expect(screen.getByRole('button', { name: 'Request certificate' })).toBeInTheDocument(); }); + it('requests the certificate when the request certificate link is clicked', async () => { + setMetadata({ certificate_data: { cert_status: 'requesting' } }); + await fetchAndRender(); + await userEvent.click(screen.getByRole('button', { name: 'Request certificate' })); + await waitFor(() => expect( + axiosMock.history.post.some(req => req.url.includes('generate_user_cert')), + ).toBe(true)); + }); + it('Displays social share icons', async () => { setMetadata({ certificate_data: { cert_status: 'unverified' }, marketing_url: 'https://edx.org' }); await fetchAndRender();