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

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

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

394 lines
11 KiB
TypeScript

/**
* 分类管理批量操作和同步功能的单元测试
*
* @jest-environment node
* @created 2024-12-13
*/
import {
parseCSVOptions,
parseJSONOptions,
exportToCSV,
exportToJSON,
type ImportedOption,
type ExportedField,
} from '@/lib/category-batch-operations'
import {
getSyncLogs,
clearSyncLogs,
} from '@/lib/category-sync'
describe('Category Batch Operations', () => {
describe('parseCSVOptions', () => {
it('should parse simple CSV content', () => {
const csv = `fieldKey,value,label
categoryName,店铺A,店铺A标签
categoryName,店铺B,店铺B标签
sceneName,场景1,`
const result = parseCSVOptions(csv)
expect(result).toHaveLength(3)
expect(result[0]).toEqual({
fieldKey: 'categoryName',
value: '店铺A',
label: '店铺A标签',
})
expect(result[1]).toEqual({
fieldKey: 'categoryName',
value: '店铺B',
label: '店铺B标签',
})
expect(result[2]).toEqual({
fieldKey: 'sceneName',
value: '场景1',
label: undefined,
})
})
it('should handle CSV without header', () => {
const csv = `categoryName,店铺A,店铺A标签
categoryName,店铺B,`
const result = parseCSVOptions(csv)
expect(result).toHaveLength(2)
expect(result[0].fieldKey).toBe('categoryName')
})
it('should handle quoted values with commas', () => {
const csv = `fieldKey,value,label
categoryName,"店铺A,分店",标签`
const result = parseCSVOptions(csv)
expect(result).toHaveLength(1)
expect(result[0].value).toBe('店铺A,分店')
})
it('should handle empty lines', () => {
const csv = `fieldKey,value,label
categoryName,店铺A,标签
sceneName,场景1,`
const result = parseCSVOptions(csv)
expect(result).toHaveLength(2)
})
it('should handle escaped quotes', () => {
const csv = `fieldKey,value,label
categoryName,"店铺""A""",标签`
const result = parseCSVOptions(csv)
expect(result).toHaveLength(1)
expect(result[0].value).toBe('店铺"A"')
})
})
describe('parseJSONOptions', () => {
it('should parse array format', () => {
const json = JSON.stringify([
{ fieldKey: 'categoryName', value: '店铺A', label: '标签A' },
{ fieldKey: 'categoryName', value: '店铺B' },
{ fieldKey: 'sceneName', value: '场景1' },
])
const result = parseJSONOptions(json)
expect(result).toHaveLength(3)
expect(result[0]).toEqual({
fieldKey: 'categoryName',
value: '店铺A',
label: '标签A',
})
expect(result[1].label).toBeUndefined()
})
it('should parse object format', () => {
const json = JSON.stringify({
categoryName: [
{ value: '店铺A', label: '标签A' },
{ value: '店铺B' },
],
sceneName: ['场景1', '场景2'],
})
const result = parseJSONOptions(json)
expect(result).toHaveLength(4)
expect(result.filter(o => o.fieldKey === 'categoryName')).toHaveLength(2)
expect(result.filter(o => o.fieldKey === 'sceneName')).toHaveLength(2)
})
it('should handle invalid JSON', () => {
const result = parseJSONOptions('invalid json')
expect(result).toEqual([])
})
it('should filter out invalid entries', () => {
const json = JSON.stringify([
{ fieldKey: 'categoryName', value: '店铺A' },
{ fieldKey: 'categoryName', value: '店铺B' },
{ fieldKey: 'categoryName' }, // 缺少value - 应被过滤
{ value: '店铺C' }, // 缺少fieldKey - 应被过滤
])
const result = parseJSONOptions(json)
// 只有前两个有效条目应该被保留
expect(result).toHaveLength(2)
expect(result[0].value).toBe('店铺A')
expect(result[1].value).toBe('店铺B')
})
})
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 },
],
},
{
key: 'sceneName',
label: '场景类别',
filterDisplayName: '场景',
filterOrder: 1,
isFilterEnabled: true,
options: [
{ value: '场景1', label: null },
],
},
]
const csv = exportToCSV(fields)
const lines = csv.split('\n')
expect(lines[0]).toBe('fieldKey,value,label')
expect(lines[1]).toBe('categoryName,店铺A,标签A')
expect(lines[2]).toBe('categoryName,店铺B,')
expect(lines[3]).toBe('sceneName,场景1,')
})
it('should escape special characters', () => {
const fields: ExportedField[] = [
{
key: 'test',
label: 'Test',
filterDisplayName: null,
filterOrder: 0,
isFilterEnabled: true,
options: [
{ value: '值,带逗号', label: null },
{ value: '值"带引号"', label: null },
],
},
]
const csv = exportToCSV(fields)
const lines = csv.split('\n')
expect(lines[1]).toBe('test,"值,带逗号",')
expect(lines[2]).toBe('test,"值""带引号""",')
})
})
describe('exportToJSON', () => {
it('should export fields to JSON format', () => {
const fields: ExportedField[] = [
{
key: 'categoryName',
label: '使用店铺',
filterDisplayName: null,
filterOrder: 0,
isFilterEnabled: true,
options: [
{ value: '店铺A', label: '标签A' },
],
},
]
const json = exportToJSON(fields)
const parsed = JSON.parse(json)
expect(parsed).toHaveLength(1)
expect(parsed[0].key).toBe('categoryName')
expect(parsed[0].options).toHaveLength(1)
})
it('should produce valid JSON', () => {
const fields: ExportedField[] = [
{
key: 'test',
label: 'Test',
filterDisplayName: null,
filterOrder: 0,
isFilterEnabled: true,
options: [
{ value: '值"带引号"', label: null },
],
},
]
const json = exportToJSON(fields)
expect(() => JSON.parse(json)).not.toThrow()
})
})
})
describe('Category Sync', () => {
beforeEach(() => {
clearSyncLogs()
})
describe('getSyncLogs', () => {
it('should return empty array initially', () => {
const logs = getSyncLogs()
expect(logs).toEqual([])
})
it('should respect limit parameter', () => {
const logs = getSyncLogs(10)
expect(logs.length).toBeLessThanOrEqual(10)
})
})
describe('clearSyncLogs', () => {
it('should clear all logs', () => {
clearSyncLogs()
const logs = getSyncLogs()
expect(logs).toEqual([])
})
})
})
describe('Batch Operation Logic', () => {
it('should correctly identify fields to toggle', () => {
const fieldKeys = ['categoryName', 'sceneName', 'platformName']
const enabled = true
// 模拟批量操作逻辑
const operations = fieldKeys.map(key => ({
key,
data: { isFilterEnabled: enabled },
}))
expect(operations).toHaveLength(3)
expect(operations[0].data.isFilterEnabled).toBe(true)
})
it('should group options by field for import', () => {
const options: ImportedOption[] = [
{ fieldKey: 'categoryName', value: '店铺A' },
{ fieldKey: 'categoryName', value: '店铺B' },
{ fieldKey: 'sceneName', value: '场景1' },
]
const grouped = new Map<string, ImportedOption[]>()
for (const option of options) {
const existing = grouped.get(option.fieldKey) || []
existing.push(option)
grouped.set(option.fieldKey, existing)
}
expect(grouped.size).toBe(2)
expect(grouped.get('categoryName')).toHaveLength(2)
expect(grouped.get('sceneName')).toHaveLength(1)
})
})
describe('Sync Logic', () => {
it('should detect option changes correctly', () => {
const existingOptions = ['A', 'B', 'C']
const newOptions = ['A', 'B', 'D']
const added = newOptions.filter(opt => !existingOptions.includes(opt))
const removed = existingOptions.filter(opt => !newOptions.includes(opt))
expect(added).toEqual(['D'])
expect(removed).toEqual(['C'])
})
it('should handle option rename correctly', () => {
const options = ['A', 'B', 'C']
const oldValue = 'B'
const newValue = 'B-renamed'
const updated = options.map(opt => opt === oldValue ? newValue : opt)
expect(updated).toEqual(['A', 'B-renamed', 'C'])
})
it('should handle option delete correctly', () => {
const options = ['A', 'B', 'C']
const toDelete = 'B'
const filtered = options.filter(opt => opt !== toDelete)
expect(filtered).toEqual(['A', 'C'])
})
it('should detect version conflicts', () => {
const originalUpdatedAt = new Date('2024-01-01')
const currentUpdatedAt = new Date('2024-01-02')
const hasConflict = currentUpdatedAt > originalUpdatedAt
expect(hasConflict).toBe(true)
})
it('should merge options without duplicates', () => {
const existingOptions = ['A', 'B', 'C']
const newOptions = ['B', 'C', 'D', 'E']
const existingSet = new Set(existingOptions)
const toAdd = newOptions.filter(opt => !existingSet.has(opt))
expect(toAdd).toEqual(['D', 'E'])
})
})
describe('Impact Analysis', () => {
it('should generate correct warning message', () => {
const templateCount = 3
const templateNames = ['模板A', '模板B', '模板C']
const optionCount = 10
let warning = ''
if (templateCount > 0) {
warning = `该字段被 ${templateCount} 个模板使用(${templateNames.slice(0, 3).join('、')}${templateCount > 3 ? '等' : ''}),删除后这些模板将失去该字段。`
}
if (optionCount > 0) {
warning += warning ? '\n' : ''
warning += `该字段有 ${optionCount} 个选项将被一并删除。`
}
expect(warning).toContain('3 个模板')
expect(warning).toContain('模板A、模板B、模板C')
expect(warning).toContain('10 个选项')
})
it('should truncate long template list', () => {
const templateNames = ['模板A', '模板B', '模板C', '模板D', '模板E']
const templateCount = templateNames.length
const displayNames = templateNames.slice(0, 3).join('、') + (templateCount > 3 ? '等' : '')
expect(displayNames).toBe('模板A、模板B、模板C等')
})
})