Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion src/course-home/data/apiHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { initializeMockApp } from '../../setupTest';
import { ToastProvider, useToast } from '../../generic/ToastContext';
import { useResetDeadlines, usePostEvent, useRequestCert } from './apiHooks';
import {
useOutlineTabData, useResetDeadlines, usePostEvent, useRequestCert, useDismissWelcomeMessage,
useSaveWeeklyLearningGoal,
} from './apiHooks';

const { loggingService } = initializeMockApp();

Expand Down Expand Up @@ -142,4 +145,81 @@ describe('course-home apiHooks', () => {
await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
});
});

describe('useDismissWelcomeMessage', () => {
const dismissUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`;

it('POSTs to the dismiss url', async () => {
axiosMock.onPost(dismissUrl).reply(201);
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useDismissWelcomeMessage(), { wrapper });

await act(async () => { await result.current.mutateAsync({ courseId: 'course-1' }); });

expect(axiosMock.history.post[0].url).toEqual(dismissUrl);
});

it('logs the error when the POST fails', async () => {
axiosMock.onPost(dismissUrl).reply(500);
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useDismissWelcomeMessage(), { wrapper });

await act(async () => {
await result.current.mutateAsync({ courseId: 'course-1' }).catch(() => {});
});

await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
});
});

describe('useOutlineTabData', () => {
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/course-1`;

it('resolves to an empty object on a 403 (access is handled via the metadata request)', async () => {
axiosMock.onGet(outlineUrl).reply(403, {});
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useOutlineTabData('course-1'), { wrapper });

await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({});
});

it('surfaces the error on a non-403 failure', async () => {
axiosMock.onGet(outlineUrl).reply(500);
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useOutlineTabData('course-1'), { wrapper });

await waitFor(() => expect(result.current.isError).toBe(true));
});
});

describe('useSaveWeeklyLearningGoal', () => {
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;

it('POSTs the weekly learning goal', async () => {
axiosMock.onPost(goalUrl).reply(200, {});
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useSaveWeeklyLearningGoal(), { wrapper });

await act(async () => {
await result.current.mutateAsync({ courseId: 'course-1', daysPerWeek: 3, subscribedToReminders: true });
});

expect(axiosMock.history.post[0].url).toEqual(goalUrl);
});

it('logs the error when the POST fails', async () => {
axiosMock.onPost(goalUrl).reply(500);
const { wrapper } = buildWrapper();
const { result } = renderHook(() => useSaveWeeklyLearningGoal(), { wrapper });

await act(async () => {
await result.current.mutateAsync(
{ courseId: 'course-1', daysPerWeek: 3, subscribedToReminders: true },
).catch(() => {});
});

await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
});
});
});
27 changes: 26 additions & 1 deletion src/course-home/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { useMutation, useQuery } from '@tanstack/react-query';

import { useToast, ToastContent } from '@src/generic/ToastContext';
import {
executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, postCourseDeadlines, postRequestCert,
executePostFromPostEvent,
getCourseHomeCourseMetadata,
getDatesTabData,
getOutlineTabData,
postCourseDeadlines,
postDismissWelcomeMessage,
postRequestCert,
postWeeklyLearningGoal,
} from './api';
import { courseHomeQueryKeys } from './queryKeys';

Expand Down Expand Up @@ -61,7 +68,25 @@ export const useDatesTabData = (courseId: string) => useQuery({
meta: { modelType: 'dates', courseId },
});

export const useOutlineTabData = (courseId: string) => useQuery({
queryKey: courseHomeQueryKeys.outlineTab(courseId),
queryFn: () => getOutlineTabData(courseId),
meta: { modelType: 'outline', courseId },
});

export const useRequestCert = () => useMutation({
mutationFn: ({ courseId }: { courseId: string }) => postRequestCert(courseId),
onError: (error) => logError(error),
});

export const useDismissWelcomeMessage = () => useMutation({
mutationFn: ({ courseId }: { courseId: string }) => postDismissWelcomeMessage(courseId),
onError: (error) => logError(error),
});

export const useSaveWeeklyLearningGoal = () => useMutation({
mutationFn: ({ courseId, daysPerWeek, subscribedToReminders }: {
courseId: string; daysPerWeek: number; subscribedToReminders: boolean;
}) => postWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders),
onError: (error) => logError(error),
});
2 changes: 0 additions & 2 deletions src/course-home/data/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export {
fetchOutlineTab,
fetchProgressTab,
deprecatedSaveCourseGoal,
saveWeeklyLearningGoal,
} from './thunks';

export { reducer } from './slice';
1 change: 1 addition & 0 deletions src/course-home/data/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const courseHomeQueryKeys = {
all: [appId, 'courseHome'] as const,
metadata: (courseId: string) => [...courseHomeQueryKeys.all, 'metadata', courseId] as const,
datesTab: (courseId: string) => [...courseHomeQueryKeys.all, 'datesTab', courseId] as const,
outlineTab: (courseId: string) => [...courseHomeQueryKeys.all, 'outlineTab', courseId] as const,
};
51 changes: 0 additions & 51 deletions src/course-home/data/redux.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,45 +42,6 @@ describe('Data layer integration tests', () => {
store = initializeStore();
});

describe('Test fetchOutlineTab', () => {
const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`;
const outlineUrl = `${outlineBaseUrl}/${courseId}`;

it('Should result in fetch failure if error occurs', async () => {
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(outlineUrl).networkError();

await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);

expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});

it('Should fetch, normalize, and save metadata', async () => {
const outlineTabData = Factory.build('outlineTabData', { courseId });

axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);

await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);

const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
});

it.each([401, 403, 404])(
'should result in fetch denied if course access is denied, regardless of outline API status',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(outlineUrl).reply(errorStatus, {});

await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);

expect(store.getState().courseHome.courseStatus).toEqual('denied');
},
);
});

describe('Test fetchProgressTab', () => {
const progressBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/progress`;

Expand Down Expand Up @@ -148,18 +109,6 @@ describe('Data layer integration tests', () => {
});
});

describe('Test dismissWelcomeMessage', () => {
it('Should dismiss welcome message', async () => {
const dismissUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`;
axiosMock.onPost(dismissUrl).reply(201);

await executeThunk(thunks.dismissWelcomeMessage(courseId), store.dispatch);

expect(axiosMock.history.post[0].url).toEqual(dismissUrl);
expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}"}`);
});
});

describe('Test fetchExamAttemptsData', () => {
const sequenceIds = [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
Expand Down
15 changes: 0 additions & 15 deletions src/course-home/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { logError } from '@edx/frontend-platform/logging';
import {
getCourseHomeCourseMetadata,
getExamsData,
getOutlineTabData,
getProgressTabData,
deprecatedPostCourseGoals,
postWeeklyLearningGoal,
postDismissWelcomeMessage,
getLiveTabIframe,
} from './api';

Expand Down Expand Up @@ -88,10 +85,6 @@ export function fetchProgressTab(courseId, targetUserId) {
return fetchTab(courseId, 'progress', getProgressTabData, parseInt(targetUserId, 10) || targetUserId);
}

export function fetchOutlineTab(courseId) {
return fetchTab(courseId, 'outline', getOutlineTabData);
}

export function fetchLiveTab(courseId) {
return fetchTab(courseId, 'live', getLiveTabIframe);
}
Expand All @@ -100,18 +93,10 @@ export function fetchDiscussionTab(courseId) {
return fetchTab(courseId, 'discussion');
}

export function dismissWelcomeMessage(courseId) {
return async () => postDismissWelcomeMessage(courseId);
}

export async function deprecatedSaveCourseGoal(courseId, goalKey) {
return deprecatedPostCourseGoals(courseId, goalKey);
}

export async function saveWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders) {
return postWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders);
}

export function fetchExamAttemptsData(courseId, sequenceIds) {
return async (dispatch) => {
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
Expand Down
6 changes: 2 additions & 4 deletions src/course-home/outline-tab/DateSummary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { FormattedDate } from '@edx/frontend-platform/i18n';
import React from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useModel } from '../../generic/model-store';
import { isLearnerAssignment } from '../dates-tab/utils';
Expand All @@ -13,9 +13,7 @@ const DateSummary = ({
dateBlock,
userTimezone,
}) => {
const {
courseId,
} = useSelector(state => state.courseHome);
const { courseId } = useParams();
const {
org,
} = useModel('courseHomeMeta', courseId);
Expand Down
31 changes: 23 additions & 8 deletions src/course-home/outline-tab/OutlineTab.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
Expand All @@ -13,7 +13,7 @@ import CourseHandouts from './widgets/CourseHandouts';
import StartOrResumeCourseCard from './widgets/StartOrResumeCourseCard';
import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
import CourseTools from './widgets/CourseTools';
import { fetchOutlineTab } from '../data';
import { useCourseHomeMeta, useOutlineTabData } from '../data/apiHooks';
import messages from './messages';
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
Expand All @@ -27,13 +27,12 @@ import WelcomeMessage from './widgets/WelcomeMessage';
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
import CourseHomeSectionOutlineSlot from '../../plugin-slots/CourseHomeSectionOutlineSlot';
import { TabWithTimer } from '../../tab-page';

const OutlineTab = () => {
const OutlineTabContent = () => {
const intl = useIntl();
const {
courseId,
proctoringPanelStatus,
} = useSelector(state => state.courseHome);
const { courseId } = useParams();
const { proctoringPanelStatus } = useSelector(state => state.courseHome);

const {
isSelfPaced,
Expand Down Expand Up @@ -146,7 +145,7 @@ const OutlineTab = () => {
/>
{isSelfPaced && hasDeadlines && (
<>
<ShiftDatesAlert model="outline" fetch={fetchOutlineTab} />
<ShiftDatesAlert model="outline" />
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
</>
)}
Expand Down Expand Up @@ -191,4 +190,20 @@ const OutlineTab = () => {
);
};

const OutlineTab = () => {
const { courseId } = useParams();
const metadataQuery = useCourseHomeMeta(courseId);
const tabDataQuery = useOutlineTabData(courseId);

return (
<TabWithTimer
activeTabSlug="outline"
courseId={courseId}
courseStatus={{ metadataQuery, tabDataQuery }}
>
<OutlineTabContent />
</TabWithTimer>
);
};

export default OutlineTab;
Loading