309 lines
9.2 KiB
TypeScript
309 lines
9.2 KiB
TypeScript
/**
|
||
* 菜单管理:菜单入口权限点(s_power)派生 + 新增/删除时的落库请求。
|
||
*
|
||
* 这两块是保存编排里最容易出错、也最难靠肉眼验证的部分:
|
||
* - 权限点行完全由菜单行派生(menu.{编码}),名称 / 顺序 / 启用态跟随菜单,
|
||
* 目录不产生权限点;
|
||
* - 删除菜单要连带清掉多语言资源、模块绑定与入口权限点,
|
||
* 且权限点删除必须带 dependent_deletes(授权行随权限点删除)。
|
||
*/
|
||
|
||
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
buildMenuI18nKey,
|
||
buildMenuPowerCode,
|
||
buildMenuPowerRow,
|
||
normalizeMenuPowerRow,
|
||
menuRowOf,
|
||
MENU_NON_LEAF_TYPES,
|
||
MENU_TYPE_ICON_MAP,
|
||
MENU_TYPE_OPTIONS,
|
||
type MenuNode,
|
||
} from '@/views/module/menu-management/utils'
|
||
import { diffRow, diffRows, stripInternalKeys, cloneData } from '@/utils/dataChanges'
|
||
|
||
describe('菜单类型与图标', () => {
|
||
it('三种菜单类型都有图标映射,且只有目录能承载子节点', () => {
|
||
for (const option of MENU_TYPE_OPTIONS) {
|
||
expect(MENU_TYPE_ICON_MAP[option.value]).toBeTruthy()
|
||
}
|
||
expect(MENU_NON_LEAF_TYPES).toEqual(['directory'])
|
||
})
|
||
})
|
||
|
||
describe('菜单入口权限点派生', () => {
|
||
it('页面菜单派生 menu.{编码} 行,字段跟随菜单', () => {
|
||
const row = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_name: '联系人',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})
|
||
|
||
expect(row).toEqual({
|
||
b_id: 'menu.base_contact',
|
||
b_name: '联系人',
|
||
b_i18n: 'menu.base_contact',
|
||
b_type: 'menu',
|
||
b_object_type: 'menu',
|
||
b_object_id: 'base_contact',
|
||
b_action: 'access',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})
|
||
})
|
||
|
||
it('外链菜单同样派生权限点(外链也是「能否进入」的入口)', () => {
|
||
const row = buildMenuPowerRow({
|
||
b_id: 'ext_docs',
|
||
b_name: '外部文档',
|
||
b_menu_type: 'external',
|
||
b_canuse: 1,
|
||
})
|
||
|
||
expect(row?.b_id).toBe('menu.ext_docs')
|
||
})
|
||
|
||
it('目录不派生权限点(目录只是导航分组)', () => {
|
||
const row = buildMenuPowerRow({
|
||
b_id: 'group_base',
|
||
b_name: '基础资料',
|
||
b_menu_type: 'directory',
|
||
})
|
||
|
||
expect(row).toBeNull()
|
||
})
|
||
|
||
it('编码为空时返回 null(避免派生出 menu. 这种空编码权限点)', () => {
|
||
expect(buildMenuPowerRow({ b_id: '', b_menu_type: 'page' })).toBeNull()
|
||
expect(buildMenuPowerRow(null)).toBeNull()
|
||
})
|
||
|
||
it('停用菜单派生出停用的权限点(b_canuse 跟随)', () => {
|
||
const row = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_menu_type: 'page',
|
||
b_canuse: 0,
|
||
})
|
||
|
||
expect(row?.b_canuse).toBe(0)
|
||
})
|
||
|
||
it('b_canuse 缺省时按启用处理(不把历史数据的空值当停用)', () => {
|
||
const row = buildMenuPowerRow({ b_id: 'base_contact', b_menu_type: 'page' })
|
||
|
||
expect(row?.b_canuse).toBe(1)
|
||
})
|
||
})
|
||
|
||
describe('权限点基线归一化', () => {
|
||
it('剔除 b_bz 等不参与同步的列,数字列统一为 number', () => {
|
||
const normalized = normalizeMenuPowerRow({
|
||
b_id: 'menu.base_contact',
|
||
b_name: '联系人',
|
||
b_bz: '备注不该参与比较',
|
||
b_canuse: '1',
|
||
b_xh: '30',
|
||
})
|
||
|
||
expect(normalized).not.toHaveProperty('b_bz')
|
||
expect(normalized?.b_canuse).toBe(1)
|
||
expect(normalized?.b_xh).toBe(30)
|
||
})
|
||
|
||
it('空基线返回 null', () => {
|
||
expect(normalizeMenuPowerRow(null)).toBeNull()
|
||
})
|
||
|
||
it('归一化后与派生行比较:仅备注不同不算变更(避免「打开即脏」)', () => {
|
||
// 库里那行带了 b_bz,而派生行没有;归一化会把 b_bz 摘掉,两者应判为无差异
|
||
const org = normalizeMenuPowerRow({
|
||
b_id: 'menu.base_contact',
|
||
b_name: '联系人',
|
||
b_i18n: 'menu.base_contact',
|
||
b_type: 'menu',
|
||
b_object_type: 'menu',
|
||
b_object_id: 'base_contact',
|
||
b_action: 'access',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
b_bz: '人工写的备注',
|
||
})
|
||
const target = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_name: '联系人',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})
|
||
|
||
const changes = diffRows([target!], [org!], { keyFields: ['b_id'] })
|
||
expect(changes.inserts).toHaveLength(0)
|
||
expect(changes.updates).toHaveLength(0)
|
||
expect(changes.deletes).toHaveLength(0)
|
||
})
|
||
})
|
||
|
||
describe('权限点随菜单变化同步', () => {
|
||
const org = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_name: '联系人',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})!
|
||
|
||
it('菜单改名 → 权限点 update', () => {
|
||
const target = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_name: '联系人(新)',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})!
|
||
const changes = diffRows([target], [org], { keyFields: ['b_id'] })
|
||
|
||
expect(changes.updates).toHaveLength(1)
|
||
expect(changes.updates[0]?.b_name).toBe('联系人(新)')
|
||
})
|
||
|
||
it('菜单由页面改成目录 → 权限点 delete(目录没有入口权限)', () => {
|
||
const target = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_menu_type: 'directory',
|
||
})
|
||
const changes = diffRows(target ? [target] : [], [org], { keyFields: ['b_id'] })
|
||
|
||
expect(changes.deletes).toEqual([{ b_id: 'menu.base_contact' }])
|
||
})
|
||
|
||
it('菜单由目录改成页面 → 权限点 insert', () => {
|
||
const target = buildMenuPowerRow({
|
||
b_id: 'group_base',
|
||
b_name: '基础资料',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
})!
|
||
const changes = diffRows([target], [], { keyFields: ['b_id'] })
|
||
|
||
expect(changes.inserts).toHaveLength(1)
|
||
expect(changes.inserts[0]?.b_id).toBe('menu.group_base')
|
||
})
|
||
|
||
it('无任何变化 → 不产生请求(保存时提示「没有需要保存的变更」)', () => {
|
||
const target = buildMenuPowerRow({
|
||
b_id: 'base_contact',
|
||
b_name: '联系人',
|
||
b_menu_type: 'page',
|
||
b_canuse: 1,
|
||
b_xh: 30,
|
||
})!
|
||
const changes = diffRows([target], [org], { keyFields: ['b_id'] })
|
||
|
||
expect(
|
||
changes.inserts.length || changes.updates.length || changes.deletes.length,
|
||
).toBeFalsy()
|
||
})
|
||
})
|
||
|
||
describe('多语言键与权限编码', () => {
|
||
it('多语言键与权限编码同形但语义不同(一个进 s_i18n,一个进 s_power)', () => {
|
||
expect(buildMenuI18nKey('base_contact')).toBe('menu.base_contact')
|
||
expect(buildMenuPowerCode('base_contact')).toBe('menu.base_contact')
|
||
})
|
||
|
||
it('编码为空时两者都返回 null', () => {
|
||
expect(buildMenuI18nKey('')).toBeNull()
|
||
expect(buildMenuPowerCode('')).toBeNull()
|
||
})
|
||
})
|
||
|
||
describe('menuRowOf:树节点 → 主表行', () => {
|
||
it('剔除 children / isLeaf / icon 等树注入字段', () => {
|
||
const node = {
|
||
b_id: 'base_contact',
|
||
b_parent_id: 'group_base',
|
||
b_name: '联系人',
|
||
b_menu_type: 'page',
|
||
b_route: '/base/contact',
|
||
b_xh: 10,
|
||
b_canuse: 1,
|
||
children: [{ b_id: 'x' }],
|
||
isLeaf: false,
|
||
icon: 'file-text',
|
||
} as unknown as MenuNode
|
||
|
||
const row = menuRowOf(node)
|
||
|
||
expect(row).not.toHaveProperty('children')
|
||
expect(row).not.toHaveProperty('isLeaf')
|
||
expect(row).not.toHaveProperty('icon')
|
||
expect(row.b_id).toBe('base_contact')
|
||
})
|
||
|
||
it('b_path 缺省时退化为空串(非空列,不能为 null)', () => {
|
||
const row = menuRowOf({ b_id: 'a', b_menu_type: 'page' })
|
||
|
||
expect(row.b_path).toBe('')
|
||
expect(row.b_depth).toBe(0)
|
||
})
|
||
|
||
it('b_parent_id 空值归一成 null', () => {
|
||
const row = menuRowOf({ b_id: 'a', b_parent_id: '', b_menu_type: 'page' })
|
||
|
||
expect(row.b_parent_id).toBeNull()
|
||
})
|
||
})
|
||
|
||
describe('菜单主表 diff', () => {
|
||
it('新建(无基线)→ 整行 insert', () => {
|
||
const change = diffRow({
|
||
current: { b_id: 'new_menu', b_name: '新菜单', b_menu_type: 'page' },
|
||
original: null,
|
||
})
|
||
|
||
expect(change.hasChanges).toBe(true)
|
||
expect(change.inserts).toHaveLength(1)
|
||
expect(change.inserts[0]?.b_id).toBe('new_menu')
|
||
})
|
||
|
||
it('字段级 update 只携带变化列 + 主键', () => {
|
||
const change = diffRow({
|
||
current: { b_id: 'a', b_name: '新名', b_route: '/x', b_xh: 10 },
|
||
original: { b_id: 'a', b_name: '旧名', b_route: '/x', b_xh: 10 },
|
||
})
|
||
|
||
expect(change.updates).toHaveLength(1)
|
||
expect(change.updates[0]).toEqual({ b_id: 'a', b_name: '新名' })
|
||
expect(change.updates[0]).not.toHaveProperty('b_route')
|
||
})
|
||
|
||
it('派生字段(_org / v_*)不参与比较', () => {
|
||
const change = diffRow({
|
||
current: { b_id: 'a', b_name: '同名', _org: { x: 1 } },
|
||
original: { b_id: 'a', b_name: '同名', _org: { x: 2 } },
|
||
})
|
||
|
||
expect(change.hasChanges).toBe(false)
|
||
})
|
||
|
||
it('回环序列化:cloneData 的基线与草稿修改前不产生差异', () => {
|
||
const org = { b_id: 'a', b_name: '联系人', b_xh: 10 }
|
||
const draft = cloneData(org)
|
||
|
||
expect(diffRow({ current: draft, original: org }).hasChanges).toBe(false)
|
||
|
||
draft.b_name = '改了'
|
||
expect(diffRow({ current: draft, original: org }).hasChanges).toBe(true)
|
||
})
|
||
|
||
it('stripInternalKeys 递归剔除 _ / v_ 前缀(数组逐元素)', () => {
|
||
const stripped = stripInternalKeys([
|
||
{ b_id: 'a', _rowKey: 'k', v_count: 3, b_name: 'n' },
|
||
])
|
||
|
||
expect(stripped[0]).toEqual({ b_id: 'a', b_name: 'n' })
|
||
})
|
||
})
|