Files
workspace/code/fms/fms-vue/tests/unit/module-relation-panel.spec.js
T
2026-09-17 21:29:08 +08:00

304 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 模块关联面板(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)
})
})