mirror of
https://github.com/langgenius/dify.git
synced 2026-03-31 18:00:55 -04:00
test: enhance download operation tests to cover text and binary file handling scenarios
This commit is contained in:
@@ -12,6 +12,10 @@ type DownloadResponse = {
|
||||
download_url: string
|
||||
}
|
||||
|
||||
type FileContentResponse = {
|
||||
content: string
|
||||
}
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>
|
||||
resolve: (value: T) => void
|
||||
@@ -30,12 +34,16 @@ const createDeferred = <T,>(): Deferred<T> => {
|
||||
|
||||
const {
|
||||
mockGetFileDownloadUrl,
|
||||
mockGetFileContent,
|
||||
mockDownloadUrl,
|
||||
mockDownloadBlob,
|
||||
mockToastSuccess,
|
||||
mockToastError,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetFileDownloadUrl: vi.fn<(request: DownloadRequest) => Promise<DownloadResponse>>(),
|
||||
mockGetFileContent: vi.fn<(request: DownloadRequest) => Promise<FileContentResponse>>(),
|
||||
mockDownloadUrl: vi.fn<(payload: { url: string, fileName?: string }) => void>(),
|
||||
mockDownloadBlob: vi.fn<(payload: { data: Blob, fileName: string }) => void>(),
|
||||
mockToastSuccess: vi.fn(),
|
||||
mockToastError: vi.fn(),
|
||||
}))
|
||||
@@ -44,12 +52,14 @@ vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
appAsset: {
|
||||
getFileDownloadUrl: mockGetFileDownloadUrl,
|
||||
getFileContent: mockGetFileContent,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadUrl: mockDownloadUrl,
|
||||
downloadBlob: mockDownloadBlob,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@@ -63,6 +73,7 @@ describe('useDownloadOperation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetFileDownloadUrl.mockResolvedValue({ download_url: 'https://example.com/file.txt' })
|
||||
mockGetFileContent.mockResolvedValue({ content: '{"content":"# Skill\\n\\nOriginal markdown"}' })
|
||||
})
|
||||
|
||||
// Scenario: hook should no-op when required identifiers are missing.
|
||||
@@ -86,9 +97,9 @@ describe('useDownloadOperation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Scenario: successful downloads should fetch URL and trigger browser download.
|
||||
// Scenario: successful downloads should unwrap text files and keep binary downloads on URL flow.
|
||||
describe('Success', () => {
|
||||
it('should download file when API call succeeds', async () => {
|
||||
it('should download text file from raw content when file is markdown', async () => {
|
||||
const onClose = vi.fn()
|
||||
const { result } = renderHook(() => useDownloadOperation({
|
||||
appId: 'app-1',
|
||||
@@ -102,29 +113,59 @@ describe('useDownloadOperation', () => {
|
||||
})
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetFileContent).toHaveBeenCalledWith({
|
||||
params: {
|
||||
appId: 'app-1',
|
||||
nodeId: 'node-1',
|
||||
},
|
||||
})
|
||||
expect(mockGetFileDownloadUrl).not.toHaveBeenCalled()
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fileName: 'notes.md',
|
||||
}))
|
||||
const downloadedBlob = mockDownloadBlob.mock.calls[0][0].data
|
||||
await expect(downloadedBlob.text()).resolves.toBe('# Skill\n\nOriginal markdown')
|
||||
expect(mockToastSuccess).not.toHaveBeenCalled()
|
||||
expect(mockToastError).not.toHaveBeenCalled()
|
||||
expect(result.current.isDownloading).toBe(false)
|
||||
})
|
||||
|
||||
it('should download binary file from download url when file is not text', async () => {
|
||||
const onClose = vi.fn()
|
||||
const { result } = renderHook(() => useDownloadOperation({
|
||||
appId: 'app-1',
|
||||
nodeId: 'node-1',
|
||||
fileName: 'diagram.png',
|
||||
onClose,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDownload()
|
||||
})
|
||||
|
||||
expect(mockGetFileDownloadUrl).toHaveBeenCalledWith({
|
||||
params: {
|
||||
appId: 'app-1',
|
||||
nodeId: 'node-1',
|
||||
},
|
||||
})
|
||||
expect(mockGetFileContent).not.toHaveBeenCalled()
|
||||
expect(mockDownloadUrl).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/file.txt',
|
||||
fileName: 'notes.md',
|
||||
fileName: 'diagram.png',
|
||||
})
|
||||
expect(mockToastSuccess).not.toHaveBeenCalled()
|
||||
expect(mockToastError).not.toHaveBeenCalled()
|
||||
expect(result.current.isDownloading).toBe(false)
|
||||
expect(mockDownloadBlob).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set isDownloading true while download request is pending', async () => {
|
||||
const deferred = createDeferred<DownloadResponse>()
|
||||
mockGetFileDownloadUrl.mockReturnValueOnce(deferred.promise)
|
||||
const deferred = createDeferred<FileContentResponse>()
|
||||
mockGetFileContent.mockReturnValueOnce(deferred.promise)
|
||||
const onClose = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useDownloadOperation({
|
||||
appId: 'app-2',
|
||||
nodeId: 'node-2',
|
||||
fileName: 'notes.md',
|
||||
onClose,
|
||||
}))
|
||||
|
||||
@@ -137,15 +178,14 @@ describe('useDownloadOperation', () => {
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
deferred.resolve({ download_url: 'https://example.com/slow.txt' })
|
||||
deferred.resolve({ content: '{"content":"slow"}' })
|
||||
await deferred.promise
|
||||
})
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(mockDownloadUrl).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/slow.txt',
|
||||
fileName: undefined,
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fileName: 'notes.md',
|
||||
}))
|
||||
expect(result.current.isDownloading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { getFileExtension, isTextLikeFile } from '@/app/components/workflow/skill/utils/file-utils'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { downloadBlob, downloadUrl } from '@/utils/download'
|
||||
|
||||
type UseDownloadOperationOptions = {
|
||||
appId: string
|
||||
@@ -21,6 +22,8 @@ export function useDownloadOperation({
|
||||
}: UseDownloadOperationOptions) {
|
||||
const { t } = useTranslation('workflow')
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const extension = getFileExtension(fileName)
|
||||
const shouldDownloadAsText = !!fileName && isTextLikeFile(extension)
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (!nodeId || !appId)
|
||||
@@ -30,11 +33,31 @@ export function useDownloadOperation({
|
||||
|
||||
setIsDownloading(true)
|
||||
try {
|
||||
const { download_url } = await consoleClient.appAsset.getFileDownloadUrl({
|
||||
params: { appId, nodeId },
|
||||
})
|
||||
if (shouldDownloadAsText) {
|
||||
const { content } = await consoleClient.appAsset.getFileContent({
|
||||
params: { appId, nodeId },
|
||||
})
|
||||
let rawText = content
|
||||
try {
|
||||
const parsed = JSON.parse(content) as { content?: string }
|
||||
if (typeof parsed?.content === 'string')
|
||||
rawText = parsed.content
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
downloadUrl({ url: download_url, fileName })
|
||||
downloadBlob({
|
||||
data: new Blob([rawText], { type: 'text/plain;charset=utf-8' }),
|
||||
fileName: fileName || 'download.txt',
|
||||
})
|
||||
}
|
||||
else {
|
||||
const { download_url } = await consoleClient.appAsset.getFileDownloadUrl({
|
||||
params: { appId, nodeId },
|
||||
})
|
||||
|
||||
downloadUrl({ url: download_url, fileName })
|
||||
}
|
||||
}
|
||||
catch {
|
||||
toast.error(t('skillSidebar.menu.downloadError'))
|
||||
@@ -42,7 +65,7 @@ export function useDownloadOperation({
|
||||
finally {
|
||||
setIsDownloading(false)
|
||||
}
|
||||
}, [appId, nodeId, fileName, onClose, t])
|
||||
}, [appId, nodeId, fileName, onClose, shouldDownloadAsText, t])
|
||||
|
||||
return {
|
||||
handleDownload,
|
||||
|
||||
Reference in New Issue
Block a user