Skip to content

Commit ecdc4df

Browse files
committed
test: added tests for cms lifecycle app
1 parent a19e685 commit ecdc4df

8 files changed

Lines changed: 926 additions & 0 deletions
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
fireEvent, initializeMocks, render, screen,
3+
} from '@src/testUtils';
4+
import { BlockCommentsPanel } from './BlockCommentsPanel';
5+
import { mockBlockReviewComment } from '../data/api.mock';
6+
7+
const mockUseBlockComments = jest.fn();
8+
const mockCreateMutate = jest.fn();
9+
const mockResolveMutate = jest.fn();
10+
const mockDeleteMutate = jest.fn();
11+
12+
jest.mock('@src/course-lifecycle/data/apiHooks', () => ({
13+
useBlockComments: (...args: any[]) => mockUseBlockComments(...args),
14+
useCreateComment: () => ({ mutate: mockCreateMutate, isPending: false }),
15+
useResolveComment: () => ({ mutate: mockResolveMutate, isPending: false }),
16+
useDeleteComment: () => ({ mutate: mockDeleteMutate, isPending: false }),
17+
}));
18+
19+
const usageKey = 'block-v1:TestOrg+TestCourse+2025_T1+type@vertical+block@unit1';
20+
21+
/** Expand the Collapsible panel — its body is not rendered until opened. */
22+
const expandPanel = () => {
23+
fireEvent.click(screen.getByRole('button', { name: 'Comments' }));
24+
};
25+
26+
describe('<BlockCommentsPanel />', () => {
27+
beforeEach(() => {
28+
initializeMocks();
29+
mockUseBlockComments.mockReturnValue({ data: [], isLoading: false });
30+
});
31+
32+
it('shows loading spinner while fetching comments', () => {
33+
mockUseBlockComments.mockReturnValue({ data: undefined, isLoading: true });
34+
const { container } = render(<BlockCommentsPanel usageKey={usageKey} />);
35+
expandPanel();
36+
expect(container.querySelector('.spinner-border')).toBeInTheDocument();
37+
});
38+
39+
it('shows "No comments yet." when comments list is empty', () => {
40+
render(<BlockCommentsPanel usageKey={usageKey} />);
41+
expandPanel();
42+
expect(screen.getByText('No comments yet.')).toBeInTheDocument();
43+
});
44+
45+
it('renders comment author and text', () => {
46+
const comment = mockBlockReviewComment({ author: 'reviewer', comment: 'Please fix this issue' });
47+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
48+
render(<BlockCommentsPanel usageKey={usageKey} />);
49+
expandPanel();
50+
expect(screen.getByText('reviewer')).toBeInTheDocument();
51+
expect(screen.getByText('Please fix this issue')).toBeInTheDocument();
52+
});
53+
54+
it('shows "Resolve" button for unresolved comment', () => {
55+
const comment = mockBlockReviewComment({ resolved: false });
56+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
57+
render(<BlockCommentsPanel usageKey={usageKey} />);
58+
expandPanel();
59+
expect(screen.getByRole('button', { name: 'Resolve' })).toBeInTheDocument();
60+
});
61+
62+
it('shows "Resolved" badge and no Resolve button for resolved comment', () => {
63+
const comment = mockBlockReviewComment({ resolved: true });
64+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
65+
render(<BlockCommentsPanel usageKey={usageKey} />);
66+
expandPanel();
67+
expect(screen.getByText('Resolved')).toBeInTheDocument();
68+
expect(screen.queryByRole('button', { name: 'Resolve' })).not.toBeInTheDocument();
69+
});
70+
71+
it('shows "Delete" button for comment authored by the current user (abc123)', () => {
72+
// Default initializeMocks user is { username: 'abc123' }
73+
const comment = mockBlockReviewComment({ author: 'abc123' });
74+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
75+
render(<BlockCommentsPanel usageKey={usageKey} />);
76+
expandPanel();
77+
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
78+
});
79+
80+
it('does not show "Delete" button for comment authored by another user', () => {
81+
const comment = mockBlockReviewComment({ author: 'some_other_user' });
82+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
83+
render(<BlockCommentsPanel usageKey={usageKey} />);
84+
expandPanel();
85+
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
86+
});
87+
88+
it('calls resolveMutation.mutate with comment id when Resolve is clicked', () => {
89+
const comment = mockBlockReviewComment({ id: 42, resolved: false });
90+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
91+
render(<BlockCommentsPanel usageKey={usageKey} />);
92+
expandPanel();
93+
fireEvent.click(screen.getByRole('button', { name: 'Resolve' }));
94+
expect(mockResolveMutate).toHaveBeenCalledWith(42);
95+
});
96+
97+
it('calls deleteMutation.mutate with comment id when Delete is clicked', () => {
98+
const comment = mockBlockReviewComment({ id: 42, author: 'abc123' });
99+
mockUseBlockComments.mockReturnValue({ data: [comment], isLoading: false });
100+
render(<BlockCommentsPanel usageKey={usageKey} />);
101+
expandPanel();
102+
fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
103+
expect(mockDeleteMutate).toHaveBeenCalledWith(42);
104+
});
105+
106+
it('calls createMutation.mutate with trimmed text when Add Comment is clicked', () => {
107+
render(<BlockCommentsPanel usageKey={usageKey} />);
108+
expandPanel();
109+
const textarea = screen.getByPlaceholderText('Add a comment...');
110+
fireEvent.change(textarea, { target: { value: ' Review this block ' } });
111+
fireEvent.click(screen.getByRole('button', { name: 'Add Comment' }));
112+
expect(mockCreateMutate).toHaveBeenCalledWith('Review this block', expect.any(Object));
113+
});
114+
115+
it('"Add Comment" button is disabled when textarea is empty', () => {
116+
render(<BlockCommentsPanel usageKey={usageKey} />);
117+
expandPanel();
118+
expect(screen.getByRole('button', { name: 'Add Comment' })).toBeDisabled();
119+
});
120+
121+
it('"Add Comment" button becomes enabled when textarea has content', () => {
122+
render(<BlockCommentsPanel usageKey={usageKey} />);
123+
expandPanel();
124+
const textarea = screen.getByPlaceholderText('Add a comment...');
125+
fireEvent.change(textarea, { target: { value: 'A new comment' } });
126+
expect(screen.getByRole('button', { name: 'Add Comment' })).not.toBeDisabled();
127+
});
128+
129+
it('hides the Add Comment form in readOnly mode', () => {
130+
render(<BlockCommentsPanel usageKey={usageKey} readOnly />);
131+
expandPanel();
132+
expect(screen.queryByPlaceholderText('Add a comment...')).not.toBeInTheDocument();
133+
expect(screen.queryByRole('button', { name: 'Add Comment' })).not.toBeInTheDocument();
134+
});
135+
136+
it('renders multiple comments in the panel body', () => {
137+
const comments = [
138+
mockBlockReviewComment({ id: 1, comment: 'First comment' }),
139+
mockBlockReviewComment({ id: 2, comment: 'Second comment' }),
140+
];
141+
mockUseBlockComments.mockReturnValue({ data: comments, isLoading: false });
142+
render(<BlockCommentsPanel usageKey={usageKey} />);
143+
expandPanel();
144+
expect(screen.getByText('First comment')).toBeInTheDocument();
145+
expect(screen.getByText('Second comment')).toBeInTheDocument();
146+
});
147+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
fireEvent, initializeMocks, render, screen, waitFor,
3+
} from '@src/testUtils';
4+
import { CourseCommentsPanel } from './CourseCommentsPanel';
5+
import { mockBlockReviewComment } from '../data/api.mock';
6+
7+
const mockUseCourseComments = jest.fn();
8+
const mockCreateCourseMutate = jest.fn();
9+
const mockResolveComment = jest.fn();
10+
const mockDeleteComment = jest.fn();
11+
12+
jest.mock('@src/course-lifecycle/data/apiHooks', () => ({
13+
useCourseComments: (...args: any[]) => mockUseCourseComments(...args),
14+
useCreateCourseComment: () => ({ mutate: mockCreateCourseMutate, isPending: false }),
15+
lifecycleQueryKeys: {
16+
courseComments: (id: string) => ['lifecycle', 'course', id, 'comments'],
17+
},
18+
}));
19+
20+
jest.mock('@src/course-lifecycle/data/api', () => ({
21+
resolveComment: (...args: any[]) => mockResolveComment(...args),
22+
deleteComment: (...args: any[]) => mockDeleteComment(...args),
23+
}));
24+
25+
const courseId = 'course-v1:TestOrg+TestCourse+2025_T1';
26+
27+
/** Expand the Collapsible panel — its body is not rendered until opened. */
28+
const expandPanel = () => {
29+
fireEvent.click(screen.getByRole('button', { name: 'Comments' }));
30+
};
31+
32+
describe('<CourseCommentsPanel />', () => {
33+
beforeEach(() => {
34+
initializeMocks();
35+
mockUseCourseComments.mockReturnValue({ data: [], isLoading: false });
36+
mockResolveComment.mockResolvedValue({ id: 1, resolved: true });
37+
mockDeleteComment.mockResolvedValue(undefined);
38+
});
39+
40+
it('shows loading spinner while fetching comments', () => {
41+
mockUseCourseComments.mockReturnValue({ data: undefined, isLoading: true });
42+
const { container } = render(<CourseCommentsPanel courseId={courseId} />);
43+
expandPanel();
44+
expect(container.querySelector('.spinner-border')).toBeInTheDocument();
45+
});
46+
47+
it('shows "No comments yet." when comments list is empty', () => {
48+
render(<CourseCommentsPanel courseId={courseId} />);
49+
expandPanel();
50+
expect(screen.getByText('No comments yet.')).toBeInTheDocument();
51+
});
52+
53+
it('renders comment author and text', () => {
54+
const comment = mockBlockReviewComment({ author: 'admin', comment: 'Course needs more examples' });
55+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
56+
render(<CourseCommentsPanel courseId={courseId} />);
57+
expandPanel();
58+
expect(screen.getByText('admin')).toBeInTheDocument();
59+
expect(screen.getByText('Course needs more examples')).toBeInTheDocument();
60+
});
61+
62+
it('shows "Resolve" button for unresolved comment', () => {
63+
const comment = mockBlockReviewComment({ resolved: false });
64+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
65+
render(<CourseCommentsPanel courseId={courseId} />);
66+
expandPanel();
67+
expect(screen.getByRole('button', { name: 'Resolve' })).toBeInTheDocument();
68+
});
69+
70+
it('shows "Resolved" badge and no Resolve button for resolved comment', () => {
71+
const comment = mockBlockReviewComment({ resolved: true });
72+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
73+
render(<CourseCommentsPanel courseId={courseId} />);
74+
expandPanel();
75+
expect(screen.getByText('Resolved')).toBeInTheDocument();
76+
expect(screen.queryByRole('button', { name: 'Resolve' })).not.toBeInTheDocument();
77+
});
78+
79+
it('shows "Delete" button for comment authored by the current user', () => {
80+
const comment = mockBlockReviewComment({ author: 'abc123' });
81+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
82+
render(<CourseCommentsPanel courseId={courseId} />);
83+
expandPanel();
84+
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
85+
});
86+
87+
it('does not show "Delete" button for comment authored by another user', () => {
88+
const comment = mockBlockReviewComment({ author: 'another_user' });
89+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
90+
render(<CourseCommentsPanel courseId={courseId} />);
91+
expandPanel();
92+
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
93+
});
94+
95+
it('calls resolveComment api when Resolve is clicked', async () => {
96+
const comment = mockBlockReviewComment({ id: 7, resolved: false });
97+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
98+
render(<CourseCommentsPanel courseId={courseId} />);
99+
expandPanel();
100+
fireEvent.click(screen.getByRole('button', { name: 'Resolve' }));
101+
await waitFor(() => expect(mockResolveComment).toHaveBeenCalledWith(7));
102+
});
103+
104+
it('calls deleteComment api when Delete is clicked', async () => {
105+
const comment = mockBlockReviewComment({ id: 7, author: 'abc123' });
106+
mockUseCourseComments.mockReturnValue({ data: [comment], isLoading: false });
107+
render(<CourseCommentsPanel courseId={courseId} />);
108+
expandPanel();
109+
fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
110+
await waitFor(() => expect(mockDeleteComment).toHaveBeenCalledWith(7));
111+
});
112+
113+
it('calls createMutation.mutate with trimmed text when Add Comment is clicked', () => {
114+
render(<CourseCommentsPanel courseId={courseId} />);
115+
expandPanel();
116+
const textarea = screen.getByPlaceholderText('Add a comment...');
117+
fireEvent.change(textarea, { target: { value: ' Course feedback ' } });
118+
fireEvent.click(screen.getByRole('button', { name: 'Add Comment' }));
119+
expect(mockCreateCourseMutate).toHaveBeenCalledWith('Course feedback', expect.any(Object));
120+
});
121+
122+
it('"Add Comment" button is disabled when textarea is empty', () => {
123+
render(<CourseCommentsPanel courseId={courseId} />);
124+
expandPanel();
125+
expect(screen.getByRole('button', { name: 'Add Comment' })).toBeDisabled();
126+
});
127+
128+
it('always shows the Add Comment form (no readOnly mode)', () => {
129+
render(<CourseCommentsPanel courseId={courseId} />);
130+
expandPanel();
131+
expect(screen.getByPlaceholderText('Add a comment...')).toBeInTheDocument();
132+
expect(screen.getByRole('button', { name: 'Add Comment' })).toBeInTheDocument();
133+
});
134+
});

0 commit comments

Comments
 (0)