import { chunkUpload, getAllPendingUploads } from '@/lib/chunk-upload' jest.mock('@/lib/chunk-upload-config', () => ({ getOptimalChunkSize: jest.fn().mockResolvedValue(1024 * 1024), getOptimalConcurrency: jest.fn().mockResolvedValue(1), getChunkTimeout: jest.fn(() => 30_000), })) type MockRequest = { result?: T error?: unknown onsuccess?: ((event?: { target: { result: T } }) => void) | null onerror?: ((event?: { target: { error: unknown } }) => void) | null onupgradeneeded?: ((event?: { target: { result: unknown } }) => void) | null } function createAsyncRequest(executor: () => T): MockRequest { const request: MockRequest = {} setTimeout(() => { try { request.result = executor() request.onsuccess?.({ target: { result: request.result as T } }) } catch (error) { request.error = error request.onerror?.({ target: { error } }) } }, 0) return request } function createIndexedDbMock(): IDBFactory { const records = new Map() let initialized = false const objectStore = { put: (value: { fileId: string }) => createAsyncRequest(() => { records.set(value.fileId, value) return value.fileId }), get: (key: string) => createAsyncRequest(() => records.get(key)), delete: (key: string) => createAsyncRequest(() => { records.delete(key) return undefined }), count: () => createAsyncRequest(() => records.size), getAll: () => createAsyncRequest(() => Array.from(records.values())), clear: () => createAsyncRequest(() => { records.clear() return undefined }), } const db = { objectStoreNames: { contains: () => initialized, }, createObjectStore: () => { initialized = true return objectStore }, transaction: () => ({ objectStore: () => objectStore, }), } return { open: jest.fn(() => { const request: MockRequest = {} setTimeout(() => { request.result = db if (!initialized) { request.onupgradeneeded?.({ target: { result: db } }) initialized = true } request.onsuccess?.({ target: { result: db } }) }, 0) return request as unknown as IDBOpenDBRequest }), } as unknown as IDBFactory } class MockXMLHttpRequest { static instances: MockXMLHttpRequest[] = [] timeout = 0 status = 413 responseText = '' upload = { onloadstart: null as (() => void) | null, onprogress: null as ((event: ProgressEvent) => void) | null, } onload: (() => void) | null = null onerror: (() => void) | null = null onabort: (() => void) | null = null ontimeout: (() => void) | null = null headers: Record = {} method = '' url = '' constructor() { MockXMLHttpRequest.instances.push(this) } open(method: string, url: string) { this.method = method this.url = url } setRequestHeader(name: string, value: string) { this.headers[name] = value } send() { setTimeout(() => { this.upload.onloadstart?.() this.onload?.() }, 0) } } describe('chunkUpload', () => { beforeEach(() => { jest.clearAllMocks() MockXMLHttpRequest.instances = [] Object.defineProperty(global, 'indexedDB', { configurable: true, writable: true, value: createIndexedDbMock(), }) Object.defineProperty(global, 'fetch', { configurable: true, writable: true, value: jest.fn(), }) Object.defineProperty(global, 'XMLHttpRequest', { configurable: true, writable: true, value: MockXMLHttpRequest, }) }) test('空文件会在初始化前被直接拦截', async () => { const emptyFile = new File([], 'empty.psd', { type: 'application/octet-stream' }) await expect(chunkUpload(emptyFile, { materialType: '任务-详情' })).rejects.toThrow('为空') expect(global.fetch).not.toHaveBeenCalled() await expect(getAllPendingUploads()).resolves.toEqual([]) }) test('upload/init 返回 400 时不再无限重试', async () => { ;(global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 400, json: async () => ({ error: '上传文件不能为空' }), }) const file = new File(['hello'], 'source.psb', { type: 'application/octet-stream' }) await expect( chunkUpload( file, { materialType: '任务-详情' }, { maxRetries: 5, retryDelay: 0 }, ), ).rejects.toThrow('上传文件不能为空') expect(global.fetch).toHaveBeenCalledTimes(1) await expect(getAllPendingUploads()).resolves.toEqual([]) }) test('upload/chunk 返回 413 时直接失败并清理会话', async () => { ;(global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ uploadId: 'upload-too-large' }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ message: '清理成功' }), }) const file = new File(['hello'], 'source.psb', { type: 'application/octet-stream' }) await expect( chunkUpload( file, { materialType: '任务-详情' }, { maxRetries: 5, retryDelay: 0 }, ), ).rejects.toThrow('上传分片超过入口服务器限制') expect(MockXMLHttpRequest.instances).toHaveLength(1) expect(global.fetch).toHaveBeenCalledTimes(2) expect(global.fetch).toHaveBeenNthCalledWith( 2, '/api/materials/upload/cleanup', expect.objectContaining({ method: 'POST', body: JSON.stringify({ uploadId: 'upload-too-large' }), }), ) await expect(getAllPendingUploads()).resolves.toEqual([]) }) })