229 lines
9.2 KiB
TypeScript
229 lines
9.2 KiB
TypeScript
import { changeField, selectOption } from '@/test-utils/select'
|
||
/* 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 <img {...rest} />
|
||
},
|
||
}))
|
||
|
||
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: () => <div />,
|
||
}))
|
||
|
||
jest.mock('@/components/upload/VideoScreenshotUploader', () => ({
|
||
__esModule: true,
|
||
default: () => <div />,
|
||
}))
|
||
|
||
jest.mock('@/components/upload/TaskPageUploader', () => ({
|
||
__esModule: true,
|
||
default: () => <div />,
|
||
}))
|
||
|
||
function createDeferred<T>() {
|
||
let resolve!: (value: T) => void
|
||
const promise = new Promise<T>((resolvePromise) => {
|
||
resolve = resolvePromise
|
||
})
|
||
return { promise, resolve }
|
||
}
|
||
|
||
async function chooseVideoMaterialType(user: ReturnType<typeof userEvent.setup>) {
|
||
const trigger = screen.getByRole('combobox', { name: '素材类型 *' })
|
||
await user.click(trigger)
|
||
await user.click(await screen.findByRole('option', { name: '淘系-主图视频' }))
|
||
return trigger
|
||
}
|
||
|
||
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<File>()
|
||
;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise)
|
||
|
||
const view = render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
||
const materialTypeSelect = await chooseVideoMaterialType(user)
|
||
|
||
const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement
|
||
changeField(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 user = userEvent.setup()
|
||
const deferredCompression = createDeferred<File>()
|
||
;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise)
|
||
|
||
render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
||
await chooseVideoMaterialType(user)
|
||
|
||
const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement
|
||
changeField(previewInput, {
|
||
target: { files: [new File(['cover-1'], 'cover-1.png', { type: 'image/png' })] },
|
||
})
|
||
changeField(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 user = userEvent.setup()
|
||
const onClose = jest.fn()
|
||
const deferredCompression = createDeferred<File>()
|
||
;(compressVideoPreviewImage as jest.Mock).mockReturnValue(deferredCompression.promise)
|
||
|
||
const view = render(<UploadModal onClose={onClose} onSuccess={jest.fn()} />)
|
||
await chooseVideoMaterialType(user)
|
||
|
||
const previewInput = screen.getByLabelText(/视频预览图/) as HTMLInputElement
|
||
changeField(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(<UploadModal onClose={onClose} onSuccess={onSuccess} />)
|
||
await chooseVideoMaterialType(user)
|
||
|
||
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').filter((control) => control.id !== 'upload-material-type')
|
||
await selectOption(firstMetadataSelects[0], '通用')
|
||
await selectOption(firstMetadataSelects[1], '通用')
|
||
|
||
const firstPreviewInput = screen.getByLabelText(/^视频预览图/) as HTMLInputElement
|
||
await waitFor(() => expect(firstPreviewInput).not.toBeDisabled())
|
||
changeField(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').filter((control) => control.id !== 'upload-material-type')
|
||
await selectOption(secondMetadataSelects[2], '通用')
|
||
await selectOption(secondMetadataSelects[3], '通用')
|
||
await waitFor(() => expect(secondPreviewInput).not.toBeDisabled())
|
||
changeField(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()
|
||
})
|
||
})
|