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