Skip to content
Merged
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
112 changes: 112 additions & 0 deletions src/cohorts/CohortsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import { useCohorts, useCohortStatus, useToggleCohorts } from './data/apiHook';
import { renderWithAlertAndIntl } from '@src/testUtils';
import messages from './messages';
import { CohortProvider } from './components/CohortContext';
import * as AlertProvider from '@src/providers/AlertProvider';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ courseId: 'course-v1:edX+Test+2024' }),
}));

jest.mock('axios', () => ({
isAxiosError: (error: any) => error?.isAxiosError === true,
}));

jest.mock('./data/apiHook', () => ({
useCohorts: jest.fn(),
useCohortStatus: jest.fn(),
Expand Down Expand Up @@ -82,4 +87,111 @@ describe('CohortsPage', () => {
await user.click(screen.getByRole('button', { name: messages.disableLabel.defaultMessage }));
expect(disableMock).toHaveBeenCalled();
});

it('shows error modal when enable cohorts fails', async () => {
const showModalMock = jest.fn();
jest.spyOn(AlertProvider, 'useAlert').mockReturnValue({
alerts: [],
addAlert: jest.fn(),
removeAlert: jest.fn(),
clearAlerts: jest.fn(),
showToast: jest.fn(),
showModal: showModalMock,
showInlineAlert: jest.fn(),
dismissInlineAlert: jest.fn(),
inlineAlerts: [],
});

const enableMock = jest.fn();
(useCohorts as jest.Mock).mockReturnValue({ data: [] });
(useCohortStatus as jest.Mock).mockReturnValue({ data: { isCohorted: false } });
(useToggleCohorts as jest.Mock).mockReturnValue({ mutate: enableMock });

renderWithCohortsProvider();
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: messages.enableCohorts.defaultMessage }));

// Simulate error callback
const callArgs = enableMock.mock.calls[0][1];
callArgs.onError(new Error('Enable failed'));

expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: messages.enableCohortError.defaultMessage,
variant: 'danger',
});
});

it('shows error modal with API message when enable cohorts fails with axios error', async () => {
const showModalMock = jest.fn();
jest.spyOn(AlertProvider, 'useAlert').mockReturnValue({
alerts: [],
addAlert: jest.fn(),
removeAlert: jest.fn(),
clearAlerts: jest.fn(),
showToast: jest.fn(),
showModal: showModalMock,
showInlineAlert: jest.fn(),
dismissInlineAlert: jest.fn(),
inlineAlerts: [],
});

const enableMock = jest.fn();
(useCohorts as jest.Mock).mockReturnValue({ data: [] });
(useCohortStatus as jest.Mock).mockReturnValue({ data: { isCohorted: false } });
(useToggleCohorts as jest.Mock).mockReturnValue({ mutate: enableMock });

renderWithCohortsProvider();
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: messages.enableCohorts.defaultMessage }));

// Simulate axios error with developer_message
const axiosError = {
isAxiosError: true,
response: { data: { developer_message: 'API specific error' } },
};
const callArgs = enableMock.mock.calls[0][1];
callArgs.onError(axiosError);

expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: 'API specific error',
variant: 'danger',
});
});

it('shows error modal when disable cohorts fails', async () => {
const showModalMock = jest.fn();
jest.spyOn(AlertProvider, 'useAlert').mockReturnValue({
alerts: [],
addAlert: jest.fn(),
removeAlert: jest.fn(),
clearAlerts: jest.fn(),
showToast: jest.fn(),
showModal: showModalMock,
showInlineAlert: jest.fn(),
dismissInlineAlert: jest.fn(),
inlineAlerts: [],
});

const disableMock = jest.fn();
(useCohorts as jest.Mock).mockReturnValue({ data: [{ id: '1', name: 'Cohort 1' }] });
(useCohortStatus as jest.Mock).mockReturnValue({ data: { isCohorted: true } });
(useToggleCohorts as jest.Mock).mockReturnValue({ mutate: disableMock });

renderWithCohortsProvider();
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: messages.disableCohorts.defaultMessage }));
await user.click(screen.getByRole('button', { name: messages.disableLabel.defaultMessage }));

// Simulate error callback
const callArgs = disableMock.mock.calls[0][1];
callArgs.onError(new Error('Disable failed'));

expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: messages.disableCohortError.defaultMessage,
variant: 'danger',
});
});
});
25 changes: 21 additions & 4 deletions src/cohorts/CohortsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { isAxiosError } from 'axios';
import { useIntl } from '@openedx/frontend-base';
import { IconButton } from '@openedx/paragon';
import { Settings } from '@openedx/paragon/icons';
import { useParams } from 'react-router-dom';
import { useState } from 'react';
import { CohortProvider, useCohortContext } from '@src/cohorts/components/CohortContext';
import DisableCohortsModal from '@src/cohorts/components/DisableCohortsModal';
import DisabledCohortsView from '@src/cohorts/components/DisabledCohortsView';
import EnabledCohortsView from '@src/cohorts/components/EnabledCohortsView';
import { useCohortStatus, useToggleCohorts } from '@src/cohorts/data/apiHook';
import messages from '@src/cohorts/messages';
import { useAlert } from '@src/providers/AlertProvider';
import './CohortsPage.scss';

const CohortsPageContent = () => {
Expand All @@ -19,19 +21,34 @@ const CohortsPageContent = () => {
const [isOpenDisableModal, setIsOpenDisableModal] = useState(false);
const { clearSelectedCohort } = useCohortContext();
const { isCohorted = false } = cohortStatus ?? {};
const { showModal } = useAlert();

const handleEnableCohorts = () => {
toggleCohortsMutate({ isCohorted: true },
{
onError: (error) => console.log(error)
onError: (error) => {
const errorMessage = (isAxiosError(error) && error?.response?.data?.developer_message) || intl.formatMessage(messages.enableCohortError);
showModal({
confirmText: intl.formatMessage(messages.closeButton),
message: errorMessage,
variant: 'danger',
});
}
});
};

const handleDisableCohorts = () => {
toggleCohortsMutate({ isCohorted: false },
{
onSuccess: () => clearSelectedCohort(),
onError: (error) => console.log(error)
onError: (error) => {
const errorMessage = (isAxiosError(error) && error?.response?.data?.developer_message) || intl.formatMessage(messages.disableCohortError);
showModal({
confirmText: intl.formatMessage(messages.closeButton),
message: errorMessage,
variant: 'danger',
});
}
});
setIsOpenDisableModal(false);
};
Expand Down
64 changes: 64 additions & 0 deletions src/cohorts/components/CohortCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}));

jest.mock('axios', () => ({
isAxiosError: (error: any) => error?.isAxiosError === true,
}));

jest.mock('@src/cohorts/data/apiHook', () => ({
usePatchCohort: jest.fn(),
useAddLearnersToCohort: jest.fn(),
Expand Down Expand Up @@ -212,4 +216,64 @@ describe('CohortCard', () => {
expect(screen.getByText(/0 students/)).toBeInTheDocument();
});
});

describe('Error Handling', () => {
it('should show error modal when edit cohort fails with generic error', async () => {
const showModalMock = jest.fn();
mockUseAlert.mockReturnValue({ clearAlerts: mockClearAlerts, showModal: showModalMock });

const user = userEvent.setup();
renderCohortCard();

const settingsTab = screen.getByText(messages.settings.defaultMessage);
await user.click(settingsTab);
await waitFor(() => {
expect(screen.getByText(messages.cohortAssignmentMethod.defaultMessage)).toBeInTheDocument();
});

mockMutate.mockImplementation((_options: any, callbacks: { onError: (error: any) => void }) => {
callbacks.onError(new Error('Generic error'));
});

const submitBtn = screen.getByRole('button', { name: messages.saveLabel.defaultMessage });
await user.click(submitBtn);

expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: messages.editCohortError.defaultMessage,
variant: 'danger',
});
});

it('should show error modal with API message when edit cohort fails with axios error', async () => {
const showModalMock = jest.fn();
mockUseAlert.mockReturnValue({ clearAlerts: mockClearAlerts, showModal: showModalMock });

const user = userEvent.setup();
renderCohortCard();

const settingsTab = screen.getByText(messages.settings.defaultMessage);
await user.click(settingsTab);
await waitFor(() => {
expect(screen.getByText(messages.cohortAssignmentMethod.defaultMessage)).toBeInTheDocument();
});

const axiosError = {
isAxiosError: true,
response: { data: { developer_message: 'Cohort name already exists' } },
};
mockMutate.mockImplementation((_options: any, callbacks: { onError: (error: any) => void }) => {
callbacks.onError(axiosError);
});

const submitBtn = screen.getByRole('button', { name: messages.saveLabel.defaultMessage });
await user.click(submitBtn);

expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: 'Cohort name already exists',
variant: 'danger',
});
});
});
});
10 changes: 7 additions & 3 deletions src/cohorts/components/CohortCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useParams } from 'react-router-dom';
import { useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { isAxiosError } from 'axios';
import { FormattedMessage, getExternalLinkUrl, useIntl } from '@openedx/frontend-base';
import { Card, Hyperlink, Tab, Tabs, Toast } from '@openedx/paragon';
import messages from '@src/cohorts/messages';
Expand Down Expand Up @@ -41,9 +42,12 @@ const CohortCard = () => {
setShowSuccessMessage(true);
setSelectedCohort({ ...selectedCohort, ...updatedCohort });
},
onError: (error: Error) => {
onError: (error) => {
const errorMessage = (isAxiosError(error) && error?.response?.data?.developer_message) || intl.formatMessage(messages.editCohortError);
showModal({
message: error.message,
confirmText: intl.formatMessage(messages.closeButton),
message: errorMessage,
variant: 'danger',
});
}
}
Expand Down
21 changes: 18 additions & 3 deletions src/cohorts/components/EnabledCohortsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ describe('EnabledCohortsView', () => {
});

it('handles cohort creation error', async () => {
const showModalMock = jest.fn();
useAlertSpy.mockReturnValue({
alerts: [],
addAlert: addAlertMock,
removeAlert: removeAlertMock,
clearAlerts: clearAlertsMock,
showToast: jest.fn(),
showModal: showModalMock,
showInlineAlert: jest.fn(),
dismissInlineAlert: jest.fn(),
inlineAlerts: []
});

(useCohorts as jest.Mock).mockReturnValue({ data: [] });
renderWithCohortProvider();
const user = userEvent.setup();
Expand All @@ -169,11 +182,13 @@ describe('EnabledCohortsView', () => {

// Simulate error callback
const createArgs = createCohortMock.mock.calls[0][1];
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
createArgs.onError('Creation failed');

expect(consoleErrorSpy).toHaveBeenCalledWith('Creation failed');
consoleErrorSpy.mockRestore();
expect(showModalMock).toHaveBeenCalledWith({
confirmText: messages.closeButton.defaultMessage,
message: messages.enableCohortError.defaultMessage,
variant: 'danger',
});
});

it('handles successful cohort creation', async () => {
Expand Down
10 changes: 8 additions & 2 deletions src/cohorts/components/EnabledCohortsView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { isAxiosError } from 'axios';
import { useIntl } from '@openedx/frontend-base';
import { FormControl, Button, Card, Alert } from '@openedx/paragon';
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
Expand All @@ -25,7 +26,7 @@ const EnabledCohortsView = () => {
const { mutate: createCohort } = useCreateCohort(courseId);
const { clearSelectedCohort, selectedCohort, setSelectedCohort } = useCohortContext();
const [displayAddForm, setDisplayAddForm] = useState(false);
const { alerts, addAlert, removeAlert, clearAlerts } = useAlert();
const { alerts, addAlert, removeAlert, clearAlerts, showModal } = useAlert();

const cohortsList = [{ id: 'null', name: intl.formatMessage(messages.selectCohortPlaceholder) }, ...data];

Expand Down Expand Up @@ -89,7 +90,12 @@ const EnabledCohortsView = () => {
hideAddForm();
},
onError: (error) => {
console.error(error);
const errorMessage = (isAxiosError(error) && error?.response?.data?.developer_message) || intl.formatMessage(messages.enableCohortError);
showModal({
confirmText: intl.formatMessage(messages.closeButton),
message: errorMessage,
variant: 'danger',
});
}
});
};
Expand Down
20 changes: 20 additions & 0 deletions src/cohorts/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,26 @@ const messages = defineMessages({
defaultMessage: 'No file found in upload data. Please try again.',
description: 'Error message displayed when no file is found in the uploaded data'
},
closeButton: {
id: 'instruct.cohorts.closeButton',
defaultMessage: 'Close',
description: 'Label for the close button'
},
enableCohortError: {
id: 'instruct.cohorts.enableCohortError',
defaultMessage: 'An error occurred while enabling cohorts. Please try again later.',
description: 'Error message displayed when enabling cohorts fails'
},
disableCohortError: {
id: 'instruct.cohorts.disableCohortError',
defaultMessage: 'An error occurred while disabling cohorts. Please try again later.',
description: 'Error message displayed when disabling cohorts fails'
},
editCohortError: {
id: 'instruct.cohorts.editCohortError',
defaultMessage: 'An error occurred while saving your changes. Please try again later.',
description: 'Error message displayed when editing a cohort fails'
}
});

export default messages;
Loading