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

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

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

472 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 模板CRUD API单元测试
*
* 测试模板的创建、读取、更新、删除功能
* 包括版本控制、数据验证、错误处理
*/
import { NextRequest } from 'next/server'
// Mock dependencies
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
uploadTemplate: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
material: {
findMany: jest.fn(),
},
$transaction: jest.fn(),
},
}))
jest.mock('@/lib/field-pool', () => ({
syncFieldPoolFromTemplates: jest.fn(),
}))
const { getServerSession } = require('next-auth')
const { prisma } = require('@/lib/prisma')
const { syncFieldPoolFromTemplates } = require('@/lib/field-pool')
// 导入API处理函数
import { GET as listTemplates, POST as createTemplate } from '../route'
import {
GET as getTemplate,
PUT as updateTemplate,
DELETE as deleteTemplate
} from '../[id]/route'
describe('模板CRUD API测试', () => {
beforeEach(() => {
jest.clearAllMocks()
// 默认模拟管理员会话
getServerSession.mockResolvedValue({
user: { id: 'admin-id', role: 'ADMIN' }
})
})
describe('GET /api/admin/upload-templates - 获取模板列表', () => {
test('应该返回模板列表(包含materialCount)', async () => {
const mockTemplates = [
{
id: 'template-1',
name: '测试模板1',
status: 'active',
version: 1,
_count: { materials: 5 },
},
{
id: 'template-2',
name: '测试模板2',
status: 'draft',
version: 2,
_count: { materials: 0 },
},
]
prisma.uploadTemplate.findMany.mockResolvedValue(mockTemplates)
const response = await listTemplates()
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toHaveLength(2)
expect(data[0].materialCount).toBe(5)
expect(data[1].materialCount).toBe(0)
})
test('应该拒绝非管理员访问', async () => {
getServerSession.mockResolvedValue({
user: { id: 'user-id', role: 'USER' }
})
const response = await listTemplates()
const data = await response.json()
expect(response.status).toBe(403)
expect(data.errorCode).toBe('UNAUTHORIZED')
})
test('应该处理数据库错误', async () => {
prisma.uploadTemplate.findMany.mockRejectedValue(new Error('数据库错误'))
const response = await listTemplates()
const data = await response.json()
expect(response.status).toBe(500)
expect(data.errorCode).toBe('LIST_FAILED')
})
})
describe('POST /api/admin/upload-templates - 创建模板', () => {
test('应该成功创建模板', async () => {
const mockTemplate = {
id: 'new-template-id',
name: '新模板',
materialType: '3DMAX',
materialKind: 'IMAGE',
status: 'draft',
version: 1,
}
prisma.uploadTemplate.create.mockResolvedValue(mockTemplate)
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '新模板',
materialType: '3DMAX',
materialKind: 'IMAGE',
fields: [],
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(201)
expect(data.name).toBe('新模板')
expect(syncFieldPoolFromTemplates).toHaveBeenCalled()
})
test('应该验证必填字段 - name', async () => {
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
materialType: '3DMAX',
materialKind: 'IMAGE',
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('VALIDATION_ERROR')
expect(data.field).toBe('name')
})
test('应该验证必填字段 - materialType', async () => {
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '测试模板',
materialKind: 'IMAGE',
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('VALIDATION_ERROR')
expect(data.field).toBe('materialType')
})
test('应该验证materialKind的有效值', async () => {
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '测试模板',
materialType: '3DMAX',
materialKind: 'INVALID',
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('VALIDATION_ERROR')
expect(data.field).toBe('materialKind')
})
test('应该检查字段key的唯一性', async () => {
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '测试模板',
materialType: '3DMAX',
materialKind: 'IMAGE',
fields: [
{ key: 'field1', label: '字段1', type: 'text' },
{ key: 'field1', label: '字段2', type: 'text' }, // 重复的key
],
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('VALIDATION_ERROR')
expect(data.details.duplicateKeys).toContain('field1')
})
test('应该自动转换optionsStatic数组为JSON字符串', async () => {
const mockTemplate = {
id: 'new-template-id',
name: '新模板',
version: 1,
}
prisma.uploadTemplate.create.mockResolvedValue(mockTemplate)
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '新模板',
materialType: '3DMAX',
materialKind: 'IMAGE',
fields: [
{
key: 'category',
label: '分类',
type: 'select',
optionsStatic: ['选项1', '选项2', '选项3'] // 数组格式
},
],
}),
})
await createTemplate(request)
expect(prisma.uploadTemplate.create).toHaveBeenCalled()
const createData = prisma.uploadTemplate.create.mock.calls[0][0].data
const field = createData.fields.createMany.data[0]
expect(typeof field.optionsStatic).toBe('string')
expect(JSON.parse(field.optionsStatic)).toEqual(['选项1', '选项2', '选项3'])
})
test('应该处理重复名称错误', async () => {
const prismaError = new Error('Unique constraint failed')
;(prismaError as any).code = 'P2002'
;(prismaError as any).meta = { target: ['name'] }
prisma.uploadTemplate.create.mockRejectedValue(prismaError)
const request = new NextRequest('http://localhost/api/admin/upload-templates', {
method: 'POST',
body: JSON.stringify({
name: '重复模板',
materialType: '3DMAX',
materialKind: 'IMAGE',
}),
})
const response = await createTemplate(request)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('DUPLICATE_ENTRY')
})
})
describe('GET /api/admin/upload-templates/[id] - 获取单个模板', () => {
test('应该返回模板详情(包含统计信息)', async () => {
const mockTemplate = {
id: 'template-1',
name: '测试模板',
version: 1,
fields: [],
_count: { materials: 10 },
}
const mockMaterials = [
{ id: 'mat-1', productModel: '产品1', previewUrl: 'url1', createdAt: new Date() },
]
prisma.uploadTemplate.findUnique.mockResolvedValue(mockTemplate)
prisma.material.findMany.mockResolvedValue(mockMaterials)
const response = await getTemplate(
new NextRequest('http://localhost'),
{ params: { id: 'template-1' } }
)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.materialCount).toBe(10)
expect(data.materials).toHaveLength(1)
})
test('应该返回404当模板不存在', async () => {
prisma.uploadTemplate.findUnique.mockResolvedValue(null)
const response = await getTemplate(
new NextRequest('http://localhost'),
{ params: { id: 'non-existent' } }
)
const data = await response.json()
expect(response.status).toBe(404)
expect(data.errorCode).toBe('NOT_FOUND')
})
})
describe('PUT /api/admin/upload-templates/[id] - 更新模板', () => {
test('应该成功更新模板并递增版本号', async () => {
const existingTemplate = {
id: 'template-1',
name: '旧名称',
version: 1,
updatedAt: new Date(),
}
const updatedTemplate = {
id: 'template-1',
name: '新名称',
version: 2,
}
prisma.uploadTemplate.findUnique.mockResolvedValue(existingTemplate)
prisma.$transaction.mockImplementation(async (callback: (tx: any) => Promise<any>) => {
return callback({
uploadTemplate: {
update: jest.fn().mockResolvedValue({ id: 'template-1', version: 2 }),
},
})
})
prisma.uploadTemplate.findUnique.mockResolvedValueOnce(existingTemplate)
.mockResolvedValueOnce(updatedTemplate)
const request = new NextRequest('http://localhost', {
method: 'PUT',
body: JSON.stringify({
name: '新名称',
version: 1, // 客户端版本
}),
})
const response = await updateTemplate(request, { params: { id: 'template-1' } })
const data = await response.json()
expect(response.status).toBe(200)
expect(data.version).toBe(2)
})
test('应该检测版本冲突', async () => {
const existingTemplate = {
id: 'template-1',
name: '模板',
version: 3, // 服务器版本已更新
updatedAt: new Date(),
}
prisma.uploadTemplate.findUnique.mockResolvedValue(existingTemplate)
const request = new NextRequest('http://localhost', {
method: 'PUT',
body: JSON.stringify({
name: '新名称',
version: 2, // 客户端版本过旧
}),
})
const response = await updateTemplate(request, { params: { id: 'template-1' } })
const data = await response.json()
expect(response.status).toBe(409)
expect(data.errorCode).toBe('VERSION_CONFLICT')
expect(data.details.clientVersion).toBe(2)
expect(data.details.serverVersion).toBe(3)
})
test('应该验证字段key唯一性', async () => {
const existingTemplate = {
id: 'template-1',
version: 1,
updatedAt: new Date(),
}
prisma.uploadTemplate.findUnique.mockResolvedValue(existingTemplate)
const request = new NextRequest('http://localhost', {
method: 'PUT',
body: JSON.stringify({
version: 1,
fields: [
{ key: 'field1', label: '字段1', type: 'text' },
{ key: 'field1', label: '字段2', type: 'text' },
],
}),
})
const response = await updateTemplate(request, { params: { id: 'template-1' } })
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('VALIDATION_ERROR')
})
})
describe('DELETE /api/admin/upload-templates/[id] - 删除模板', () => {
test('应该成功删除未使用的模板', async () => {
const mockTemplate = {
id: 'template-1',
name: '测试模板',
_count: { materials: 0 }, // 没有素材使用
}
prisma.uploadTemplate.findUnique.mockResolvedValue(mockTemplate)
prisma.uploadTemplate.delete.mockResolvedValue(mockTemplate)
const response = await deleteTemplate(
new NextRequest('http://localhost'),
{ params: { id: 'template-1' } }
)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(syncFieldPoolFromTemplates).toHaveBeenCalled()
})
test('应该阻止删除正在使用的模板', async () => {
const mockTemplate = {
id: 'template-1',
name: '使用中的模板',
_count: { materials: 5 }, // 有5个素材使用
}
prisma.uploadTemplate.findUnique.mockResolvedValue(mockTemplate)
const response = await deleteTemplate(
new NextRequest('http://localhost'),
{ params: { id: 'template-1' } }
)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.errorCode).toBe('TEMPLATE_IN_USE')
expect(data.details.materialCount).toBe(5)
expect(prisma.uploadTemplate.delete).not.toHaveBeenCalled()
})
test('应该返回404当模板不存在', async () => {
prisma.uploadTemplate.findUnique.mockResolvedValue(null)
const response = await deleteTemplate(
new NextRequest('http://localhost'),
{ params: { id: 'non-existent' } }
)
const data = await response.json()
expect(response.status).toBe(404)
expect(data.error).toContain('不存在')
})
})
})