Skip to content

Commit a685422

Browse files
feat: increase test coverage
1 parent 3bdb178 commit a685422

5 files changed

Lines changed: 284 additions & 0 deletions

File tree

src/certificates/CertificatesPage.test.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,136 @@ describe('CertificatesPage', () => {
401401
expect(screen.queryByText(messages.grantExceptionsModalTitle.defaultMessage)).not.toBeInTheDocument();
402402
});
403403
});
404+
405+
it('handles CSV upload with success', async () => {
406+
const mockUploadCsv = jest.fn();
407+
mockUseUploadBulkExceptionsCsv.mockReturnValue({
408+
mutate: mockUploadCsv,
409+
isPending: false,
410+
} as unknown as ReturnType<typeof useUploadBulkExceptionsCsv>);
411+
412+
mockUploadCsv.mockImplementation((_data, options) => {
413+
if (options?.onSuccess) {
414+
options.onSuccess({ success: ['user1', 'user2'], errors: [] });
415+
}
416+
});
417+
418+
renderWithAlertAndIntl(<CertificatesPage />);
419+
const user = userEvent.setup();
420+
421+
const grantButton = screen.getByText(messages.grantExceptionsButton.defaultMessage);
422+
await user.click(grantButton);
423+
424+
await waitFor(() => {
425+
expect(screen.getByText(messages.grantExceptionsModalTitle.defaultMessage)).toBeInTheDocument();
426+
});
427+
428+
// Switch to Bulk tab
429+
const bulkTab = screen.getByRole('tab', { name: messages.bulkUploadTab.defaultMessage });
430+
await user.click(bulkTab);
431+
432+
// Create and upload a mock file
433+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
434+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
435+
436+
if (fileInput) {
437+
await user.upload(fileInput, csvFile);
438+
await screen.findByText(/test\.csv/i);
439+
440+
const saveButton = screen.getByText(messages.save.defaultMessage);
441+
await user.click(saveButton);
442+
443+
expect(mockUploadCsv).toHaveBeenCalled();
444+
}
445+
});
446+
447+
it('handles CSV upload with partial errors', async () => {
448+
const mockUploadCsv = jest.fn();
449+
mockUseUploadBulkExceptionsCsv.mockReturnValue({
450+
mutate: mockUploadCsv,
451+
isPending: false,
452+
} as unknown as ReturnType<typeof useUploadBulkExceptionsCsv>);
453+
454+
mockUploadCsv.mockImplementation((_data, options) => {
455+
if (options?.onSuccess) {
456+
options.onSuccess({
457+
success: ['user1'],
458+
errors: [{ learner: 'user2', message: 'User not found' }]
459+
});
460+
}
461+
});
462+
463+
renderWithAlertAndIntl(<CertificatesPage />);
464+
const user = userEvent.setup();
465+
466+
const grantButton = screen.getByText(messages.grantExceptionsButton.defaultMessage);
467+
await user.click(grantButton);
468+
469+
await waitFor(() => {
470+
expect(screen.getByText(messages.grantExceptionsModalTitle.defaultMessage)).toBeInTheDocument();
471+
});
472+
473+
// Switch to Bulk tab
474+
const bulkTab = screen.getByRole('tab', { name: messages.bulkUploadTab.defaultMessage });
475+
await user.click(bulkTab);
476+
477+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
478+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
479+
480+
if (fileInput) {
481+
await user.upload(fileInput, csvFile);
482+
await screen.findByText(/test\.csv/i);
483+
484+
const saveButton = screen.getByText(messages.save.defaultMessage);
485+
await user.click(saveButton);
486+
487+
// Wait for modal to close and error modal to appear
488+
await waitFor(() => {
489+
expect(screen.queryByText(messages.grantExceptionsModalTitle.defaultMessage)).not.toBeInTheDocument();
490+
});
491+
}
492+
});
493+
494+
it('handles CSV upload error', async () => {
495+
const mockUploadCsv = jest.fn();
496+
mockUseUploadBulkExceptionsCsv.mockReturnValue({
497+
mutate: mockUploadCsv,
498+
isPending: false,
499+
} as unknown as ReturnType<typeof useUploadBulkExceptionsCsv>);
500+
501+
mockUploadCsv.mockImplementation((_data, options) => {
502+
if (options?.onError) {
503+
options.onError({ response: { data: { message: 'CSV upload failed' } } });
504+
}
505+
});
506+
507+
renderWithAlertAndIntl(<CertificatesPage />);
508+
const user = userEvent.setup();
509+
510+
const grantButton = screen.getByText(messages.grantExceptionsButton.defaultMessage);
511+
await user.click(grantButton);
512+
513+
await waitFor(() => {
514+
expect(screen.getByText(messages.grantExceptionsModalTitle.defaultMessage)).toBeInTheDocument();
515+
});
516+
517+
// Switch to Bulk tab
518+
const bulkTab = screen.getByRole('tab', { name: messages.bulkUploadTab.defaultMessage });
519+
await user.click(bulkTab);
520+
521+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
522+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
523+
524+
if (fileInput) {
525+
await user.upload(fileInput, csvFile);
526+
await screen.findByText(/test\.csv/i);
527+
528+
const saveButton = screen.getByText(messages.save.defaultMessage);
529+
await user.click(saveButton);
530+
531+
expect(mockUploadCsv).toHaveBeenCalled();
532+
}
533+
});
404534
});
405535

406536
describe('Invalidate Certificate', () => {

src/certificates/components/DisableCertificatesModal.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,15 @@ describe('DisableCertificatesModal', () => {
128128
const closeButtons = screen.queryAllByLabelText('Close');
129129
expect(closeButtons.length).toBeGreaterThan(0);
130130
});
131+
132+
it('calls onClose when save button is clicked without changes', async () => {
133+
renderWithIntl(<DisableCertificatesModal {...defaultProps} />);
134+
const user = userEvent.setup();
135+
136+
const saveButton = screen.getByRole('button', { name: messages.save.defaultMessage });
137+
await user.click(saveButton);
138+
139+
expect(mockOnClose).toHaveBeenCalled();
140+
expect(mockOnConfirm).not.toHaveBeenCalled();
141+
});
131142
});

src/certificates/components/GrantExceptionsModal.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,70 @@ describe('GrantExceptionsModal', () => {
113113
expect(screen.getByText(messages.csvFileLabel.defaultMessage)).toBeInTheDocument();
114114
expect(screen.getByText(messages.csvInstructions.defaultMessage)).toBeInTheDocument();
115115
});
116+
117+
it('clears form and closes modal when cancel is clicked', async () => {
118+
renderWithIntl(<GrantExceptionsModal {...defaultProps} />);
119+
const user = userEvent.setup();
120+
121+
const learnerInput = screen.getByPlaceholderText(messages.studentEmailUsername.defaultMessage);
122+
const notesInput = screen.getByPlaceholderText(messages.notesOptional.defaultMessage);
123+
124+
await user.type(learnerInput, 'user@example.com');
125+
await user.type(notesInput, 'Test notes');
126+
127+
const cancelButton = screen.getByRole('button', { name: messages.cancel.defaultMessage });
128+
await user.click(cancelButton);
129+
130+
expect(mockOnClose).toHaveBeenCalled();
131+
});
132+
133+
it('trims whitespace from learner input before submit', async () => {
134+
renderWithIntl(<GrantExceptionsModal {...defaultProps} />);
135+
const user = userEvent.setup();
136+
137+
const learnerInput = screen.getByPlaceholderText(messages.studentEmailUsername.defaultMessage);
138+
await user.type(learnerInput, ' user@example.com ');
139+
140+
const saveButton = screen.getByRole('button', { name: messages.save.defaultMessage });
141+
await user.click(saveButton);
142+
143+
expect(mockOnSubmit).toHaveBeenCalledWith(['user@example.com'], '');
144+
});
145+
146+
it('does not call onUploadCsv when no file is selected', async () => {
147+
renderWithIntl(<GrantExceptionsModal {...defaultProps} />);
148+
const user = userEvent.setup();
149+
150+
const bulkTab = screen.getByRole('tab', { name: messages.bulkUploadTab.defaultMessage });
151+
await user.click(bulkTab);
152+
153+
const saveButton = screen.getByRole('button', { name: messages.save.defaultMessage });
154+
expect(saveButton).toBeDisabled();
155+
});
156+
157+
it('calls onUploadCsv when CSV file is uploaded and submitted', async () => {
158+
renderWithIntl(<GrantExceptionsModal {...defaultProps} />);
159+
const user = userEvent.setup();
160+
161+
const bulkTab = screen.getByRole('tab', { name: messages.bulkUploadTab.defaultMessage });
162+
await user.click(bulkTab);
163+
164+
// Create a mock file
165+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
166+
167+
// Find the file input
168+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
169+
170+
if (fileInput) {
171+
await user.upload(fileInput, csvFile);
172+
173+
// Wait for file to be processed
174+
await screen.findByText(/test\.csv/i);
175+
176+
const saveButton = screen.getByRole('button', { name: messages.save.defaultMessage });
177+
await user.click(saveButton);
178+
179+
expect(mockOnUploadCsv).toHaveBeenCalledWith(csvFile);
180+
}
181+
});
116182
});

src/certificates/data/api.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getIssuedCertificates,
55
getInstructorTasks,
66
grantBulkExceptions,
7+
uploadBulkExceptionsCsv,
78
invalidateCertificate,
89
removeException,
910
removeInvalidation,
@@ -191,6 +192,36 @@ describe('Certificate API', () => {
191192
});
192193
});
193194

195+
describe('uploadBulkExceptionsCsv', () => {
196+
it('uploads CSV file for bulk exceptions', async () => {
197+
mockPost.mockResolvedValue({ data: { success: ['user1', 'user2'], errors: [] } });
198+
199+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
200+
201+
await uploadBulkExceptionsCsv('course-v1:edX+Test+2024', csvFile);
202+
203+
expect(mockPost).toHaveBeenCalledWith(
204+
'http://localhost:18000/api/instructor/v2/courses/course-v1:edX+Test+2024/certificates/exceptions/bulk',
205+
expect.any(FormData),
206+
{
207+
headers: {
208+
'Content-Type': 'multipart/form-data',
209+
},
210+
}
211+
);
212+
});
213+
214+
it('handles errors when uploading CSV', async () => {
215+
mockPost.mockRejectedValue(new Error('Invalid CSV format'));
216+
217+
const csvFile = new File(['invalid'], 'test.csv', { type: 'text/csv' });
218+
219+
await expect(
220+
uploadBulkExceptionsCsv('course-v1:edX+Test+2024', csvFile)
221+
).rejects.toThrow('Invalid CSV format');
222+
});
223+
});
224+
194225
describe('invalidateCertificate', () => {
195226
it('invalidates certificates for learners', async () => {
196227
mockPost.mockResolvedValue({ data: { success: ['user1', 'user2'], errors: [] } });

src/certificates/data/apiHook.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useIssuedCertificates,
66
useInstructorTasks,
77
useGrantBulkExceptions,
8+
useUploadBulkExceptionsCsv,
89
useInvalidateCertificate,
910
useRemoveException,
1011
useRemoveInvalidation,
@@ -16,6 +17,7 @@ import {
1617
getIssuedCertificates,
1718
getInstructorTasks,
1819
grantBulkExceptions,
20+
uploadBulkExceptionsCsv,
1921
invalidateCertificate,
2022
removeException,
2123
removeInvalidation,
@@ -30,6 +32,7 @@ jest.mock('@src/certificates/data/api');
3032
const mockGetIssuedCertificates = getIssuedCertificates as jest.MockedFunction<typeof getIssuedCertificates>;
3133
const mockGetInstructorTasks = getInstructorTasks as jest.MockedFunction<typeof getInstructorTasks>;
3234
const mockGrantBulkExceptions = grantBulkExceptions as jest.MockedFunction<typeof grantBulkExceptions>;
35+
const mockUploadBulkExceptionsCsv = uploadBulkExceptionsCsv as jest.MockedFunction<typeof uploadBulkExceptionsCsv>;
3336
const mockInvalidateCertificate = invalidateCertificate as jest.MockedFunction<typeof invalidateCertificate>;
3437
const mockRemoveException = removeException as jest.MockedFunction<typeof removeException>;
3538
const mockRemoveInvalidation = removeInvalidation as jest.MockedFunction<typeof removeInvalidation>;
@@ -270,6 +273,49 @@ describe('certificates api hooks', () => {
270273
});
271274
});
272275

276+
describe('useUploadBulkExceptionsCsv', () => {
277+
it('uploads CSV file successfully', async () => {
278+
mockUploadBulkExceptionsCsv.mockResolvedValue({ success: ['user1', 'user2'], errors: [] });
279+
280+
const { Wrapper, queryClient: qc } = createWrapper();
281+
queryClient = qc;
282+
283+
const { result } = renderHook(() => useUploadBulkExceptionsCsv('course-v1:Test+Course+2024'), {
284+
wrapper: Wrapper,
285+
});
286+
287+
const csvFile = new File(['username,notes\nuser1,note1'], 'test.csv', { type: 'text/csv' });
288+
result.current.mutate(csvFile);
289+
290+
await waitFor(() => {
291+
expect(result.current.isSuccess).toBe(true);
292+
});
293+
294+
expect(mockUploadBulkExceptionsCsv).toHaveBeenCalledWith('course-v1:Test+Course+2024', csvFile);
295+
});
296+
297+
it('handles error when uploading CSV', async () => {
298+
const mockError = new Error('Invalid CSV format');
299+
mockUploadBulkExceptionsCsv.mockRejectedValue(mockError);
300+
301+
const { Wrapper, queryClient: qc } = createWrapper();
302+
queryClient = qc;
303+
304+
const { result } = renderHook(() => useUploadBulkExceptionsCsv('course-v1:Test+Course+2024'), {
305+
wrapper: Wrapper,
306+
});
307+
308+
const csvFile = new File(['invalid'], 'test.csv', { type: 'text/csv' });
309+
result.current.mutate(csvFile);
310+
311+
await waitFor(() => {
312+
expect(result.current.isError).toBe(true);
313+
});
314+
315+
expect(result.current.error).toBe(mockError);
316+
});
317+
});
318+
273319
describe('useInvalidateCertificate', () => {
274320
it('invalidates certificate successfully', async () => {
275321
mockInvalidateCertificate.mockResolvedValue({ success: ['user1'], errors: [] });

0 commit comments

Comments
 (0)