1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
409 lines
11 KiB
TypeScript
409 lines
11 KiB
TypeScript
/**
|
|
* EditMaterialModal 组件测试
|
|
* 测试素材编辑模态框的核心功能,包括音频字段支持
|
|
* @jest-environment jsdom
|
|
*/
|
|
|
|
import React from 'react'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import '@testing-library/jest-dom'
|
|
|
|
// Mock next/image
|
|
jest.mock('next/image', () => ({
|
|
__esModule: true,
|
|
default: (props: any) => {
|
|
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
|
|
return <img {...props} />
|
|
},
|
|
}))
|
|
|
|
// Mock fetch
|
|
const mockFetch = jest.fn()
|
|
global.fetch = mockFetch
|
|
|
|
// Mock URL.createObjectURL
|
|
global.URL.createObjectURL = jest.fn(() => 'blob:test-url')
|
|
global.URL.revokeObjectURL = jest.fn()
|
|
|
|
import EditMaterialModal from '../EditMaterialModal'
|
|
|
|
describe('EditMaterialModal 组件', () => {
|
|
const mockOnClose = jest.fn()
|
|
const mockOnSuccess = jest.fn()
|
|
|
|
const baseMaterial = {
|
|
id: 'test-material-id',
|
|
previewUrl: 'https://example.com/preview.jpg',
|
|
productModel: 'Test Product',
|
|
categoryName: 'Test Category',
|
|
sceneName: 'Test Scene',
|
|
materialType: '3DMAX',
|
|
renderer: 'CR',
|
|
authorName: 'Test Author',
|
|
}
|
|
|
|
const audioMaterial = {
|
|
...baseMaterial,
|
|
materialType: '音频',
|
|
audioDuration: '03:45',
|
|
audioCategory: '背景音乐',
|
|
audioScene: '产品展示',
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
// Mock API responses
|
|
mockFetch.mockImplementation((url: string) => {
|
|
if (url.includes('/api/template-categories')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve([
|
|
{ fieldKey: 'categoryName', options: ['Category 1', 'Category 2'] },
|
|
{ fieldKey: 'sceneName', options: ['Scene 1', 'Scene 2'] },
|
|
{ fieldKey: 'audioCategory', options: ['背景音乐', '音效', '配音'] },
|
|
{ fieldKey: 'audioScene', options: ['产品展示', '广告', '教程'] },
|
|
]),
|
|
})
|
|
}
|
|
if (url.includes('/api/upload-templates')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve([]),
|
|
})
|
|
}
|
|
if (url.includes('/api/materials/update')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true }),
|
|
})
|
|
}
|
|
return Promise.resolve({
|
|
ok: false,
|
|
json: () => Promise.resolve({ error: 'Not found' }),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('基础渲染测试', () => {
|
|
it('应该正确渲染模态框', () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
expect(screen.getByText('预览图')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该显示预览图', () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
const previewImage = screen.getByAltText('预览图')
|
|
expect(previewImage).toBeInTheDocument()
|
|
expect(previewImage).toHaveAttribute('src', baseMaterial.previewUrl)
|
|
})
|
|
|
|
it('应该显示取消和保存按钮', () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('取消')).toBeInTheDocument()
|
|
expect(screen.getByText('保存修改')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('音频字段支持测试', () => {
|
|
it('应该正确初始化音频相关字段', async () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={audioMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
// 等待组件初始化
|
|
await waitFor(() => {
|
|
expect(mockFetch).toHaveBeenCalled()
|
|
})
|
|
|
|
// 验证音频字段值被正确初始化(通过检查组件内部状态)
|
|
// 由于字段是通过模板动态渲染的,我们验证 API 调用
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'/api/template-categories',
|
|
expect.any(Object)
|
|
)
|
|
})
|
|
|
|
it('应该接受 audioDuration 字段', () => {
|
|
const materialWithDuration = {
|
|
...baseMaterial,
|
|
audioDuration: '05:30',
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithDuration}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
// 组件应该正常渲染,不抛出错误
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该接受 audioCategory 字段', () => {
|
|
const materialWithCategory = {
|
|
...baseMaterial,
|
|
audioCategory: '背景音乐',
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithCategory}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该接受 audioScene 字段', () => {
|
|
const materialWithScene = {
|
|
...baseMaterial,
|
|
audioScene: '产品展示',
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithScene}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该处理所有音频字段为 null 的情况', () => {
|
|
const materialWithNullAudio = {
|
|
...baseMaterial,
|
|
audioDuration: null,
|
|
audioCategory: null,
|
|
audioScene: null,
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithNullAudio}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该处理音频字段为 undefined 的情况', () => {
|
|
const materialWithUndefinedAudio = {
|
|
...baseMaterial,
|
|
// 不设置音频字段,它们将是 undefined
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithUndefinedAudio}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('交互测试', () => {
|
|
it('点击取消按钮应该调用 onClose', () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('取消'))
|
|
expect(mockOnClose).toHaveBeenCalled()
|
|
})
|
|
|
|
it('应该能够更换预览图', async () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
// 找到隐藏的文件输入
|
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
|
|
expect(fileInput).toBeInTheDocument()
|
|
|
|
// 模拟文件选择
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
fireEvent.change(fileInput, { target: { files: [file] } })
|
|
|
|
// 验证 URL.createObjectURL 被调用
|
|
expect(global.URL.createObjectURL).toHaveBeenCalledWith(file)
|
|
})
|
|
})
|
|
|
|
describe('表单提交测试', () => {
|
|
it('提交表单应该调用更新 API', async () => {
|
|
render(
|
|
<EditMaterialModal
|
|
material={baseMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
// 等待模板加载
|
|
await waitFor(() => {
|
|
expect(mockFetch).toHaveBeenCalled()
|
|
})
|
|
|
|
// 点击保存按钮
|
|
fireEvent.click(screen.getByText('保存修改'))
|
|
|
|
// 验证 API 被调用
|
|
await waitFor(() => {
|
|
const updateCalls = mockFetch.mock.calls.filter(
|
|
(call: any[]) => call[0].includes('/api/materials/update')
|
|
)
|
|
expect(updateCalls.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('边界情况测试', () => {
|
|
it('应该处理空字符串的音频字段', () => {
|
|
const materialWithEmptyAudio = {
|
|
...baseMaterial,
|
|
audioDuration: '',
|
|
audioCategory: '',
|
|
audioScene: '',
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={materialWithEmptyAudio}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
|
|
it('应该处理所有可选字段都存在的情况', () => {
|
|
const fullMaterial = {
|
|
...baseMaterial,
|
|
brandName: 'Test Brand',
|
|
seriesName: 'Test Series',
|
|
skuCode: 'SKU001',
|
|
materialTexture: 'Leather',
|
|
renderTime: '2h',
|
|
imageResolution: '4K',
|
|
colorMode: 'RGB',
|
|
templateId: 'template-1',
|
|
platformName: 'Platform A',
|
|
audioDuration: '03:45',
|
|
audioCategory: '背景音乐',
|
|
audioScene: '产品展示',
|
|
}
|
|
|
|
render(
|
|
<EditMaterialModal
|
|
material={fullMaterial}
|
|
onClose={mockOnClose}
|
|
onSuccess={mockOnSuccess}
|
|
isOpen={true}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('编辑素材')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('EditMaterialModal 类型安全测试', () => {
|
|
it('material 接口应该包含音频字段', () => {
|
|
// 这个测试验证 TypeScript 类型定义是否正确
|
|
// 如果类型定义不正确,编译时就会报错
|
|
const material: Parameters<typeof EditMaterialModal>[0]['material'] = {
|
|
id: 'test',
|
|
previewUrl: 'url',
|
|
productModel: 'model',
|
|
categoryName: 'category',
|
|
sceneName: 'scene',
|
|
materialType: 'type',
|
|
renderer: 'renderer',
|
|
authorName: 'author',
|
|
audioDuration: '03:00',
|
|
audioCategory: 'music',
|
|
audioScene: 'scene',
|
|
}
|
|
|
|
expect(material.audioDuration).toBe('03:00')
|
|
expect(material.audioCategory).toBe('music')
|
|
expect(material.audioScene).toBe('scene')
|
|
})
|
|
|
|
it('音频字段应该是可选的', () => {
|
|
const materialWithoutAudio: Parameters<typeof EditMaterialModal>[0]['material'] = {
|
|
id: 'test',
|
|
previewUrl: 'url',
|
|
productModel: 'model',
|
|
categoryName: 'category',
|
|
sceneName: 'scene',
|
|
materialType: 'type',
|
|
renderer: 'renderer',
|
|
authorName: 'author',
|
|
}
|
|
|
|
expect(materialWithoutAudio.audioDuration).toBeUndefined()
|
|
expect(materialWithoutAudio.audioCategory).toBeUndefined()
|
|
expect(materialWithoutAudio.audioScene).toBeUndefined()
|
|
})
|
|
})
|