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

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

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

523 lines
17 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.

/**
* 系统数据完整性模块测试
* 测试 lib/system-integrity.ts 的核心功能
*/
import {
BUILTIN_TEMPLATE_KEYS,
BUILTIN_TEMPLATE_CONFIG,
type BuiltinTemplateKey,
checkSystemIntegrity,
repairSystemIntegrity,
ensureBuiltinTemplates,
} from '../system-integrity'
import { prisma } from '@/lib/prisma'
// Mock prisma
jest.mock('@/lib/prisma', () => ({
prisma: {
uploadTemplate: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
count: jest.fn(),
},
uploadField: {
create: jest.fn(),
},
material: {
findMany: jest.fn(),
count: jest.fn(),
updateMany: jest.fn(),
},
$transaction: jest.fn(),
},
}))
const mockPrisma = prisma as jest.Mocked<typeof prisma>
describe('系统数据完整性模块', () => {
describe('BUILTIN_TEMPLATE_KEYS', () => {
it('应该包含6个内置模板key', () => {
expect(BUILTIN_TEMPLATE_KEYS).toHaveLength(6)
})
it('应该包含所有预期的模板key', () => {
const expectedKeys = [
'3dmax-material',
'psd-material',
'audio-material',
'video-material',
'model-material',
'other-material',
]
expect([...BUILTIN_TEMPLATE_KEYS]).toEqual(expectedKeys)
})
it('所有key应该是字符串类型', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
expect(typeof key).toBe('string')
}
})
})
describe('BUILTIN_TEMPLATE_CONFIG', () => {
it('每个模板key都应该有对应的配置', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
expect(BUILTIN_TEMPLATE_CONFIG[key]).toBeDefined()
}
})
it('每个模板配置应该包含必要字段', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
expect(config.name).toBeTruthy()
expect(typeof config.name).toBe('string')
expect(config.description).toBeTruthy()
expect(typeof config.description).toBe('string')
expect(config.materialType).toBeTruthy()
expect(typeof config.materialType).toBe('string')
expect(config.materialKind).toBeTruthy()
expect(Array.isArray(config.fields)).toBe(true)
expect(config.fields.length).toBeGreaterThan(0)
}
})
it('materialKind 应该是有效值 (IMAGE/VIDEO/AUDIO/OTHER)', () => {
const validKinds = ['IMAGE', 'VIDEO', 'AUDIO', 'OTHER']
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
expect(validKinds).toContain(config.materialKind)
}
})
it('3DMAX模板应该包含renderer字段', () => {
const config = BUILTIN_TEMPLATE_CONFIG['3dmax-material']
const fieldKeys = config.fields.map(f => f.key)
expect(fieldKeys).toContain('renderer')
})
it('视频模板的materialKind应该是VIDEO', () => {
const config = BUILTIN_TEMPLATE_CONFIG['video-material']
expect(config.materialKind).toBe('VIDEO')
})
it('其他模板的materialKind应该是OTHER', () => {
const config = BUILTIN_TEMPLATE_CONFIG['other-material']
expect(config.materialKind).toBe('OTHER')
})
it('所有IMAGE类型模板应该正确标记', () => {
const imageTemplates: BuiltinTemplateKey[] = [
'3dmax-material',
'psd-material',
'model-material',
]
for (const key of imageTemplates) {
expect(BUILTIN_TEMPLATE_CONFIG[key].materialKind).toBe('IMAGE')
}
})
it('音频模板的materialKind应该是AUDIO', () => {
const config = BUILTIN_TEMPLATE_CONFIG['audio-material']
expect(config.materialKind).toBe('AUDIO')
})
})
describe('字段配置验证', () => {
it('每个字段应该有完整的配置', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
for (const field of config.fields) {
expect(field.key).toBeTruthy()
expect(typeof field.key).toBe('string')
expect(field.label).toBeTruthy()
expect(typeof field.label).toBe('string')
expect(field.type).toBeTruthy()
expect(typeof field.required).toBe('boolean')
expect(field.layoutWidth).toBeTruthy()
expect(typeof field.visibleOnHomeFilter).toBe('boolean')
expect(typeof field.isSearchable).toBe('boolean')
}
}
})
it('字段类型应该是有效值', () => {
const validTypes = ['text', 'select', 'number', 'textarea', 'date', 'checkbox']
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
for (const field of config.fields) {
expect(validTypes).toContain(field.type)
}
}
})
it('所有模板都应该包含productModel字段', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
const fieldKeys = config.fields.map(f => f.key)
expect(fieldKeys).toContain('productModel')
}
})
it('所有模板都应该包含分类字段(categoryName或audioCategory)', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
const fieldKeys = config.fields.map(f => f.key)
// 音频模板使用 audioCategory,其他模板使用 categoryName
const hasCategoryField = fieldKeys.includes('categoryName') || fieldKeys.includes('audioCategory')
expect(hasCategoryField).toBe(true)
}
})
it('所有模板都应该包含场景字段(sceneName或audioScene)', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
const fieldKeys = config.fields.map(f => f.key)
// 音频模板使用 audioScene,其他模板使用 sceneName
const hasSceneField = fieldKeys.includes('sceneName') || fieldKeys.includes('audioScene')
expect(hasSceneField).toBe(true)
}
})
it('分类和场景字段应该标记addToCategory', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
// 查找分类字段(categoryName 或 audioCategory)
const categoryField = config.fields.find(f => f.key === 'categoryName' || f.key === 'audioCategory')
// 查找场景字段(sceneName 或 audioScene)
const sceneField = config.fields.find(f => f.key === 'sceneName' || f.key === 'audioScene')
expect(categoryField?.addToCategory).toBe(true)
expect(sceneField?.addToCategory).toBe(true)
}
})
it('select类型字段应该标记addToCategory', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
const selectFields = config.fields.filter(f => f.type === 'select')
for (const field of selectFields) {
expect(field.addToCategory).toBe(true)
}
}
})
})
describe('模板ID生成规则', () => {
it('内置模板ID应该以builtin-开头', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const templateId = `builtin-${key}`
expect(templateId).toMatch(/^builtin-/)
}
})
it('生成的模板ID应该唯一', () => {
const ids = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
})
it('模板ID格式应该符合规范', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const templateId = `builtin-${key}`
// 只包含字母、数字和连字符
expect(templateId).toMatch(/^[a-z0-9-]+$/)
}
})
})
describe('模板配置一致性', () => {
it('所有模板的materialType应该唯一', () => {
const materialTypes = BUILTIN_TEMPLATE_KEYS.map(
key => BUILTIN_TEMPLATE_CONFIG[key].materialType
)
const uniqueTypes = new Set(materialTypes)
expect(uniqueTypes.size).toBe(materialTypes.length)
})
it('模板名称应该唯一', () => {
const names = BUILTIN_TEMPLATE_KEYS.map(
key => BUILTIN_TEMPLATE_CONFIG[key].name
)
const uniqueNames = new Set(names)
expect(uniqueNames.size).toBe(names.length)
})
it('每个模板的字段key应该唯一', () => {
for (const key of BUILTIN_TEMPLATE_KEYS) {
const config = BUILTIN_TEMPLATE_CONFIG[key]
const fieldKeys = config.fields.map(f => f.key)
const uniqueKeys = new Set(fieldKeys)
expect(uniqueKeys.size).toBe(fieldKeys.length)
}
})
})
})
describe('checkSystemIntegrity', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('当所有内置模板存在且无孤儿素材时应返回healthy=true', async () => {
// Mock: 所有内置模板都存在
const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`)
;(mockPrisma.uploadTemplate.findMany as jest.Mock)
.mockResolvedValueOnce(expectedIds.map(id => ({ id }))) // 内置模板检查
.mockResolvedValueOnce([]) // 引用模板检查
// Mock: 没有素材引用模板
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
const result = await checkSystemIntegrity()
expect(result.healthy).toBe(true)
expect(result.checks.builtinTemplates.healthy).toBe(true)
expect(result.checks.builtinTemplates.missing).toHaveLength(0)
expect(result.checks.orphanedMaterials.healthy).toBe(true)
expect(result.recommendations).toHaveLength(0)
})
it('当缺少内置模板时应返回healthy=false', async () => {
// Mock: 只有部分模板存在
;(mockPrisma.uploadTemplate.findMany as jest.Mock)
.mockResolvedValueOnce([
{ id: 'builtin-3dmax-material' },
{ id: 'builtin-psd-material' },
])
.mockResolvedValueOnce([])
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
const result = await checkSystemIntegrity()
expect(result.healthy).toBe(false)
expect(result.checks.builtinTemplates.healthy).toBe(false)
expect(result.checks.builtinTemplates.existing).toBe(2)
expect(result.checks.builtinTemplates.missing.length).toBe(4)
expect(result.recommendations.length).toBeGreaterThan(0)
})
it('当存在孤儿素材时应返回healthy=false', async () => {
// Mock: 所有内置模板存在
const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`)
;(mockPrisma.uploadTemplate.findMany as jest.Mock)
.mockResolvedValueOnce(expectedIds.map(id => ({ id })))
.mockResolvedValueOnce([]) // 引用的模板不存在
// Mock: 有素材引用不存在的模板
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([
{ templateId: 'deleted-template-1' },
{ templateId: 'deleted-template-2' },
])
;(mockPrisma.material.count as jest.Mock).mockResolvedValue(5)
const result = await checkSystemIntegrity()
expect(result.healthy).toBe(false)
expect(result.checks.orphanedMaterials.healthy).toBe(false)
expect(result.checks.orphanedMaterials.count).toBe(5)
expect(result.checks.orphanedMaterials.templateIds).toContain('deleted-template-1')
})
it('应该返回正确的时间戳格式', async () => {
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
const result = await checkSystemIntegrity()
expect(result.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
})
})
describe('repairSystemIntegrity', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('应该创建缺失的内置模板', async () => {
// Mock transaction 执行创建逻辑
;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => {
const tx = {
uploadTemplate: {
findUnique: jest.fn().mockResolvedValue(null), // 模板不存在
create: jest.fn().mockResolvedValue({ id: 'test' }),
},
uploadField: {
create: jest.fn().mockResolvedValue({}),
},
}
return callback(tx)
})
// Mock 孤儿素材检查
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
const result = await repairSystemIntegrity({
createMissingBuiltinTemplates: true,
fixOrphanedMaterials: false,
})
expect(result.success).toBe(true)
expect(result.repairs.templatesCreated.length).toBe(6)
expect(result.errors).toHaveLength(0)
})
it('当模板已存在时应跳过创建', async () => {
;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => {
const tx = {
uploadTemplate: {
findUnique: jest.fn().mockResolvedValue({ id: 'existing' }), // 模板已存在
create: jest.fn(),
},
uploadField: {
create: jest.fn(),
},
}
return callback(tx)
})
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
const result = await repairSystemIntegrity({
createMissingBuiltinTemplates: true,
fixOrphanedMaterials: false,
})
expect(result.success).toBe(true)
expect(result.repairs.templatesCreated).toHaveLength(0)
})
it('应该修复孤儿素材', async () => {
;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => {
const tx = {
uploadTemplate: {
findUnique: jest.fn().mockResolvedValue({ id: 'existing' }),
create: jest.fn(),
},
uploadField: {
create: jest.fn(),
},
}
return callback(tx)
})
// Mock: 有素材引用不存在的自定义模板
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([
{ templateId: 'custom-deleted-template' },
])
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.material.updateMany as jest.Mock).mockResolvedValue({ count: 3 })
const result = await repairSystemIntegrity({
createMissingBuiltinTemplates: false,
fixOrphanedMaterials: true,
})
expect(result.success).toBe(true)
expect(result.repairs.orphanedMaterialsFixed).toBe(3)
})
it('创建模板失败时应记录错误', async () => {
;(mockPrisma.$transaction as jest.Mock).mockRejectedValue(new Error('Database error'))
;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
const result = await repairSystemIntegrity({
createMissingBuiltinTemplates: true,
fixOrphanedMaterials: false,
})
expect(result.success).toBe(false)
expect(result.errors.length).toBeGreaterThan(0)
expect(result.repairs.templatesFailed.length).toBe(6)
})
})
describe('ensureBuiltinTemplates', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('当所有模板已存在时应返回空created数组', async () => {
const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`)
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue(
expectedIds.map(id => ({ id }))
)
const result = await ensureBuiltinTemplates()
expect(result.checked).toBe(true)
expect(result.created).toHaveLength(0)
expect(result.errors).toHaveLength(0)
})
it('应该只创建缺失的模板', async () => {
// Mock: 只有2个模板存在
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([
{ id: 'builtin-3dmax-material' },
{ id: 'builtin-psd-material' },
])
;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => {
const tx = {
uploadTemplate: {
findUnique: jest.fn().mockResolvedValue(null),
create: jest.fn().mockResolvedValue({}),
},
uploadField: {
create: jest.fn().mockResolvedValue({}),
},
}
await callback(tx)
})
const result = await ensureBuiltinTemplates()
expect(result.checked).toBe(true)
expect(result.created.length).toBe(4) // 6 - 2 = 4
})
it('并发创建时应忽略唯一约束冲突', async () => {
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.$transaction as jest.Mock).mockRejectedValue(
new Error('Unique constraint failed')
)
const result = await ensureBuiltinTemplates()
expect(result.checked).toBe(true)
expect(result.errors).toHaveLength(0) // 唯一约束错误被忽略
})
it('其他错误应该被记录', async () => {
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([])
;(mockPrisma.$transaction as jest.Mock).mockRejectedValue(
new Error('Connection timeout')
)
const result = await ensureBuiltinTemplates()
expect(result.checked).toBe(true)
expect(result.errors.length).toBeGreaterThan(0)
expect(result.errors[0]).toContain('Connection timeout')
})
it('初始查询失败时应返回checked=false', async () => {
;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockRejectedValue(
new Error('Database unavailable')
)
const result = await ensureBuiltinTemplates()
expect(result.checked).toBe(false)
expect(result.errors).toContain('Database unavailable')
})
})