/** * 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() }) })