flnxi-Wu/components/__tests__/EditMaterialModal.test.tsx
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

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