import { describe, expect, it } from 'vitest' import { astToSql, buildFilterStateAst, buildKeywordAst, buildKeywordPlaceholder, buildQuickAst, buildViewAst, combineAst, defaultOperator, describeConditionText, emptyConditionValue, hasConditionValue, isDatePresetValue, operatorsForField, resolveDatePreset, resolveDatePresetValue, } from '@/components/fms-module-common/queryUtils' const fields = new Map([ ['name', { b_field: 'name', b_type: 'input' }], ['amount', { b_field: 'amount', b_type: 'money' }], ['created', { b_field: 'created', b_type: 'datetime' }], ['status', { b_field: 'status', b_type: 'select' }], ]) describe('查询条件 AST 构造', () => { it('按字段类型给出默认操作符:文本 like、日期 between、其余 eq', () => { expect(defaultOperator({ b_type: 'input' })).toBe('like') expect(defaultOperator({ b_type: 'datetime' })).toBe('between') expect(defaultOperator({ b_type: 'select' })).toBe('eq') expect(operatorsForField({ b_type: 'money' })).toContain('between') }) 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%'") expect( astToSql({ type: 'condition', field: 'status', operator: 'ne', value: 'done' }, fields), ).toBe("[status] <> N'done'") expect( astToSql({ type: 'condition', field: 'name', operator: 'is_null', value: '' }, fields), ).toBe('[name] IS NULL') }) it('区间条件可退化为单边比较,日期单边取对应端点', () => { expect( astToSql( { type: 'condition', field: 'amount', operator: 'between', value: ['10', ''] }, fields, ), ).toBe('[amount] >= 10') expect( astToSql( { type: 'condition', field: 'created', operator: 'ge', value: ['2026-01-01', ''] }, fields, ), ).toBe("[created] >= N'2026-01-01 00:00:00'") expect( astToSql( { type: 'condition', field: 'created', operator: 'le', value: ['', '2026-01-31'] }, fields, ), ).toBe("[created] <= N'2026-01-31 23:59:59'") }) it('嵌套条件组按各自逻辑加括号,空组不产生条件', () => { const ast = { type: 'group', logic: 'and', children: [ { type: 'condition', field: 'status', operator: 'eq', value: 'new' }, { type: 'group', logic: 'or', children: [ { type: 'condition', field: 'name', operator: 'like', value: 'a' }, { type: 'condition', field: 'name', operator: 'like', value: '' }, ], }, ], } expect(astToSql(ast, fields)).toBe("([status] = N'new' AND [name] LIKE N'%a%')") expect(astToSql({ type: 'group', logic: 'and', children: [] }, fields)).toBe('') }) it('区间 / 多选只要有一项即算已填写,空值按操作符重置', () => { expect(hasConditionValue(['', '20'])).toBe(true) expect(hasConditionValue(['', ''])).toBe(false) expect(hasConditionValue(undefined)).toBe(false) expect(emptyConditionValue({ b_type: 'money' }, 'between')).toEqual(['', '']) expect(emptyConditionValue({ b_type: 'select' }, 'eq')).toBeUndefined() }) }) 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('语义值 { 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( { 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 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() }) it('页签条件为 SQL 片段时整段成组,优先于旧结构 conditions', () => { const view = { code: 'pending', conditionSql: "b_status = N'1'", conditions: [{ field: 'status', operator: 'eq', value: '失败' }], } expect(astToSql(buildViewAst(view, fields), fields)).toBe("(b_status = N'1')") expect(buildViewAst({ code: 'all', conditionSql: ' ' }, fields)).toBeNull() }) it('页签 SQL 与其他来源按「且」合并', () => { const ast = combineAst( buildViewAst({ code: 'pending', conditionSql: "b_status = N'1'" }, fields), buildKeywordAst('东方', [{ field: fields.get('name'), config: {} }]), ) expect(astToSql(ast, fields)).toBe( "((b_status = N'1') AND ([name] LIKE N'%东方%'))", ) }) 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('快捷筛选跳过空值,操作符取模块配置,日期预设被解析', () => { 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')", ) }) 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('combineAst 把多个来源按「且」合并,空 AST 跳过', () => { const ast = combineAst( buildKeywordAst('东方', [{ field: fields.get('name'), config: {} }]), { type: 'group', children: [] }, { type: 'condition', field: 'status', operator: 'eq', value: 'new' }, ) 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('') }) it('关键词占位文案按字段名生成:多于 5 个只列前 5 个加「等」', () => { const makeItems = (labels) => labels.map((label, index) => ({ field: { b_field: `f${index}`, b_name: label, b_type: 'input' }, })) expect(buildKeywordPlaceholder(makeItems(['单号', '客户']))).toBe('搜索 单号、客户') expect(buildKeywordPlaceholder([])).toBe('') expect( buildKeywordPlaceholder(makeItems(['a', 'b', 'c', 'd', 'e', 'f'])), ).toBe('搜索 a、b、c、d、e 等') // 传入翻译函数时前缀与「等」走多语言 expect( buildKeywordPlaceholder(makeItems(['Order No']), (key, fallback) => key === 'common.keywordSearch' ? 'Search' : fallback, ), ).toBe('Search Order No') }) })