203 lines
6.6 KiB
TypeScript
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()
|
|
})
|
|
})
|