dify/web/app/components/datasets/common/image-uploader/utils.spec.ts
qiuqiua 9ef6b90843
feat: sync main branch (#31938)
Signed-off-by: majiayu000 <1835304752@qq.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: 盐粒 Yanli <yanli@dify.ai>
Co-authored-by: wangxiaolei <fatelei@gmail.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Cursx <33718736+Cursx@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: lif <1835304752@qq.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: fenglin <790872612@qq.com>
Co-authored-by: qiaofenglin <qiaofenglin@baidu.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: TomoOkuyama <49631611+TomoOkuyama@users.noreply.github.com>
Co-authored-by: Tomo Okuyama <tomo.okuyama@intersystems.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: zyssyz123 <916125788@qq.com>
Co-authored-by: hj24 <mambahj24@gmail.com>
Co-authored-by: Coding On Star <447357187@qq.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Xiangxuan Qu <fghpdf@outlook.com>
Co-authored-by: fghpdf <fghpdf@users.noreply.github.com>
Co-authored-by: coopercoder <whitetiger0127@163.com>
Co-authored-by: zhaiguangpeng <zhaiguangpeng@didiglobal.com>
Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com>
Co-authored-by: E.G <146701565+GlobalStar117@users.noreply.github.com>
Co-authored-by: GlobalStar117 <GlobalStar117@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: heyszt <270985384@qq.com>
Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: moonpanda <chuanzegao@163.com>
Co-authored-by: warlocgao <warlocgao@tencent.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: eux <euxx@users.noreply.github.com>
Co-authored-by: bangjiehan <bangjiehan@gmail.com>
Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: Nie Ronghua <nieronghua@sf-express.com>
Co-authored-by: JQSevenMiao <141806521+JQSevenMiao@users.noreply.github.com>
Co-authored-by: jiasiqi <jiasiqi3@tal.com>
Co-authored-by: Seokrin Taron Sung <sungsjade@gmail.com>
Co-authored-by: CrabSAMA <40541269+CrabSAMA@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: Jax <anobaka@qq.com>
Co-authored-by: niveshdandyan <155956228+niveshdandyan@users.noreply.github.com>
Co-authored-by: OSS Contributor <oss-contributor@example.com>
Co-authored-by: niveshdandyan <niveshdandyan@users.noreply.github.com>
Co-authored-by: Sean Kenneth Doherty <Smaster7772@gmail.com>
2026-02-04 19:04:24 +08:00

311 lines
11 KiB
TypeScript

import type { FileEntity } from './types'
import type { FileUploadConfigResponse } from '@/models/common'
import { describe, expect, it } from 'vitest'
import {
DEFAULT_IMAGE_FILE_BATCH_LIMIT,
DEFAULT_IMAGE_FILE_SIZE_LIMIT,
DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
} from './constants'
import { fileIsUploaded, getFileType, getFileUploadConfig, traverseFileEntry } from './utils'
describe('image-uploader utils', () => {
describe('getFileType', () => {
it('should return file extension for a simple filename', () => {
const file = { name: 'image.png' } as File
expect(getFileType(file)).toBe('png')
})
it('should return file extension for filename with multiple dots', () => {
const file = { name: 'my.photo.image.jpg' } as File
expect(getFileType(file)).toBe('jpg')
})
it('should return empty string for null/undefined file', () => {
expect(getFileType(null as unknown as File)).toBe('')
expect(getFileType(undefined as unknown as File)).toBe('')
})
it('should return filename for file without extension', () => {
const file = { name: 'README' } as File
expect(getFileType(file)).toBe('README')
})
it('should handle various file extensions', () => {
expect(getFileType({ name: 'doc.pdf' } as File)).toBe('pdf')
expect(getFileType({ name: 'image.jpeg' } as File)).toBe('jpeg')
expect(getFileType({ name: 'video.mp4' } as File)).toBe('mp4')
expect(getFileType({ name: 'archive.tar.gz' } as File)).toBe('gz')
})
})
describe('fileIsUploaded', () => {
it('should return true when uploadedId is set', () => {
const file = { uploadedId: 'some-id', progress: 50 } as Partial<FileEntity>
expect(fileIsUploaded(file as FileEntity)).toBe(true)
})
it('should return true when progress is 100', () => {
const file = { progress: 100 } as Partial<FileEntity>
expect(fileIsUploaded(file as FileEntity)).toBe(true)
})
it('should return undefined when neither uploadedId nor 100 progress', () => {
const file = { progress: 50 } as Partial<FileEntity>
expect(fileIsUploaded(file as FileEntity)).toBeUndefined()
})
it('should return undefined when progress is 0', () => {
const file = { progress: 0 } as Partial<FileEntity>
expect(fileIsUploaded(file as FileEntity)).toBeUndefined()
})
it('should return true when uploadedId is empty string and progress is 100', () => {
const file = { uploadedId: '', progress: 100 } as Partial<FileEntity>
expect(fileIsUploaded(file as FileEntity)).toBe(true)
})
})
describe('getFileUploadConfig', () => {
it('should return default values when response is undefined', () => {
const result = getFileUploadConfig(undefined)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should return values from response when valid', () => {
const response: Partial<FileUploadConfigResponse> = {
image_file_batch_limit: 20,
single_chunk_attachment_limit: 10,
attachment_image_file_size_limit: 5,
}
const result = getFileUploadConfig(response as FileUploadConfigResponse)
expect(result).toEqual({
imageFileSizeLimit: 5,
imageFileBatchLimit: 20,
singleChunkAttachmentLimit: 10,
})
})
it('should use default values when response values are 0', () => {
const response: Partial<FileUploadConfigResponse> = {
image_file_batch_limit: 0,
single_chunk_attachment_limit: 0,
attachment_image_file_size_limit: 0,
}
const result = getFileUploadConfig(response as FileUploadConfigResponse)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should use default values when response values are negative', () => {
const response: Partial<FileUploadConfigResponse> = {
image_file_batch_limit: -5,
single_chunk_attachment_limit: -10,
attachment_image_file_size_limit: -1,
}
const result = getFileUploadConfig(response as FileUploadConfigResponse)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should handle string values in response', () => {
const response = {
image_file_batch_limit: '15',
single_chunk_attachment_limit: '8',
attachment_image_file_size_limit: '3',
} as unknown as FileUploadConfigResponse
const result = getFileUploadConfig(response)
expect(result).toEqual({
imageFileSizeLimit: 3,
imageFileBatchLimit: 15,
singleChunkAttachmentLimit: 8,
})
})
it('should handle null values in response', () => {
const response = {
image_file_batch_limit: null,
single_chunk_attachment_limit: null,
attachment_image_file_size_limit: null,
} as unknown as FileUploadConfigResponse
const result = getFileUploadConfig(response)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should handle undefined values in response', () => {
const response = {
image_file_batch_limit: undefined,
single_chunk_attachment_limit: undefined,
attachment_image_file_size_limit: undefined,
} as unknown as FileUploadConfigResponse
const result = getFileUploadConfig(response)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should handle partial response', () => {
const response: Partial<FileUploadConfigResponse> = {
image_file_batch_limit: 25,
}
const result = getFileUploadConfig(response as FileUploadConfigResponse)
expect(result.imageFileBatchLimit).toBe(25)
expect(result.imageFileSizeLimit).toBe(DEFAULT_IMAGE_FILE_SIZE_LIMIT)
expect(result.singleChunkAttachmentLimit).toBe(DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT)
})
it('should handle non-number non-string values (object, boolean, etc) with default fallback', () => {
// This tests the getNumberValue function's final return 0 case
// When value is neither number nor string (e.g., object, boolean, array)
const response = {
image_file_batch_limit: { invalid: 'object' }, // Object - not number or string
single_chunk_attachment_limit: true, // Boolean - not number or string
attachment_image_file_size_limit: ['array'], // Array - not number or string
} as unknown as FileUploadConfigResponse
const result = getFileUploadConfig(response)
// All should fall back to defaults since getNumberValue returns 0 for these types
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
it('should handle NaN string values', () => {
const response = {
image_file_batch_limit: 'not-a-number',
single_chunk_attachment_limit: '',
attachment_image_file_size_limit: 'abc',
} as unknown as FileUploadConfigResponse
const result = getFileUploadConfig(response)
// NaN values should result in defaults (since NaN > 0 is false)
expect(result).toEqual({
imageFileSizeLimit: DEFAULT_IMAGE_FILE_SIZE_LIMIT,
imageFileBatchLimit: DEFAULT_IMAGE_FILE_BATCH_LIMIT,
singleChunkAttachmentLimit: DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
})
})
})
describe('traverseFileEntry', () => {
type MockFile = { name: string, relativePath?: string }
type FileCallback = (file: MockFile) => void
type EntriesCallback = (entries: FileSystemEntry[]) => void
it('should resolve with file array for file entry', async () => {
const mockFile: MockFile = { name: 'test.png' }
const mockEntry = {
isFile: true,
isDirectory: false,
file: (callback: FileCallback) => callback(mockFile),
}
const result = await traverseFileEntry(mockEntry)
expect(result).toHaveLength(1)
expect(result[0].name).toBe('test.png')
expect(result[0].relativePath).toBe('test.png')
})
it('should resolve with file array with prefix for nested file', async () => {
const mockFile: MockFile = { name: 'test.png' }
const mockEntry = {
isFile: true,
isDirectory: false,
file: (callback: FileCallback) => callback(mockFile),
}
const result = await traverseFileEntry(mockEntry, 'folder/')
expect(result).toHaveLength(1)
expect(result[0].relativePath).toBe('folder/test.png')
})
it('should resolve empty array for unknown entry type', async () => {
const mockEntry = {
isFile: false,
isDirectory: false,
}
const result = await traverseFileEntry(mockEntry)
expect(result).toEqual([])
})
it('should handle directory with no files', async () => {
const mockEntry = {
isFile: false,
isDirectory: true,
name: 'empty-folder',
createReader: () => ({
readEntries: (callback: EntriesCallback) => callback([]),
}),
}
const result = await traverseFileEntry(mockEntry)
expect(result).toEqual([])
})
it('should handle directory with files', async () => {
const mockFile1: MockFile = { name: 'file1.png' }
const mockFile2: MockFile = { name: 'file2.png' }
const mockFileEntry1 = {
isFile: true,
isDirectory: false,
file: (callback: FileCallback) => callback(mockFile1),
}
const mockFileEntry2 = {
isFile: true,
isDirectory: false,
file: (callback: FileCallback) => callback(mockFile2),
}
let readCount = 0
const mockEntry = {
isFile: false,
isDirectory: true,
name: 'folder',
createReader: () => ({
readEntries: (callback: EntriesCallback) => {
if (readCount === 0) {
readCount++
callback([mockFileEntry1, mockFileEntry2] as unknown as FileSystemEntry[])
}
else {
callback([])
}
},
}),
}
const result = await traverseFileEntry(mockEntry)
expect(result).toHaveLength(2)
expect(result[0].relativePath).toBe('folder/file1.png')
expect(result[1].relativePath).toBe('folder/file2.png')
})
})
})