/** * 分类管理批量操作工具测试 * * 测试覆盖: * - CSV解析功能 * - JSON解析功能 * - CSV导出功能 * - JSON导出功能 * * 注意:数据库相关的函数需要集成测试或mock * * @jest-environment node * @created 2024-12-13 */ import { parseCSVOptions, parseJSONOptions, exportToCSV, exportToJSON, ImportedOption, ExportedField, } from '../category-batch-operations' describe('category-batch-operations', () => { describe('parseCSVOptions', () => { it('should parse simple CSV without header', () => { const csv = `categoryName,店铺A categoryName,店铺B sceneName,场景1` const result = parseCSVOptions(csv) expect(result).toHaveLength(3) expect(result[0]).toEqual({ fieldKey: 'categoryName', value: '店铺A', label: undefined }) expect(result[1]).toEqual({ fieldKey: 'categoryName', value: '店铺B', label: undefined }) expect(result[2]).toEqual({ fieldKey: 'sceneName', value: '场景1', label: undefined }) }) it('should parse CSV with header row', () => { const csv = `fieldKey,value,label categoryName,店铺A,店铺A显示名 categoryName,店铺B,店铺B显示名` const result = parseCSVOptions(csv) expect(result).toHaveLength(2) expect(result[0]).toEqual({ fieldKey: 'categoryName', value: '店铺A', label: '店铺A显示名' }) expect(result[1]).toEqual({ fieldKey: 'categoryName', value: '店铺B', label: '店铺B显示名' }) }) it('should handle CSV with optional label', () => { const csv = `categoryName,店铺A,显示名 categoryName,店铺B` const result = parseCSVOptions(csv) expect(result).toHaveLength(2) expect(result[0].label).toBe('显示名') expect(result[1].label).toBeUndefined() }) it('should handle quoted values with commas', () => { const csv = `categoryName,"店铺A,分店",显示名 categoryName,"店铺B"` const result = parseCSVOptions(csv) expect(result).toHaveLength(2) expect(result[0].value).toBe('店铺A,分店') expect(result[1].value).toBe('店铺B') }) it('should handle escaped quotes in values', () => { const csv = `categoryName,"店铺""A""",显示名` const result = parseCSVOptions(csv) expect(result).toHaveLength(1) expect(result[0].value).toBe('店铺"A"') }) it('should skip empty lines', () => { const csv = `categoryName,店铺A categoryName,店铺B ` const result = parseCSVOptions(csv) expect(result).toHaveLength(2) }) it('should skip lines with insufficient columns', () => { const csv = `categoryName,店铺A invalidLine categoryName,店铺B` const result = parseCSVOptions(csv) expect(result).toHaveLength(2) }) it('should return empty array for empty input', () => { expect(parseCSVOptions('')).toEqual([]) expect(parseCSVOptions(' ')).toEqual([]) }) it('should trim whitespace from values', () => { const csv = ` categoryName , 店铺A , 显示名 ` const result = parseCSVOptions(csv) expect(result[0].fieldKey).toBe('categoryName') expect(result[0].value).toBe('店铺A') expect(result[0].label).toBe('显示名') }) }) describe('parseJSONOptions', () => { describe('array format', () => { it('should parse array of options', () => { const json = JSON.stringify([ { fieldKey: 'categoryName', value: '店铺A' }, { fieldKey: 'categoryName', value: '店铺B', label: '显示名B' }, ]) const result = parseJSONOptions(json) expect(result).toHaveLength(2) expect(result[0]).toEqual({ fieldKey: 'categoryName', value: '店铺A', label: undefined }) expect(result[1]).toEqual({ fieldKey: 'categoryName', value: '店铺B', label: '显示名B' }) }) it('should filter out invalid items', () => { const json = JSON.stringify([ { fieldKey: 'categoryName', value: '店铺A' }, { fieldKey: 'categoryName' }, // missing value { value: '店铺B' }, // missing fieldKey { fieldKey: '', value: '' }, // empty values ]) const result = parseJSONOptions(json) expect(result).toHaveLength(1) }) it('should convert non-string values to strings', () => { const json = JSON.stringify([ { fieldKey: 'count', value: 123, label: 456 }, ]) const result = parseJSONOptions(json) expect(result[0].fieldKey).toBe('count') expect(result[0].value).toBe('123') expect(result[0].label).toBe('456') }) }) describe('object format', () => { it('should parse object with string arrays', () => { const json = JSON.stringify({ categoryName: ['店铺A', '店铺B'], sceneName: ['场景1'], }) const result = parseJSONOptions(json) expect(result).toHaveLength(3) expect(result.filter(o => o.fieldKey === 'categoryName')).toHaveLength(2) expect(result.filter(o => o.fieldKey === 'sceneName')).toHaveLength(1) }) it('should parse object with option objects', () => { const json = JSON.stringify({ categoryName: [ { value: '店铺A', label: '显示名A' }, { value: '店铺B' }, ], }) const result = parseJSONOptions(json) expect(result).toHaveLength(2) expect(result[0]).toEqual({ fieldKey: 'categoryName', value: '店铺A', label: '显示名A' }) expect(result[1]).toEqual({ fieldKey: 'categoryName', value: '店铺B', label: undefined }) }) it('should handle mixed string and object values', () => { const json = JSON.stringify({ categoryName: [ '店铺A', { value: '店铺B', label: '显示名B' }, ], }) const result = parseJSONOptions(json) expect(result).toHaveLength(2) expect(result[0].value).toBe('店铺A') expect(result[1].value).toBe('店铺B') expect(result[1].label).toBe('显示名B') }) it('should skip non-array values', () => { const json = JSON.stringify({ categoryName: ['店铺A'], invalidField: 'not an array', anotherInvalid: 123, }) const result = parseJSONOptions(json) expect(result).toHaveLength(1) expect(result[0].fieldKey).toBe('categoryName') }) }) it('should return empty array for invalid JSON', () => { expect(parseJSONOptions('invalid json')).toEqual([]) expect(parseJSONOptions('{broken')).toEqual([]) }) it('should return empty array for null/primitive JSON', () => { expect(parseJSONOptions('null')).toEqual([]) expect(parseJSONOptions('"string"')).toEqual([]) expect(parseJSONOptions('123')).toEqual([]) }) it('should return empty array for empty input', () => { expect(parseJSONOptions('')).toEqual([]) }) }) describe('exportToCSV', () => { it('should export fields to CSV format', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺A', label: '显示名A' }, { value: '店铺B', label: null }, ], }, ] const result = exportToCSV(fields) const lines = result.split('\n') expect(lines[0]).toBe('fieldKey,value,label') expect(lines[1]).toBe('categoryName,店铺A,显示名A') expect(lines[2]).toBe('categoryName,店铺B,') }) it('should escape values with commas', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺A,分店', label: null }, ], }, ] const result = exportToCSV(fields) const lines = result.split('\n') expect(lines[1]).toBe('categoryName,"店铺A,分店",') }) it('should escape values with quotes', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺"A"', label: null }, ], }, ] const result = exportToCSV(fields) const lines = result.split('\n') expect(lines[1]).toBe('categoryName,"店铺""A""",') }) it('should escape values with newlines', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺A\n分店', label: null }, ], }, ] const result = exportToCSV(fields) const lines = result.split('\n') // 第二行应该包含转义的换行符 expect(lines[1]).toContain('"店铺A') }) it('should handle empty options', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [], }, ] const result = exportToCSV(fields) const lines = result.split('\n') expect(lines).toHaveLength(1) // Only header expect(lines[0]).toBe('fieldKey,value,label') }) it('should handle multiple fields', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [{ value: '店铺A', label: null }], }, { key: 'sceneName', label: '场景类别', filterDisplayName: null, filterOrder: 1, isFilterEnabled: true, options: [{ value: '场景1', label: null }], }, ] const result = exportToCSV(fields) const lines = result.split('\n') expect(lines).toHaveLength(3) expect(lines[1]).toContain('categoryName') expect(lines[2]).toContain('sceneName') }) }) describe('exportToJSON', () => { it('should export fields to JSON format', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: '店铺筛选', filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺A', label: '显示名A' }, ], }, ] const result = exportToJSON(fields) const parsed = JSON.parse(result) expect(parsed).toHaveLength(1) expect(parsed[0].key).toBe('categoryName') expect(parsed[0].label).toBe('使用店铺') expect(parsed[0].filterDisplayName).toBe('店铺筛选') expect(parsed[0].options).toHaveLength(1) }) it('should format JSON with indentation', () => { const fields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [], }, ] const result = exportToJSON(fields) // Should have newlines (formatted) expect(result).toContain('\n') // Should have indentation expect(result).toContain(' ') }) it('should handle empty array', () => { const result = exportToJSON([]) expect(JSON.parse(result)).toEqual([]) }) }) describe('CSV round-trip', () => { it('should be able to parse exported CSV', () => { const originalFields: ExportedField[] = [ { key: 'categoryName', label: '使用店铺', filterDisplayName: null, filterOrder: 0, isFilterEnabled: true, options: [ { value: '店铺A', label: '显示名A' }, { value: '店铺B', label: null }, ], }, { key: 'sceneName', label: '场景类别', filterDisplayName: null, filterOrder: 1, isFilterEnabled: true, options: [ { value: '场景1', label: null }, ], }, ] const csv = exportToCSV(originalFields) const parsed = parseCSVOptions(csv) expect(parsed).toHaveLength(3) expect(parsed[0].fieldKey).toBe('categoryName') expect(parsed[0].value).toBe('店铺A') expect(parsed[0].label).toBe('显示名A') }) }) describe('type definitions', () => { it('BatchOperationResult should have correct structure', () => { const result = { success: true, processed: 5, failed: 1, errors: [{ key: 'field1', error: 'error message' }], } expect(result.success).toBe(true) expect(result.processed).toBe(5) expect(result.failed).toBe(1) expect(result.errors).toHaveLength(1) }) it('ImportedOption should have correct structure', () => { const option: ImportedOption = { fieldKey: 'categoryName', value: '店铺A', label: '显示名', } expect(option.fieldKey).toBe('categoryName') expect(option.value).toBe('店铺A') expect(option.label).toBe('显示名') }) it('ExportedField should have correct structure', () => { const field: ExportedField = { key: 'categoryName', label: '使用店铺', filterDisplayName: '店铺筛选', filterOrder: 0, isFilterEnabled: true, options: [{ value: '店铺A', label: null }], } expect(field.key).toBe('categoryName') expect(field.isFilterEnabled).toBe(true) expect(field.options).toHaveLength(1) }) }) })