Files
workspace/code/fms/fms-vue/tests/views/menu-module-binding.spec.js
T
2026-09-22 17:29:39 +08:00

320 lines
10 KiB
JavaScript
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.
/**
* 回归用例:菜单管理 - 随菜单保存同步的两份子表数据。
*
* 1) 绑定模块(s_menu_module)
* s_menu 表没有模块列:绑定关系存 s_menu_module,行身份 = (b_menu_id, b_module_id),
* 只登记 data / virtual 模块(module 只是模块树上的分类,不参与权限)。
* 保存时与加载基线做行级 diff,只提交增删行;目录 / 外链菜单不绑定模块。
*
* 2) 菜单入口权限点(s_power, b_object_type = 'menu')
* 页面 / 外链菜单各派生一行 menu.{菜单编码}(b_action = 'access',名称/顺序/启用跟随菜单),
* 目录没有;保存时按菜单行与该菜单的加载基线 diff;删除菜单时只删权限点行,
* 引用它的授权行用 dependent_deletes 声明,由保存层按同一条件查出编码再删(设计 §14.6)。
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import MenuManagement from '@/views/module/menu-management/index.vue'
import { mountPage } from '../helpers/app.js'
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn(),
saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })),
nextIdApi: vi.fn(() => Promise.resolve({ data: 9001, code: 0, success: true })),
describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })),
}))
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, saveObjectApi } from '@/services/api'
const directoryMenu = {
b_id: 'base',
b_parent_id: null,
b_depth: 0,
b_path: '/base/',
b_name: '基础资料',
b_i18n: 'menu.base',
b_menu_type: 'directory',
b_route: null,
b_icon: null,
b_xh: 10,
b_canuse: 1,
}
const pageMenu = {
b_id: 'otherdata',
b_parent_id: 'base',
b_depth: 1,
b_path: '/base/otherdata/',
b_name: '其他数据',
b_i18n: 'menu.otherdata',
b_menu_type: 'page',
b_route: '/base/otherdata',
b_icon: null,
b_xh: 10,
b_canuse: 1,
}
/** 模块清单:base / base_otherdata 为分类节点,b_port / b_money 为数据模块 */
const modules = [
{
b_id: 'base',
b_parent_id: null,
b_depth: 0,
b_path: '/base/',
b_module_type: 'module',
b_name: '基础资料',
b_canuse: 1,
b_xh: 10,
},
{
b_id: 'base_otherdata',
b_parent_id: 'base',
b_depth: 1,
b_path: '/base/base_otherdata/',
b_module_type: 'module',
b_name: '其他数据',
b_canuse: 1,
b_xh: 30,
},
{
b_id: 'b_port',
b_parent_id: 'base_otherdata',
b_depth: 2,
b_path: '/base/base_otherdata/b_port/',
b_module_type: 'data',
b_name: '港口',
b_canuse: 1,
b_xh: 10,
},
{
b_id: 'b_money',
b_parent_id: 'base_otherdata',
b_depth: 2,
b_path: '/base/base_otherdata/b_money/',
b_module_type: 'data',
b_name: '币种',
b_canuse: 1,
b_xh: 20,
},
]
/** 库中 s_menu_module 的现有行(每个用例前重置) */
let boundRows = []
/** 库中该菜单的 s_power(menu.*) 行(每个用例前重置) */
let powerRows = []
/** 从查询条件里取 N'xxx' 字面量(mock 按 id 过滤用;'1 = 1' 等条件返回 null) */
function literalOf(condition) {
return /N'([^']*)'/.exec(String(condition || ''))?.[1] ?? null
}
beforeEach(() => {
boundRows = [{ b_menu_id: 'otherdata', b_module_id: 'b_port', b_xh: 10, b_canuse: 1 }]
powerRows = []
loadDataApi.mockReset()
saveObjectApi.mockClear()
loadDataApi.mockImplementation((view, condition) => {
if (view === 's_menu') {
const id = literalOf(condition)
const rows = [directoryMenu, pageMenu].filter((row) => !id || row.b_id === id)
return Promise.resolve({ data: rows })
}
if (view === 's_module') return Promise.resolve({ data: modules })
if (view === 's_menu_module') {
const id = literalOf(condition)
return Promise.resolve({
data: boundRows.filter((row) => !id || row.b_menu_id === id),
})
}
if (view === 's_power') {
const id = literalOf(condition)
return Promise.resolve({
data: powerRows.filter((row) => !id || row.b_object_id === id),
})
}
return Promise.resolve({ data: [] })
})
})
/** 取出最近一次保存请求里的指定表请求 */
function lastRequestOf(table) {
const requests = saveObjectApi.mock.calls.at(-1)[0]
return requests.find((request) => request.table === table)
}
describe('菜单管理:绑定模块(s_menu_module)', () => {
it('选中菜单时按 b_menu_id 加载绑定模块', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
expect(loadDataApi).toHaveBeenCalledWith(
's_menu_module',
"b_menu_id = N'otherdata'",
'b_xh ASC, b_module_id ASC',
)
expect(wrapper.vm.menuModules).toEqual(['b_port'])
})
it('新增绑定:只提交 s_menu_module 的 insert 行', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
wrapper.vm.menuModules = ['b_port', 'b_money']
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.key_field).toBe('b_menu_id,b_module_id')
expect(request.inserts).toEqual([
{ b_menu_id: 'otherdata', b_module_id: 'b_money', b_xh: 20, b_canuse: 1 },
])
expect(request.deletes).toBeUndefined()
})
it('移除绑定:只提交 s_menu_module 的 delete 主键行', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
wrapper.vm.menuModules = []
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.deletes).toEqual([{ b_menu_id: 'otherdata', b_module_id: 'b_port' }])
expect(request.inserts).toBeUndefined()
})
it('目录菜单不绑定模块:保存时清空并删除既有关系行', async () => {
boundRows = [{ b_menu_id: 'base', b_module_id: 'b_port', b_xh: 10, b_canuse: 1 }]
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'base'
await flushPromises()
expect(wrapper.vm.menuModules).toEqual(['b_port'])
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.deletes).toEqual([{ b_menu_id: 'base', b_module_id: 'b_port' }])
})
})
describe('菜单管理:入口权限点(s_power, menu.*)', () => {
/** 库中该菜单的入口权限点行(名称故意与菜单不同,用于验证保存会同步) */
function makePowerRow(over = {}) {
return {
b_id: 'menu.otherdata',
b_name: '其他数据',
b_i18n: 'menu.otherdata',
b_type: 'menu',
b_object_type: 'menu',
b_object_id: 'otherdata',
b_action: 'access',
b_canuse: 1,
b_xh: 10,
...over,
}
}
it('选中页面菜单时按 b_object_id 加载入口权限点基线', async () => {
powerRows = [makePowerRow()]
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
expect(loadDataApi).toHaveBeenCalledWith(
's_power',
"b_object_type = 'menu' and b_object_id = N'otherdata'",
'b_id ASC',
)
expect(wrapper.vm.menuPower_org?.b_id).toBe('menu.otherdata')
})
it('新增页面菜单:自动派生 menu.{编码} 权限点行', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
// 改菜单名,保证有可提交的差异
wrapper.vm.maindata.b_name = '其他数据(新)'
await wrapper.vm.handleSave()
const request = lastRequestOf('s_power')
expect(request.key_field).toBe('b_id')
expect(request.inserts).toHaveLength(1)
expect(request.inserts[0]).toMatchObject({
b_id: 'menu.otherdata',
b_name: '其他数据(新)',
b_i18n: 'menu.otherdata',
b_type: 'menu',
b_object_type: 'menu',
b_object_id: 'otherdata',
b_action: 'access',
b_canuse: 1,
b_xh: 10,
})
})
it('保存页面菜单:入口权限点的名称跟随菜单同步更新', async () => {
powerRows = [makePowerRow({ b_name: '旧名称' })]
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
expect(wrapper.vm.menuPower_org?.b_name).toBe('旧名称')
// 菜单本身没有改动,只有权限点名称与菜单不一致 → 也应同步
await wrapper.vm.handleSave()
const request = lastRequestOf('s_power')
expect(request.updates).toHaveLength(1)
expect(request.updates[0]).toMatchObject({ b_id: 'menu.otherdata', b_name: '其他数据' })
})
it('目录菜单没有入口权限点:无差异时不提交任何保存请求', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'base'
await flushPromises()
await wrapper.vm.handleSave()
// 目录不派生权限点;菜单 / 绑定 / 多语言都无差异 → 不调用保存接口
expect(saveObjectApi).not.toHaveBeenCalled()
})
it('删除页面菜单:删掉入口权限点(授权行由保存层连带清理)', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.deleteMenu('otherdata')
await flushPromises()
await wrapper.vm.confirmState.onOk()
await flushPromises()
const requests = saveObjectApi.mock.calls.at(-1)[0]
const powerReq = requests.find((request) => request.table === 's_power')
expect(powerReq.deletes).toEqual([{ b_id: 'menu.otherdata' }])
// 授权行不在这里拼:用 dependent_deletes 声明依赖表 / 键列 / 取值列,
// 由保存层按同一条件查出权限编码再删(前端不查询、不逐个拼授权行)
expect(powerReq.dependent_deletes).toEqual([
{ table: 's_user_power', key_field: 'b_power_id', source_field: 'b_id' },
])
// 菜单主表最后删
expect(requests.at(-1)).toMatchObject({ table: 's_menu' })
})
})