20260917212907
This commit is contained in:
1 parent
6fa3d8535e
commit
5cbf745bbf
115 files changed
+4104
-85
No files matched your search
@@ -0,0 +1,57 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createTestPinia, createTestRouter } from '../helpers/app'
|
||||
import App from '@/App.vue'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
import { uiConfig } from '@/components/ui/config'
|
||||
import { resetUIConfig } from '@/components/ui'
|
||||
|
||||
const loadDataApi = vi.fn()
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: (...args) => loadDataApi(...args),
|
||||
}))
|
||||
|
||||
async function mountApp() {
|
||||
const pinia = createTestPinia()
|
||||
const router = createTestRouter('/dashboard')
|
||||
await router.isReady()
|
||||
const wrapper = mount(App, {
|
||||
global: { plugins: [pinia, router], stubs: { FpsMonitor: true } },
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
return { wrapper, i18n: useI18nStore() }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
loadDataApi.mockReset()
|
||||
loadDataApi.mockResolvedValue({ data: [] })
|
||||
resetUIConfig()
|
||||
})
|
||||
|
||||
describe('App 语言联动(表单布局 + 译文)', () => {
|
||||
it('中文 → 表单横排,并按当前语言加载译文', async () => {
|
||||
const { i18n } = await mountApp()
|
||||
i18n.setLocale('zh-CN')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(uiConfig.form.layout).toBe('horizontal')
|
||||
expect(loadDataApi.mock.calls.some(([, cond]) => String(cond).includes("b_locale = N'zh-CN'"))).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('切到英文 → 表单竖排,并重新拉取 en-US 译文', async () => {
|
||||
const { i18n } = await mountApp()
|
||||
i18n.setLocale('en-US')
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(uiConfig.form.layout).toBe('vertical')
|
||||
expect(loadDataApi.mock.calls.some(([, cond]) => String(cond).includes("b_locale = N'en-US'"))).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -262,7 +262,8 @@ describe('FmsModuleListPage 冒烟', () => {
|
||||
|
||||
// 模块只配置了 b_name 一个查询条件,未写 quick 列表时它必须出现在常用区
|
||||
expect(wrapper.findAll('.query-field')).toHaveLength(1)
|
||||
expect(wrapper.find('.query-field__label').text()).toBe('语言名称')
|
||||
// 条件字段已改用 FormItem 渲染:label 由组件库输出
|
||||
expect(wrapper.find('.query-field .fms-form-item-label').text()).toBe('语言名称')
|
||||
querySchema.quick = original
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ModuleAutoCodePanel from '@/views/module/module-management/ModuleAutoCodePanel.vue'
|
||||
import Switch from '@/components/ui/switch/switch.vue'
|
||||
|
||||
const fields = [
|
||||
{ b_field: 'b_id', b_name: '编码', b_type: 'input', b_canuse: 1 },
|
||||
{ b_field: 'b_no', b_name: '业务单号', b_type: 'input', b_canuse: 1 },
|
||||
{ b_field: 'b_bz', b_name: '备注', b_type: 'textarea', b_canuse: 1 },
|
||||
]
|
||||
|
||||
/** 模拟父级 v-model:emit 回来要显式写回 prop,否则复现不了「立刻回读旧值」的问题 */
|
||||
function mountPanel() {
|
||||
let wrapper
|
||||
wrapper = mount(ModuleAutoCodePanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
fields,
|
||||
modelValue: [],
|
||||
'onUpdate:modelValue': (value) => wrapper.setProps({ modelValue: value }),
|
||||
},
|
||||
})
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('自动编码面板', () => {
|
||||
it('首次拨「启用」时惰性建行并写回 b_canuse(defineModel 不能写完立刻回读)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
// 模块从未配置自动编码:草稿为空,此时拨开关会走 ensureRow 建行
|
||||
wrapper.findComponent(Switch).vm.$emit('update:modelValue', true)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeTruthy()
|
||||
const row = emitted[0][0][0]
|
||||
expect(row.b_canuse).toBe(1)
|
||||
expect(row.b_module_id).toBe('m1')
|
||||
// 业务编号字段自动取第一个可用文本字段(排除 b_id)
|
||||
expect(row.b_field).toBe('b_no')
|
||||
})
|
||||
|
||||
it('已有一行时就地改,不再新建也不重新 emit 整个数组', async () => {
|
||||
let wrapper
|
||||
wrapper = mount(ModuleAutoCodePanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
fields,
|
||||
modelValue: [{ b_module_id: 'm1', b_field: 'b_no', b_canuse: 0, b_prefix: 'A' }],
|
||||
'onUpdate:modelValue': (value) => wrapper.setProps({ modelValue: value }),
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.findComponent(Switch).vm.$emit('update:modelValue', true)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 已落草稿:直接改这一行的属性(父级 ref 是深度响应式,改得见)
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
const row = wrapper.props('modelValue')[0]
|
||||
expect(row.b_canuse).toBe(1)
|
||||
expect(row.b_prefix).toBe('A') // 沿用原行,未被占位行覆盖
|
||||
})
|
||||
|
||||
it('紧凑网格固定竖排,不跟随全局横排配置', () => {
|
||||
const wrapper = mountPanel()
|
||||
expect(wrapper.find('form').classes()).toContain('fms-form-vertical')
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import FmsQueryControl from '@/components/fms-module-list/FmsQueryControl.vue'
|
||||
import FmsQueryToolbar from '@/components/fms-module-list/FmsQueryToolbar.vue'
|
||||
import FmsAdvancedQueryPanel from '@/components/fms-module-list/FmsAdvancedQueryPanel.vue'
|
||||
import FmsField from '@/components/fms-field/FmsField.vue'
|
||||
import { resetUIConfig, setUIConfig } from '@/components/ui'
|
||||
|
||||
/** 构造一个查询条件项(字段 + 配置),供工具栏布局用例复用 */
|
||||
const queryItem = (field) => ({
|
||||
@@ -20,6 +21,7 @@ const rangeQueryItem = (bType) => ({
|
||||
|
||||
describe('fms-module-list components', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
afterEach(() => resetUIConfig())
|
||||
|
||||
it('mounts a text query control with the new UI kit', () => {
|
||||
const wrapper = mount(FmsQueryControl, {
|
||||
@@ -40,6 +42,32 @@ describe('fms-module-list components', () => {
|
||||
expect(wrapper.html()).not.toContain('<a-')
|
||||
})
|
||||
|
||||
it('条件字段用 FormItem 渲染,label 排版跟随全局布局配置', () => {
|
||||
const mountToolbar = () =>
|
||||
mount(FmsQueryToolbar, {
|
||||
props: {
|
||||
queryItems: [queryItem('b_name')],
|
||||
queryLayout: { area: { b_name: 'quick' }, order: { b_name: 10 } },
|
||||
},
|
||||
})
|
||||
|
||||
// 中文(默认):横排,label 在左
|
||||
const zh = mountToolbar()
|
||||
const item = zh.find('.query-field')
|
||||
expect(item.classes()).toContain('fms-form-item')
|
||||
expect(item.classes()).not.toContain('fms-form-item-vertical')
|
||||
expect(item.find('.fms-form-item-label').text()).toBe('b_name')
|
||||
// 定宽标签会在短标签后拖出一段空白,查询区按内容收缩
|
||||
expect(item.find('.fms-form-item-label').attributes('style')).toContain('width: auto')
|
||||
zh.unmount()
|
||||
|
||||
// 切到非中文:竖排,label 在上
|
||||
setUIConfig({ form: { layout: 'vertical' } })
|
||||
const en = mountToolbar()
|
||||
expect(en.find('.query-field').classes()).toContain('fms-form-item-vertical')
|
||||
en.unmount()
|
||||
})
|
||||
|
||||
it('查询条件网格列数按条件数收缩(只有一个条件时不占 4 列宽)', () => {
|
||||
const one = mount(FmsQueryToolbar, {
|
||||
props: {
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 模块关联面板(s_relation):
|
||||
* - 加载回填行只补本地行键(_rowKey),不得改动业务列——否则打开模块即提示「未保存」;
|
||||
* - 新增关联:追加「源模块 = 当前模块」的空行,主键四列留待用户填写;
|
||||
* - 换目标模块:原目标字段必然失效,需清空并按目标模块加载字段选项;
|
||||
* - 保存前校验:四列非空、关系类型合法、四列组合不重复(空列会撞行身份,落库即主键冲突)。
|
||||
*/
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import ModuleRelationPanel from '@/views/module/module-management/ModuleRelationPanel.vue'
|
||||
import { stripInternalKeys } from '@/utils/dataChanges'
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: vi.fn(),
|
||||
nextIdApi: vi.fn(() => Promise.resolve({ data: '1900000000000000009', code: 0 })),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/message/message-manager.js', () => {
|
||||
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
|
||||
return { Message: m, default: m }
|
||||
})
|
||||
|
||||
import { loadDataApi } from '@/services/api'
|
||||
import { Message } from '@/components/ui/message/message-manager.js'
|
||||
|
||||
// jsdom 无布局:StkTable 在 clientWidth 为 0 时启用虚拟列裁剪,补成宽容器
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return 5000
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const MODULES = [
|
||||
{ b_id: 'order', b_name: '订单', b_canuse: 1 },
|
||||
{ b_id: 'order_item', b_name: '订单明细', b_canuse: 1 },
|
||||
{ b_id: 'legacy', b_name: '历史模块', b_canuse: 0 },
|
||||
]
|
||||
|
||||
const FIELDS = [{ b_field: 'b_id', b_name: '主键', b_canuse: 1 }]
|
||||
|
||||
/** 库中已有的一行:源侧(当前模块 order)→ 目标模块 order_item */
|
||||
const sourceRow = {
|
||||
b_source_module_id: 'order',
|
||||
b_source_field: 'b_id',
|
||||
b_target_module_id: 'order_item',
|
||||
b_target_field: 'b_order_id',
|
||||
b_relation_type: 'one_to_many',
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
}
|
||||
|
||||
async function mountPanel(modelValue = [], { refRules = [] } = {}) {
|
||||
const pinia = createPinia()
|
||||
pinia.install({ provide() {}, config: { globalProperties: {} } })
|
||||
setActivePinia(pinia)
|
||||
const wrapper = mount(ModuleRelationPanel, {
|
||||
props: {
|
||||
moduleId: 'order',
|
||||
fields: FIELDS,
|
||||
modules: MODULES,
|
||||
reverseRows: [],
|
||||
loading: false,
|
||||
modelValue,
|
||||
refRules,
|
||||
},
|
||||
global: { plugins: [pinia] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const lastRelations = (wrapper) => {
|
||||
const events = wrapper.emitted('update:modelValue') || []
|
||||
return events.length ? events[events.length - 1][0] : null
|
||||
}
|
||||
|
||||
const lastRefRules = (wrapper) => {
|
||||
const events = wrapper.emitted('update:refRules') || []
|
||||
return events.length ? events[events.length - 1][0] : null
|
||||
}
|
||||
|
||||
/** 已勾选引用规则的关系行(refRules 里挂着对应的 relation 规则) */
|
||||
const REF_SCOPE_ID = 'order|b_id|order_item|b_order_id'
|
||||
const refRuleRow = {
|
||||
b_id: '1900000000000000001',
|
||||
b_scope_type: 'relation',
|
||||
b_scope_id: REF_SCOPE_ID,
|
||||
b_hook: 'delete.pre',
|
||||
b_predicate: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
loadDataApi.mockReset()
|
||||
loadDataApi.mockResolvedValue({ data: [] })
|
||||
Message.warning.mockClear()
|
||||
Message.info.mockClear()
|
||||
Message.success.mockClear()
|
||||
})
|
||||
|
||||
describe('模块关联面板', () => {
|
||||
it('加载回填只补本地行键,业务列不变', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }])
|
||||
const next = lastRelations(wrapper)
|
||||
|
||||
expect(next).toHaveLength(1)
|
||||
expect(next[0]._rowKey).toBeTruthy()
|
||||
// 剥掉 _ 前缀的内部键后应与库中行完全一致(不置脏)
|
||||
expect(stripInternalKeys(next[0])).toEqual(sourceRow)
|
||||
})
|
||||
|
||||
it('新增关联:追加源模块为当前模块的空行并进入编辑', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }])
|
||||
await wrapper.vm.addRow()
|
||||
await nextTick()
|
||||
|
||||
const next = lastRelations(wrapper)
|
||||
expect(next).toHaveLength(2)
|
||||
expect(next[1]).toMatchObject({
|
||||
b_source_module_id: 'order',
|
||||
b_source_field: null,
|
||||
b_target_module_id: null,
|
||||
b_target_field: null,
|
||||
b_relation_type: 'many_to_one',
|
||||
b_canuse: 1,
|
||||
b_xh: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it('换目标模块:清空原目标字段并按新模块加载字段选项', async () => {
|
||||
loadDataApi.mockResolvedValue({
|
||||
data: [{ b_field: 'b_item_id', b_name: '明细主键', b_canuse: 1 }],
|
||||
})
|
||||
const wrapper = await mountPanel([{ ...sourceRow }])
|
||||
const row = wrapper.vm.relations[0]
|
||||
|
||||
wrapper.vm.onCellChange(row, { dataIndex: 'b_target_module_id' })
|
||||
expect(row.b_target_field).toBe(null)
|
||||
|
||||
await flushPromises()
|
||||
expect(loadDataApi).toHaveBeenCalledWith(
|
||||
's_field',
|
||||
"b_module_id = N'order_item'",
|
||||
'b_xh ASC, b_field ASC',
|
||||
)
|
||||
expect(wrapper.vm.fieldOptionsOf('order_item')).toEqual([
|
||||
{ label: '明细主键(b_item_id)', value: 'b_item_id' },
|
||||
])
|
||||
})
|
||||
|
||||
it('停用模块与历史字段降级为裸编码,不丢行', async () => {
|
||||
const wrapper = await mountPanel([
|
||||
{ ...sourceRow, b_target_module_id: 'legacy', b_target_field: 'b_gone' },
|
||||
])
|
||||
|
||||
// 停用模块不在可选项里,但名称解析仍可用(不阻断展示)
|
||||
expect(wrapper.vm.moduleOptions.map((item) => item.value)).toEqual(['order', 'order_item'])
|
||||
expect(wrapper.vm.moduleText('legacy')).toBe('历史模块(legacy)')
|
||||
expect(wrapper.vm.moduleText('removed')).toBe('removed')
|
||||
expect(wrapper.vm.fieldText('order', 'b_id')).toBe('主键(b_id)')
|
||||
expect(wrapper.vm.fieldText('order', 'b_gone')).toBe('b_gone')
|
||||
})
|
||||
|
||||
it('校验:空列与四列组合重复都被拦截', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }])
|
||||
wrapper.vm.relations = [
|
||||
{ ...sourceRow, _rowKey: 'r1' },
|
||||
{ ...sourceRow, _rowKey: 'r2' },
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.validate()).toContain('第 2 行:与前面的关联重复')
|
||||
|
||||
wrapper.vm.relations = [
|
||||
{
|
||||
_rowKey: 'r1',
|
||||
b_source_module_id: 'order',
|
||||
b_source_field: null,
|
||||
b_target_module_id: null,
|
||||
b_target_field: null,
|
||||
b_relation_type: 'many_to_one',
|
||||
b_canuse: 1,
|
||||
},
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
const errors = wrapper.vm.validate()
|
||||
expect(errors).toContain('第 1 行:请选择源字段')
|
||||
expect(errors).toContain('第 1 行:请选择目标模块')
|
||||
expect(errors).toContain('第 1 行:请选择目标字段')
|
||||
// 四列都空时行身份相同:只有一条时不算重复
|
||||
expect(errors.some((item) => item.includes('重复'))).toBe(false)
|
||||
})
|
||||
|
||||
it('批量删除:移除选中行并清空选择', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }])
|
||||
await wrapper.vm.addRow()
|
||||
await nextTick()
|
||||
const next = lastRelations(wrapper)
|
||||
wrapper.vm.selectedRowKeys = [String(next[1]._rowKey)]
|
||||
|
||||
wrapper.vm.batchDelete()
|
||||
await nextTick()
|
||||
|
||||
expect(lastRelations(wrapper)).toHaveLength(1)
|
||||
expect(wrapper.vm.selectedRowKeys).toEqual([])
|
||||
})
|
||||
|
||||
it('删除连带:加载回填不置脏,多对一自动清回不连带', async () => {
|
||||
const cascadeRow = { ...sourceRow, b_on_delete: 'cascade' }
|
||||
// 传入副本:面板会往行对象上补 _ 前缀派生列
|
||||
const wrapper = await mountPanel([{ ...cascadeRow }])
|
||||
const row = lastRelations(wrapper)[0]
|
||||
expect(stripInternalKeys(row)).toEqual(cascadeRow)
|
||||
|
||||
// 类型改成多对一:连带策略不再成立,自动清回 none 并提示
|
||||
row.b_relation_type = 'many_to_one'
|
||||
wrapper.vm.onCellChange(row, { dataIndex: 'b_relation_type' })
|
||||
await nextTick()
|
||||
|
||||
expect(row.b_on_delete).toBe('none')
|
||||
expect(Message.warning).toHaveBeenCalledWith(
|
||||
'多对一关系不支持连带删除/连带归档,已重置为不连带',
|
||||
)
|
||||
})
|
||||
|
||||
it('校验:多对一上配连带删除/归档被拦,取值非法被拦', async () => {
|
||||
const wrapper = await mountPanel([
|
||||
{ ...sourceRow, b_relation_type: 'many_to_one', b_on_delete: 'cascade' },
|
||||
{ ...sourceRow, b_target_field: 'b_other', b_on_delete: 'remove' },
|
||||
])
|
||||
const errors = wrapper.vm.validate()
|
||||
|
||||
expect(errors).toContain('第 1 行:多对一关系不支持连带删除/连带归档')
|
||||
expect(errors).toContain('第 2 行:删除连带取值不合法')
|
||||
})
|
||||
|
||||
it('引用规则:勾选取号建行、取消删行,回填按业务键还原勾选态', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }], { refRules: [] })
|
||||
const row = wrapper.vm.relations[0]
|
||||
|
||||
row._hasRefRule = 1
|
||||
wrapper.vm.onCellChange(row, { dataIndex: '_hasRefRule' })
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
const added = lastRefRules(wrapper)
|
||||
expect(added).toHaveLength(1)
|
||||
expect(added[0]).toMatchObject({
|
||||
b_id: '1900000000000000009',
|
||||
b_scope_type: 'relation',
|
||||
b_scope_id: REF_SCOPE_ID,
|
||||
b_predicate: null,
|
||||
})
|
||||
|
||||
// 已有规则时加载:勾选态由业务键还原(不置脏)
|
||||
const withRule = await mountPanel([{ ...sourceRow }], { refRules: [{ ...refRuleRow }] })
|
||||
expect(stripInternalKeys(withRule.vm.relations[0])).toEqual(sourceRow)
|
||||
expect(withRule.vm.relations[0]._hasRefRule).toBe(1)
|
||||
|
||||
// 取消勾选:规则行被移除
|
||||
const target = withRule.vm.relations[0]
|
||||
target._hasRefRule = 0
|
||||
withRule.vm.onCellChange(target, { dataIndex: '_hasRefRule' })
|
||||
await nextTick()
|
||||
expect(lastRefRules(withRule)).toEqual([])
|
||||
})
|
||||
|
||||
it('改关系主键:自动取消引用保护并删掉旧键规则,避免库中残留孤儿规则', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }], { refRules: [{ ...refRuleRow }] })
|
||||
const row = wrapper.vm.relations[0]
|
||||
expect(row._hasRefRule).toBe(1)
|
||||
|
||||
row.b_target_module_id = 'legacy'
|
||||
wrapper.vm.onCellChange(row, { dataIndex: 'b_target_module_id' })
|
||||
await nextTick()
|
||||
|
||||
expect(row._hasRefRule).toBe(0)
|
||||
expect(lastRefRules(wrapper)).toEqual([])
|
||||
expect(Message.info).toHaveBeenCalledWith(
|
||||
'关系主键已变化,「有引用时拒绝删除」已取消,请重新勾选',
|
||||
)
|
||||
})
|
||||
|
||||
it('删掉关系行时,挂在它上面的引用规则一并移除', async () => {
|
||||
const wrapper = await mountPanel([{ ...sourceRow }], { refRules: [{ ...refRuleRow }] })
|
||||
wrapper.vm.removeRow(wrapper.vm.relations[0])
|
||||
await nextTick()
|
||||
|
||||
expect(lastRefRules(wrapper)).toEqual([])
|
||||
expect(lastRelations(wrapper)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 删除规则编辑弹窗(ModuleRuleEditModal):
|
||||
* - 勾条件 → predicate 存 {"kind":"dsl","expr":AST};写 SQL → predicate 存文本;
|
||||
* - 两种类型各自的必填校验(无条件 / 缺提示文案)不通过时不提交;
|
||||
* - 打开时按 predicate 三态回显类型与内容;标题按新增/编辑切换。
|
||||
*/
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import ModuleRuleEditModal from '@/views/module/module-management/ModuleRuleEditModal.vue'
|
||||
import { buildRuleQueryItems } from '@/views/module/module-management/ruleUtils'
|
||||
|
||||
vi.mock('@/services/api', () => ({ loadDataApi: vi.fn() }))
|
||||
|
||||
vi.mock('@/components/ui/message/message-manager.js', () => {
|
||||
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
|
||||
return { Message: m, default: m }
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return 5000
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const ITEMS = buildRuleQueryItems(
|
||||
[
|
||||
{ b_field: 'b_status', b_name: '状态', b_type: 'select', b_canuse: 1 },
|
||||
{ b_field: 'b_no', b_name: '单号', b_type: 'input', b_canuse: 1 },
|
||||
],
|
||||
{},
|
||||
)
|
||||
|
||||
const ruleRow = () => ({
|
||||
_rowKey: '-1',
|
||||
b_id: '1900000000000000001',
|
||||
b_scope_type: 'module',
|
||||
b_scope_id: 'order',
|
||||
b_hook: 'delete.pre',
|
||||
b_predicate: null,
|
||||
b_message: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
})
|
||||
|
||||
async function mountModal(row = ruleRow(), isNew = false) {
|
||||
const pinia = createPinia()
|
||||
pinia.install({ provide() {}, config: { globalProperties: {} } })
|
||||
setActivePinia(pinia)
|
||||
const wrapper = mount(ModuleRuleEditModal, {
|
||||
props: { open: true, row, isNew, items: ITEMS },
|
||||
global: { plugins: [pinia] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('删除规则编辑弹窗', () => {
|
||||
it('勾条件:没有条件时不提交,选了条件后按 AST 组装 predicate', async () => {
|
||||
const row = ruleRow()
|
||||
const wrapper = await mountModal(row)
|
||||
|
||||
await wrapper.vm.onOk()
|
||||
expect(wrapper.vm.conditionError).toBe('勾条件规则至少要有一个条件')
|
||||
expect(wrapper.emitted('ok')).toBeFalsy()
|
||||
|
||||
wrapper.vm.conditionState = {
|
||||
nodes: [{ id: 'n1', at: 1, join: 'and', field: 'b_status', operator: 'eq', value: 'audit' }],
|
||||
groups: [],
|
||||
}
|
||||
await nextTick()
|
||||
expect(wrapper.vm.conditionSummary).toBe('状态 等于 audit')
|
||||
|
||||
await wrapper.vm.onOk()
|
||||
const saved = wrapper.emitted('ok')?.[0]?.[0]
|
||||
expect(saved.b_hook).toBe('delete.pre')
|
||||
const predicate = JSON.parse(saved.b_predicate)
|
||||
expect(predicate.kind).toBe('dsl')
|
||||
expect(predicate.expr.children[0]).toMatchObject({
|
||||
type: 'condition',
|
||||
field: 'b_status',
|
||||
operator: 'eq',
|
||||
value: 'audit',
|
||||
})
|
||||
expect(wrapper.emitted('update:open')?.at(-1)).toEqual([false])
|
||||
})
|
||||
|
||||
it('写 SQL:缺提示文案不提交,填好后原样存文本', async () => {
|
||||
const row = ruleRow()
|
||||
const wrapper = await mountModal(row)
|
||||
wrapper.vm.ruleType = 'sql'
|
||||
wrapper.vm.sqlText = 'select b_id from bill where b_id in (:ids) and b_settled = 1'
|
||||
await nextTick()
|
||||
|
||||
await wrapper.vm.onOk()
|
||||
expect(wrapper.vm.sqlError).toBe('写 SQL 的规则必须填写提示文案')
|
||||
expect(wrapper.emitted('ok')).toBeFalsy()
|
||||
|
||||
row.b_message = '单据已销账,不能删除'
|
||||
await wrapper.vm.onOk()
|
||||
expect(wrapper.emitted('ok')?.[0]?.[0].b_predicate).toBe(
|
||||
'select b_id from bill where b_id in (:ids) and b_settled = 1',
|
||||
)
|
||||
})
|
||||
|
||||
it('写 SQL:SQL 为空时不提交', async () => {
|
||||
const wrapper = await mountModal(ruleRow())
|
||||
wrapper.vm.ruleType = 'sql'
|
||||
wrapper.vm.sqlText = ' '
|
||||
await nextTick()
|
||||
|
||||
await wrapper.vm.onOk()
|
||||
expect(wrapper.vm.sqlError).toBe('请填写规则 SQL')
|
||||
expect(wrapper.emitted('ok')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('回显与标题:SQL 类型直接进 SQL 分支;新增/编辑标题不同', async () => {
|
||||
const row = { ...ruleRow(), b_predicate: 'select 1 as b_id' }
|
||||
const wrapper = await mountModal(row, true)
|
||||
|
||||
expect(wrapper.vm.ruleType).toBe('sql')
|
||||
expect(wrapper.vm.sqlText).toBe('select 1 as b_id')
|
||||
expect(wrapper.vm.modalTitle).toBe('新增删除规则')
|
||||
|
||||
const editWrapper = await mountModal({ ...ruleRow() }, false)
|
||||
expect(editWrapper.vm.modalTitle).toBe('编辑删除规则')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 删除规则面板(module-management/ModuleRulePanel):
|
||||
* - 加载回填只补界面派生列(_rowKey / _typeText / _labelText),业务列不变(不置脏);
|
||||
* - 列表识别:勾条件显示条件中文摘要,写 SQL 显示提示文案;
|
||||
* - 保存前校验:勾条件要有条件、写 SQL 要有 SQL 与提示文案;
|
||||
* - 其他模块在本模块上配的引用规则只读展示,可跳转到源模块。
|
||||
*/
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import ModuleRulePanel from '@/views/module/module-management/ModuleRulePanel.vue'
|
||||
import { stripInternalKeys } from '@/utils/dataChanges'
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: vi.fn(),
|
||||
nextIdApi: vi.fn(() => Promise.resolve({ data: '1900000000000000009', code: 0 })),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/message/message-manager.js', () => {
|
||||
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
|
||||
return { Message: m, default: m }
|
||||
})
|
||||
|
||||
// jsdom 无布局:StkTable 在 clientWidth 为 0 时启用虚拟列裁剪,补成宽容器
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return 5000
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const FIELDS = [
|
||||
{ b_field: 'b_status', b_name: '状态', b_type: 'select', b_canuse: 1 },
|
||||
{ b_field: 'b_settled', b_name: '已销账', b_type: 'checkbox', b_canuse: 1 },
|
||||
]
|
||||
|
||||
const MODULES = [
|
||||
{ b_id: 'order', b_name: '订单', b_canuse: 1 },
|
||||
{ b_id: 'order_item', b_name: '订单明细', b_canuse: 1 },
|
||||
]
|
||||
|
||||
/** 库中已有的一条勾条件规则:状态等于 audit */
|
||||
const dslRow = {
|
||||
b_id: '1900000000000000001',
|
||||
b_scope_type: 'module',
|
||||
b_scope_id: 'order',
|
||||
b_hook: 'delete.pre',
|
||||
b_predicate: JSON.stringify({
|
||||
kind: 'dsl',
|
||||
expr: {
|
||||
type: 'group',
|
||||
children: [
|
||||
{ type: 'condition', field: 'b_status', operator: 'eq', value: 'audit', join: 'and' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
b_message: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
}
|
||||
|
||||
async function mountPanel({ rules = [], refRulesReverse = [] } = {}) {
|
||||
const pinia = createPinia()
|
||||
pinia.install({ provide() {}, config: { globalProperties: {} } })
|
||||
setActivePinia(pinia)
|
||||
const wrapper = mount(ModuleRulePanel, {
|
||||
props: {
|
||||
moduleId: 'order',
|
||||
fields: FIELDS,
|
||||
querySchema: {},
|
||||
refRulesReverse,
|
||||
modules: MODULES,
|
||||
loading: false,
|
||||
modelValue: rules,
|
||||
},
|
||||
global: { plugins: [pinia] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const lastRules = (wrapper) => {
|
||||
const events = wrapper.emitted('update:modelValue') || []
|
||||
return events.length ? events[events.length - 1][0] : null
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('删除规则面板', () => {
|
||||
it('加载回填只补界面派生列,业务列不变', async () => {
|
||||
const wrapper = await mountPanel({ rules: [{ ...dslRow }] })
|
||||
const next = lastRules(wrapper)
|
||||
|
||||
expect(next).toHaveLength(1)
|
||||
expect(next[0]._rowKey).toBeTruthy()
|
||||
expect(next[0]._typeText).toBe('条件')
|
||||
expect(next[0]._labelText).toBe('状态 等于 audit')
|
||||
// 剥掉 _ 前缀的内部键后与库中行完全一致(不置脏)
|
||||
expect(stripInternalKeys(next[0])).toEqual(dslRow)
|
||||
})
|
||||
|
||||
it('识别文案:写 SQL 的规则用提示文案当列表文案', async () => {
|
||||
const sqlRow = {
|
||||
...dslRow,
|
||||
b_id: '1900000000000000002',
|
||||
b_predicate: 'select b_id from bill where b_settled = 1',
|
||||
b_message: '单据已销账,不能删除',
|
||||
}
|
||||
const wrapper = await mountPanel({ rules: [sqlRow] })
|
||||
const next = lastRules(wrapper)
|
||||
|
||||
expect(next[0]._typeText).toBe('SQL')
|
||||
expect(next[0]._labelText).toBe('单据已销账,不能删除')
|
||||
})
|
||||
|
||||
it('校验:勾条件必须真有条件,写 SQL 必须有 SQL 与提示文案', async () => {
|
||||
const emptyDsl = {
|
||||
...dslRow,
|
||||
b_id: '1900000000000000003',
|
||||
b_predicate: JSON.stringify({ kind: 'dsl', expr: { type: 'group', children: [] } }),
|
||||
}
|
||||
const sqlWithoutMessage = {
|
||||
...dslRow,
|
||||
b_id: '1900000000000000004',
|
||||
b_predicate: 'select b_id from bill where b_id in (:ids)',
|
||||
b_message: '',
|
||||
}
|
||||
const wrapper = await mountPanel({ rules: [emptyDsl, sqlWithoutMessage] })
|
||||
const errors = wrapper.vm.validate()
|
||||
|
||||
expect(errors).toContain('第 1 条规则:至少配置一个判定条件')
|
||||
expect(errors).toContain('第 2 条规则:写 SQL 的规则必须填写提示文案')
|
||||
|
||||
// 引用规则不该出现在模块级列表里;没有主键的行也要拦住(新增取号失败的情况)
|
||||
const refWrapper = await mountPanel({ rules: [{ ...dslRow, b_predicate: null }] })
|
||||
expect(refWrapper.vm.validate()).toContain('第 1 条规则:引用规则请到「模块关联」面板维护')
|
||||
const noIdWrapper = await mountPanel({ rules: [{ ...dslRow, b_id: '' }] })
|
||||
expect(noIdWrapper.vm.validate()).toContain('第 1 条规则:缺少主键(请重新新增)')
|
||||
})
|
||||
|
||||
it('新增规则:取号后打开弹窗,确定时才落草稿', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
await wrapper.vm.openRuleModal(null)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.vm.editingIsNew).toBe(true)
|
||||
expect(wrapper.vm.editingRow.b_id).toBe('1900000000000000009')
|
||||
expect(wrapper.vm.ruleModalOpen).toBe(true)
|
||||
// 未确定前不进列表
|
||||
expect(lastRules(wrapper)).toBe(null)
|
||||
|
||||
wrapper.vm.onRuleOk({ ...wrapper.vm.editingRow, b_predicate: 'select 1 as b_id' })
|
||||
await nextTick()
|
||||
expect(lastRules(wrapper)).toHaveLength(1)
|
||||
expect(lastRules(wrapper)[0].b_id).toBe('1900000000000000009')
|
||||
})
|
||||
|
||||
it('只读区:把引用规则还原成模块名/字段列,并支持跳转到源模块', async () => {
|
||||
const wrapper = await mountPanel({
|
||||
refRulesReverse: [
|
||||
{
|
||||
b_id: '1900000000000000005',
|
||||
b_scope_type: 'relation',
|
||||
b_scope_id: 'order_item|b_order_id|order|b_id',
|
||||
b_hook: 'delete.pre',
|
||||
b_predicate: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
},
|
||||
],
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('其他模块在本模块上配置的引用规则')
|
||||
expect(wrapper.text()).toContain('订单明细(order_item)')
|
||||
expect(wrapper.text()).toContain('订单(order)')
|
||||
|
||||
const viewButton = wrapper.findAll('button').find((button) => button.text().includes('查看'))
|
||||
expect(viewButton, '只读区应有查看按钮').toBeTruthy()
|
||||
await viewButton.trigger('click')
|
||||
expect(wrapper.emitted('navigate')?.[0]).toEqual(['order_item'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 删除规则配置工具(module-management/ruleUtils):
|
||||
* 关系业务键拼串/还原、predicate 三态推断、识别文案、新增行构造、条件面板 items 构造。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
REF_RULE_HINT,
|
||||
buildRuleQueryItems,
|
||||
createRefRuleRow,
|
||||
createRuleRow,
|
||||
isCompleteRelationKey,
|
||||
itemMapOf,
|
||||
parseRelationScopeId,
|
||||
parseRulePredicate,
|
||||
relationScopeId,
|
||||
ruleLabel,
|
||||
ruleSummary,
|
||||
ruleTypeText,
|
||||
} from '@/views/module/module-management/ruleUtils'
|
||||
|
||||
const relation = {
|
||||
b_source_module_id: 'order',
|
||||
b_source_field: 'b_id',
|
||||
b_target_module_id: 'order_item',
|
||||
b_target_field: 'b_order_id',
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{ b_field: 'b_status', b_name: '状态', b_type: 'select', b_canuse: 1 },
|
||||
{ b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 },
|
||||
{ b_field: 'b_off', b_name: '停用字段', b_type: 'input', b_canuse: 0 },
|
||||
]
|
||||
|
||||
describe('删除规则工具', () => {
|
||||
it('关系业务键与四列互逆', () => {
|
||||
expect(relationScopeId(relation)).toBe('order|b_id|order_item|b_order_id')
|
||||
expect(parseRelationScopeId('order|b_id|order_item|b_order_id')).toEqual({
|
||||
sourceModuleId: 'order',
|
||||
sourceField: 'b_id',
|
||||
targetModuleId: 'order_item',
|
||||
targetField: 'b_order_id',
|
||||
})
|
||||
expect(isCompleteRelationKey(relation)).toBe(true)
|
||||
expect(isCompleteRelationKey({ ...relation, b_target_field: null })).toBe(false)
|
||||
})
|
||||
|
||||
it('predicate 三态:空=引用、JSON=勾条件、其余=SQL(非法 JSON 降级为 SQL)', () => {
|
||||
expect(parseRulePredicate({ b_predicate: null }).kind).toBe('reference')
|
||||
expect(parseRulePredicate({ b_predicate: ' ' }).kind).toBe('reference')
|
||||
|
||||
const dsl = parseRulePredicate({
|
||||
b_predicate: JSON.stringify({
|
||||
kind: 'dsl',
|
||||
expr: { type: 'group', children: [{ type: 'condition', field: 'b_status' }] },
|
||||
}),
|
||||
})
|
||||
expect(dsl.kind).toBe('dsl')
|
||||
expect(dsl.expr.children).toHaveLength(1)
|
||||
|
||||
expect(parseRulePredicate({ b_predicate: 'select 1 where b_id in (:ids)' })).toMatchObject({
|
||||
kind: 'sql',
|
||||
})
|
||||
expect(parseRulePredicate({ b_predicate: '{不是合法 JSON' })).toMatchObject({ kind: 'sql' })
|
||||
// JSON 合法但不是 dsl 形状:按 SQL 文本处理,内容不丢
|
||||
expect(parseRulePredicate({ b_predicate: '{"foo":1}' })).toMatchObject({ kind: 'sql' })
|
||||
})
|
||||
|
||||
it('类型文案与摘要:条件走中文描述、SQL 单行截断、引用固定说明', () => {
|
||||
const itemByField = itemMapOf(buildRuleQueryItems(fields, {}))
|
||||
|
||||
const dslRow = {
|
||||
b_predicate: JSON.stringify({
|
||||
kind: 'dsl',
|
||||
expr: {
|
||||
type: 'group',
|
||||
children: [{ type: 'condition', field: 'b_amount', operator: 'gt', value: 0 }],
|
||||
},
|
||||
}),
|
||||
}
|
||||
expect(ruleTypeText(dslRow)).toBe('条件')
|
||||
expect(ruleSummary(dslRow, itemByField)).toBe('金额 大于 0')
|
||||
|
||||
const sqlRow = { b_predicate: 'select\n b_id\nfrom bill where b_settled = 1' }
|
||||
expect(ruleTypeText(sqlRow)).toBe('SQL')
|
||||
expect(ruleSummary(sqlRow, itemByField)).toBe('select b_id from bill where b_settled = 1')
|
||||
|
||||
const refRow = { b_predicate: null }
|
||||
expect(ruleTypeText(refRow)).toBe('引用')
|
||||
expect(ruleSummary(refRow, itemByField)).toBe(REF_RULE_HINT)
|
||||
})
|
||||
|
||||
it('识别文案:勾条件显示条件摘要,写 SQL 显示提示文案(缺文案退回 SQL 片段)', () => {
|
||||
const itemByField = itemMapOf(buildRuleQueryItems(fields, {}))
|
||||
|
||||
const dslRow = {
|
||||
b_predicate: JSON.stringify({
|
||||
kind: 'dsl',
|
||||
expr: {
|
||||
type: 'group',
|
||||
children: [{ type: 'condition', field: 'b_status', operator: 'eq', value: 'audit' }],
|
||||
},
|
||||
}),
|
||||
}
|
||||
expect(ruleLabel(dslRow, itemByField)).toBe('状态 等于 audit')
|
||||
|
||||
const sqlRow = { b_predicate: 'select 1 as b_id', b_message: '单据已销账,不能删除' }
|
||||
expect(ruleLabel(sqlRow, itemByField)).toBe('单据已销账,不能删除')
|
||||
expect(ruleLabel({ ...sqlRow, b_message: null }, itemByField)).toBe('select 1 as b_id')
|
||||
|
||||
expect(ruleLabel({ b_predicate: null }, itemByField)).toBe(REF_RULE_HINT)
|
||||
})
|
||||
|
||||
it('新增行:主键由调用方取号传入,无名称/编码字段', () => {
|
||||
const rule = createRuleRow('order', { id: '1900000000000000001', xh: 30 })
|
||||
expect(rule).toMatchObject({
|
||||
b_id: '1900000000000000001',
|
||||
b_scope_type: 'module',
|
||||
b_scope_id: 'order',
|
||||
b_hook: 'delete.pre',
|
||||
b_predicate: null,
|
||||
b_canuse: 1,
|
||||
b_xh: 30,
|
||||
})
|
||||
expect(rule.b_code).toBeUndefined()
|
||||
expect(rule.b_name).toBeUndefined()
|
||||
|
||||
const ref = createRefRuleRow('order|b_id|order_item|b_order_id', '1900000000000000002')
|
||||
expect(ref).toMatchObject({
|
||||
b_id: '1900000000000000002',
|
||||
b_scope_type: 'relation',
|
||||
b_scope_id: 'order|b_id|order_item|b_order_id',
|
||||
b_predicate: null,
|
||||
b_hook: 'delete.pre',
|
||||
})
|
||||
})
|
||||
|
||||
it('items 构造:只列启用字段,类型映射控件,select 缺选项时降级为输入', () => {
|
||||
const items = buildRuleQueryItems(fields, {
|
||||
groups: [
|
||||
{
|
||||
conditions: [
|
||||
{ field: 'b_status', operator: 'in', component: 'select', options: [{ value: 'a' }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(items.map((item) => item.field.b_field)).toEqual(['b_status', 'b_amount'])
|
||||
// 查询配置里定义过:沿用其操作符/控件/选项
|
||||
expect(items[0].config).toMatchObject({ b_operator: 'in', b_component: 'select' })
|
||||
expect(items[0].options).toEqual([{ value: 'a', label: 'a' }])
|
||||
// 未定义过:按字段类型推导(金额 → 数字输入)
|
||||
expect(items[1].config.b_component).toBe('number')
|
||||
expect(items[1].config.b_operator).toBe('eq')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
advancedQueryFromAst,
|
||||
astToSql,
|
||||
buildAdvancedAst,
|
||||
buildQuickAst,
|
||||
@@ -148,3 +149,89 @@ describe('查询条件 AST 构造', () => {
|
||||
expect(emptyConditionValue({ b_type: 'select' }, 'eq')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
it('单层「行 + 组」往返:还原后再构造语义等价', () => {
|
||||
const ast = buildAdvancedAst({
|
||||
nodes: [
|
||||
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
|
||||
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
join: 'and',
|
||||
nodes: [
|
||||
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
|
||||
{ join: 'or', field: 'status', operator: 'eq', value: 'done' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
const state = advancedQueryFromAst(ast, { idFactory })
|
||||
|
||||
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('组内再嵌套组:拍平成单层并置 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 })
|
||||
|
||||
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('单条件 AST(非 group)也能还原成一行', () => {
|
||||
const state = advancedQueryFromAst(
|
||||
{ type: 'condition', field: 'status', operator: 'eq', value: 'draft' },
|
||||
{ idFactory },
|
||||
)
|
||||
expect(state.nodes).toHaveLength(1)
|
||||
expect(state.nodes[0]).toMatchObject({ field: 'status', operator: 'eq', value: 'draft' })
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user