diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 62fd74d6..eb3ff448 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -460,3 +460,151 @@ Object { }, } `; + +exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = ` +Object { + "courseHome": Object { + "courseId": "course-v1:edX+DemoX+Demo_Course_1", + "courseStatus": "loaded", + "toastBodyLink": null, + "toastBodyText": null, + "toastHeader": "", + }, + "courseware": Object { + "courseId": null, + "courseStatus": "loading", + "sequenceId": null, + "sequenceStatus": "loading", + }, + "models": Object { + "courseHomeMeta": Object { + "course-v1:edX+DemoX+Demo_Course_1": Object { + "canLoadCourseware": false, + "id": "course-v1:edX+DemoX+Demo_Course_1", + "isEnrolled": false, + "isSelfPaced": false, + "isStaff": false, + "number": "DemoX", + "org": "edX", + "originalUserIsStaff": false, + "tabs": Array [ + Object { + "slug": "outline", + "title": "Course", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/", + }, + Object { + "slug": "discussion", + "title": "Discussion", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/", + }, + Object { + "slug": "wiki", + "title": "Wiki", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki", + }, + Object { + "slug": "progress", + "title": "Progress", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress", + }, + Object { + "slug": "instructor", + "title": "Instructor", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor", + }, + Object { + "slug": "dates", + "title": "Dates", + "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates", + }, + ], + "title": "Demonstration Course", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, + }, + }, + "progress": Object { + "course-v1:edX+DemoX+Demo_Course_1": Object { + "certificateData": Object {}, + "completionSummary": Object { + "completeCount": 1, + "incompleteCount": 1, + "lockedCount": 0, + }, + "courseGrade": Object { + "isPassing": false, + "letterGrade": null, + "percent": 0, + }, + "courseId": "course-v1:edX+DemoX+Demo_Course_1", + "end": "3027-03-31T00:00:00Z", + "enrollmentMode": "audit", + "gradingPolicy": Object { + "assignmentPolicies": Array [ + Object { + "numDroppable": 1, + "shortLabel": "HW", + "type": "Homework", + "weight": 1, + }, + ], + "gradeRange": Object { + "pass": 0.75, + }, + }, + "hasScheduledContent": false, + "id": "course-v1:edX+DemoX+Demo_Course_1", + "sectionScores": Array [ + Object { + "displayName": "First section", + "subsections": Array [ + Object { + "assignmentType": "Homework", + "displayName": "First subsection", + "hasGradedAssignment": true, + "numPointsEarned": 0, + "numPointsPossible": 1, + "percentGraded": 0, + "showCorrectness": "always", + "showGrades": true, + "url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection", + }, + ], + }, + Object { + "displayName": "Second section", + "subsections": Array [ + Object { + "assignmentType": "Homework", + "displayName": "Second subsection", + "hasGradedAssignment": true, + "numPointsEarned": 1, + "numPointsPossible": 1, + "percentGraded": 1, + "showCorrectness": "always", + "showGrades": true, + "url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection", + }, + ], + }, + ], + "studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run", + "userHasPassingGrade": false, + "verificationData": Object { + "link": null, + "status": "none", + "statusDate": null, + }, + "verifiedMode": null, + }, + }, + }, + "recommendations": Object { + "recommendationsStatus": "loading", + }, +} +`; diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 96501be7..245343ee 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -118,8 +118,8 @@ export async function getDatesTabData(courseId) { const { httpErrorStatus } = error && error.customAttributes; if (httpErrorStatus === 404) { global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`); - return {}; } + // 401 can be returned for unauthenticated users or users who are not enrolled if (httpErrorStatus === 401) { global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`); } @@ -139,6 +139,10 @@ export async function getProgressTabData(courseId) { if (httpErrorStatus === 404) { global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`); } + // 401 can be returned for unauthenticated users or users who are not enrolled + if (httpErrorStatus === 401) { + global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`); + } throw error; } } diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index 1b9760c4..62bd0d6b 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -88,6 +88,35 @@ describe('Data layer integration tests', () => { }); }); + describe('Test fetchProgressTab', () => { + const progressBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress`; + + it('Should result in fetch failure if error occurs', async () => { + axiosMock.onGet(courseMetadataUrl).networkError(); + axiosMock.onGet(`${progressBaseUrl}/${courseId}`).networkError(); + + await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); + + expect(loggingService.logError).toHaveBeenCalled(); + expect(store.getState().courseHome.courseStatus).toEqual('failed'); + }); + + it('Should fetch, normalize, and save metadata', async () => { + const progressTabData = Factory.build('progressTabData', { courseId }); + + const progressUrl = `${progressBaseUrl}/${courseId}`; + + axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata); + axiosMock.onGet(progressUrl).reply(200, progressTabData); + + await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); + + const state = store.getState(); + expect(state.courseHome.courseStatus).toEqual('loaded'); + expect(state).toMatchSnapshot(); + }); + }); + describe('Test saveCourseGoal', () => { it('Should save course goal', async () => { const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`; diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx index c84b38fb..23555e29 100644 --- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx +++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx @@ -23,8 +23,11 @@ function CertificateStatus({ intl }) { const { certificateData, + end, hasScheduledContent, userHasPassingGrade, + verificationData, + verifiedMode, } = useModel('progress', courseId); const mode = getCourseExitMode( @@ -35,16 +38,15 @@ function CertificateStatus({ intl }) { ); const dispatch = useDispatch(); - const { - end, - verificationData, - certificateData: { - certStatus, - certWebViewUrl, - downloadUrl, - }, - verifiedMode, - } = useModel('progress', courseId); + let certStatus; + let certWebViewUrl; + let downloadUrl; + + if (certificateData) { + certStatus = certificateData.certStatus; + certWebViewUrl = certificateData.certWebViewUrl; + downloadUrl = certificateData.downloadUrl; + } let certCase; let body; @@ -66,7 +68,6 @@ function CertificateStatus({ intl }) { } else if (mode === COURSE_EXIT_MODES.celebration) { switch (certStatus) { case 'requesting': - // Requestable certCase = 'requestable'; buttonAction = () => { dispatch(requestCert(courseId)); }; body = intl.formatMessage(messages[`${certCase}Body`]); diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx index ddeb67e9..95f08482 100644 --- a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx +++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx @@ -22,8 +22,8 @@ function CompletionDonutChart({ intl }) { } = useModel('progress', courseId); const numTotalUnits = completeCount + incompleteCount + lockedCount; - const completePercentage = Number(((completeCount / numTotalUnits) * 100).toFixed(0)); - const lockedPercentage = Number(((lockedCount / numTotalUnits) * 100).toFixed(0)); + const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0; + const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0; const incompletePercentage = 100 - completePercentage - lockedPercentage; return ( diff --git a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx index 97a627e6..bdb79173 100644 --- a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx @@ -24,7 +24,7 @@ AssignmentTypeCell.propTypes = { AssignmentTypeCell.defaultProps = { footnoteId: '', - footnoteMarker: '', + footnoteMarker: null, }; export default AssignmentTypeCell; diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx index 4d407ffa..6f81f0d4 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -38,7 +38,7 @@ function GradeSummaryTable({ const gradeSummaryData = assignmentPolicies.map((assignment) => { let footnoteId = ''; - let footnoteMarker = ''; + let footnoteMarker; if (assignment.numDroppable > 0) { footnoteId = getFootnoteId(assignment); diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index c9fe09da..a559bdd9 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -188,22 +188,24 @@ function StreakModal({ StreakModal.defaultProps = { isStreakCelebrationOpen: false, + streakLengthToCelebrate: -1, + verifiedMode: {}, AA759ExperimentEnabled: false, }; StreakModal.propTypes = { courseId: PropTypes.string.isRequired, metadataModel: PropTypes.string.isRequired, - streakLengthToCelebrate: PropTypes.number.isRequired, + streakLengthToCelebrate: PropTypes.number, intl: intlShape.isRequired, isStreakCelebrationOpen: PropTypes.bool, closeStreakCelebration: PropTypes.func.isRequired, AA759ExperimentEnabled: PropTypes.bool, verifiedMode: PropTypes.shape({ - currencySymbol: PropTypes.string.isRequired, - price: PropTypes.number.isRequired, - upgradeUrl: PropTypes.string.isRequired, - }).isRequired, + currencySymbol: PropTypes.string, + price: PropTypes.number, + upgradeUrl: PropTypes.string, + }), }; export default injectIntl(StreakModal); diff --git a/src/tab-page/LoadedTabPage.jsx b/src/tab-page/LoadedTabPage.jsx index 8450fee5..5360b121 100644 --- a/src/tab-page/LoadedTabPage.jsx +++ b/src/tab-page/LoadedTabPage.jsx @@ -45,7 +45,7 @@ function LoadedTabPage({ return ( <> - {`${activeTab.title} | ${title} | ${getConfig().SITE_NAME}`} + {`${activeTab ? `${activeTab.title} | ` : ''}${title} | ${getConfig().SITE_NAME}`}