231 lines
8.2 KiB
JavaScript
231 lines
8.2 KiB
JavaScript
/**
|
||
* 模块关联面板(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 行:关联删除处理取值不合法')
|
||
})
|
||
})
|