200 lines
6.8 KiB
TypeScript
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)
|
|
})
|
|
})
|