/* eslint-disable @next/next/no-img-element, jsx-a11y/alt-text */ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import UploadModal from '@/components/UploadModal' import { compressVideoPreviewImage } from '@/components/upload/image-processor' jest.mock('next/image', () => ({ __esModule: true, default: (props: any) => { const { priority, fill, unoptimized, sizes, ...rest } = props return }, })) jest.mock('@/components/upload/image-processor', () => ({ assertVideoPreviewImageIsSafeToDecode: jest.fn().mockResolvedValue(undefined), compressPreviewImage: jest.fn(), compressVideoPreviewImage: jest.fn(), })) jest.mock('@/hooks/useCategoryData', () => ({ useCategoryData: () => ({ data: { categories: [{ id: 'category-1', name: '通用' }], scenes: [{ id: 'scene-1', name: '通用' }], platforms: [], productCategories: [], }, refreshCategories: jest.fn(), refreshScenes: jest.fn(), }), })) jest.mock('@/components/upload/ProductPackageUploader', () => ({ __esModule: true, default: () =>
, })) jest.mock('@/components/upload/VideoScreenshotUploader', () => ({ __esModule: true, default: () =>
, })) jest.mock('@/components/upload/TaskPageUploader', () => ({ __esModule: true, default: () =>
, })) function createDeferred() { let resolve!: (value: T) => void const promise = new Promise((resolvePromise) => { resolve = resolvePromise }) return { promise, resolve } } describe('UploadModal - 视频预览图处理状态', () => { const originalCreateObjectUrl = Object.getOwnPropertyDescriptor(URL, 'createObjectURL') const originalRevokeObjectUrl = Object.getOwnPropertyDescriptor(URL, 'revokeObjectURL') beforeEach(() => { jest.clearAllMocks() Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:video-preview'), }) Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn(), }) global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ materialTypes: ['3D场景', '淘系-主图视频'] }), }) as any }) afterAll(() => { if (originalCreateObjectUrl) Object.defineProperty(URL, 'createObjectURL', originalCreateObjectUrl) else delete (URL as { createObjectURL?: unknown }).createObjectURL if (originalRevokeObjectUrl) Object.defineProperty(URL, 'revokeObjectURL', originalRevokeObjectUrl) else delete (URL as { revokeObjectURL?: unknown }).revokeObjectURL }) test('压缩完成前阻止提交,并在关闭时释放预览 URL', async () => { const user = userEvent.setup() const deferredCompression = createDeferred() ;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise) const view = render() const materialTypeSelect = (await screen.findAllByRole('combobox'))[0] await user.selectOptions(materialTypeSelect, '淘系-主图视频') const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement fireEvent.change(previewInput, { target: { files: [new File(['cover'], 'cover.png', { type: 'image/png' })] }, }) await waitFor(() => expect(compressVideoPreviewImage).toHaveBeenCalledTimes(1)) expect(screen.getByRole('button', { name: '正在处理预览图...' })).toBeDisabled() expect(materialTypeSelect).toBeDisabled() fireEvent.submit(screen.getByRole('button', { name: '正在处理预览图...' }).closest('form')!) expect(await screen.findByRole('alert')).toHaveTextContent('视频预览图仍在处理中') const compressedFile = new File(['compressed'], 'cover.jpg', { type: 'image/jpeg' }) deferredCompression.resolve(compressedFile) await waitFor(() => expect(screen.getByAltText('已选择的视频预览图')).toBeInTheDocument()) expect(screen.getByRole('button', { name: '上传' })).toBeEnabled() view.unmount() expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:video-preview') }) test('处理中忽略同一控件的重复选择', async () => { const deferredCompression = createDeferred() ;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise) render() const materialTypeSelect = (await screen.findAllByRole('combobox'))[0] fireEvent.change(materialTypeSelect, { target: { value: '淘系-主图视频' } }) const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement fireEvent.change(previewInput, { target: { files: [new File(['cover-1'], 'cover-1.png', { type: 'image/png' })] }, }) fireEvent.change(previewInput, { target: { files: [new File(['cover-2'], 'cover-2.png', { type: 'image/png' })] }, }) await waitFor(() => expect(compressVideoPreviewImage).toHaveBeenCalledTimes(1)) deferredCompression.resolve(new File(['compressed'], 'cover.jpg', { type: 'image/jpeg' })) await waitFor(() => expect(screen.getByAltText('已选择的视频预览图')).toBeInTheDocument()) }) test('处理中关闭后丢弃异步结果且不创建 Blob URL', async () => { const onClose = jest.fn() const deferredCompression = createDeferred() ;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise) const view = render() const materialTypeSelect = (await screen.findAllByRole('combobox'))[0] fireEvent.change(materialTypeSelect, { target: { value: '淘系-主图视频' } }) const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement fireEvent.change(previewInput, { target: { files: [new File(['cover'], 'cover.png', { type: 'image/png' })] }, }) await waitFor(() => expect(compressVideoPreviewImage).toHaveBeenCalledTimes(1)) fireEvent.click(screen.getByRole('button', { name: '关闭上传素材窗口' })) expect(onClose).toHaveBeenCalledTimes(1) view.unmount() deferredCompression.resolve(new File(['compressed'], 'cover.jpg', { type: 'image/jpeg' })) await Promise.resolve() await Promise.resolve() expect(URL.createObjectURL).not.toHaveBeenCalled() }) test('通用批量视频的预览图处理失败时保留文件并允许重试', async () => { const user = userEvent.setup() const onClose = jest.fn() const onSuccess = jest.fn() ;(compressVideoPreviewImage as jest.Mock).mockImplementation(async (file: File) => file) render() const materialTypeSelect = (await screen.findAllByRole('combobox'))[0] await user.selectOptions(materialTypeSelect, '淘系-主图视频') const mainDropZone = screen.getByRole('button', { name: '选择素材文件' }).closest('div.border-dashed') expect(mainDropZone).not.toBeNull() fireEvent.drop(mainDropZone!, { dataTransfer: { files: [ new File(['video-1'], 'VIDEO-001.mp4', { type: 'video/mp4' }), new File(['video-2'], 'VIDEO-002.mp4', { type: 'video/mp4' }), ], }, }) expect(await screen.findByText('批量上传模式(2 个文件)')).toBeInTheDocument() const firstMetadataSelects = screen.getAllByRole('combobox') await user.selectOptions(firstMetadataSelects[1], '通用') await user.selectOptions(firstMetadataSelects[2], '通用') const firstPreviewInput = screen.getByLabelText(/^视频预览图/) as HTMLInputElement await waitFor(() => expect(firstPreviewInput).not.toBeDisabled()) fireEvent.change(firstPreviewInput, { target: { files: [new File(['cover-0'], 'cover-0.png', { type: 'image/png' })], }, }) await waitFor(() => expect(screen.getAllByAltText('已选择的视频预览图')).toHaveLength(1)) await user.click(screen.getByText('VIDEO-002.mp4')) const previewInputs = await screen.findAllByLabelText(/^视频预览图/) expect(previewInputs).toHaveLength(2) const secondPreviewInput = previewInputs[1] as HTMLInputElement const secondMetadataSelects = screen.getAllByRole('combobox') await user.selectOptions(secondMetadataSelects[3], '通用') await user.selectOptions(secondMetadataSelects[4], '通用') await waitFor(() => expect(secondPreviewInput).not.toBeDisabled()) fireEvent.change(secondPreviewInput, { target: { files: [new File(['cover-1'], 'cover-1.png', { type: 'image/png' })], }, }) await waitFor(() => expect(screen.getAllByAltText('已选择的视频预览图')).toHaveLength(2)) ;(compressVideoPreviewImage as jest.Mock).mockRejectedValue(new Error('预览图无法解码')) fireEvent.click(screen.getByRole('button', { name: '上传' })) expect(await screen.findByRole('alert')).toHaveTextContent('2 个文件上传失败,请检查后重试') expect(screen.getByText('批量上传模式(2 个文件)')).toBeInTheDocument() expect(onSuccess).not.toHaveBeenCalled() expect(onClose).not.toHaveBeenCalled() }) })