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

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

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

377 lines
13 KiB
TypeScript

/**
* 字段迁移工具测试
*/
import {
checkTypeCompatibility,
convertFieldValue,
renameFieldInMetadata,
deleteFieldFromMetadata,
restoreFieldInMetadata,
validateMigrationOperation,
generateMigrationPreview,
createRollbackData,
MigrationOperation,
} from '../field-migration'
describe('field-migration', () => {
describe('checkTypeCompatibility', () => {
it('should return compatible for same type', () => {
const result = checkTypeCompatibility('text', 'text')
expect(result.compatible).toBe(true)
expect(result.canConvert).toBe(true)
})
it('should allow text to textarea', () => {
const result = checkTypeCompatibility('text', 'textarea')
expect(result.compatible).toBe(true)
})
it('should warn for textarea to text', () => {
const result = checkTypeCompatibility('textarea', 'text')
expect(result.compatible).toBe(true)
expect(result.warning).toContain('截断')
})
it('should not allow text to number', () => {
const result = checkTypeCompatibility('text', 'number')
expect(result.compatible).toBe(false)
expect(result.canConvert).toBe(false)
})
it('should allow number to text', () => {
const result = checkTypeCompatibility('number', 'text')
expect(result.compatible).toBe(true)
})
it('should warn for text to select', () => {
const result = checkTypeCompatibility('text', 'select')
expect(result.compatible).toBe(true)
expect(result.warning).toContain('选项列表')
})
it('should handle unknown type combinations', () => {
const result = checkTypeCompatibility('unknown', 'another')
expect(result.compatible).toBe(false)
expect(result.canConvert).toBe(false)
})
})
describe('convertFieldValue', () => {
it('should convert to text', () => {
expect(convertFieldValue(123, 'number', 'text')).toEqual({ success: true, value: '123' })
expect(convertFieldValue(true, 'checkbox', 'text')).toEqual({ success: true, value: 'true' })
})
it('should convert to number', () => {
expect(convertFieldValue('123', 'text', 'number')).toEqual({ success: true, value: 123 })
expect(convertFieldValue('45.67', 'text', 'number')).toEqual({ success: true, value: 45.67 })
})
it('should fail converting invalid string to number', () => {
const result = convertFieldValue('abc', 'text', 'number')
expect(result.success).toBe(false)
expect(result.error).toContain('无法将')
})
it('should convert to checkbox', () => {
expect(convertFieldValue('true', 'text', 'checkbox')).toEqual({ success: true, value: true })
expect(convertFieldValue('1', 'text', 'checkbox')).toEqual({ success: true, value: true })
expect(convertFieldValue('false', 'text', 'checkbox')).toEqual({ success: true, value: false })
expect(convertFieldValue('0', 'text', 'checkbox')).toEqual({ success: true, value: false })
})
it('should convert to date', () => {
const result = convertFieldValue('2024-01-15', 'text', 'date')
expect(result.success).toBe(true)
expect(result.value).toBe('2024-01-15')
})
it('should fail converting invalid string to date', () => {
const result = convertFieldValue('not a date', 'text', 'date')
expect(result.success).toBe(false)
expect(result.error).toContain('无法将')
})
it('should handle null/undefined', () => {
expect(convertFieldValue(null, 'text', 'number')).toEqual({ success: true, value: null })
expect(convertFieldValue(undefined, 'text', 'number')).toEqual({ success: true, value: null })
})
it('should keep value for select type', () => {
const result = convertFieldValue('option1', 'text', 'select')
expect(result.success).toBe(true)
expect(result.value).toBe('option1')
})
})
describe('renameFieldInMetadata', () => {
it('should rename field in metadata', () => {
const metadata = JSON.stringify({
templateFields: { oldField: 'value' },
})
const result = renameFieldInMetadata(metadata, 'oldField', 'newField')
expect(result.migrated).toBe(true)
expect(result.oldValue).toBe('value')
const parsed = JSON.parse(result.metadata!)
expect(parsed.templateFields.newField).toBe('value')
expect(parsed.templateFields.oldField).toBeUndefined()
})
it('should return unchanged if field not found', () => {
const metadata = JSON.stringify({
templateFields: { otherField: 'value' },
})
const result = renameFieldInMetadata(metadata, 'oldField', 'newField')
expect(result.migrated).toBe(false)
expect(result.oldValue).toBeUndefined()
})
it('should handle null metadata', () => {
const result = renameFieldInMetadata(null, 'oldField', 'newField')
expect(result.migrated).toBe(false)
expect(result.metadata).toBeNull()
})
})
describe('deleteFieldFromMetadata', () => {
it('should delete field from metadata', () => {
const metadata = JSON.stringify({
templateFields: { field1: 'value1', field2: 'value2' },
})
const result = deleteFieldFromMetadata(metadata, 'field1')
expect(result.deleted).toBe(true)
expect(result.oldValue).toBe('value1')
const parsed = JSON.parse(result.metadata!)
expect(parsed.templateFields.field1).toBeUndefined()
expect(parsed.templateFields.field2).toBe('value2')
})
it('should return unchanged if field not found', () => {
const metadata = JSON.stringify({
templateFields: { otherField: 'value' },
})
const result = deleteFieldFromMetadata(metadata, 'nonExistent')
expect(result.deleted).toBe(false)
})
it('should handle null metadata', () => {
const result = deleteFieldFromMetadata(null, 'field')
expect(result.deleted).toBe(false)
expect(result.metadata).toBeNull()
})
})
describe('restoreFieldInMetadata', () => {
it('should restore field to metadata', () => {
const metadata = JSON.stringify({
templateFields: { existingField: 'value' },
})
const result = restoreFieldInMetadata(metadata, 'newField', 'restoredValue')
expect(result.restored).toBe(true)
const parsed = JSON.parse(result.metadata!)
expect(parsed.templateFields.newField).toBe('restoredValue')
expect(parsed.templateFields.existingField).toBe('value')
})
it('should not override existing field', () => {
const metadata = JSON.stringify({
templateFields: { existingField: 'originalValue' },
})
const result = restoreFieldInMetadata(metadata, 'existingField', 'newValue')
expect(result.restored).toBe(false)
const parsed = JSON.parse(result.metadata!)
expect(parsed.templateFields.existingField).toBe('originalValue')
})
it('should handle null metadata', () => {
const result = restoreFieldInMetadata(null, 'field', 'value')
expect(result.restored).toBe(true)
const parsed = JSON.parse(result.metadata!)
expect(parsed.templateFields.field).toBe('value')
})
})
describe('validateMigrationOperation', () => {
describe('rename operation', () => {
it('should validate valid rename', () => {
const op: MigrationOperation = { type: 'rename', oldKey: 'customField', newKey: 'newCustomField' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(true)
})
it('should reject empty keys', () => {
const op: MigrationOperation = { type: 'rename', oldKey: '', newKey: 'newField' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
it('should reject same keys', () => {
const op: MigrationOperation = { type: 'rename', oldKey: 'field', newKey: 'field' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
it('should reject renaming standard fields', () => {
const op: MigrationOperation = { type: 'rename', oldKey: 'productModel', newKey: 'newField' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
expect(result.error).toContain('标准字段')
})
})
describe('delete operation', () => {
it('should validate valid delete', () => {
const op: MigrationOperation = { type: 'delete', key: 'customField' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(true)
})
it('should reject empty key', () => {
const op: MigrationOperation = { type: 'delete', key: '' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
it('should reject deleting standard fields', () => {
const op: MigrationOperation = { type: 'delete', key: 'categoryName' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
expect(result.error).toContain('标准字段')
})
})
describe('restore operation', () => {
it('should validate valid restore', () => {
const op: MigrationOperation = { type: 'restore', key: 'field', defaultValue: 'default' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(true)
})
it('should reject empty key', () => {
const op: MigrationOperation = { type: 'restore', key: '' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
})
describe('typeChange operation', () => {
it('should validate compatible type change', () => {
const op: MigrationOperation = { type: 'typeChange', key: 'field', oldType: 'text', newType: 'textarea' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(true)
})
it('should reject incompatible type change', () => {
const op: MigrationOperation = { type: 'typeChange', key: 'field', oldType: 'text', newType: 'number' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
it('should reject missing parameters', () => {
const op: MigrationOperation = { type: 'typeChange', key: '', oldType: 'text', newType: 'textarea' }
const result = validateMigrationOperation(op)
expect(result.valid).toBe(false)
})
})
})
describe('generateMigrationPreview', () => {
const materials = [
{ id: 'mat1', metadata: JSON.stringify({ templateFields: { field1: 'value1' } }) },
{ id: 'mat2', metadata: JSON.stringify({ templateFields: { field1: 'value2', field2: 'other' } }) },
{ id: 'mat3', metadata: JSON.stringify({ templateFields: { field2: 'only2' } }) },
]
it('should preview rename operation', () => {
const op: MigrationOperation = { type: 'rename', oldKey: 'field1', newKey: 'newField1' }
const result = generateMigrationPreview(materials, op)
expect(result.affectedCount).toBe(2)
expect(result.sampleChanges).toHaveLength(2)
expect(result.sampleChanges[0].before).toEqual({ field1: 'value1' })
expect(result.sampleChanges[0].after).toEqual({ newField1: 'value1' })
})
it('should preview delete operation', () => {
const op: MigrationOperation = { type: 'delete', key: 'field1' }
const result = generateMigrationPreview(materials, op)
expect(result.affectedCount).toBe(2)
expect(result.sampleChanges[0].after).toEqual({ field1: undefined })
})
it('should preview restore operation', () => {
const op: MigrationOperation = { type: 'restore', key: 'newField', defaultValue: 'default' }
const result = generateMigrationPreview(materials, op)
expect(result.affectedCount).toBe(3) // All materials don't have newField
expect(result.sampleChanges[0].after).toEqual({ newField: 'default' })
})
it('should preview typeChange operation', () => {
const op: MigrationOperation = { type: 'typeChange', key: 'field1', oldType: 'text', newType: 'textarea' }
const result = generateMigrationPreview(materials, op)
expect(result.affectedCount).toBe(2)
expect(result.sampleChanges[0].before).toHaveProperty('type', 'text')
expect(result.sampleChanges[0].after).toHaveProperty('type', 'textarea')
})
it('should limit sample changes to 5', () => {
const manyMaterials = Array.from({ length: 10 }, (_, i) => ({
id: `mat${i}`,
metadata: JSON.stringify({ templateFields: { field1: `value${i}` } }),
}))
const op: MigrationOperation = { type: 'rename', oldKey: 'field1', newKey: 'newField1' }
const result = generateMigrationPreview(manyMaterials, op)
expect(result.affectedCount).toBe(10)
expect(result.sampleChanges).toHaveLength(5)
})
})
describe('createRollbackData', () => {
it('should create rollback data', () => {
const op: MigrationOperation = { type: 'rename', oldKey: 'old', newKey: 'new' }
const metadata = JSON.stringify({ templateFields: { old: 'value' } })
const result = createRollbackData('mat1', op, metadata)
expect(result.materialId).toBe('mat1')
expect(result.operation).toEqual(op)
expect(result.originalMetadata).toBe(metadata)
expect(result.timestamp).toBeDefined()
})
it('should handle null metadata', () => {
const op: MigrationOperation = { type: 'delete', key: 'field' }
const result = createRollbackData('mat1', op, null)
expect(result.originalMetadata).toBeNull()
})
})
})