/** * 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 }, })) // 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( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() expect(screen.getByText('预览图')).toBeInTheDocument() }) it('应该显示预览图', () => { render( ) const previewImage = screen.getByAltText('预览图') expect(previewImage).toBeInTheDocument() expect(previewImage).toHaveAttribute('src', baseMaterial.previewUrl) }) it('应该显示取消和保存按钮', () => { render( ) expect(screen.getByText('取消')).toBeInTheDocument() expect(screen.getByText('保存修改')).toBeInTheDocument() }) }) describe('音频字段支持测试', () => { it('应该正确初始化音频相关字段', async () => { render( ) // 等待组件初始化 await waitFor(() => { expect(mockFetch).toHaveBeenCalled() }) // 验证音频字段值被正确初始化(通过检查组件内部状态) // 由于字段是通过模板动态渲染的,我们验证 API 调用 expect(mockFetch).toHaveBeenCalledWith( '/api/template-categories', expect.any(Object) ) }) it('应该接受 audioDuration 字段', () => { const materialWithDuration = { ...baseMaterial, audioDuration: '05:30', } render( ) // 组件应该正常渲染,不抛出错误 expect(screen.getByText('编辑素材')).toBeInTheDocument() }) it('应该接受 audioCategory 字段', () => { const materialWithCategory = { ...baseMaterial, audioCategory: '背景音乐', } render( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() }) it('应该接受 audioScene 字段', () => { const materialWithScene = { ...baseMaterial, audioScene: '产品展示', } render( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() }) it('应该处理所有音频字段为 null 的情况', () => { const materialWithNullAudio = { ...baseMaterial, audioDuration: null, audioCategory: null, audioScene: null, } render( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() }) it('应该处理音频字段为 undefined 的情况', () => { const materialWithUndefinedAudio = { ...baseMaterial, // 不设置音频字段,它们将是 undefined } render( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() }) }) describe('交互测试', () => { it('点击取消按钮应该调用 onClose', () => { render( ) fireEvent.click(screen.getByText('取消')) expect(mockOnClose).toHaveBeenCalled() }) it('应该能够更换预览图', async () => { render( ) // 找到隐藏的文件输入 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( ) // 等待模板加载 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( ) 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( ) expect(screen.getByText('编辑素材')).toBeInTheDocument() }) }) }) describe('EditMaterialModal 类型安全测试', () => { it('material 接口应该包含音频字段', () => { // 这个测试验证 TypeScript 类型定义是否正确 // 如果类型定义不正确,编译时就会报错 const material: Parameters[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[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() }) })