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
77 changes: 77 additions & 0 deletions src/courseTeam/CourseTeamPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '@src/testUtils';
import CourseTeamPage from './CourseTeamPage';

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

jest.mock('./data/apiHook', () => ({
useAddTeamMember: () => ({ mutate: jest.fn() }),
}));

// Mock the child components, each component should have its own test suite
jest.mock('./components/MembersContent', () => {
return function MembersContent() {
return <div>Members Content</div>;
};
});

jest.mock('./components/RolesContent', () => {
return function RolesContent() {
return <div>Roles Content</div>;
};
});

jest.mock('./components/AddTeamMemberModal', () => {
return function AddTeamMemberModal() {
return <div>Add Team Member Modal</div>;
};
});

describe('CourseTeamPage', () => {
it('renders the course team title', () => {
renderWithIntl(<CourseTeamPage />);
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
});

it('renders the add team member button', () => {
renderWithIntl(<CourseTeamPage />);
expect(screen.getByRole('button', { name: /add team member/i })).toBeInTheDocument();
});

it('renders both tabs', () => {
renderWithIntl(<CourseTeamPage />);
expect(screen.getByRole('tab', { name: /members/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /roles/i })).toBeInTheDocument();
});

it('renders MembersContent by default', () => {
renderWithIntl(<CourseTeamPage />);
expect(screen.getByText('Members Content')).toBeInTheDocument();
});

it('has correct CSS classes on title', () => {
renderWithIntl(<CourseTeamPage />);
const title = screen.getByRole('heading', { level: 3 });
expect(title).toHaveClass('text-primary-700', 'mb-0');
});

it('shows the AddTeamMemberModal when add button is clicked', async () => {
renderWithIntl(<CourseTeamPage />);
const button = screen.getByRole('button', { name: /add team member/i });
const user = userEvent.setup();
await user.click(button);
expect(screen.getByText('Add Team Member Modal')).toBeInTheDocument();
});

it('renders RolesContent when Roles tab is selected', async () => {
renderWithIntl(<CourseTeamPage />);
const rolesTab = screen.getByRole('tab', { name: /roles/i });
const user = userEvent.setup();
await user.click(rolesTab);
expect(screen.getByText('Roles Content')).toBeInTheDocument();
});
});
48 changes: 45 additions & 3 deletions src/courseTeam/CourseTeamPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,50 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useIntl } from '@openedx/frontend-base';
import { Button, Tab, Tabs, useToggle } from '@openedx/paragon';
import messages from './messages';
import MembersContent from './components/MembersContent';
import RolesContent from './components/RolesContent';
import AddTeamMemberModal from './components/AddTeamMemberModal';
import EditTeamMemberModal from './components/EditTeamMemberModal';
import { useAddTeamMember } from './data/apiHook';
import { CourseTeamMember } from './types';

const CourseTeamPage = () => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const [isOpenAddModal, openAddModal, closeAddModal] = useToggle(false);
const [isOpenEditModal, openEditModal, closeEditModal] = useToggle(false);
const [selectedUser, setSelectedUser] = useState<CourseTeamMember | null>(null);
const { mutate: addTeamMember } = useAddTeamMember(courseId);

const handleAdd = ({ users, role }: { users: string[], role: string }) => {
addTeamMember({ users, role });
closeAddModal();
};

const handleEdit = (user: CourseTeamMember) => {
setSelectedUser(user);
openEditModal();
};

return (
<div>
<h3>Course Team</h3>
</div>
<>
<div className="d-flex justify-content-between align-items-center mb-3">
<h3 className="text-primary-700 mb-0">{intl.formatMessage(messages.courseTeamTitle)}</h3>
<Button variant="primary" onClick={openAddModal}>+ {intl.formatMessage(messages.addTeamMember)}</Button>
</div>
<Tabs>
<Tab eventKey="members" title={intl.formatMessage(messages.membersTab)}>
<MembersContent onEdit={handleEdit} />
</Tab>
<Tab eventKey="roles" title={intl.formatMessage(messages.rolesTab)}>
<RolesContent />
</Tab>
</Tabs>
{isOpenAddModal && <AddTeamMemberModal isOpen={isOpenAddModal} onClose={closeAddModal} onSave={handleAdd} />}
{isOpenEditModal && selectedUser && <EditTeamMemberModal isOpen={isOpenEditModal} user={selectedUser} onClose={closeEditModal} />}
</>
);
};

Expand Down
72 changes: 72 additions & 0 deletions src/courseTeam/components/AddTeamMemberModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '@src/testUtils';
import AddTeamMemberModal from './AddTeamMemberModal';
import messages from '../messages';
import { useRoles } from '../data/apiHook';

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

jest.mock('@src/data/apiHook', () => ({
useCourseInfo: () => ({ data: { displayName: 'Test Course' } }),
}));

jest.mock('../data/apiHook', () => ({
useRoles: jest.fn(),
}));

describe('AddTeamMemberModal', () => {
const defaultProps = {
isOpen: true,
onClose: jest.fn(),
onSave: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
(useRoles as jest.Mock).mockReturnValue({ data: [{ id: 'admin', name: 'Admin' }] });
});

it('renders modal with correct title and description', () => {
renderWithIntl(<AddTeamMemberModal {...defaultProps} />);
expect(screen.getByText(messages.addNewTeamMember.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.addNewTeamMemberDescription.defaultMessage.replace('{courseName}', 'Test Course'))).toBeInTheDocument();
});

it('renders users textarea and role select', () => {
renderWithIntl(<AddTeamMemberModal {...defaultProps} />);
expect(screen.getByLabelText(messages.addUsersLabel.defaultMessage)).toBeInTheDocument();
expect(screen.getByPlaceholderText(messages.usersPlaceholder.defaultMessage)).toBeInTheDocument();
expect(screen.getByLabelText(messages.roleLabel.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.rolePlaceholder.defaultMessage)).toBeInTheDocument();
});

it('calls onClose when Cancel button is clicked', async () => {
renderWithIntl(<AddTeamMemberModal {...defaultProps} />);
const user = userEvent.setup();
await user.click(screen.getByText(messages.cancelButton.defaultMessage));
expect(defaultProps.onClose).toHaveBeenCalled();
});

it('calls onSave when Save button is clicked', async () => {
renderWithIntl(<AddTeamMemberModal {...defaultProps} />);
const user = userEvent.setup();
await user.click(screen.getByText(messages.saveButton.defaultMessage));
expect(defaultProps.onSave).toHaveBeenCalledWith({ users: [''], role: '' });
});

it('does not render modal when isOpen is false', () => {
renderWithIntl(<AddTeamMemberModal {...defaultProps} isOpen={false} />);
expect(screen.queryByText(messages.addNewTeamMember.defaultMessage)).not.toBeInTheDocument();
});

it('disables role select when only placeholder role exists', () => {
(useRoles as jest.Mock).mockReturnValue({ data: [] });
renderWithIntl(<AddTeamMemberModal {...defaultProps} />);
expect(screen.getByLabelText(messages.roleLabel.defaultMessage)).toBeInTheDocument();
expect(screen.getByPlaceholderText(messages.rolePlaceholder.defaultMessage)).toBeDisabled();
});
});
64 changes: 64 additions & 0 deletions src/courseTeam/components/AddTeamMemberModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useParams } from 'react-router-dom';
import { useIntl } from '@openedx/frontend-base';
import { ActionRow, Button, Form, ModalDialog } from '@openedx/paragon';
import messages from '../messages';
import { useCourseInfo } from '@src/data/apiHook';
import { useRoles } from '../data/apiHook';

interface AddTeamMemberModalProps {
isOpen: boolean,
onClose: () => void,
onSave: ({ users, role }: { users: string[], role: string }) => void,
}

const AddTeamMemberModal = ({
isOpen,
onClose,
onSave,
}: AddTeamMemberModalProps) => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { data: { displayName } = { displayName: '' } } = useCourseInfo(courseId);
const { data } = useRoles(courseId);

const roles = [{ id: '', name: intl.formatMessage(messages.rolePlaceholder) }, ...(data || [])];

const handleSave = () => {
onSave({ users: [''], role: '' });
};

return (
<ModalDialog isOpen={isOpen} onClose={onClose} title={intl.formatMessage(messages.addNewTeamMember)} isOverflowVisible={false} size="lg">
<ModalDialog.Header>
<h3 className="text-primary-500">{intl.formatMessage(messages.addNewTeamMember)}</h3>
</ModalDialog.Header>
<ModalDialog.Body>
<p>{intl.formatMessage(messages.addNewTeamMemberDescription, { courseName: displayName })}</p>
<Form.Group>
<Form.Label>{intl.formatMessage(messages.addUsersLabel)}</Form.Label>
<Form.Control as="textarea" rows={3} placeholder={intl.formatMessage(messages.usersPlaceholder)} />
</Form.Group>
<Form.Group>
<Form.Label>{intl.formatMessage(messages.roleLabel)}</Form.Label>
<Form.Control as="select" defaultValue="" placeholder={intl.formatMessage(messages.rolePlaceholder)} disabled={roles.length === 1}>
{
roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))
}
</Form.Control>
</Form.Group>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<Button variant="tertiary" onClick={onClose}>{intl.formatMessage(messages.cancelButton)}</Button>
<Button variant="primary" onClick={handleSave}>{intl.formatMessage(messages.saveButton)}</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};

export default AddTeamMemberModal;
Loading
Loading