/** * 字段迁移工具测试 */ 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() }) }) })