/* 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()
})
})