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

309 lines
9.2 KiB
TypeScript
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_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' })
})
})