1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
390 lines
14 KiB
TypeScript
390 lines
14 KiB
TypeScript
/**
|
|
* 字段条件显示逻辑测试
|
|
*/
|
|
|
|
import {
|
|
parseFieldConditions,
|
|
serializeFieldConditions,
|
|
evaluateFieldVisibility,
|
|
detectCircularDependencies,
|
|
getOperatorLabel,
|
|
getAllOperators,
|
|
FieldConditions,
|
|
SingleCondition,
|
|
} from '../field-conditions'
|
|
|
|
describe('field-conditions', () => {
|
|
describe('parseFieldConditions', () => {
|
|
it('should return null for empty string', () => {
|
|
expect(parseFieldConditions('')).toBeNull()
|
|
expect(parseFieldConditions(' ')).toBeNull()
|
|
})
|
|
|
|
it('should return null for null/undefined', () => {
|
|
expect(parseFieldConditions(null)).toBeNull()
|
|
expect(parseFieldConditions(undefined)).toBeNull()
|
|
})
|
|
|
|
it('should parse valid conditions', () => {
|
|
const json = JSON.stringify({
|
|
logic: 'AND',
|
|
conditions: [
|
|
{ fieldKey: 'category', operator: 'equals', value: 'A' }
|
|
]
|
|
})
|
|
const result = parseFieldConditions(json)
|
|
expect(result).not.toBeNull()
|
|
expect(result?.logic).toBe('AND')
|
|
expect(result?.conditions).toHaveLength(1)
|
|
expect(result?.conditions[0].fieldKey).toBe('category')
|
|
})
|
|
|
|
it('should default logic to AND', () => {
|
|
const json = JSON.stringify({
|
|
conditions: [{ fieldKey: 'test', operator: 'notEmpty' }]
|
|
})
|
|
const result = parseFieldConditions(json)
|
|
expect(result?.logic).toBe('AND')
|
|
})
|
|
|
|
it('should parse OR logic', () => {
|
|
const json = JSON.stringify({
|
|
logic: 'OR',
|
|
conditions: [{ fieldKey: 'test', operator: 'notEmpty' }]
|
|
})
|
|
const result = parseFieldConditions(json)
|
|
expect(result?.logic).toBe('OR')
|
|
})
|
|
|
|
it('should filter invalid conditions', () => {
|
|
const json = JSON.stringify({
|
|
logic: 'AND',
|
|
conditions: [
|
|
{ fieldKey: 'valid', operator: 'equals', value: 'A' },
|
|
{ fieldKey: '', operator: 'equals' }, // invalid - no fieldKey
|
|
{ operator: 'equals' }, // invalid - no fieldKey
|
|
{ fieldKey: 'test' }, // invalid - no operator
|
|
]
|
|
})
|
|
const result = parseFieldConditions(json)
|
|
expect(result?.conditions).toHaveLength(1)
|
|
})
|
|
|
|
it('should return null for invalid JSON', () => {
|
|
expect(parseFieldConditions('invalid')).toBeNull()
|
|
expect(parseFieldConditions('{broken')).toBeNull()
|
|
})
|
|
|
|
it('should return null for non-object', () => {
|
|
expect(parseFieldConditions('"string"')).toBeNull()
|
|
expect(parseFieldConditions('123')).toBeNull()
|
|
})
|
|
|
|
it('should return null for missing conditions array', () => {
|
|
expect(parseFieldConditions('{"logic":"AND"}')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('serializeFieldConditions', () => {
|
|
it('should return empty string for null', () => {
|
|
expect(serializeFieldConditions(null)).toBe('')
|
|
})
|
|
|
|
it('should return empty string for empty conditions', () => {
|
|
expect(serializeFieldConditions({ logic: 'AND', conditions: [] })).toBe('')
|
|
})
|
|
|
|
it('should serialize valid conditions', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [
|
|
{ fieldKey: 'category', operator: 'equals', value: 'A' }
|
|
]
|
|
}
|
|
const result = serializeFieldConditions(conditions)
|
|
const parsed = JSON.parse(result)
|
|
expect(parsed.logic).toBe('AND')
|
|
expect(parsed.conditions).toHaveLength(1)
|
|
})
|
|
|
|
it('should filter invalid conditions', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'OR',
|
|
conditions: [
|
|
{ fieldKey: 'valid', operator: 'equals', value: 'A' },
|
|
{ fieldKey: '', operator: 'equals' } as SingleCondition, // invalid
|
|
]
|
|
}
|
|
const result = serializeFieldConditions(conditions)
|
|
const parsed = JSON.parse(result)
|
|
expect(parsed.conditions).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('evaluateFieldVisibility', () => {
|
|
describe('equals operator', () => {
|
|
it('should return true when value equals', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'category', operator: 'equals', value: 'A' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { category: 'A' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when value not equals', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'category', operator: 'equals', value: 'A' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { category: 'B' })).toBe(false)
|
|
})
|
|
|
|
it('should handle empty value comparison', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'category', operator: 'equals', value: '' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { category: '' })).toBe(true)
|
|
expect(evaluateFieldVisibility(conditions, {})).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('notEquals operator', () => {
|
|
it('should return true when value not equals', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'category', operator: 'notEquals', value: 'A' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { category: 'B' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when value equals', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'category', operator: 'notEquals', value: 'A' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { category: 'A' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('contains operator', () => {
|
|
it('should return true when value contains substring', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'contains', value: 'test' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: 'this is a test' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when value does not contain substring', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'contains', value: 'test' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: 'hello world' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('notEmpty operator', () => {
|
|
it('should return true when value is not empty', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'notEmpty' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: 'hello' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when value is empty', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'notEmpty' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: '' })).toBe(false)
|
|
expect(evaluateFieldVisibility(conditions, { name: ' ' })).toBe(false)
|
|
expect(evaluateFieldVisibility(conditions, {})).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isEmpty operator', () => {
|
|
it('should return true when value is empty', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'isEmpty' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: '' })).toBe(true)
|
|
expect(evaluateFieldVisibility(conditions, {})).toBe(true)
|
|
})
|
|
|
|
it('should return false when value is not empty', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'isEmpty' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: 'hello' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('AND logic', () => {
|
|
it('should return true when all conditions are met', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [
|
|
{ fieldKey: 'a', operator: 'equals', value: '1' },
|
|
{ fieldKey: 'b', operator: 'equals', value: '2' },
|
|
]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { a: '1', b: '2' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when any condition is not met', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [
|
|
{ fieldKey: 'a', operator: 'equals', value: '1' },
|
|
{ fieldKey: 'b', operator: 'equals', value: '2' },
|
|
]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { a: '1', b: '3' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('OR logic', () => {
|
|
it('should return true when any condition is met', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'OR',
|
|
conditions: [
|
|
{ fieldKey: 'a', operator: 'equals', value: '1' },
|
|
{ fieldKey: 'b', operator: 'equals', value: '2' },
|
|
]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { a: '1', b: '3' })).toBe(true)
|
|
expect(evaluateFieldVisibility(conditions, { a: '3', b: '2' })).toBe(true)
|
|
})
|
|
|
|
it('should return false when no condition is met', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'OR',
|
|
conditions: [
|
|
{ fieldKey: 'a', operator: 'equals', value: '1' },
|
|
{ fieldKey: 'b', operator: 'equals', value: '2' },
|
|
]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { a: '3', b: '3' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('null/empty conditions', () => {
|
|
it('should return true for null conditions', () => {
|
|
expect(evaluateFieldVisibility(null, { any: 'value' })).toBe(true)
|
|
})
|
|
|
|
it('should return true for empty conditions array', () => {
|
|
expect(evaluateFieldVisibility({ logic: 'AND', conditions: [] }, { any: 'value' })).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('type coercion', () => {
|
|
it('should handle number values', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'count', operator: 'equals', value: '5' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { count: 5 })).toBe(true)
|
|
})
|
|
|
|
it('should handle null values', () => {
|
|
const conditions: FieldConditions = {
|
|
logic: 'AND',
|
|
conditions: [{ fieldKey: 'name', operator: 'isEmpty' }]
|
|
}
|
|
expect(evaluateFieldVisibility(conditions, { name: null })).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('detectCircularDependencies', () => {
|
|
it('should return empty array for no dependencies', () => {
|
|
const fields = [
|
|
{ key: 'a', conditions: null },
|
|
{ key: 'b', conditions: null },
|
|
]
|
|
expect(detectCircularDependencies(fields)).toEqual([])
|
|
})
|
|
|
|
it('should return empty array for linear dependencies', () => {
|
|
const fields = [
|
|
{ key: 'a', conditions: null },
|
|
{ key: 'b', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'a', operator: 'notEmpty' }] }) },
|
|
{ key: 'c', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'b', operator: 'notEmpty' }] }) },
|
|
]
|
|
expect(detectCircularDependencies(fields)).toEqual([])
|
|
})
|
|
|
|
it('should detect simple circular dependency', () => {
|
|
const fields = [
|
|
{ key: 'a', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'b', operator: 'notEmpty' }] }) },
|
|
{ key: 'b', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'a', operator: 'notEmpty' }] }) },
|
|
]
|
|
const result = detectCircularDependencies(fields)
|
|
expect(result.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should detect indirect circular dependency', () => {
|
|
const fields = [
|
|
{ key: 'a', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'c', operator: 'notEmpty' }] }) },
|
|
{ key: 'b', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'a', operator: 'notEmpty' }] }) },
|
|
{ key: 'c', conditions: JSON.stringify({ logic: 'AND', conditions: [{ fieldKey: 'b', operator: 'notEmpty' }] }) },
|
|
]
|
|
const result = detectCircularDependencies(fields)
|
|
expect(result.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('getOperatorLabel', () => {
|
|
it('should return correct labels', () => {
|
|
expect(getOperatorLabel('equals')).toBe('等于')
|
|
expect(getOperatorLabel('notEquals')).toBe('不等于')
|
|
expect(getOperatorLabel('contains')).toBe('包含')
|
|
expect(getOperatorLabel('notEmpty')).toBe('非空')
|
|
expect(getOperatorLabel('isEmpty')).toBe('为空')
|
|
})
|
|
|
|
it('should return operator as fallback', () => {
|
|
expect(getOperatorLabel('unknown' as any)).toBe('unknown')
|
|
})
|
|
})
|
|
|
|
describe('getAllOperators', () => {
|
|
it('should return all operators with correct structure', () => {
|
|
const operators = getAllOperators()
|
|
expect(operators.length).toBe(5)
|
|
|
|
for (const op of operators) {
|
|
expect(op).toHaveProperty('value')
|
|
expect(op).toHaveProperty('label')
|
|
expect(op).toHaveProperty('needsValue')
|
|
}
|
|
})
|
|
|
|
it('should mark notEmpty and isEmpty as not needing value', () => {
|
|
const operators = getAllOperators()
|
|
const notEmpty = operators.find(o => o.value === 'notEmpty')
|
|
const isEmpty = operators.find(o => o.value === 'isEmpty')
|
|
|
|
expect(notEmpty?.needsValue).toBe(false)
|
|
expect(isEmpty?.needsValue).toBe(false)
|
|
})
|
|
|
|
it('should mark equals, notEquals, contains as needing value', () => {
|
|
const operators = getAllOperators()
|
|
const equals = operators.find(o => o.value === 'equals')
|
|
const notEquals = operators.find(o => o.value === 'notEquals')
|
|
const contains = operators.find(o => o.value === 'contains')
|
|
|
|
expect(equals?.needsValue).toBe(true)
|
|
expect(notEquals?.needsValue).toBe(true)
|
|
expect(contains?.needsValue).toBe(true)
|
|
})
|
|
})
|
|
})
|