diff --git a/src/containers/course-section/resource-item/ResourceItem.tsx b/src/containers/course-section/resource-item/ResourceItem.tsx index c7f811d50..68a86d779 100644 --- a/src/containers/course-section/resource-item/ResourceItem.tsx +++ b/src/containers/course-section/resource-item/ResourceItem.tsx @@ -35,6 +35,7 @@ import { } from '~/types' import { getFormattedDate } from '~/utils/helper-functions' import TitleWithDescription from '~/components/title-with-description/TitleWithDescription' +import { downloadFile } from '~/utils/download-file' interface ResourceItemProps { resource: CourseResource @@ -210,7 +211,10 @@ const ResourceItem: FC = ({ if (type === ResourceType.Attachment) { const fileName = (resource as Attachment).fileName - void ResourceService.downloadAttachment(resource._id, fileName) + void downloadFile( + ResourceService.downloadAttachment(resource._id), + fileName + ) return } diff --git a/src/services/resource-service.ts b/src/services/resource-service.ts index dfe959a69..92afc5ecd 100644 --- a/src/services/resource-service.ts +++ b/src/services/resource-service.ts @@ -76,21 +76,11 @@ export const ResourceService = { headers: { 'Content-Type': 'multipart/form-data' } }) }, - downloadAttachment: async (id: string, fileName: string): Promise => { - const response = await axiosClient.get( + downloadAttachment: async (id: string) => { + return axiosClient.get( createUrlPath(URLs.resources.attachments.download, id), { responseType: 'blob' } ) - - const url = window.URL.createObjectURL(new Blob([response.data])) - const link = document.createElement('a') - link.href = url - link.setAttribute('download', fileName) - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - - setTimeout(() => window.URL.revokeObjectURL(url), 100) }, getQuestions: ( params?: GetResourcesParams diff --git a/src/utils/download-file.ts b/src/utils/download-file.ts new file mode 100644 index 000000000..770c37e34 --- /dev/null +++ b/src/utils/download-file.ts @@ -0,0 +1,17 @@ +import { AxiosResponse } from 'axios' + +export async function downloadFile( + response: Promise, + fileName: string +) { + const blobResponse = await response + const url = window.URL.createObjectURL(new Blob([blobResponse.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', fileName) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + setTimeout(() => window.URL.revokeObjectURL(url), 100) +} diff --git a/tests/unit/containers/course-section/resource-item/ResourceItem.spec.jsx b/tests/unit/containers/course-section/resource-item/ResourceItem.spec.jsx index d7fd3767f..46b40cef0 100644 --- a/tests/unit/containers/course-section/resource-item/ResourceItem.spec.jsx +++ b/tests/unit/containers/course-section/resource-item/ResourceItem.spec.jsx @@ -15,11 +15,16 @@ import { } from '~tests/unit/containers/course-section/resource-item/ResourceItem.spec.constants' import { ResourceService } from '~/services/resource-service' import ResourceItem from '~/containers/course-section/resource-item/ResourceItem' +import { downloadFile } from '~/utils/download-file' const mockDeleteResource = vi.fn() const mockEditResource = vi.fn() const mockUpdateAvailability = vi.fn() +vi.mock('~/utils/download-file', () => ({ + downloadFile: vi.fn() +})) + vi.mock('@mui/x-date-pickers/LocalizationProvider', async () => { const actual = await vi.importActual( '@mui/x-date-pickers/LocalizationProvider' @@ -310,13 +315,13 @@ describe('ResourceItem component', () => { }) }) - it('calls downloadAttachment if resource type is Attachment', () => { + it('calls downloadFile if resource type is Attachment', () => { renderWithProviders( { ) fireEvent.click(screen.getByTestId('resourceItem')) - expect(ResourceService.downloadAttachment).toHaveBeenCalledWith( - '123', - 'example.pdf' - ) + expect(downloadFile).toHaveBeenCalled() }) }) diff --git a/tests/unit/utils/download-file.spec.js b/tests/unit/utils/download-file.spec.js new file mode 100644 index 000000000..7a1f09300 --- /dev/null +++ b/tests/unit/utils/download-file.spec.js @@ -0,0 +1,53 @@ +import { describe, it, vi, expect } from 'vitest' +import { downloadFile } from '~/utils/download-file' + +describe('downloadFile', () => { + beforeEach(() => { + global.URL.createObjectURL = vi.fn(() => 'blob:http://localhost/test-url') + global.URL.revokeObjectURL = vi.fn(() => 'blob:http://localhost/test-url') + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should download a file with the correct filename', async () => { + const mockData = 'test file content' + const mockResponse = Promise.resolve({ + data: mockData + }) + + const mockUrl = 'blob:http://localhost/test-url' + const createObjectURLMock = vi + .spyOn(window.URL, 'createObjectURL') + .mockReturnValue(mockUrl) + const revokeObjectURLMock = vi.spyOn(window.URL, 'revokeObjectURL') + + const link = { + href: '', + setAttribute: vi.fn(), + click: vi.fn() + } + const appendChildMock = vi + .spyOn(document.body, 'appendChild') + .mockImplementation(() => {}) + const removeChildMock = vi + .spyOn(document.body, 'removeChild') + .mockImplementation(() => {}) + vi.spyOn(document, 'createElement').mockReturnValue(link) + + const fileName = 'test-file.txt' + + await downloadFile(mockResponse, fileName) + + expect(createObjectURLMock).toHaveBeenCalledOnce() + expect(createObjectURLMock).toHaveBeenCalledWith(new Blob([mockData])) + expect(link.setAttribute).toHaveBeenCalledWith('download', fileName) + expect(link.click).toHaveBeenCalledOnce() + expect(appendChildMock).toHaveBeenCalledWith(link) + expect(removeChildMock).toHaveBeenCalledWith(link) + + await new Promise((resolve) => setTimeout(resolve, 150)) + expect(revokeObjectURLMock).toHaveBeenCalledWith(mockUrl) + }) +})