1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
377 lines
13 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|