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 { 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 } 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, 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, 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) }) })