import { fireEvent, render, screen, waitFor } from '@testing-library/react' import TaskPageUploader from '@/components/upload/TaskPageUploader' import { chunkUpload } from '@/lib/chunk-upload' import { TASK_DETAIL_UPLOAD_LIMITS } from '@/lib/task-detail-upload-policy' jest.mock('@/lib/chunk-upload', () => ({ chunkUpload: jest.fn(), })) const originalFetch = global.fetch describe('TaskPageUploader upload contract', () => { beforeEach(() => { jest.clearAllMocks() Object.defineProperty(URL, 'createObjectURL', { configurable: true, writable: true, value: jest.fn(() => 'blob:task-page-preview'), }) Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, writable: true, value: jest.fn(), }) }) afterAll(() => { global.fetch = originalFetch }) test('应读取初始化接口顶层 materialId 并提交完成请求', async () => { const fetchMock = jest .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, materialId: 'material-task-page-1' }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, materialId: 'material-task-page-1' }), }) global.fetch = fetchMock as unknown as typeof fetch ;(chunkUpload as jest.Mock).mockResolvedValue({ uploadId: 'upload-preview-1' }) const onSuccess = jest.fn() const { container } = render( , ) fireEvent.change(screen.getByLabelText(/SPU型号/), { target: { value: 'SJ0371' } }) fireEvent.change(screen.getByLabelText(/使用店铺/), { target: { value: '索菲亚' } }) fireEvent.change(screen.getByLabelText(/场景类别/), { target: { value: '书房' } }) const imageInput = container.querySelector('input[type="file"][accept="image/*"]') as HTMLInputElement const image = new File(['preview'], 'page-1.jpg', { type: 'image/jpeg' }) fireEvent.change(imageInput, { target: { files: [image] } }) fireEvent.click(screen.getByRole('button', { name: '提交' })) await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)) expect(fetchMock.mock.calls[0][0]).toBe('/api/materials/upload/task-page/init') expect(fetchMock.mock.calls[1][0]).toBe('/api/materials/upload/task-page/complete') expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual( expect.objectContaining({ materialId: 'material-task-page-1', assets: { main: [ expect.objectContaining({ uploadId: 'upload-preview-1', fileName: 'page-1.jpg', }), ], }, }), ) await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1), { timeout: 2000 }) }) test('选择文件时应在上传前拒绝超过 100MB 的单张图片', () => { const { container } = render( , ) const imageInput = container.querySelector('input[type="file"][accept="image/*"]') as HTMLInputElement const oversized = new File(['small fixture'], 'too-large.jpg', { type: 'image/jpeg' }) Object.defineProperty(oversized, 'size', { configurable: true, value: TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes + 1, }) fireEvent.change(imageInput, { target: { files: [oversized] } }) expect(screen.getByText(/too-large\.jpg/)).toHaveTextContent('超过 100MB') expect(screen.queryByAltText('预览图 1')).not.toBeInTheDocument() }) })