Skip to content

Commit 862d04a

Browse files
Muhammad Faraz  MaqsoodMuhammad Faraz  Maqsood
authored andcommitted
test: coverage
1 parent 604c11e commit 862d04a

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

src/CourseTeamManagement/CoursesTable.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,13 @@ export default function CoursesTable({
382382
{sortedAndFilteredData.length > 0 && <DataTable.TableFooter />}
383383
</DataTable>
384384
<div className="py-4 my-2 d-flex justify-content-end align-items-center">
385-
<Button onClick={() => setShowModal(true)} disabled={!isSaveBtnEnabled} variant="brand" style={{ width: '8%' }}>
385+
<Button
386+
onClick={() => setShowModal(true)}
387+
disabled={!isSaveBtnEnabled}
388+
variant="brand"
389+
style={{ width: '8%' }}
390+
data-testid="save-course-changes"
391+
>
386392
{intl.formatMessage(messages.saveButtonLabel)}
387393
</Button>
388394
</div>

src/CourseTeamManagement/CoursesTable.test.jsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { mount } from 'enzyme';
22
import { IntlProvider } from '@edx/frontend-platform/i18n';
3+
import { act } from 'react-dom/test-utils';
34
import CoursesTable from './CoursesTable';
5+
import * as api from './data/api';
46

57
export const intlProviderWrapper = (component) => (
68
<IntlProvider locale="en" messages={{}}>
@@ -359,4 +361,138 @@ describe('CoursesTable', () => {
359361
expect(wrapper.text()).toContain('No results found');
360362
});
361363
});
364+
365+
describe('CoursesTable save workflow', () => {
366+
const mockCourses = [
367+
{
368+
course_name: 'Test Course A',
369+
number: 'CS101',
370+
run: 'run1',
371+
status: 'active',
372+
role: 'staff',
373+
org: 'edx',
374+
course_url: 'https://example.com/course-a',
375+
},
376+
{
377+
course_name: 'Test Course B',
378+
number: 'CS102',
379+
run: 'run2',
380+
status: 'archived',
381+
role: 'instructor',
382+
org: 'mitx',
383+
course_url: 'https://example.com/course-b',
384+
},
385+
{
386+
course_name: 'Test Course c',
387+
number: 'CS103',
388+
run: 'run3',
389+
status: 'active',
390+
org: 'harvard',
391+
role: null,
392+
course_url: 'https://example.com/course-c',
393+
},
394+
{
395+
course_name: 'Test Course d',
396+
number: 'CS104',
397+
run: 'run4',
398+
status: 'active',
399+
org: 'harvard',
400+
role: null,
401+
course_url: 'https://example.com/course-d',
402+
},
403+
{
404+
course_name: 'Test Course e',
405+
number: 'CS105',
406+
run: 'run5',
407+
status: 'active',
408+
org: 'harvard',
409+
role: null,
410+
course_url: 'https://example.com/course-e',
411+
},
412+
{
413+
course_name: 'Test Course f',
414+
number: 'CS106',
415+
run: 'run6',
416+
status: 'active',
417+
org: 'harvard',
418+
role: null,
419+
course_url: 'https://example.com/course-f',
420+
},
421+
{
422+
course_name: 'Test Course g',
423+
number: 'CS107',
424+
run: 'run7',
425+
status: 'active',
426+
org: 'harvard',
427+
role: null,
428+
course_url: 'https://example.com/course-g',
429+
},
430+
];
431+
432+
const setCourseUpdateErrorsMock = jest.fn();
433+
const defaultProps = {
434+
userCourses: mockCourses,
435+
username: 'test',
436+
437+
setCourseUpdateErrors: setCourseUpdateErrorsMock,
438+
};
439+
440+
beforeEach(() => {
441+
jest.spyOn(api, 'updateUserRolesInCourses').mockResolvedValue([]); // mock API success
442+
});
443+
444+
afterEach(() => {
445+
jest.clearAllMocks();
446+
});
447+
448+
it('opens ChangeConfirmationModal and confirms save', async () => {
449+
wrapper = mount(intlProviderWrapper(<CoursesTable {...defaultProps} />));
450+
451+
// make changes in course table
452+
wrapper.find('input[type="checkbox"]').at(1).simulate('change');
453+
wrapper.find('input[type="checkbox"]').at(3).simulate('change');
454+
wrapper.find('input[type="checkbox"]').at(4).simulate('change');
455+
wrapper.find('input[type="checkbox"]').at(5).simulate('change');
456+
wrapper.find('input[type="checkbox"]').at(6).simulate('change');
457+
wrapper.find('input[type="checkbox"]').at(7).simulate('change');
458+
wrapper.find('[data-testid="role-dropdown-run2"]').at(0).simulate('click');
459+
wrapper.find('[data-testid="role-dropdown-item-staff-run2"]').at(0).simulate('click');
460+
461+
// open, then close, then re-open modal and also try show more course changes
462+
wrapper.find('[data-testid="save-course-changes"]').at(0).simulate('click');
463+
wrapper.find('[data-testid="cancel-save-course-changes"]').at(0).simulate('click');
464+
wrapper.find('[data-testid="save-course-changes"]').at(0).simulate('click');
465+
wrapper.find('[data-testid="show-more-changes"]').at(0).simulate('click');
466+
467+
// confirm save in modal
468+
wrapper.find('[data-testid="confirm-save-course-changes"]').at(0).simulate('click');
469+
470+
// wait for async useEffect
471+
jest.useFakeTimers();
472+
await act(async () => {
473+
await Promise.resolve();
474+
jest.runAllTimers(); // for the setTimeout in useEffect
475+
});
476+
477+
wrapper.update();
478+
479+
expect(api.updateUserRolesInCourses).toHaveBeenCalledWith({
480+
userEmail: defaultProps.email,
481+
changedCourses: expect.any(Object),
482+
});
483+
});
484+
it('adds beforeunload listener and prevents unload when there are unsaved changes', () => {
485+
wrapper = mount(intlProviderWrapper(<CoursesTable {...defaultProps} />));
486+
// make changes in course table and it will set hasUnsavedChangesRef.current to true
487+
wrapper.find('input[type="checkbox"]').at(1).simulate('change');
488+
const event = new Event('beforeunload');
489+
Object.defineProperty(event, 'returnValue', {
490+
writable: true,
491+
value: undefined,
492+
});
493+
window.dispatchEvent(event);
494+
495+
expect(event.returnValue).toBe('');
496+
});
497+
});
362498
});

src/CourseTeamManagement/changeConfirmationModal.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default function ChangeConfirmationModal({
4141
<Button
4242
variant="link"
4343
onClick={() => setExpanded(prev => ({ ...prev, [type]: true }))}
44+
data-testid="show-more-changes"
4445
>
4546
{
4647
intl.formatMessage(
@@ -131,6 +132,7 @@ export default function ChangeConfirmationModal({
131132
className="mr-3"
132133
variant="outline-primary"
133134
disabled={submitButtonState === 'saving'}
135+
data-testid="cancel-save-course-changes"
134136
onClick={() => {
135137
setExpanded({ added: false, removed: false, updated: false });
136138
onCancel();
@@ -144,6 +146,7 @@ export default function ChangeConfirmationModal({
144146
pending: 'Saving',
145147
complete: 'Saved',
146148
}}
149+
data-testid="confirm-save-course-changes"
147150
variant={submitButtonState === 'Error' ? 'danger' : 'brand'}
148151
icons={{
149152
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import MockAdapter from 'axios-mock-adapter';
2+
import { getConfig } from '@edx/frontend-platform';
3+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
4+
import { fetchUserRoleBasedCourses, updateUserRolesInCourses } from './api';
5+
import * as utils from '../utils';
6+
7+
describe('Manage Course Team API', () => {
8+
let mockAdapter;
9+
const { LMS_BASE_URL } = getConfig();
10+
const email = '[email protected]';
11+
12+
beforeEach(() => {
13+
mockAdapter = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: 'throwException' });
14+
});
15+
16+
afterEach(() => {
17+
mockAdapter.reset();
18+
});
19+
20+
describe('fetchUserRoleBasedCourses', () => {
21+
const apiUrl = `${LMS_BASE_URL}/api/support/v1/manage_course_team/?email=${encodeURIComponent(email)}`;
22+
23+
it('returns data on successful GET', async () => {
24+
const mockData = [
25+
{
26+
course_id: 'course-v1:Arbisoft+t1+t1',
27+
course_name: 'Introduction to Computing using Python (2024)',
28+
course_url: 'http://localhost:18010/course/course-v1:Arbisoft+t1+t1',
29+
role: 'instructor',
30+
status: 'archived',
31+
org: 'Arbisoft',
32+
run: 't1',
33+
number: 't1',
34+
},
35+
{
36+
course_id: 'course-v1:Arbisoft+t2+t2',
37+
course_name: 'HTML5 Coding Essentials and Best Practices',
38+
course_url: 'http://localhost:18010/course/course-v1:Arbisoft+t2+t2',
39+
role: 'staff',
40+
status: 'archived',
41+
org: 'Arbisoft',
42+
run: 't2',
43+
number: 't2',
44+
},
45+
{
46+
course_id: 'course-v1:Arbisoft+t3+t3',
47+
course_name: 'Behavioral Neuroscience: Advanced Insights from Mouse Models',
48+
course_url: 'http://localhost:18010/course/course-v1:Arbisoft+t3+t3',
49+
role: null,
50+
status: 'archived',
51+
org: 'Arbisoft',
52+
run: 't3',
53+
number: 't3',
54+
},
55+
];
56+
mockAdapter.onGet(apiUrl).reply(200, mockData);
57+
58+
const result = await fetchUserRoleBasedCourses(email);
59+
expect(result).toEqual(mockData);
60+
});
61+
62+
it('returns empty array on error', async () => {
63+
mockAdapter.onGet(apiUrl).reply(500);
64+
65+
const result = await fetchUserRoleBasedCourses(email);
66+
expect(result).toEqual([]);
67+
});
68+
});
69+
70+
describe('updateUserRolesInCourses', () => {
71+
const apiUrl = `${LMS_BASE_URL}/api/support/v1/manage_course_team/`;
72+
const changedCourses = {
73+
newlyCheckedWithRole: [],
74+
uncheckedWithRole: [],
75+
roleChangedRows: [
76+
{
77+
runId: 't2',
78+
from: 'staff',
79+
to: 'instructor',
80+
courseName: 'HTML5 Coding Essentials and Best Practices',
81+
number: 't2',
82+
courseId: 'course-v1:Arbisoft+t2+t2',
83+
},
84+
],
85+
};
86+
const coursesToUpdate = [
87+
{
88+
course_id: 'course-v1:Arbisoft+t2+t2',
89+
role: 'instructor',
90+
action: 'assign',
91+
},
92+
];
93+
const errors = {
94+
newlyCheckedWithRoleErrors: [],
95+
uncheckedWithRoleErrors: [],
96+
roleChangedRowsErrors: [],
97+
};
98+
const mockPutData = {
99+
100+
results: [
101+
{
102+
course_id: 'course-v1:Arbisoft+t2+t2',
103+
role: 'staff',
104+
action: 'assign',
105+
status: 'success',
106+
},
107+
],
108+
};
109+
110+
it('calls PUT and returns extracted errors on success', async () => {
111+
const expectedBulkOps = utils.getDataToUpdateForPutRequest(changedCourses);
112+
expect(expectedBulkOps).toEqual(coursesToUpdate);
113+
const expectedErrors = utils.extractErrorsFromUpdateResponse(mockPutData);
114+
expect(expectedErrors).toEqual(errors);
115+
116+
mockAdapter.onPut(apiUrl, {
117+
email,
118+
bulk_role_operations: expectedBulkOps,
119+
}).reply(200, mockPutData);
120+
121+
const result = await updateUserRolesInCourses({ userEmail: email, changedCourses });
122+
123+
expect(result).toEqual(expectedErrors);
124+
});
125+
126+
it('returns empty array on error', async () => {
127+
mockAdapter.onPut(apiUrl).reply(500);
128+
129+
const result = await updateUserRolesInCourses({ userEmail: email, changedCourses });
130+
expect(result).toEqual([]);
131+
});
132+
});
133+
});

0 commit comments

Comments
 (0)