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

231 lines
8.2 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),不得改动业务列——否则打开模块即提示「未保存」;
* - 新增关联:追加「源模块 = 当前模块」的空行,主键四列留待用户填写;
* 新建关系默认关联删除处理为 restrict(设计 §3.1);
* - 换目标模块:原目标字段必然失效,需清空并按目标模块加载字段选项;
* - 关联删除处理三值对多对一同样开放:类型切成多对一时不得重置 b_on_delete(设计 §3.2);
* - 保存前校验:四列非空、关系类型合法、删除处置合法、四列组合不重复。
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, 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_on_delete: 'restrict',
b_canuse: 1,
b_xh: 10,
}
async function mountPanel(modelValue = []) {
const pinia = createPinia()
setActivePinia(pinia)
const wrapper = mount(ModuleRelationPanel, {
props: {
moduleId: 'order',
fields: FIELDS,
modules: MODULES,
reverseRows: [],
loading: false,
modelValue,
},
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
}
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('新增关联:追加源模块为当前模块的空行,默认关联删除处理为 restrict', 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_on_delete: 'restrict',
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('关联删除处理:加载回填不置脏,多对一不再被重置(允许 cascade)', async () => {
const cascadeRow = { ...sourceRow, b_relation_type: 'many_to_one', 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('cascade')
expect(Message.warning).not.toHaveBeenCalled()
})
it('校验:多对一配 cascade 合法,仅取值非法被拦', 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()
// 多对一 + cascade 是允许的场景(如「明细 → 主单」删主单级联删明细)
expect(errors.some((item) => item.includes('第 1 行'))).toBe(false)
expect(errors).toContain('第 2 行:关联删除处理取值不合法')
})
})