Skip to content

Commit 3262996

Browse files
Muhammad Faraz  MaqsoodMuhammad Faraz  Maqsood
authored andcommitted
test: coverage
1 parent 8f5050e commit 3262996

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed

src/CourseTeamManagement/CoursesTable.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export default function CoursesTable({
6060
changedCourses: unsavedChangesRef.current,
6161
intl,
6262
}).then((response) => {
63+
// APIs already tested in api.test.js
64+
/* istanbul ignore next */
6365
if (response?.error) {
6466
setShowModal(false);
6567
setCourseUpdateErrors({
@@ -420,6 +422,7 @@ export default function CoursesTable({
420422
disableFilters: true,
421423
},
422424
]}
425+
/* istanbul ignore next */
423426
getRowId={(row) => row.course_id}
424427
>
425428
<DataTable.TableControlBar />

src/CourseTeamManagement/CoursesTable.test.jsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,67 @@ describe('CoursesTable', () => {
381381

382382
expect(wrapper.text()).toContain('No results found');
383383
});
384+
385+
it('clears organization filter when pressing Enter on All Organizations option', () => {
386+
// First filter to mitx via click to set a non-empty org filter
387+
const orgDropdownToggle = wrapper.find('[data-testid="org-dropdown-toggle"]').at(0);
388+
orgDropdownToggle.simulate('click');
389+
wrapper.update();
390+
391+
wrapper.find('[data-testid="org-dropdown-option-mitx"]').simulate('click');
392+
wrapper.update();
393+
394+
// Confirm only Test Course B is shown
395+
let courseNames = wrapper.find('a').map((a) => a.text());
396+
expect(courseNames).not.toContain('Test Course A');
397+
expect(courseNames).toContain('Test Course B');
398+
399+
// Reopen and use keyboard Enter on All option
400+
wrapper.find('[data-testid="org-dropdown-toggle"]').at(0).simulate('click');
401+
wrapper.update();
402+
403+
const preventDefault = jest.fn();
404+
wrapper.find('[data-testid="org-dropdown-option-all"]').at(0)
405+
.simulate('keyDown', { key: 'Enter', preventDefault });
406+
wrapper.update();
407+
408+
// Confirm both courses are shown again and menu closed
409+
courseNames = wrapper.find('a').map((a) => a.text());
410+
expect(courseNames).toContain('Test Course A');
411+
expect(courseNames).toContain('Test Course B');
412+
expect(wrapper.find('[data-testid="org-dropdown-option-all"]').exists()).toBe(false);
413+
});
414+
415+
it('pressing Space on an org option closes and clears filter per current handler', () => {
416+
// Ensure no filter initially
417+
let courseNames = wrapper.find('a').map((a) => a.text());
418+
expect(courseNames).toContain('Test Course A');
419+
expect(courseNames).toContain('Test Course B');
420+
421+
// First apply mitx filter
422+
wrapper.find('[data-testid="org-dropdown-toggle"]').at(0).simulate('click');
423+
wrapper.update();
424+
wrapper.find('[data-testid="org-dropdown-option-mitx"]').simulate('click');
425+
wrapper.update();
426+
427+
courseNames = wrapper.find('a').map((a) => a.text());
428+
expect(courseNames).not.toContain('Test Course A');
429+
expect(courseNames).toContain('Test Course B');
430+
431+
// Reopen and Space key on mitx option
432+
wrapper.find('[data-testid="org-dropdown-toggle"]').at(0).simulate('click');
433+
wrapper.update();
434+
const preventDefault = jest.fn();
435+
wrapper.find('[data-testid="org-dropdown-option-mitx"]').at(0)
436+
.simulate('keyDown', { key: ' ', preventDefault });
437+
wrapper.update();
438+
439+
// Based on component logic this clears to '' and closes
440+
courseNames = wrapper.find('a').map((a) => a.text());
441+
expect(courseNames).toContain('Test Course A');
442+
expect(courseNames).toContain('Test Course B');
443+
expect(wrapper.find('[data-testid="org-dropdown-option-mitx"]').exists()).toBe(false);
444+
});
384445
});
385446

386447
describe('CoursesTable save workflow', () => {

src/users/UserPage.test.jsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
55
import { waitFor } from '@testing-library/react';
66
import UserMessagesProvider from '../userMessages/UserMessagesProvider';
77
import UserPage from './UserPage';
8+
import * as courseTeamApi from '../CourseTeamManagement/data/api';
89
import * as ssoAndUserApi from './data/api';
910
import UserSummaryData from './data/test/userSummary';
1011
import verifiedNameHistoryData from './data/test/verifiedNameHistory';
@@ -32,6 +33,16 @@ const UserPageWrapper = () => (
3233
</MemoryRouter>
3334
);
3435

36+
const UserPageWithProviders = (props) => (
37+
<MemoryRouter>
38+
<IntlProvider locale="en">
39+
<UserMessagesProvider>
40+
<UserPage {...props} />
41+
</UserMessagesProvider>
42+
</IntlProvider>
43+
</MemoryRouter>
44+
);
45+
3546
describe('User Page', () => {
3647
let wrapper;
3748
let mockedGetUserData;
@@ -62,4 +73,101 @@ describe('User Page', () => {
6273
expect(mockedGetUserData).toHaveBeenCalled();
6374
});
6475
});
76+
77+
describe('Course Team Management mode', () => {
78+
const sampleCourses = [
79+
{
80+
course_id: 'course-v1:edX+Test+1',
81+
course_name: 'CTM Test Course',
82+
course_url: 'https://example.com/course',
83+
role: 'staff',
84+
status: 'active',
85+
org: 'edx',
86+
number: 'Test',
87+
run: '1',
88+
},
89+
];
90+
91+
const baseProps = {
92+
isOnCourseTeamPage: true,
93+
courseUpdateErrors: {
94+
email: '',
95+
username: '',
96+
success: false,
97+
errors: {
98+
newlyCheckedWithRoleErrors: [],
99+
uncheckedWithRoleErrors: [],
100+
roleChangedRowsErrors: [],
101+
},
102+
},
103+
setCourseUpdateErrors: jest.fn(),
104+
showErrorsModal: false,
105+
apiErrors: false,
106+
setApiErrors: jest.fn(),
107+
isAlertDismissed: false,
108+
setIsAlertDismissed: jest.fn(),
109+
};
110+
111+
beforeEach(() => {
112+
jest.spyOn(courseTeamApi, 'fetchUserRoleBasedCourses').mockResolvedValue(sampleCourses);
113+
jest
114+
.spyOn(ssoAndUserApi, 'getAllUserData')
115+
.mockImplementation(() => Promise.resolve({ user: { id: 123, email: 'test@example.com', username: 'tester' }, errors: [] }));
116+
});
117+
118+
afterEach(() => {
119+
jest.clearAllMocks();
120+
});
121+
122+
it('loads user courses and renders CoursesTable when on course team page', async () => {
123+
wrapper = mount(<UserPageWithProviders {...baseProps} />);
124+
125+
// trigger a search to populate user data and then load courses
126+
wrapper.find("input[name='userIdentifier']").instance().value = 'tester';
127+
wrapper.find('.btn.btn-primary').simulate('click');
128+
129+
await waitFor(() => {
130+
expect(courseTeamApi.fetchUserRoleBasedCourses).toHaveBeenCalledWith('test@example.com', expect.any(Object));
131+
wrapper.update();
132+
expect(wrapper.find('.course-team-management-courses-table')).toHaveLength(1);
133+
});
134+
});
135+
136+
it('sets API errors when course loading fails', async () => {
137+
courseTeamApi.fetchUserRoleBasedCourses.mockResolvedValueOnce({ error: [{ text: 'err', type: 'error' }] });
138+
139+
const setApiErrors = jest.fn();
140+
wrapper = mount(<UserPageWithProviders {...baseProps} setApiErrors={setApiErrors} />);
141+
142+
wrapper.find("input[name='userIdentifier']").instance().value = 'tester';
143+
wrapper.find('.btn.btn-primary').simulate('click');
144+
145+
await waitFor(() => {
146+
expect(setApiErrors).toHaveBeenCalledWith({ error: expect.any(Array) });
147+
});
148+
});
149+
150+
it('reloads courses when courseUpdateErrors.success becomes true', async () => {
151+
wrapper = mount(<UserPageWithProviders {...baseProps} />);
152+
153+
wrapper.find("input[name='userIdentifier']").instance().value = 'tester';
154+
wrapper.find('.btn.btn-primary').simulate('click');
155+
156+
await waitFor(() => {
157+
expect(courseTeamApi.fetchUserRoleBasedCourses).toHaveBeenCalledTimes(1);
158+
});
159+
160+
// simulate success to trigger reload
161+
wrapper.setProps({
162+
courseUpdateErrors: {
163+
...baseProps.courseUpdateErrors,
164+
success: true,
165+
},
166+
});
167+
168+
await waitFor(() => {
169+
expect(courseTeamApi.fetchUserRoleBasedCourses).toHaveBeenCalledTimes(2);
170+
});
171+
});
172+
});
65173
});

src/users/UserSearch.test.jsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,84 @@ describe('User Search Page', () => {
5353
expect(courseTeamWrapper).toMatchSnapshot();
5454
});
5555
});
56+
57+
describe('course team page behaviors (unsaved changes modal)', () => {
58+
beforeEach(() => {
59+
sessionStorage.clear();
60+
});
61+
62+
it('opens modal instead of searching when unsaved changes exist', () => {
63+
const searchHandler = jest.fn();
64+
const username = 'tester';
65+
sessionStorage.setItem(`${username}hasUnsavedChanges`, 'true');
66+
67+
const ctmWrapper = mount(intlProviderWrapper(
68+
<UserSearch userIdentifier="foo" searchHandler={searchHandler} isOnCourseTeamPage username={username} />,
69+
));
70+
71+
ctmWrapper.find("input[name='userIdentifier']").instance().value = 'nextQuery';
72+
ctmWrapper.find('button').simulate('click');
73+
74+
expect(searchHandler).not.toHaveBeenCalled();
75+
const modal = ctmWrapper.find('CustomLeaveModalPopup');
76+
expect(modal.prop('isOpen')).toBe(true);
77+
});
78+
79+
it('confirming modal proceeds with search and clears unsaved changes flag', () => {
80+
const searchHandler = jest.fn();
81+
const username = 'tester2';
82+
sessionStorage.setItem(`${username}hasUnsavedChanges`, 'true');
83+
84+
const ctmWrapper = mount(intlProviderWrapper(
85+
<UserSearch userIdentifier="foo" searchHandler={searchHandler} isOnCourseTeamPage username={username} />,
86+
));
87+
88+
ctmWrapper.find("input[name='userIdentifier']").instance().value = 'confirmedQuery';
89+
ctmWrapper.find('button').simulate('click');
90+
91+
const modal = ctmWrapper.find('CustomLeaveModalPopup');
92+
modal.prop('onConfirm')();
93+
ctmWrapper.update();
94+
95+
expect(searchHandler).toHaveBeenCalledWith('confirmedQuery');
96+
expect(sessionStorage.getItem(`${username}hasUnsavedChanges`)).toBe('false');
97+
expect(ctmWrapper.find('CustomLeaveModalPopup').prop('isOpen')).toBe(false);
98+
});
99+
100+
it('canceling modal closes it without searching and keeps flag intact', () => {
101+
const searchHandler = jest.fn();
102+
const username = 'tester3';
103+
sessionStorage.setItem(`${username}hasUnsavedChanges`, 'true');
104+
105+
const ctmWrapper = mount(intlProviderWrapper(
106+
<UserSearch userIdentifier="foo" searchHandler={searchHandler} isOnCourseTeamPage username={username} />,
107+
));
108+
109+
ctmWrapper.find('button').simulate('click');
110+
111+
const modal = ctmWrapper.find('CustomLeaveModalPopup');
112+
modal.prop('onCancel')();
113+
ctmWrapper.update();
114+
115+
expect(searchHandler).not.toHaveBeenCalled();
116+
expect(ctmWrapper.find('CustomLeaveModalPopup').prop('isOpen')).toBe(false);
117+
expect(sessionStorage.getItem(`${username}hasUnsavedChanges`)).toBe('true');
118+
});
119+
120+
it('searches immediately when no unsaved changes exist', () => {
121+
const searchHandler = jest.fn();
122+
const username = 'tester4';
123+
sessionStorage.setItem(`${username}hasUnsavedChanges`, 'false');
124+
125+
const ctmWrapper = mount(intlProviderWrapper(
126+
<UserSearch userIdentifier="foo" searchHandler={searchHandler} isOnCourseTeamPage username={username} />,
127+
));
128+
129+
ctmWrapper.find("input[name='userIdentifier']").instance().value = 'directQuery';
130+
ctmWrapper.find('button').simulate('click');
131+
132+
expect(searchHandler).toHaveBeenCalledWith('directQuery');
133+
expect(ctmWrapper.find('CustomLeaveModalPopup').prop('isOpen')).toBe(false);
134+
});
135+
});
56136
});

0 commit comments

Comments
 (0)