20260923165454
This commit is contained in:
1 parent
0abe3706c8
commit
e6935b8fa4
173 files changed
+14975
-2295
No files matched your search
@@ -1,13 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
advancedQueryFromAst,
|
||||
astToSql,
|
||||
buildAdvancedAst,
|
||||
buildFilterStateAst,
|
||||
buildKeywordAst,
|
||||
buildQuickAst,
|
||||
buildViewAst,
|
||||
combineAst,
|
||||
defaultOperator,
|
||||
describeConditionText,
|
||||
emptyConditionValue,
|
||||
hasConditionValue,
|
||||
isDatePresetValue,
|
||||
operatorsForField,
|
||||
resolveDatePreset,
|
||||
resolveDatePresetValue,
|
||||
} from '@/components/fms-module-common/queryUtils'
|
||||
|
||||
const fields = new Map([
|
||||
@@ -25,10 +31,13 @@ describe('查询条件 AST 构造', () => {
|
||||
expect(operatorsForField({ b_type: 'money' })).toContain('between')
|
||||
})
|
||||
|
||||
it('生成转义后的 like / 不等 / 空值条件', () => {
|
||||
it('生成转义后的 like / 开头是 / 不等 / 空值条件', () => {
|
||||
expect(
|
||||
astToSql({ type: 'condition', field: 'name', operator: 'like', value: "O'Reilly" }, fields),
|
||||
).toBe("[name] LIKE N'%O''Reilly%'")
|
||||
expect(
|
||||
astToSql({ type: 'condition', field: 'name', operator: 'starts_with', value: 'a' }, fields),
|
||||
).toBe("[name] LIKE N'a%'")
|
||||
expect(
|
||||
astToSql({ type: 'condition', field: 'name', operator: 'not_like', value: 'a' }, fields),
|
||||
).toBe("[name] NOT LIKE N'%a%'")
|
||||
@@ -81,66 +90,6 @@ describe('查询条件 AST 构造', () => {
|
||||
expect(astToSql({ type: 'group', logic: 'and', children: [] }, fields)).toBe('')
|
||||
})
|
||||
|
||||
it('常用条件跳过空值,操作符取模块配置', () => {
|
||||
const items = [
|
||||
{ field: fields.get('name'), config: { b_operator: 'like' } },
|
||||
{ field: fields.get('status'), config: { b_operator: 'eq' } },
|
||||
]
|
||||
const ast = buildQuickAst(items, { name: 'abc', status: '' })
|
||||
expect(ast.children).toHaveLength(1)
|
||||
expect(astToSql(ast, fields)).toBe("[name] LIKE N'%abc%'")
|
||||
})
|
||||
|
||||
it('高级条件按每项 join 生成 AST,单条件的组不再套一层', () => {
|
||||
const ast = buildAdvancedAst({
|
||||
nodes: [
|
||||
{ join: 'or', field: 'name', operator: 'like', value: 'a' },
|
||||
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'g1',
|
||||
join: 'or',
|
||||
nodes: [{ join: 'and', field: 'status', operator: 'eq', value: 'new' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
// join 决定每项与上一相邻项的连接;首项 join 忽略;单条件的组等价于一个普通条件
|
||||
expect(astToSql(ast, fields)).toBe(
|
||||
"([name] LIKE N'%a%' OR [name] LIKE N'%b%' OR [status] = N'new')",
|
||||
)
|
||||
})
|
||||
|
||||
it('支持 (A 且 B) 或 C:组行内且、组与前一行或', () => {
|
||||
const ast = buildAdvancedAst({
|
||||
nodes: [{ join: 'or', field: 'name', operator: 'eq', value: '张三' }],
|
||||
groups: [
|
||||
{
|
||||
id: 'g1',
|
||||
join: 'or',
|
||||
nodes: [
|
||||
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
|
||||
{ join: 'and', field: 'amount', operator: 'gt', value: 1000 },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(astToSql(ast, fields)).toBe("([name] = N'张三' OR ([amount] > 100 AND [amount] > 1000))")
|
||||
})
|
||||
|
||||
it('同一层可混用 且/或:(a 且 b) 或 c 从左到右加括号', () => {
|
||||
const ast = buildAdvancedAst({
|
||||
nodes: [
|
||||
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
|
||||
{ join: 'and', field: 'name', operator: 'like', value: 'b' },
|
||||
{ join: 'or', field: 'name', operator: 'like', value: 'c' },
|
||||
],
|
||||
})
|
||||
expect(astToSql(ast, fields)).toBe(
|
||||
"(([name] LIKE N'%a%' AND [name] LIKE N'%b%') OR [name] LIKE N'%c%')",
|
||||
)
|
||||
})
|
||||
|
||||
it('区间 / 多选只要有一项即算已填写,空值按操作符重置', () => {
|
||||
expect(hasConditionValue(['', '20'])).toBe(true)
|
||||
expect(hasConditionValue(['', ''])).toBe(false)
|
||||
@@ -150,88 +99,118 @@ describe('查询条件 AST 构造', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('高级条件 AST → 界面状态(advancedQueryFromAst)', () => {
|
||||
let seq = 0
|
||||
const idFactory = () => `n${(seq += 1)}`
|
||||
|
||||
it('空 AST / 空组都给空结构且不降级', () => {
|
||||
expect(advancedQueryFromAst(null, { idFactory })).toEqual({
|
||||
nodes: [],
|
||||
groups: [],
|
||||
degraded: false,
|
||||
})
|
||||
expect(advancedQueryFromAst({ type: 'group', children: [] }, { idFactory })).toEqual({
|
||||
nodes: [],
|
||||
groups: [],
|
||||
degraded: false,
|
||||
})
|
||||
describe('日期预设区间', () => {
|
||||
it('预设解析为具体日期区间,周以周一为起点', () => {
|
||||
// 2026-09-23 是周三:本周为 09-21(周一)~ 09-27(周日)
|
||||
expect(resolveDatePreset('today', '2026-09-23')).toEqual(['2026-09-23', '2026-09-23'])
|
||||
expect(resolveDatePreset('this_week', '2026-09-23')).toEqual(['2026-09-21', '2026-09-27'])
|
||||
expect(resolveDatePreset('this_month', '2026-09-23')).toEqual(['2026-09-01', '2026-09-30'])
|
||||
expect(resolveDatePreset('recent_3_months', '2026-09-23')).toEqual(['2026-06-24', '2026-09-23'])
|
||||
expect(resolveDatePreset('nope', '2026-09-23')).toBeNull()
|
||||
})
|
||||
|
||||
it('单层「行 + 组」往返:还原后再构造语义等价', () => {
|
||||
const ast = buildAdvancedAst({
|
||||
nodes: [
|
||||
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
|
||||
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
|
||||
],
|
||||
groups: [
|
||||
it('语义值 { preset } 只在组装 AST 时解析,配置不写死日期', () => {
|
||||
expect(isDatePresetValue({ preset: 'today' })).toBe(true)
|
||||
expect(isDatePresetValue(['2026-09-23'])).toBe(false)
|
||||
expect(resolveDatePresetValue({ preset: 'today' })).toEqual(['2026-09-23', '2026-09-23'])
|
||||
expect(resolveDatePresetValue('2026-09-23')).toBe('2026-09-23')
|
||||
expect(
|
||||
astToSql(
|
||||
{
|
||||
join: 'and',
|
||||
nodes: [
|
||||
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
|
||||
{ join: 'or', field: 'status', operator: 'eq', value: 'done' },
|
||||
],
|
||||
type: 'condition',
|
||||
field: 'created',
|
||||
operator: 'between',
|
||||
value: { preset: 'today' },
|
||||
},
|
||||
fields,
|
||||
),
|
||||
).toBe("[created] BETWEEN N'2026-09-23 00:00:00' AND N'2026-09-23 23:59:59'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('查询入口 → AST(页签 / 关键词 / 快捷筛选 / 更多筛选)', () => {
|
||||
it('页签条件生成 AST:未知字段丢弃、语义值解析', () => {
|
||||
const view = {
|
||||
code: 'failed',
|
||||
conditions: [
|
||||
{ field: 'status', operator: 'eq', value: '失败' },
|
||||
{ field: 'ghost', operator: 'eq', value: 'x' },
|
||||
{ field: 'created', operator: 'between', value: { preset: 'today' } },
|
||||
],
|
||||
})
|
||||
const state = advancedQueryFromAst(ast, { idFactory })
|
||||
}
|
||||
const ast = buildViewAst(view, fields)
|
||||
expect(astToSql(ast, fields)).toBe(
|
||||
"([status] = N'失败' AND [created] BETWEEN N'2026-09-23 00:00:00' AND N'2026-09-23 23:59:59')",
|
||||
)
|
||||
expect(buildViewAst({ code: 'all' }, fields)).toBeNull()
|
||||
})
|
||||
|
||||
expect(state.degraded).toBe(false)
|
||||
expect(state.nodes.map((node) => [node.field, node.join])).toEqual([
|
||||
['name', 'and'],
|
||||
['name', 'or'],
|
||||
])
|
||||
expect(state.groups).toHaveLength(1)
|
||||
expect(state.groups[0].nodes.map((node) => [node.field, node.join])).toEqual([
|
||||
['amount', 'and'],
|
||||
['status', 'or'],
|
||||
])
|
||||
expect(astToSql(buildAdvancedAst(state), fields)).toBe(astToSql(ast, fields))
|
||||
it('关键词跨字段取「或」,空关键词不产生条件', () => {
|
||||
const items = [
|
||||
{ field: fields.get('name'), config: {} },
|
||||
{ field: fields.get('status'), config: {} },
|
||||
]
|
||||
const ast = buildKeywordAst('东方', items)
|
||||
expect(astToSql(ast, fields)).toBe("([name] LIKE N'%东方%' OR [status] LIKE N'%东方%')")
|
||||
expect(buildKeywordAst(' ', items)).toBeNull()
|
||||
expect(buildKeywordAst('东方', [])).toBeNull()
|
||||
})
|
||||
|
||||
it('组内再嵌套组:拍平成单层并置 degraded(根层的组不算降级)', () => {
|
||||
const ast = {
|
||||
type: 'group',
|
||||
children: [
|
||||
{ type: 'condition', field: 'name', operator: 'like', value: 'a', join: 'and' },
|
||||
{
|
||||
type: 'group',
|
||||
join: 'or',
|
||||
children: [
|
||||
{ type: 'condition', field: 'amount', operator: 'gt', value: 1, join: 'and' },
|
||||
{
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
children: [{ type: 'condition', field: 'amount', operator: 'lt', value: 9 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const state = advancedQueryFromAst(ast, { idFactory })
|
||||
it('快捷筛选跳过空值,操作符取模块配置,日期预设被解析', () => {
|
||||
const items = [
|
||||
{ field: fields.get('name'), config: { b_operator: 'like' } },
|
||||
{ field: fields.get('status'), config: { b_operator: 'eq' } },
|
||||
{ field: fields.get('created'), config: { b_operator: 'between' } },
|
||||
]
|
||||
const ast = buildQuickAst(items, {
|
||||
name: 'abc',
|
||||
status: '',
|
||||
created: { preset: 'this_month' },
|
||||
})
|
||||
expect(ast.children).toHaveLength(2)
|
||||
expect(astToSql(ast, fields)).toBe(
|
||||
"([name] LIKE N'%abc%' AND [created] BETWEEN N'2026-09-01 00:00:00' AND N'2026-09-30 23:59:59')",
|
||||
)
|
||||
})
|
||||
|
||||
expect(state.degraded).toBe(true)
|
||||
expect(state.nodes.map((node) => node.field)).toEqual(['name'])
|
||||
expect(state.groups).toHaveLength(1)
|
||||
expect(state.groups[0].nodes.map((node) => node.field)).toEqual(['amount', 'amount'])
|
||||
expect(state.groups[0].join).toBe('or')
|
||||
it('更多筛选草稿只收已填值,未授权字段被忽略', () => {
|
||||
const items = new Map([['name', { field: fields.get('name'), config: {} }]])
|
||||
const ast = buildFilterStateAst(
|
||||
{
|
||||
name: { operator: 'like', value: 'a' },
|
||||
status: { operator: 'eq', value: 'new' },
|
||||
amount: { operator: 'eq', value: '' },
|
||||
},
|
||||
items,
|
||||
)
|
||||
expect(astToSql(ast, fields)).toBe("[name] LIKE N'%a%'")
|
||||
expect(buildFilterStateAst({}, items)).toBeNull()
|
||||
})
|
||||
|
||||
it('单条件 AST(非 group)也能还原成一行', () => {
|
||||
const state = advancedQueryFromAst(
|
||||
{ type: 'condition', field: 'status', operator: 'eq', value: 'draft' },
|
||||
{ idFactory },
|
||||
it('combineAst 把多个来源按「且」合并,空 AST 跳过', () => {
|
||||
const ast = combineAst(
|
||||
buildKeywordAst('东方', [{ field: fields.get('name'), config: {} }]),
|
||||
{ type: 'group', children: [] },
|
||||
{ type: 'condition', field: 'status', operator: 'eq', value: 'new' },
|
||||
)
|
||||
expect(state.nodes).toHaveLength(1)
|
||||
expect(state.nodes[0]).toMatchObject({ field: 'status', operator: 'eq', value: 'draft' })
|
||||
expect(astToSql(ast, fields)).toBe(
|
||||
"(([name] LIKE N'%东方%') AND [status] = N'new')",
|
||||
)
|
||||
expect(combineAst(null, { type: 'group', children: [] })).toBeNull()
|
||||
})
|
||||
|
||||
it('条件回显文案:字段名 + 操作符 + 值,未填值返回空串', () => {
|
||||
const items = new Map([
|
||||
['name', { field: { b_field: 'name', b_name: '姓名', b_type: 'input' } }],
|
||||
['status', { field: { b_field: 'status', b_name: '状态', b_type: 'select' } }],
|
||||
])
|
||||
expect(
|
||||
describeConditionText({ field: 'name', operator: 'like', value: '东方' }, items),
|
||||
).toBe('姓名 包含 东方')
|
||||
expect(
|
||||
describeConditionText({ field: 'status', operator: 'is_null', value: '' }, items),
|
||||
).toBe('状态 为空')
|
||||
expect(describeConditionText({ field: 'name', operator: 'like', value: '' }, items)).toBe('')
|
||||
expect(describeConditionText({ field: 'ghost', operator: 'eq', value: 'x' }, items)).toBe('')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user