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
119 changes: 119 additions & 0 deletions src/course-home/data/apiHooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

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

const { loggingService } = initializeMockApp();

const buildWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<ToastProvider>{children}</ToastProvider>
</QueryClientProvider>
);
return { wrapper };
};

describe('course-home apiHooks', () => {
let axiosMock: MockAdapter;

beforeEach(() => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
loggingService.logError.mockReset();
});

describe('useResetDeadlines', () => {
const resetUrl = `${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`;

it('POSTs and surfaces the server response as an open toast', async () => {
axiosMock.onPost(resetUrl).reply(201, {
header: 'test-toast-header', link: 'test-toast-link', link_text: 'test-toast-body',
});
const { wrapper } = buildWrapper();
const { result } = renderHook(() => ({ reset: useResetDeadlines(), toast: useToast() }), { wrapper });

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

expect(axiosMock.history.post[0].data).toEqual(
'{"course_key":"course-1","research_event_data":{"location":"dates-tab"}}',
);
expect(result.current.toast.toastContent).toEqual({
message: 'test-toast-header',
action: { label: 'test-toast-body', href: 'test-toast-link' },
});
expect(result.current.toast.isToastOpen).toBe(true);
});

it('omits the toast action when the response has no link text', async () => {
axiosMock.onPost(resetUrl).reply(200, { header: 'done', link: null, link_text: '' });
const { wrapper } = buildWrapper();
const { result } = renderHook(() => ({ reset: useResetDeadlines(), toast: useToast() }), { wrapper });

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

expect(result.current.toast.toastContent).toEqual({ message: 'done', action: undefined });
expect(result.current.toast.isToastOpen).toBe(true);
});

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

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

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

describe('usePostEvent', () => {
const postUrl = 'http://example.com/post-event';

it('POSTs to the event url and surfaces the response as an open toast', async () => {
axiosMock.onPost(postUrl).reply(200, {
header: 'post-header', link: 'post-link', link_text: 'post-body',
});
const { wrapper } = buildWrapper();
const { result } = renderHook(() => ({ post: usePostEvent(), toast: useToast() }), { wrapper });

await act(async () => {
await result.current.post.mutateAsync({
postData: { url: postUrl, bodyParams: { courseId: 'course-1' } },
researchEventData: { location: 'unit' },
});
});

expect(axiosMock.history.post[0].url).toEqual(postUrl);
expect(result.current.toast.toastContent).toEqual({
message: 'post-header',
action: { label: 'post-body', href: 'post-link' },
});
expect(result.current.toast.isToastOpen).toBe(true);
});

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

await act(async () => {
await result.current.mutateAsync({
postData: { url: postUrl, bodyParams: { courseId: 'course-1' } },
researchEventData: { location: 'unit' },
}).catch(() => {});
});

await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
});
});
});
47 changes: 47 additions & 0 deletions src/course-home/data/apiHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { logError } from '@edx/frontend-platform/logging';
import { useMutation } from '@tanstack/react-query';

import { useToast, ToastContent } from '@src/generic/ToastContext';
import { executePostFromPostEvent, postCourseDeadlines } from './api';

interface CallToActionResponse {
header: string;
link: string;
link_text: string;
}

interface PostData {
url: string;
bodyParams: { courseId: string };
}

const toastFrom = ({ header, link, link_text: linkText }: CallToActionResponse): ToastContent => ({
message: header,
action: linkText ? { label: linkText, href: link } : undefined,
});

export const useResetDeadlines = () => {
const { setToastContent, openToast } = useToast();
return useMutation({
mutationFn: ({ courseId, model }: { courseId: string; model: string }) => postCourseDeadlines(courseId, model),
onSuccess: ({ data }) => {
setToastContent(toastFrom(data));
openToast();
},
onError: (error) => logError(error),
});
};

export const usePostEvent = () => {
const { setToastContent, openToast } = useToast();
return useMutation({
mutationFn: ({ postData, researchEventData }: { postData: PostData; researchEventData: unknown }) => (
executePostFromPostEvent(postData, researchEventData)
),
onSuccess: ({ data }) => {
setToastContent(toastFrom(data));
openToast();
},
onError: (error) => logError(error),
});
};
1 change: 0 additions & 1 deletion src/course-home/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export {
fetchDatesTab,
fetchOutlineTab,
fetchProgressTab,
resetDeadlines,
deprecatedSaveCourseGoal,
saveWeeklyLearningGoal,
} from './thunks';
Expand Down
19 changes: 0 additions & 19 deletions src/course-home/data/redux.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,25 +237,6 @@ describe('Data layer integration tests', () => {
});
});

describe('Test resetDeadlines', () => {
it('Should reset course deadlines', async () => {
const resetUrl = `${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`;
const model = 'dates';
axiosMock.onPost(resetUrl).reply(201, {});

const getTabDataMock = jest.fn(() => ({
type: 'MOCK_ACTION',
}));

await executeThunk(thunks.resetDeadlines(courseId, model, getTabDataMock), store.dispatch);

expect(axiosMock.history.post[0].url).toEqual(resetUrl);
expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}","research_event_data":{"location":"dates-tab"}}`);

expect(getTabDataMock).toHaveBeenCalledWith(courseId);
});
});

describe('Test dismissWelcomeMessage', () => {
it('Should dismiss welcome message', async () => {
const dismissUrl = `${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`;
Expand Down
14 changes: 0 additions & 14 deletions src/course-home/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ const slice = createSlice({
courseStatus: 'loading',
courseId: null,
proctoringPanelStatus: 'loading',
toastBodyText: null,
toastBodyLink: null,
toastHeader: '',
examsData: null,
errorMessage: null,
errorCode: null,
Expand Down Expand Up @@ -46,16 +43,6 @@ const slice = createSlice({
state.targetUserId = payload.targetUserId;
state.courseStatus = LOADED;
},
setCallToActionToast: (state, { payload }) => {
const {
header,
link,
linkText,
} = payload;
state.toastBodyLink = link;
state.toastBodyText = linkText;
state.toastHeader = header;
},
setExamsData: (state, { payload }) => {
state.examsData = payload;
},
Expand All @@ -68,7 +55,6 @@ export const {
fetchTabFailure,
fetchTabRequest,
fetchTabSuccess,
setCallToActionToast,
setExamsData,
} = slice.actions;

Expand Down
16 changes: 0 additions & 16 deletions src/course-home/data/slice.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ describe('course home data slice', () => {
metadataModel: 'courseHomeCourseMetadata',
proctoringPanelStatus: 'loading',
tabFetchStates: {},
toastBodyText: '',
toastBodyLink: null,
toastHeader: '',
examsData: null,
};

Expand Down Expand Up @@ -47,9 +44,6 @@ describe('course home data slice', () => {
metadataModel: 'courseHomeCourseMetadata',
proctoringPanelStatus: 'loading',
tabFetchStates: {},
toastBodyText: '',
toastBodyLink: null,
toastHeader: '',
examsData: [{ id: 1, examName: 'Old Exam' }],
};

Expand All @@ -76,9 +70,6 @@ describe('course home data slice', () => {
metadataModel: 'courseHomeCourseMetadata',
proctoringPanelStatus: 'loading',
tabFetchStates: {},
toastBodyText: '',
toastBodyLink: null,
toastHeader: '',
examsData: [{ id: 1, examName: 'Some Exam' }],
};

Expand All @@ -95,9 +86,6 @@ describe('course home data slice', () => {
metadataModel: 'courseHomeCourseMetadata',
proctoringPanelStatus: 'loading',
tabFetchStates: {},
toastBodyText: '',
toastBodyLink: null,
toastHeader: '',
examsData: [{ id: 1, examName: 'Some Exam' }],
};

Expand All @@ -114,9 +102,6 @@ describe('course home data slice', () => {
metadataModel: 'courseHomeCourseMetadata',
proctoringPanelStatus: 'complete',
tabFetchStates: { progress: 'loaded' },
toastBodyText: 'Toast message',
toastBodyLink: 'http://example.com',
toastHeader: 'Toast Header',
examsData: null,
};

Expand All @@ -133,7 +118,6 @@ describe('course home data slice', () => {
// Verify other properties remain unchanged
expect(newState.courseStatus).toBe(initialState.courseStatus);
expect(newState.courseId).toBe(initialState.courseId);
expect(newState.toastBodyText).toBe(initialState.toastBodyText);
});
});
});
42 changes: 1 addition & 41 deletions src/course-home/data/thunks.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform';
import {
executePostFromPostEvent,
getCourseHomeCourseMetadata,
getDatesTabData,
getExamsData,
getOutlineTabData,
getProgressTabData,
postCourseDeadlines,
deprecatedPostCourseGoals,
postWeeklyLearningGoal,
postDismissWelcomeMessage,
Expand All @@ -24,11 +21,10 @@ import {
fetchTabFailure,
fetchTabRequest,
fetchTabSuccess,
setCallToActionToast,
setExamsData,
} from './slice';

const eventTypes = {
export const eventTypes = {
POST_EVENT: 'post_event',
};

Expand Down Expand Up @@ -118,21 +114,6 @@ export function requestCert(courseId) {
return async () => postRequestCert(courseId);
}

export function resetDeadlines(courseId, model, getTabData) {
return async (dispatch) => {
postCourseDeadlines(courseId, model).then(response => {
const { data } = response;
const {
header,
link,
link_text: linkText,
} = data;
dispatch(getTabData(courseId));
dispatch(setCallToActionToast({ header, link, linkText }));
});
};
}

export async function deprecatedSaveCourseGoal(courseId, goalKey) {
return deprecatedPostCourseGoals(courseId, goalKey);
}
Expand All @@ -141,27 +122,6 @@ export async function saveWeeklyLearningGoal(courseId, daysPerWeek, subscribedTo
return postWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders);
}

export function processEvent(eventData, getTabData) {
return async (dispatch) => {
// Pulling this out early so the data doesn't get camelCased and is easier
// to use when it's passed to the backend
const { research_event_data: researchEventData } = eventData;
const event = camelCaseObject(eventData);
if (event.eventName === eventTypes.POST_EVENT) {
executePostFromPostEvent(event.postData, researchEventData).then(response => {
const { data } = response;
const {
header,
link,
link_text: linkText,
} = data;
dispatch(getTabData(event.postData.bodyParams.courseId));
dispatch(setCallToActionToast({ header, link, linkText }));
});
}
};
}

export function fetchExamAttemptsData(courseId, sequenceIds) {
return async (dispatch) => {
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
Expand Down
Loading