Files
workspace/code/fms/fms-vue/tests/menus.spec.js
T
2026-09-20 22:05:20 +08:00

181 lines
5.4 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.
/**
* menus store(s_menu 动态菜单):按 menu.* 授权裁剪可见菜单 +
* 路由地址 → 菜单反查(路由守卫用它兜住手输地址)。
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useAuthStore } from '@/stores/auth'
import { useMenuStore } from '@/stores/menus'
import { usePermissionStore } from '@/stores/permissions'
import { createTestPinia } from './helpers/app.js'
// mock 后端数据接口,避免真实网络请求
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn(),
}))
import { loadDataApi } from '@/services/api'
// 模块中心等系统管理员专用页面不进 s_menu(入口固定在 AppSidebar、路由层 adminOnly 拦截),
// 因此这里用业务菜单构造数据,验证菜单 store 本身无任何编码/账号特判。
const mockMenus = [
{
b_id: 'base',
b_name: '基础数据',
b_menu_type: 'directory',
b_parent_id: null,
b_canuse: 1,
},
{
b_id: 'contact',
b_name: '往来单位',
b_menu_type: 'page',
b_parent_id: 'base',
b_route: '/base/contact',
b_canuse: 1,
},
{
b_id: 'system',
b_name: '系统管理',
b_menu_type: 'directory',
b_parent_id: null,
b_canuse: 1,
},
{
b_id: 'user',
b_name: '用户管理',
b_menu_type: 'page',
b_parent_id: 'system',
b_route: '/system/user',
b_canuse: 1,
},
{
// 没有任何子项的目录:可见性裁剪后不应渲染
b_id: 'empty-dir',
b_name: '空目录',
b_menu_type: 'directory',
b_parent_id: null,
b_canuse: 1,
},
]
function mockData({ menus = mockMenus, powers = [], userPowers = [] } = {}) {
loadDataApi.mockImplementation((table) => {
const data =
table === 's_menu'
? menus
: table === 's_power'
? powers
: table === 's_user_power'
? userPowers
: []
return Promise.resolve({ data, code: 0, success: true })
})
}
function setUser(account, id) {
const auth = useAuthStore()
auth.setSession({ token: 't', user: { id, account } }, 'ORG')
}
/** 冲刷微任务:让 setSession 触发的 watch(clear) 先执行 */
const flush = () => Promise.resolve()
function collectIds(nodes) {
const ids = []
for (const node of nodes) {
ids.push(String(node.b_id))
if (node.children) ids.push(...collectIds(node.children))
}
return ids
}
beforeEach(() => {
createTestPinia()
loadDataApi.mockReset()
})
describe('menus store(s_menu 动态菜单)', () => {
it('普通用户:只渲染有 menu.* 授权的页面菜单,空目录剪枝', async () => {
mockData({
powers: [{ b_id: 'menu.contact' }, { b_id: 'menu.user' }],
// 只授权联系人:用户管理虽在 s_power 里,但用户没有该授权 → 系统管理目录随之消失
userPowers: [{ b_user_id: '123', b_power_id: 'menu.contact', b_canuse: 1 }],
})
setUser('u1', 123)
await flush()
const perm = usePermissionStore()
const menus = useMenuStore()
await Promise.all([perm.load(), menus.load()])
expect(collectIds(menus.visibleMenus)).toEqual(['base', 'contact'])
})
it('g3soft 提权:不查授权也全部可见(管理员不裁剪)', async () => {
// 模块中心的管控在路由守卫(meta.adminOnly)与侧栏渲染(仅 g3soft 追加固定入口),
// 菜单 store 对管理员只跳过授权裁剪,不做编码特判
mockData()
setUser('g3soft', 1)
await flush()
const perm = usePermissionStore()
const menus = useMenuStore()
await Promise.all([perm.load(), menus.load()])
expect(collectIds(menus.visibleMenus)).toEqual(['base', 'contact', 'system', 'user'])
})
it('同一主体重复 load 不重复发请求', async () => {
mockData()
setUser('u1', 123)
await flush()
const menus = useMenuStore()
await menus.load()
await menus.load()
// 只请求 s_menu(权限 store 未参与加载)
expect(loadDataApi).toHaveBeenCalledTimes(1)
})
it('切换账号:缓存清空并重新加载', async () => {
mockData()
setUser('u1', 123)
await flush()
const menus = useMenuStore()
await menus.load()
expect(menus.rawMenus.length).toBe(mockMenus.length)
// 切换到另一个主体:watch(clear) 清空已加载数据
setUser('u2', 999)
await flush()
await flush()
expect(menus.rawMenus.length).toBe(0)
expect(menus.visibleMenus).toEqual([])
// 新主体 load 重新拉取
loadDataApi.mockClear()
await menus.load()
expect(menus.rawMenus.length).toBe(mockMenus.length)
})
})
describe('menus store:路由反查(findMenuByRoute)', () => {
it('命中菜单路由与其子路径,非菜单路由返回 null', async () => {
mockData()
setUser('u1', 123)
await flush()
const menus = useMenuStore()
await menus.load()
expect(menus.findMenuByRoute('/base/contact')?.b_id).toBe('contact')
// 详情页等子路径同样算作该菜单的入口
expect(menus.findMenuByRoute('/base/contact/123')?.b_id).toBe('contact')
expect(menus.findMenuByRoute('/base/contact/?x=1')?.b_id).toBe('contact')
// 工作台、演示页、模块中心都不在 s_menu
expect(menus.findMenuByRoute('/dashboard')).toBeNull()
expect(menus.findMenuByRoute('/demo')).toBeNull()
expect(menus.findMenuByRoute('/module/module-management')).toBeNull()
// 目录没有路由,不会被匹配到
expect(menus.findMenuByRoute('/base')).toBeNull()
})
})