dify/web/app/components/datasets/common/image-uploader/utils.spec.ts

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')
})
})
})