Skip to content
Draft
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
89 changes: 46 additions & 43 deletions src/enrollments/EnrollmentsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EnrollmentsPage from './EnrollmentsPage';
import { EnrolledLearner } from './types';
import userEvent from '@testing-library/user-event';
import messages from './messages';
import { useEnrollmentByUserId, useEnrollments, useEnrollLearners, useUnenrollLearners } from './data/apiHook';
import { renderWithAlertAndIntl } from '@src/testUtils';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ courseId: 'test-course-id' }),
}));

jest.mock('./data/apiHook', () => ({
useEnrollments: jest.fn(),
useEnrollmentByUserId: jest.fn(),
useEnrollLearners: jest.fn(),
useUnenrollLearners: jest.fn(),
}));

jest.mock('./components/EnrollmentsList', () => {
return function MockEnrollmentsList({ onUnenroll }: { onUnenroll: (learner: EnrolledLearner) => void }) {
Expand All @@ -25,55 +37,46 @@ jest.mock('./components/EnrollmentsList', () => {
};
});

jest.mock('./components/EnrollmentStatusModal', () => {
return function MockEnrollmentStatusModal({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) {
return isOpen ? (
<div role="dialog">
<button onClick={onClose}>Close Modal</button>
</div>
) : null;
};
});

jest.mock('./components/UnenrollModal', () => {
return function MockUnenrollModal({ isOpen, learner, onClose }: { isOpen: boolean, learner: EnrolledLearner | null, onClose: () => void }) {
return isOpen ? (
<div role="dialog">
<span>Unenroll {learner?.fullName}</span>
<button onClick={onClose}>Close Unenroll Modal</button>
</div>
) : null;
};
});

const renderWithIntl = (component: React.ReactElement) => {
return render(
<IntlProvider locale="en">
{component}
</IntlProvider>
);
};

describe('EnrollmentsPage', () => {
beforeAll(() => {
(useEnrollments as jest.Mock).mockReturnValue({
data: { count: 1, numPages: 1, results: [{ username: 'testuser', fullName: 'Test User', email: 'test@example.com', mode: 'audit', isBetaTester: false }] },
isLoading: false,
});
(useEnrollmentByUserId as jest.Mock).mockReturnValue({
data: { enrollmentStatus: 'enrolled' },
refetch: jest.fn(),
});
(useEnrollLearners as jest.Mock).mockReturnValue({
mutate: jest.fn(),
isLoading: false,
error: null,
});
(useUnenrollLearners as jest.Mock).mockReturnValue({
mutate: jest.fn(),
isPending: false,
});
});

it('renders the page title', () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
});

it('renders action buttons', () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);
expect(screen.getByRole('button', { name: messages.checkEnrollmentStatus.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: new RegExp(messages.addBetaTesters.defaultMessage) })).toBeInTheDocument();
expect(screen.getByRole('button', { name: new RegExp(messages.enrollLearners.defaultMessage) })).toBeInTheDocument();
});

it('renders EnrollmentsList component', () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);
expect(screen.getByRole('table')).toBeInTheDocument();
});

it('opens enrollment status modal when more button is clicked', async () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);

const moreButton = screen.getByRole('button', { name: messages.checkEnrollmentStatus.defaultMessage });
const user = userEvent.setup();
Expand All @@ -83,20 +86,20 @@ describe('EnrollmentsPage', () => {
});

it('closes enrollment status modal', async () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);

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

const closeButton = screen.getByText('Close Modal');
const closeButton = screen.getByText('Close');
await user.click(closeButton);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('opens unenroll modal when unenroll is triggered', async () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);

const unenrollButton = screen.getByText('Unenroll Test Learner');
const user = userEvent.setup();
Expand All @@ -109,20 +112,20 @@ describe('EnrollmentsPage', () => {
});

it('closes unenroll modal and clears selected learner', async () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);

const unenrollButton = screen.getByText('Unenroll Test Learner');
const user = userEvent.setup();
await user.click(unenrollButton);

const closeUnenrollButton = screen.getByText('Close Unenroll Modal');
const closeUnenrollButton = screen.getByText('Cancel');
await user.click(closeUnenrollButton);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('modals are closed by default', () => {
renderWithIntl(<EnrollmentsPage />);
renderWithAlertAndIntl(<EnrollmentsPage />);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
Expand Down
20 changes: 17 additions & 3 deletions src/enrollments/EnrollmentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import messages from './messages';
import EnrollmentsList from './components/EnrollmentsList';
import EnrollmentStatusModal from './components/EnrollmentStatusModal';
import UnenrollModal from './components/UnenrollModal';
import EnrollLearnersModal from './components/EnrollLearnersModal';
import { EnrolledLearner } from './types';
import AddBetaTestersModal from './components/AddBetaTestersModal';

const EnrollmentsPage = () => {
const intl = useIntl();
const [isEnrollmentStatusModalOpen, setIsEnrollmentStatusModalOpen] = useState(false);
const [isEnrollLearnersModalOpen, setIsEnrollLearnersModalOpen] = useState(false);
const [isAddBetaTestersModalOpen, setIsAddBetaTestersModalOpen] = useState(false);
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
const [selectedLearner, setSelectedLearner] = useState<EnrolledLearner | null>(null);

Expand All @@ -32,6 +36,14 @@ const EnrollmentsPage = () => {
setIsEnrollmentStatusModalOpen(false);
};

const handleEnrollLearners = () => {
setIsEnrollLearnersModalOpen(true);
};

const handleAddBetaTesters = () => {
setIsAddBetaTestersModalOpen(true);
};

return (
<>
<div className="d-flex justify-content-between align-items-center">
Expand All @@ -43,13 +55,15 @@ const EnrollmentsPage = () => {
iconAs={MoreVert}
onClick={handleMoreButton}
/>
<Button variant="outline-primary">+ {intl.formatMessage(messages.addBetaTesters)}</Button>
<Button>+ {intl.formatMessage(messages.enrollLearners)}</Button>
<Button variant="outline-primary" onClick={handleAddBetaTesters}>+ {intl.formatMessage(messages.addBetaTesters)}</Button>
<Button onClick={handleEnrollLearners}>+ {intl.formatMessage(messages.enrollLearners)}</Button>
</ActionRow>
</div>
<EnrollmentsList onUnenroll={handleUnenroll} />
<EnrollmentStatusModal isOpen={isEnrollmentStatusModalOpen} onClose={handleCloseEnrollmentStatusModal} />
{selectedLearner && <UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} />}
{selectedLearner && <UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} onSuccess={() => {}} />}
<EnrollLearnersModal isOpen={isEnrollLearnersModalOpen} onClose={() => setIsEnrollLearnersModalOpen(false)} onSuccess={() => {}} />
<AddBetaTestersModal isOpen={isAddBetaTestersModalOpen} onClose={() => setIsAddBetaTestersModalOpen(false)} onSuccess={() => {}} />
</>
);
};
Expand Down
40 changes: 40 additions & 0 deletions src/enrollments/components/AddBetaTestersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'react-router-dom';
import { useAddBetaTesters } from '../data/apiHook';
import AddModal from './AddModal';
import { useIntl } from '@openedx/frontend-base';
import messages from '../messages';

export interface AddBetaTestersModalProps {
isOpen: boolean,
onClose: () => void,
onSuccess: () => void,
}

const AddBetaTestersModal = ({ isOpen, onClose, onSuccess }: AddBetaTestersModalProps) => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { mutate: addBetaTesters } = useAddBetaTesters(courseId);

const handleEnroll = (emailList: string[]) => {
addBetaTesters(emailList, {
onSuccess: () => {
onSuccess();
onClose();
},
onError: (error) => {
console.error(error);
}
});
};
return (
<AddModal
instructions={intl.formatMessage(messages.addBetaTestersInstructions)}
isOpen={isOpen}
title={intl.formatMessage(messages.addBetaTesters)}
onClose={onClose}
onSave={handleEnroll}
/>
);
};

export default AddBetaTestersModal;
Loading
Loading