Files
workspace/code/g3soft-erp/web/tests/views/module/menu-management/moduleBind.spec.ts
T
2026-10-15 17:31:35 +08:00

200 lines
6.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import ModuleBindPanel from '@/views/module/menu-management/ModuleBindPanel.vue'
import {
buildMenuModuleRows,
filterBindableModules,
isBindableModule,
moduleBindSignature,
normalizeMenuModulesByType,
} from '@/views/module/menu-management/utils'
/**
* 菜单「绑定模块」(s_menu_module)的纯逻辑与面板行为。
*
* 关键口径:只有页面(page)菜单绑定模块;落库只登记 data / virtual ——
* module 类型只是模块树上的分类节点,不参与绑定。
*/
const MODULE_TREE = [
{
b_id: 'sales',
b_name: '销售',
children: [
{ b_id: 'v_order', b_name: '订单' },
{ b_id: 'v_customer', b_name: '客户' },
],
},
{ b_id: 'v_other', b_name: '其他' },
]
/** 编码 → 类型:sales 是分类,其余是可绑定的数据模块 */
const TYPES: Record<string, string> = {
sales: 'module',
v_order: 'data',
v_customer: 'virtual',
v_other: 'data',
}
/** 挂面板;menuType 决定渲染树还是给一句说明,model 是已绑模块编码 */
function mountPanel(props: Record<string, unknown> = {}, model: string[] = []) {
return mount(ModuleBindPanel, {
props: {
treeData: MODULE_TREE,
moduleTypes: TYPES,
menuType: 'page',
menuId: 'm1',
modelValue: model,
'onUpdate:modelValue': () => {},
...props,
},
})
}
describe('isBindableModule', () => {
it('data / virtual 可绑定,module 不可', () => {
expect(isBindableModule('data')).toBe(true)
expect(isBindableModule('virtual')).toBe(true)
expect(isBindableModule('module')).toBe(false)
})
it('类型未知时按可绑定处理(避免误删历史绑定)', () => {
// 模块清单没加载全时,宁可保留关系行也不当成脏数据删掉
expect(isBindableModule(undefined)).toBe(true)
expect(isBindableModule(null)).toBe(true)
expect(isBindableModule('')).toBe(true)
})
})
describe('filterBindableModules', () => {
it('剔除分类节点,保留可绑定模块与原顺序', () => {
expect(filterBindableModules(['v_order', 'sales', 'v_customer'], TYPES)).toEqual([
'v_order',
'v_customer',
])
})
it('勾父级带回的子树键里,只有可绑定的留下', () => {
// 非严格模式点「销售」会把 sales / v_order / v_customer 一起回传
expect(filterBindableModules(['sales', 'v_order', 'v_customer'], TYPES)).toEqual([
'v_order',
'v_customer',
])
})
})
describe('moduleBindSignature', () => {
it('顺序无关:同集合不同顺序签名相同', () => {
expect(moduleBindSignature(['a', 'b'])).toBe(moduleBindSignature(['b', 'a']))
})
it('集合不同则签名不同', () => {
expect(moduleBindSignature(['a'])).not.toBe(moduleBindSignature(['a', 'b']))
})
it('数字与字符串编码视为同一个', () => {
expect(moduleBindSignature([1, 2])).toBe(moduleBindSignature(['1', '2']))
})
})
describe('buildMenuModuleRows', () => {
it('按勾选顺序生成关系行,b_xh 从 10 起步', () => {
expect(buildMenuModuleRows('m1', ['v_order', 'v_customer'])).toEqual([
{ b_menu_id: 'm1', b_module_id: 'v_order', b_xh: 10, b_canuse: 1 },
{ b_menu_id: 'm1', b_module_id: 'v_customer', b_xh: 20, b_canuse: 1 },
])
})
it('菜单编码为空时返回空数组(不产出悬空关系行)', () => {
expect(buildMenuModuleRows('', ['v_order'])).toEqual([])
})
it('空绑定返回空数组', () => {
expect(buildMenuModuleRows('m1', [])).toEqual([])
})
})
describe('normalizeMenuModulesByType', () => {
it('页面菜单保留绑定', () => {
expect(normalizeMenuModulesByType('page', ['v_order'])).toEqual(['v_order'])
})
it('目录 / 外链一律清空(非页面不绑定模块)', () => {
expect(normalizeMenuModulesByType('directory', ['v_order'])).toEqual([])
expect(normalizeMenuModulesByType('external', ['v_order'])).toEqual([])
})
})
describe('ModuleBindPanel', () => {
it('页面菜单渲染可勾选的模块树', () => {
const wrapper = mountPanel()
expect(wrapper.find('.biz-tree').exists()).toBe(true)
// 树默认收起(只渲染顶层节点),所以这里只断言顶层两个节点各有复选框
const boxes = wrapper.findAllComponents({ name: 'G3Checkbox' })
expect(boxes.length).toBe(2)
expect(wrapper.text()).toContain('销售')
expect(wrapper.text()).toContain('其他')
})
it('目录 / 外链不渲染树,各给一句说明', () => {
const dir = mountPanel({ menuType: 'directory' })
expect(dir.find('.biz-tree').exists()).toBe(false)
expect(dir.text()).toContain('目录不绑定模块')
const ext = mountPanel({ menuType: 'external' })
expect(ext.find('.biz-tree').exists()).toBe(false)
expect(ext.text()).toContain('外链不绑定模块')
})
it('初始已绑模块反映在顶层复选框上', async () => {
// 「其他」是顶层节点:默认收起的树里只有顶层可见,用它验证已绑态回填
const wrapper = mountPanel({}, ['v_other'])
await nextTick()
const boxes = wrapper.findAllComponents({ name: 'G3Checkbox' })
// 顶层顺序与 treeData 一致:sales、v_other
expect(boxes[0].props('modelValue')).toBe(false)
expect(boxes[1].props('modelValue')).toBe(true)
})
it('切换菜单清掉勾选草稿', async () => {
const wrapper = mountPanel({}, ['v_other'])
await nextTick()
expect(wrapper.findAllComponents({ name: 'G3Checkbox' })[1].props('modelValue')).toBe(true)
// 切到另一个菜单:父级尚未回填新绑定时,草稿必须清空而不是留着上一个菜单的勾
await wrapper.setProps({ menuId: 'm2', modelValue: [] })
await nextTick()
expect(wrapper.findAllComponents({ name: 'G3Checkbox' })[1].props('modelValue')).toBe(false)
})
it('勾选后回写模型时剔除分类节点', async () => {
const updates: string[][] = []
const wrapper = mountPanel({
'onUpdate:modelValue': (v: string[]) => updates.push(v),
})
await nextTick()
// 点「销售」分类 → 级联勾选整棵子树(含分类自身)
const boxes = wrapper.findAllComponents({ name: 'G3Checkbox' })
await boxes[0].vm.$emit('change', true)
await nextTick()
const last = updates.at(-1) ?? []
expect(last).not.toContain('sales')
expect(last.toSorted()).toEqual(['v_customer', 'v_order'])
})
it('数量标记只算可绑定的模块', async () => {
const wrapper = mountPanel({}, ['v_order', 'v_customer'])
await nextTick()
expect(wrapper.find('.g3-tag').text()).toBe('2')
})
it('未绑定时不显示数量标记', () => {
const wrapper = mountPanel({}, [])
expect(wrapper.find('.g3-tag').exists()).toBe(false)
})
})