mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
test: enhance download operation tests to cover text and binary file handling scenarios
This commit is contained in:
parent
ce2403e0db
commit
7046fd6728
@ -12,6 +12,10 @@ type DownloadResponse = {
|
|||||||
download_url: string
|
download_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileContentResponse = {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
type Deferred<T> = {
|
type Deferred<T> = {
|
||||||
promise: Promise<T>
|
promise: Promise<T>
|
||||||
resolve: (value: T) => void
|
resolve: (value: T) => void
|
||||||
@ -30,12 +34,16 @@ const createDeferred = <T,>(): Deferred<T> => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
mockGetFileDownloadUrl,
|
mockGetFileDownloadUrl,
|
||||||
|
mockGetFileContent,
|
||||||
mockDownloadUrl,
|
mockDownloadUrl,
|
||||||
|
mockDownloadBlob,
|
||||||
mockToastSuccess,
|
mockToastSuccess,
|
||||||
mockToastError,
|
mockToastError,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockGetFileDownloadUrl: vi.fn<(request: DownloadRequest) => Promise<DownloadResponse>>(),
|
mockGetFileDownloadUrl: vi.fn<(request: DownloadRequest) => Promise<DownloadResponse>>(),
|
||||||
|
mockGetFileContent: vi.fn<(request: DownloadRequest) => Promise<FileContentResponse>>(),
|
||||||
mockDownloadUrl: vi.fn<(payload: { url: string, fileName?: string }) => void>(),
|
mockDownloadUrl: vi.fn<(payload: { url: string, fileName?: string }) => void>(),
|
||||||
|
mockDownloadBlob: vi.fn<(payload: { data: Blob, fileName: string }) => void>(),
|
||||||
mockToastSuccess: vi.fn(),
|
mockToastSuccess: vi.fn(),
|
||||||
mockToastError: vi.fn(),
|
mockToastError: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -44,12 +52,14 @@ vi.mock('@/service/client', () => ({
|
|||||||
consoleClient: {
|
consoleClient: {
|
||||||
appAsset: {
|
appAsset: {
|
||||||
getFileDownloadUrl: mockGetFileDownloadUrl,
|
getFileDownloadUrl: mockGetFileDownloadUrl,
|
||||||
|
getFileContent: mockGetFileContent,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/utils/download', () => ({
|
vi.mock('@/utils/download', () => ({
|
||||||
downloadUrl: mockDownloadUrl,
|
downloadUrl: mockDownloadUrl,
|
||||||
|
downloadBlob: mockDownloadBlob,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||||
@ -63,6 +73,7 @@ describe('useDownloadOperation', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockGetFileDownloadUrl.mockResolvedValue({ download_url: 'https://example.com/file.txt' })
|
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.
|
// 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', () => {
|
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 onClose = vi.fn()
|
||||||
const { result } = renderHook(() => useDownloadOperation({
|
const { result } = renderHook(() => useDownloadOperation({
|
||||||
appId: 'app-1',
|
appId: 'app-1',
|
||||||
@ -102,29 +113,59 @@ describe('useDownloadOperation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
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({
|
expect(mockGetFileDownloadUrl).toHaveBeenCalledWith({
|
||||||
params: {
|
params: {
|
||||||
appId: 'app-1',
|
appId: 'app-1',
|
||||||
nodeId: 'node-1',
|
nodeId: 'node-1',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
expect(mockGetFileContent).not.toHaveBeenCalled()
|
||||||
expect(mockDownloadUrl).toHaveBeenCalledWith({
|
expect(mockDownloadUrl).toHaveBeenCalledWith({
|
||||||
url: 'https://example.com/file.txt',
|
url: 'https://example.com/file.txt',
|
||||||
fileName: 'notes.md',
|
fileName: 'diagram.png',
|
||||||
})
|
})
|
||||||
expect(mockToastSuccess).not.toHaveBeenCalled()
|
expect(mockDownloadBlob).not.toHaveBeenCalled()
|
||||||
expect(mockToastError).not.toHaveBeenCalled()
|
|
||||||
expect(result.current.isDownloading).toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should set isDownloading true while download request is pending', async () => {
|
it('should set isDownloading true while download request is pending', async () => {
|
||||||
const deferred = createDeferred<DownloadResponse>()
|
const deferred = createDeferred<FileContentResponse>()
|
||||||
mockGetFileDownloadUrl.mockReturnValueOnce(deferred.promise)
|
mockGetFileContent.mockReturnValueOnce(deferred.promise)
|
||||||
const onClose = vi.fn()
|
const onClose = vi.fn()
|
||||||
|
|
||||||
const { result } = renderHook(() => useDownloadOperation({
|
const { result } = renderHook(() => useDownloadOperation({
|
||||||
appId: 'app-2',
|
appId: 'app-2',
|
||||||
nodeId: 'node-2',
|
nodeId: 'node-2',
|
||||||
|
fileName: 'notes.md',
|
||||||
onClose,
|
onClose,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -137,15 +178,14 @@ describe('useDownloadOperation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
deferred.resolve({ download_url: 'https://example.com/slow.txt' })
|
deferred.resolve({ content: '{"content":"slow"}' })
|
||||||
await deferred.promise
|
await deferred.promise
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
expect(onClose).toHaveBeenCalledTimes(1)
|
||||||
expect(mockDownloadUrl).toHaveBeenCalledWith({
|
expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
url: 'https://example.com/slow.txt',
|
fileName: 'notes.md',
|
||||||
fileName: undefined,
|
}))
|
||||||
})
|
|
||||||
expect(result.current.isDownloading).toBe(false)
|
expect(result.current.isDownloading).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,8 +3,9 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from '@/app/components/base/ui/toast'
|
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 { consoleClient } from '@/service/client'
|
||||||
import { downloadUrl } from '@/utils/download'
|
import { downloadBlob, downloadUrl } from '@/utils/download'
|
||||||
|
|
||||||
type UseDownloadOperationOptions = {
|
type UseDownloadOperationOptions = {
|
||||||
appId: string
|
appId: string
|
||||||
@ -21,6 +22,8 @@ export function useDownloadOperation({
|
|||||||
}: UseDownloadOperationOptions) {
|
}: UseDownloadOperationOptions) {
|
||||||
const { t } = useTranslation('workflow')
|
const { t } = useTranslation('workflow')
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
|
const extension = getFileExtension(fileName)
|
||||||
|
const shouldDownloadAsText = !!fileName && isTextLikeFile(extension)
|
||||||
|
|
||||||
const handleDownload = useCallback(async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
if (!nodeId || !appId)
|
if (!nodeId || !appId)
|
||||||
@ -30,11 +33,31 @@ export function useDownloadOperation({
|
|||||||
|
|
||||||
setIsDownloading(true)
|
setIsDownloading(true)
|
||||||
try {
|
try {
|
||||||
const { download_url } = await consoleClient.appAsset.getFileDownloadUrl({
|
if (shouldDownloadAsText) {
|
||||||
params: { appId, nodeId },
|
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 {
|
catch {
|
||||||
toast.error(t('skillSidebar.menu.downloadError'))
|
toast.error(t('skillSidebar.menu.downloadError'))
|
||||||
@ -42,7 +65,7 @@ export function useDownloadOperation({
|
|||||||
finally {
|
finally {
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
}, [appId, nodeId, fileName, onClose, t])
|
}, [appId, nodeId, fileName, onClose, shouldDownloadAsText, t])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleDownload,
|
handleDownload,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user