gongxiangp2/components/upload/drag-drop-utils.test.ts

152 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
hasAllowedFileExtension,
hasDragLeftDropZone,
handleDropZoneKeyDown,
isSupportedImageFile,
prepareFileDrag,
validateImageFiles,
} from '@/components/upload/drag-drop-utils'
function createDragEvent(
currentTarget: HTMLElement,
options: {
clientX?: number
clientY?: number
relatedTarget?: EventTarget | null
dropEffect?: DataTransfer['dropEffect']
} = {},
): React.DragEvent<HTMLElement> {
const dataTransfer = {
dropEffect: options.dropEffect || 'none',
}
return {
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
currentTarget,
relatedTarget: options.relatedTarget || null,
clientX: options.clientX ?? 50,
clientY: options.clientY ?? 50,
dataTransfer,
} as unknown as React.DragEvent<HTMLElement>
}
describe('drag-drop-utils', () => {
test('经过上传区内部子元素时不应判定为离开', () => {
const dropZone = document.createElement('div')
const child = document.createElement('button')
dropZone.appendChild(child)
const event = createDragEvent(dropZone, { relatedTarget: child })
expect(hasDragLeftDropZone(event)).toBe(false)
})
test('指针越过上传区边界时应判定为离开', () => {
const dropZone = document.createElement('div')
Object.defineProperty(dropZone, 'getBoundingClientRect', {
configurable: true,
value: () => ({
left: 0,
right: 100,
top: 0,
bottom: 100,
}),
})
const event = createDragEvent(dropZone, { clientX: 120, clientY: 50 })
expect(hasDragLeftDropZone(event)).toBe(true)
})
test('禁用状态应阻止默认拖放并显示禁止效果', () => {
const dropZone = document.createElement('div')
const event = createDragEvent(dropZone)
prepareFileDrag(event, true)
expect(event.preventDefault).toHaveBeenCalled()
expect(event.stopPropagation).toHaveBeenCalled()
expect(event.dataTransfer.dropEffect).toBe('none')
})
test('上传区获得焦点时按 Enter 或空格应打开文件选择器', () => {
const dropZone = document.createElement('div')
const openFilePicker = jest.fn()
handleDropZoneKeyDown({
key: 'Enter',
preventDefault: jest.fn(),
currentTarget: dropZone,
target: dropZone,
} as unknown as React.KeyboardEvent<HTMLElement>, openFilePicker)
expect(openFilePicker).toHaveBeenCalledTimes(1)
})
test('上传区内的按钮按键不应再次打开文件选择器', () => {
const dropZone = document.createElement('div')
const child = document.createElement('button')
const openFilePicker = jest.fn()
handleDropZoneKeyDown({
key: ' ',
preventDefault: jest.fn(),
currentTarget: dropZone,
target: child,
} as unknown as React.KeyboardEvent<HTMLElement>, openFilePicker)
expect(openFilePicker).not.toHaveBeenCalled()
})
test('Windows 未提供 MIME 时应按常见图片扩展名识别', () => {
const file = new File(['image'], 'preview.PNG', { type: '' })
expect(isSupportedImageFile(file)).toBe(true)
})
test('应同时保留有效图片并返回非法及超大文件错误', () => {
const valid = new File(['image'], 'valid.png', { type: 'image/png' })
const invalid = new File(['text'], 'invalid.txt', { type: 'text/plain' })
const oversized = new File([new Uint8Array(11)], 'large.jpg', { type: 'image/jpeg' })
const result = validateImageFiles([valid, invalid, oversized], { maxBytes: 10 })
expect(result.acceptedFiles).toEqual([valid])
expect(result.errors.join(';')).toContain('invalid.txt')
expect(result.errors.join(';')).toContain('large.jpg')
})
test('业务链路可按浏览器处理能力限制图片格式', () => {
const jpg = new File(['image'], '1.jpg', { type: 'image/jpeg' })
const heic = new File(['image'], 'phone.heic', { type: 'image/heic' })
const result = validateImageFiles([jpg, heic], {
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
})
expect(result.acceptedFiles).toEqual([jpg])
expect(result.errors.join(';')).toContain('phone.heic')
expect(result.errors.join(';')).toContain('JPG、JPEG、PNG、GIF、WEBP')
})
test('图片数量限制应考虑已经选择的图片', () => {
const files = [
new File(['1'], '1.png', { type: 'image/png' }),
new File(['2'], '2.png', { type: 'image/png' }),
new File(['3'], '3.png', { type: 'image/png' }),
]
const result = validateImageFiles(files, { maxCount: 5, existingCount: 4 })
expect(result.acceptedFiles).toEqual([files[0]])
expect(result.errors).toContain('最多只能选择 5 张图片,已忽略 2 张')
})
test('压缩包扩展名校验应忽略大小写和前导点', () => {
const file = new File(['zip'], 'model.ZIP', { type: 'application/zip' })
expect(hasAllowedFileExtension(file, ['.zip', '.rar', '.7z'])).toBe(true)
})
})