gongxiangp2/components/upload/VideoPreviewImageUpload.test.tsx
2026-08-07 23:28:55 +08:00

203 lines
6.6 KiB
TypeScript

/* eslint-disable @next/next/no-img-element, jsx-a11y/alt-text */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import gsap from 'gsap'
import VideoPreviewImageUpload from './VideoPreviewImageUpload'
function createPngFile(name: string, width = 1920, height = 1080) {
const bytes = new Uint8Array(24)
bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
const view = new DataView(bytes.buffer)
view.setUint32(16, width)
view.setUint32(20, height)
return new File([bytes], name, { type: 'image/png' })
}
function setReducedMotionPreference(reducedMotion: boolean) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query: string) => ({
matches: query === '(prefers-reduced-motion: no-preference)' && !reducedMotion,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
}
beforeEach(() => {
setReducedMotionPreference(false)
})
jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => {
const { fill, unoptimized, sizes, ...rest } = props
return <img {...rest} />
},
}))
describe('VideoPreviewImageUpload', () => {
test('空状态明确说明选填和自动截取规则', () => {
render(
<VideoPreviewImageUpload
onFileSelect={jest.fn()}
onRemove={jest.fn()}
onValidationError={jest.fn()}
/>,
)
expect(screen.getByText('视频预览图')).toBeInTheDocument()
expect(screen.getByText('(选填)')).toBeInTheDocument()
expect(screen.getByText('未填写时自动从视频截取')).toBeInTheDocument()
expect(screen.getByText('添加预览图')).toBeInTheDocument()
})
test('选择有效图片后交给上传面板处理', async () => {
const onFileSelect = jest.fn().mockResolvedValue(undefined)
const onValidationError = jest.fn()
const onProcessingChange = jest.fn()
const { container } = render(
<VideoPreviewImageUpload
onFileSelect={onFileSelect}
onRemove={jest.fn()}
onValidationError={onValidationError}
onProcessingChange={onProcessingChange}
/>,
)
const input = container.querySelector('input[type="file"]') as HTMLInputElement
const cover = createPngFile('cover.png')
fireEvent.change(input, { target: { files: [cover] } })
await waitFor(() => expect(onFileSelect).toHaveBeenCalledWith(cover))
expect(onValidationError).toHaveBeenCalledWith('')
await waitFor(() => expect(onProcessingChange.mock.calls).toEqual([[true], [false]]))
})
test('处理失败时保留具体原因并解除处理中状态', async () => {
const onValidationError = jest.fn()
const onProcessingChange = jest.fn()
const { container } = render(
<VideoPreviewImageUpload
onFileSelect={jest.fn().mockRejectedValue(new Error('预览图处理失败'))}
onRemove={jest.fn()}
onValidationError={onValidationError}
onProcessingChange={onProcessingChange}
/>,
)
const input = container.querySelector('input[type="file"]') as HTMLInputElement
fireEvent.change(input, {
target: { files: [createPngFile('cover.png')] },
})
await waitFor(() => expect(onValidationError).toHaveBeenLastCalledWith('预览图处理失败'))
expect(onProcessingChange.mock.calls).toEqual([[true], [false]])
})
test('在交给上传面板前拒绝超大像素图片', async () => {
const onFileSelect = jest.fn()
const onValidationError = jest.fn()
const { container } = render(
<VideoPreviewImageUpload
onFileSelect={onFileSelect}
onRemove={jest.fn()}
onValidationError={onValidationError}
/>,
)
const input = container.querySelector('input[type="file"]') as HTMLInputElement
fireEvent.change(input, {
target: { files: [createPngFile('huge.png', 10_000, 7_000)] },
})
await waitFor(() => expect(onValidationError).toHaveBeenLastCalledWith(
expect.stringContaining('像素总量超过 60,000,000'),
))
expect(onFileSelect).not.toHaveBeenCalled()
})
test('拒绝不支持的图片格式并给出恢复方向', () => {
const onFileSelect = jest.fn()
const onValidationError = jest.fn()
const { container } = render(
<VideoPreviewImageUpload
onFileSelect={onFileSelect}
onRemove={jest.fn()}
onValidationError={onValidationError}
/>,
)
const input = container.querySelector('input[type="file"]') as HTMLInputElement
const gif = new File(['gif'], 'cover.gif', { type: 'image/gif' })
fireEvent.change(input, { target: { files: [gif] } })
expect(onFileSelect).not.toHaveBeenCalled()
expect(onValidationError).toHaveBeenCalledWith(expect.stringContaining('JPG、JPEG、PNG、WEBP'))
})
test('已选择状态支持更换和移除', () => {
const onRemove = jest.fn()
render(
<VideoPreviewImageUpload
previewUrl="blob:cover"
fileName="cover.jpg"
fileSize={2048}
onFileSelect={jest.fn()}
onRemove={onRemove}
onValidationError={jest.fn()}
/>,
)
expect(screen.getByAltText('已选择的视频预览图')).toBeInTheDocument()
expect(screen.getByText('cover.jpg')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '更换' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '移除视频预览图' }))
expect(onRemove).toHaveBeenCalledTimes(1)
})
test('预览图出现时使用短促的 GSAP 状态动画', async () => {
const animation = jest.spyOn(gsap, 'fromTo')
render(
<VideoPreviewImageUpload
previewUrl="blob:cover"
fileName="cover.jpg"
onFileSelect={jest.fn()}
onRemove={jest.fn()}
onValidationError={jest.fn()}
/>,
)
await waitFor(() => expect(animation).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
expect.objectContaining({ autoAlpha: 0.72, scale: 0.985, y: 4 }),
expect.objectContaining({ duration: 0.22, ease: 'power2.out' }),
))
animation.mockRestore()
})
test('系统要求减弱动态效果时不播放预览图动画', () => {
setReducedMotionPreference(true)
const animation = jest.spyOn(gsap, 'fromTo')
render(
<VideoPreviewImageUpload
previewUrl="blob:cover"
onFileSelect={jest.fn()}
onRemove={jest.fn()}
onValidationError={jest.fn()}
/>,
)
expect(animation).not.toHaveBeenCalled()
animation.mockRestore()
})
})