20260920220520
This commit is contained in:
1 parent
6f03d92425
commit
b3364fe6dc
44 files changed
+2313
-2737
No files matched your search
@@ -1,7 +1,12 @@
|
||||
/**
|
||||
* menus store(s_menu 动态菜单):按 menu.* 授权裁剪可见菜单 +
|
||||
* 路由地址 → 菜单反查(路由守卫用它兜住手输地址)。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
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', () => ({
|
||||
@@ -14,26 +19,38 @@ import { loadDataApi } from '@/services/api'
|
||||
// 因此这里用业务菜单构造数据,验证菜单 store 本身无任何编码/账号特判。
|
||||
const mockMenus = [
|
||||
{
|
||||
b_id: '1',
|
||||
b_code: 'base',
|
||||
b_id: 'base',
|
||||
b_name: '基础数据',
|
||||
b_menu_type: 'directory',
|
||||
b_parent_id: null,
|
||||
b_canuse: 1,
|
||||
},
|
||||
{
|
||||
b_id: '2',
|
||||
b_code: 'contact',
|
||||
b_id: 'contact',
|
||||
b_name: '往来单位',
|
||||
b_menu_type: 'page',
|
||||
b_parent_id: '1',
|
||||
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: '3',
|
||||
b_code: 'empty-dir',
|
||||
b_id: 'empty-dir',
|
||||
b_name: '空目录',
|
||||
b_menu_type: 'directory',
|
||||
b_parent_id: null,
|
||||
@@ -41,14 +58,18 @@ const mockMenus = [
|
||||
},
|
||||
]
|
||||
|
||||
function mockSModuleData(menus) {
|
||||
loadDataApi.mockImplementation((view) =>
|
||||
Promise.resolve({
|
||||
data: view === 's_menu' ? menus : [],
|
||||
code: 0,
|
||||
success: true,
|
||||
}),
|
||||
)
|
||||
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) {
|
||||
@@ -59,61 +80,64 @@ function setUser(account, id) {
|
||||
/** 冲刷微任务:让 setSession 触发的 watch(clear) 先执行 */
|
||||
const flush = () => Promise.resolve()
|
||||
|
||||
function collectCodes(nodes) {
|
||||
const codes = []
|
||||
function collectIds(nodes) {
|
||||
const ids = []
|
||||
for (const node of nodes) {
|
||||
codes.push(String(node.b_code))
|
||||
if (node.children) codes.push(...collectCodes(node.children))
|
||||
ids.push(String(node.b_id))
|
||||
if (node.children) ids.push(...collectIds(node.children))
|
||||
}
|
||||
return codes
|
||||
return ids
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
createTestPinia()
|
||||
loadDataApi.mockReset()
|
||||
})
|
||||
|
||||
describe('menus store(s_menu 动态菜单)', () => {
|
||||
it('普通用户:全部启用菜单按树渲染,空目录剪枝(无编码特判)', async () => {
|
||||
mockSModuleData(mockMenus)
|
||||
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 menus = useMenuStore()
|
||||
await menus.load()
|
||||
|
||||
const codes = collectCodes(menus.visibleMenus)
|
||||
expect(codes).toContain('base')
|
||||
expect(codes).toContain('contact')
|
||||
// 目录没有可见子菜单时不渲染
|
||||
expect(codes).not.toContain('empty-dir')
|
||||
const perm = usePermissionStore()
|
||||
const menus = useMenuStore()
|
||||
await Promise.all([perm.load(), menus.load()])
|
||||
|
||||
expect(collectIds(menus.visibleMenus)).toEqual(['base', 'contact'])
|
||||
})
|
||||
|
||||
it('管理员特判不在菜单 store:g3soft 与普通用户渲染一致', async () => {
|
||||
it('g3soft 提权:不查授权也全部可见(管理员不裁剪)', async () => {
|
||||
// 模块中心的管控在路由守卫(meta.adminOnly)与侧栏渲染(仅 g3soft 追加固定入口),
|
||||
// 菜单 store 对所有登录主体一视同仁
|
||||
mockSModuleData(mockMenus)
|
||||
// 菜单 store 对管理员只跳过授权裁剪,不做编码特判
|
||||
mockData()
|
||||
setUser('g3soft', 1)
|
||||
await flush()
|
||||
const menus = useMenuStore()
|
||||
await menus.load()
|
||||
|
||||
const codes = collectCodes(menus.visibleMenus)
|
||||
expect(codes).toHaveLength(2)
|
||||
expect(codes).toEqual(expect.arrayContaining(['base', 'contact']))
|
||||
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 () => {
|
||||
mockSModuleData(mockMenus)
|
||||
mockData()
|
||||
setUser('u1', 123)
|
||||
await flush()
|
||||
const menus = useMenuStore()
|
||||
await menus.load()
|
||||
await menus.load()
|
||||
// 只请求 s_menu(权限 store 未参与加载)
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('切换账号:缓存清空并重新加载', async () => {
|
||||
mockSModuleData(mockMenus)
|
||||
mockData()
|
||||
setUser('u1', 123)
|
||||
await flush()
|
||||
const menus = useMenuStore()
|
||||
@@ -133,3 +157,24 @@ describe('menus store(s_menu 动态菜单)', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,14 @@
|
||||
/**
|
||||
* permissions store(新表口径,设计文档 §14)。
|
||||
*
|
||||
* 数据来源:s_module(启用模块)+ s_power(启用权限点)+ s_user_power(用户授权)。
|
||||
* 判断口径:白名单(无授权即无权限);标准操作由 module.* 统一表达,
|
||||
* 业务动作走 action.{模块}.{动作};菜单入口走 menu.*。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permissions'
|
||||
import { createTestPinia } from './helpers/app.js'
|
||||
|
||||
// mock 后端数据接口,避免真实网络请求
|
||||
vi.mock('@/services/api', () => ({
|
||||
@@ -11,58 +18,69 @@ vi.mock('@/services/api', () => ({
|
||||
import { loadDataApi } from '@/services/api'
|
||||
|
||||
const mockModules = [
|
||||
{ b_id: 'v_b_contact', b_name: '联系人', b_module_type: 'data', b_parent_id: 'base' },
|
||||
]
|
||||
|
||||
const mockPowers = [
|
||||
{ b_id: 'menu.contact', b_type: 'menu', b_object_type: 'menu', b_object_id: 'contact' },
|
||||
{
|
||||
b_id: '1',
|
||||
b_code: 'm1',
|
||||
b_name: 'M1',
|
||||
b_canuse: 1,
|
||||
b_canmenu: 1,
|
||||
b_module_type: 'directory',
|
||||
b_parent_id: null,
|
||||
b_icon: '',
|
||||
b_route: '',
|
||||
b_id: 'module.v_b_contact',
|
||||
b_type: 'module',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'v_b_contact',
|
||||
},
|
||||
{
|
||||
b_id: 'action.v_b_contact.audit',
|
||||
b_type: 'action',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'v_b_contact',
|
||||
},
|
||||
]
|
||||
const mockUserModules = [{ b_id: '1', b_module_id: '1', b_user_id: '123' }]
|
||||
const mockUserPowers = []
|
||||
const mockPowers = []
|
||||
|
||||
function mockApiResponses() {
|
||||
loadDataApi.mockImplementation((view) => {
|
||||
const data =
|
||||
view === 's_module'
|
||||
? mockModules
|
||||
: view === 'b_user_module'
|
||||
? mockUserModules
|
||||
: view === 'b_user_power'
|
||||
? mockUserPowers
|
||||
: view === 's_module_power'
|
||||
? mockPowers
|
||||
: []
|
||||
return Promise.resolve({ data, code: 0, success: true })
|
||||
})
|
||||
/** 用户 123:能进联系人菜单、能用联系人模块,但没有审核动作权限 */
|
||||
const mockUserPowers = [
|
||||
{ b_user_id: '123', b_power_id: 'menu.contact', b_canuse: 1 },
|
||||
{ b_user_id: '123', b_power_id: 'module.v_b_contact', b_canuse: 1 },
|
||||
// s_power 中不存在的权限点不算数(停用/已删除)
|
||||
{ b_user_id: '123', b_power_id: 'module.b_money', b_canuse: 1 },
|
||||
]
|
||||
|
||||
function mockApiResponses(overrides = {}) {
|
||||
const dataByTable = {
|
||||
s_module: mockModules,
|
||||
s_power: mockPowers,
|
||||
s_user_power: mockUserPowers,
|
||||
...overrides,
|
||||
}
|
||||
loadDataApi.mockImplementation((table) =>
|
||||
Promise.resolve({ data: dataByTable[table] || [], code: 0, success: true }),
|
||||
)
|
||||
}
|
||||
|
||||
function setLoggedInUser() {
|
||||
function setLoggedInUser(account = 'u1', id = 123) {
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't', user: { id: 123, account: 'u1', name: 'U' } }, 'ORG')
|
||||
auth.setSession({ token: 't', user: { id, account, name: 'U' } }, 'ORG')
|
||||
}
|
||||
|
||||
// 冲刷微任务:让 setSession 触发的 watch(clear) 先执行,隔离“手动 clear”的断言
|
||||
const flush = () => Promise.resolve()
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
createTestPinia()
|
||||
loadDataApi.mockReset()
|
||||
})
|
||||
|
||||
describe('permissions store', () => {
|
||||
it('load 成功后写入 modules 且记录 loadedPrincipalKey', async () => {
|
||||
describe('permissions store:加载', () => {
|
||||
it('load 成功后写入 modules 与授权码,并记录 loadedPrincipalKey', async () => {
|
||||
mockApiResponses()
|
||||
setLoggedInUser()
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
expect(perm.modules.length).toBe(1)
|
||||
// s_power 中不存在的授权被过滤
|
||||
expect(perm.grantedPowerCodes).toEqual(['menu.contact', 'module.v_b_contact'])
|
||||
})
|
||||
|
||||
it('同一主体重复 load 不重复发请求(按主体去重)', async () => {
|
||||
@@ -71,8 +89,8 @@ describe('permissions store', () => {
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
await perm.load()
|
||||
// 首次发 4 个接口调用;第二次因主体已加载直接 return
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(4)
|
||||
// 首次发 3 个接口调用(s_module / s_power / s_user_power);第二次因主体已加载直接 return
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('登录场景:load 进行中触发 clear(主体相同)不作废结果', async () => {
|
||||
@@ -95,27 +113,64 @@ describe('permissions store', () => {
|
||||
const perm = usePermissionStore()
|
||||
const p = perm.load() // 主体 ORG:123 进行中
|
||||
// 模拟切换账号:authStore 变为另一主体,watch 触发 clear
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't2', user: { id: 999, account: 'u2', name: 'U2' } }, 'ORG2')
|
||||
setLoggedInUser('u2', 999)
|
||||
perm.clear()
|
||||
await p
|
||||
// 旧请求被作废:结果丢弃,modules 仍为空(且未触发新主体的 load)
|
||||
expect(perm.modules.length).toBe(0)
|
||||
})
|
||||
|
||||
it('g3soft 提权:仅加载全部启用模块', async () => {
|
||||
loadDataApi.mockImplementation((view) =>
|
||||
Promise.resolve({
|
||||
data: view === 's_module' ? mockModules : [],
|
||||
code: 0,
|
||||
success: true,
|
||||
}),
|
||||
)
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't', user: { id: 1, account: 'g3soft', name: 'G' } }, 'ORG')
|
||||
it('g3soft 提权:仅加载全部启用模块,不查授权表', async () => {
|
||||
mockApiResponses()
|
||||
setLoggedInUser('g3soft', 1)
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
expect(perm.isG3soft).toBe(true)
|
||||
expect(perm.modules.length).toBe(1)
|
||||
expect(perm.grantedPowerCodes).toEqual([])
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(1)
|
||||
expect(loadDataApi).toHaveBeenCalledWith('s_module', 'b_canuse = 1', 'b_id ASC')
|
||||
})
|
||||
})
|
||||
|
||||
describe('permissions store:判断口径', () => {
|
||||
async function loadedStore() {
|
||||
mockApiResponses()
|
||||
setLoggedInUser()
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
return perm
|
||||
}
|
||||
|
||||
it('canEnterMenu:有 menu.* 授权才能进入该菜单', async () => {
|
||||
const perm = await loadedStore()
|
||||
expect(perm.canEnterMenu('contact')).toBe(true)
|
||||
expect(perm.canEnterMenu('user')).toBe(false)
|
||||
})
|
||||
|
||||
it('canAccess:有 module.* 授权才能使用该模块', async () => {
|
||||
const perm = await loadedStore()
|
||||
expect(perm.canAccess('v_b_contact')).toBe(true)
|
||||
// 未授权模块 / 不存在的模块一律拒绝
|
||||
expect(perm.canAccess('b_money')).toBe(false)
|
||||
expect(perm.canAccess('not-exist')).toBe(false)
|
||||
})
|
||||
|
||||
it('canPower:标准操作由模块使用权限统一表达,业务动作需单独授权', async () => {
|
||||
const perm = await loadedStore()
|
||||
expect(perm.canPower('v_b_contact', 'create')).toBe(true)
|
||||
expect(perm.canPower('v_b_contact', 'delete')).toBe(true)
|
||||
expect(perm.canPower('v_b_contact', 'audit')).toBe(false)
|
||||
expect(perm.canPower('b_money', 'create')).toBe(false)
|
||||
})
|
||||
|
||||
it('g3soft 一切放行,且不需要任何授权数据', async () => {
|
||||
mockApiResponses()
|
||||
setLoggedInUser('g3soft', 1)
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
expect(perm.canAccess('b_money')).toBe(true)
|
||||
expect(perm.canPower('b_money', 'audit')).toBe(true)
|
||||
expect(perm.canEnterMenu('user')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -5,11 +5,13 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
GROUP_CODE_MAX_LENGTH,
|
||||
collectGroupIds,
|
||||
nextGroupId,
|
||||
normalizeGroupIds,
|
||||
parseSchema,
|
||||
schemaToJsonText,
|
||||
validateGroupCode,
|
||||
} from '@/utils/schemaJson'
|
||||
|
||||
const group = (over = {}) => ({ type: 'group', title: '分组', children: [], ...over })
|
||||
@@ -89,3 +91,72 @@ describe('normalizeGroupIds', () => {
|
||||
expect(() => normalizeGroupIds('view', parseSchema('view', '{bad json'))).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateGroupCode', () => {
|
||||
const REQUIRED = '分组编码不能为空'
|
||||
const FORMAT = '分组编码须以小写字母开头,且只能包含小写字母、数字和下划线'
|
||||
const LENGTH = `分组编码长度需为 2-${GROUP_CODE_MAX_LENGTH} 个字符`
|
||||
|
||||
it('空值报不能为空', () => {
|
||||
expect(validateGroupCode('')).toBe(REQUIRED)
|
||||
expect(validateGroupCode(' ')).toBe(REQUIRED)
|
||||
expect(validateGroupCode(null)).toBe(REQUIRED)
|
||||
expect(validateGroupCode(undefined)).toBe(REQUIRED)
|
||||
})
|
||||
|
||||
it('长度需落在 2 到上限之间(含边界)', () => {
|
||||
expect(validateGroupCode('a')).toBe(LENGTH)
|
||||
expect(validateGroupCode('a'.repeat(GROUP_CODE_MAX_LENGTH + 1))).toBe(LENGTH)
|
||||
expect(validateGroupCode('ab')).toBe(true)
|
||||
expect(validateGroupCode('a'.repeat(GROUP_CODE_MAX_LENGTH))).toBe(true)
|
||||
})
|
||||
|
||||
it('格式与模块编码同构:小写字母开头,只含小写字母、数字与下划线', () => {
|
||||
for (const code of ['1abc', 'Abc', 'a-b', 'a b', '分组']) {
|
||||
expect(validateGroupCode(code)).toBe(FORMAT)
|
||||
}
|
||||
})
|
||||
|
||||
it('与本配置已有编码冲突时提示已被使用', () => {
|
||||
const message = validateGroupCode('basic', { existingIds: ['basic'] })
|
||||
expect(message).toContain('basic')
|
||||
expect(message).toContain('已被本配置的其他分组使用')
|
||||
expect(message).not.toContain('多语言')
|
||||
})
|
||||
|
||||
it('与多语言资源里残留的编码冲突时给出可区分的提示', () => {
|
||||
const message = validateGroupCode('group2', { reservedIds: ['group2'] })
|
||||
expect(message).toContain('多语言')
|
||||
})
|
||||
|
||||
it('合法自定义编码通过', () => {
|
||||
expect(validateGroupCode('basic_info')).toBe(true)
|
||||
expect(validateGroupCode('cw_fee_detail')).toBe(true)
|
||||
})
|
||||
|
||||
it('group{n} 形式不冲突即允许', () => {
|
||||
expect(validateGroupCode('group7', { existingIds: ['group1'], reservedIds: ['group2'] })).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('编码集合同时兼容数组与 Set', () => {
|
||||
expect(validateGroupCode('basic', { existingIds: new Set(['basic']) })).toBe(
|
||||
validateGroupCode('basic', { existingIds: ['basic'] }),
|
||||
)
|
||||
})
|
||||
|
||||
it('输入首尾空白归一后再比对与判定', () => {
|
||||
expect(validateGroupCode(' basic ', { existingIds: ['basic'] })).toContain('basic')
|
||||
expect(validateGroupCode(' ok_code ')).toBe(true)
|
||||
})
|
||||
|
||||
it('保留字 ungrouped 不可占用(结构树虚拟未分组节点用 g:ungrouped)', () => {
|
||||
expect(validateGroupCode('ungrouped')).toContain('系统保留字')
|
||||
})
|
||||
|
||||
it('长度上限可覆盖', () => {
|
||||
expect(validateGroupCode('a'.repeat(11), { maxLength: 10 })).toContain('2-10')
|
||||
expect(validateGroupCode('a'.repeat(10), { maxLength: 10 })).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,14 @@
|
||||
/**
|
||||
* 回归用例:菜单管理 - 绑定模块(s_menu_module)的加载与保存。
|
||||
* 回归用例:菜单管理 - 随菜单保存同步的两份子表数据。
|
||||
*
|
||||
* s_menu 表没有模块列:绑定关系存 s_menu_module,行身份 = (b_menu_id, b_module_id),
|
||||
* 只登记 data / virtual 模块(module 只是模块树上的分类,不参与权限)。
|
||||
* 保存时与加载基线做行级 diff,只提交增删行;目录 / 外链菜单不绑定模块。
|
||||
* 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;删除菜单时连同授权行一并清理。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
@@ -98,6 +103,8 @@ const modules = [
|
||||
|
||||
/** 库中 s_menu_module 的现有行(每个用例前重置) */
|
||||
let boundRows = []
|
||||
/** 库中该菜单的 s_power(menu.*) 行(每个用例前重置) */
|
||||
let powerRows = []
|
||||
|
||||
/** 从查询条件里取 N'xxx' 字面量(mock 按 id 过滤用;'1 = 1' 等条件返回 null) */
|
||||
function literalOf(condition) {
|
||||
@@ -106,6 +113,7 @@ function literalOf(condition) {
|
||||
|
||||
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) => {
|
||||
@@ -121,6 +129,12 @@ beforeEach(() => {
|
||||
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: [] })
|
||||
})
|
||||
})
|
||||
@@ -193,3 +207,111 @@ describe('菜单管理:绑定模块(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]
|
||||
expect(requests.find((request) => request.table === 's_power').deletes).toEqual([
|
||||
{ b_id: 'menu.otherdata' },
|
||||
])
|
||||
const granted = requests.find((request) => request.table === 's_user_power')
|
||||
expect(granted.key_field).toBe('b_power_id')
|
||||
expect(granted.deletes).toEqual([{ b_power_id: 'menu.otherdata' }])
|
||||
// 菜单主表最后删
|
||||
expect(requests.at(-1)).toMatchObject({ table: 's_menu' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 分配权限抽屉(UserPowerDrawer):头部结构。
|
||||
*
|
||||
* 数据加载与保存由本组件负责(面板只读展示),测试只锁头部两件事:
|
||||
* 1. 头部不渲染标题:只有页签一行 + ui/Drawer 的关闭按钮,没有其它元素;
|
||||
* 2. 三个页签对应三张权限表的内容面板(切换页签不销毁,用 v-show 承载)。
|
||||
*
|
||||
* 面板自身的规则由 user-power-panel.spec.js 覆盖(含「全授权 / 全禁用」),这里不重复。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import UserPowerDrawer from '@/views/system/user/UserPowerDrawer.vue'
|
||||
import { clearMessages } from '@/stores/i18n'
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: vi.fn(),
|
||||
saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })),
|
||||
}))
|
||||
|
||||
import { loadDataApi } from '@/services/api'
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
clearMessages()
|
||||
loadDataApi.mockReset()
|
||||
loadDataApi.mockImplementation(() => Promise.resolve({ data: [] }))
|
||||
})
|
||||
|
||||
async function mountDrawer() {
|
||||
const wrapper = mount(UserPowerDrawer, {
|
||||
props: { open: true, user: { b_id: '001', b_name: '张三' } },
|
||||
// Drawer 用 Teleport 到 body;打桩后内容留在原地,选择器才能命中
|
||||
global: { stubs: { teleport: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('分配权限抽屉:头部', () => {
|
||||
it('头部只有页签一行:无标题、无批量按钮,关闭按钮由 ui/Drawer 提供', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(wrapper.find('.fms-drawer-title').exists()).toBe(false)
|
||||
expect(wrapper.find('.fms-drawer-extra .tabs-nav').exists()).toBe(true)
|
||||
expect(wrapper.find('.fms-drawer-close').exists()).toBe(true)
|
||||
// 账号级批量按钮不在头部(在功能权限面板的工具栏里)
|
||||
expect(wrapper.find('.fms-drawer-header button.button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('三个页签对应三张权限表的内容面板', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(wrapper.findAll('.tabs-tab').map((el) => el.text())).toEqual([
|
||||
'功能权限',
|
||||
'字段权限',
|
||||
'数据范围',
|
||||
])
|
||||
expect(wrapper.findAll('.power-pane').map((el) => el.attributes('data-pane'))).toEqual([
|
||||
'power',
|
||||
'field',
|
||||
'data',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* 功能权限面板(用户管理 → 分配权限 → 功能权限)。
|
||||
*
|
||||
* 数据加载与保存由父级 UserPowerDrawer 负责,面板自身只做这几件事,测试只锁这几件:
|
||||
* 1. 打开时的初始状态:只展开「全部分类」虚拟根,业务目录收起、不选中任何菜单;
|
||||
* 2. 菜单选中语义:重复点击已选中的菜单不取消选中,只有点「全部分类」才清空;
|
||||
* 3. 右侧两个分区:菜单访问(入口开关)+ 模块权限
|
||||
* (模块名 | 模块使用开关 | 操作权限 chip | 行尾全选与计数)。
|
||||
* 目录不渲染入口开关、没配权限点的模块不渲染,都不出空态文案;
|
||||
* 4. 面板顶部工具栏的「全授权 / 全禁用」作用于该账号的全部功能权限(s_power 全量),
|
||||
* 与左侧选中哪个菜单无关;
|
||||
* 5. 勾选结果经 v-model 回传,三层是「保护性单向联动」:
|
||||
* 勾动作补模块使用与入口,勾模块补入口,关模块清该模块动作,
|
||||
* 关入口收回该菜单下的全部授权。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import UserPowerPanel from '@/views/system/user/UserPowerPanel.vue'
|
||||
import { clearMessages } from '@/stores/i18n'
|
||||
|
||||
/** 菜单树夹具:顺序模拟 s_menu 的 b_depth / b_xh 排序(父目录在前) */
|
||||
const MENU_TREE = [
|
||||
{ b_id: 'base', b_parent_id: null, b_name: '基础资料', b_menu_type: 'directory', b_xh: 10 },
|
||||
{ b_id: 'system', b_parent_id: null, b_name: '系统管理', b_menu_type: 'directory', b_xh: 20 },
|
||||
{ b_id: 'partner', b_parent_id: 'base', b_name: '合作伙伴', b_menu_type: 'page', b_xh: 10 },
|
||||
// 外链菜单:不绑模块,入口就是唯一的授权单元
|
||||
{ b_id: 'portal', b_parent_id: 'base', b_name: '外部系统', b_menu_type: 'external', b_xh: 20 },
|
||||
{ b_id: 'user', b_parent_id: 'system', b_name: '用户管理', b_menu_type: 'page', b_xh: 10 },
|
||||
]
|
||||
|
||||
const MENU_MODULES = {
|
||||
partner: [{ b_menu_id: 'partner', b_module_id: 'v_b_othercompany', b_xh: 10 }],
|
||||
// 用户管理绑两个模块,其中币种(b_money)没配权限点 → 验证「未配置权限点的模块不渲染」
|
||||
user: [
|
||||
{ b_menu_id: 'user', b_module_id: 'v_b_user', b_xh: 10 },
|
||||
{ b_menu_id: 'user', b_module_id: 'b_money', b_xh: 20 },
|
||||
],
|
||||
}
|
||||
|
||||
const MODULE_BY_ID = new Map([
|
||||
['v_b_othercompany', { b_id: 'v_b_othercompany', b_name: '合作伙伴' }],
|
||||
['v_b_user', { b_id: 'v_b_user', b_name: '用户管理' }],
|
||||
['b_money', { b_id: 'b_money', b_name: '币种' }],
|
||||
])
|
||||
|
||||
const POWER_BY_MODULE = {
|
||||
v_b_othercompany: [
|
||||
{ b_id: 'module.v_b_othercompany', b_name: '模块使用', b_action: 'access' },
|
||||
{ b_id: 'action.v_b_othercompany.create', b_name: '新增', b_action: 'create' },
|
||||
],
|
||||
v_b_user: [{ b_id: 'module.v_b_user', b_name: '模块使用', b_action: 'access' }],
|
||||
}
|
||||
|
||||
/** s_power 全量启用编码(含 menu.*),父级加载后传给面板,供「全授权」使用 */
|
||||
const ALL_POWER_IDS = [
|
||||
'menu.partner',
|
||||
'menu.portal',
|
||||
'menu.user',
|
||||
'module.v_b_othercompany',
|
||||
'action.v_b_othercompany.create',
|
||||
'module.v_b_user',
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
clearMessages()
|
||||
})
|
||||
|
||||
function mountPanel(props = {}) {
|
||||
return mount(UserPowerPanel, {
|
||||
props: {
|
||||
modelValue: [],
|
||||
menuTree: MENU_TREE,
|
||||
menuModules: MENU_MODULES,
|
||||
moduleById: MODULE_BY_ID,
|
||||
powerByModule: POWER_BY_MODULE,
|
||||
allPowerIds: ALL_POWER_IDS,
|
||||
...props,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 选中一个菜单(打开时不再默认选中,多数用例只需把右侧切到目标菜单) */
|
||||
async function mountWithMenu(menuId, props = {}) {
|
||||
const wrapper = mountPanel(props)
|
||||
wrapper.vm.selectedMenuId = menuId
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** 按标题文本找树行(每次重新查询,避免拿着已过期的 DOM 包装器做断言) */
|
||||
function findRow(wrapper, text) {
|
||||
return wrapper.findAll('.tree-node').find((row) => row.text().includes(text))
|
||||
}
|
||||
|
||||
/** 分区标题清单 */
|
||||
function sectionTitles(wrapper) {
|
||||
return wrapper.findAll('.power-section__title').map((el) => el.text())
|
||||
}
|
||||
|
||||
/** 模块权限区的行名清单(跳过「菜单访问」分区的那一行) */
|
||||
function moduleNames(wrapper) {
|
||||
const section = wrapper.findAll('.power-section')[1]
|
||||
return section ? section.findAll('.power-row__name').map((el) => el.text()) : []
|
||||
}
|
||||
|
||||
/** 「菜单访问」分区的入口开关(目录没有入口权限点时返回 undefined) */
|
||||
function entrySwitch(wrapper) {
|
||||
return wrapper.findAll('.power-section')[0]?.find('[role="switch"]')
|
||||
}
|
||||
|
||||
/** 模块权限区的模块使用开关(第 index 行) */
|
||||
function moduleSwitch(wrapper, index = 0) {
|
||||
const section = wrapper.findAll('.power-section')[1]
|
||||
return section ? section.findAll('[role="switch"]')[index] : undefined
|
||||
}
|
||||
|
||||
/** 面板顶部工具栏按钮:0 = 全授权,1 = 全禁用 */
|
||||
function toolbarButtons(wrapper) {
|
||||
return wrapper.findAll('.power-toolbar__actions button')
|
||||
}
|
||||
|
||||
/** 回传的勾选结果(取最后一次 emit) */
|
||||
function lastEmitted(wrapper) {
|
||||
const events = wrapper.emitted('update:modelValue')
|
||||
return events[events.length - 1][0]
|
||||
}
|
||||
|
||||
describe('功能权限面板:打开时的初始状态', () => {
|
||||
it('只展开「全部分类」,业务目录收起、不选中任何菜单', async () => {
|
||||
const wrapper = mountPanel({ menuTree: [] })
|
||||
await nextTick()
|
||||
await wrapper.setProps({ menuTree: MENU_TREE })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.selectedMenuId).toBe('')
|
||||
// 业务目录的展开键不由面板补写:虚拟根的展开由 FmsTree 承担
|
||||
expect(wrapper.vm.expandedKeys).toEqual([])
|
||||
expect(findRow(wrapper, '全部分类')).toBeTruthy()
|
||||
expect(findRow(wrapper, '基础资料')).toBeTruthy()
|
||||
// 目录收起 → 其下的页面菜单不可见
|
||||
expect(findRow(wrapper, '合作伙伴')).toBeUndefined()
|
||||
expect(wrapper.find('.power-panel__main').text()).toContain('请选择左侧菜单')
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:菜单选中语义', () => {
|
||||
it('重复点击已选中的菜单:保持选中,不跳回「全部分类」', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
|
||||
// 展开目录 → 选中「合作伙伴」→ 再点一次(ui/Tree 会传出空 keys,即「取消选中」)
|
||||
await findRow(wrapper, '基础资料').find('.tree-node-switcher').trigger('click')
|
||||
await nextTick()
|
||||
await findRow(wrapper, '合作伙伴').find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.selectedMenuId).toBe('partner')
|
||||
|
||||
await findRow(wrapper, '合作伙伴').find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.selectedMenuId).toBe('partner')
|
||||
expect(findRow(wrapper, '合作伙伴').classes()).toContain('is-selected')
|
||||
expect(findRow(wrapper, '全部分类').classes()).not.toContain('is-selected')
|
||||
})
|
||||
|
||||
it('点「全部分类」虚拟根:清空选中,右侧回到空态提示', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
|
||||
await findRow(wrapper, '基础资料').find('.tree-node-switcher').trigger('click')
|
||||
await nextTick()
|
||||
await findRow(wrapper, '合作伙伴').find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.selectedMenuId).toBe('partner')
|
||||
|
||||
await findRow(wrapper, '全部分类').find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.selectedMenuId).toBe('')
|
||||
expect(wrapper.find('.power-panel__main').text()).toContain('请选择左侧菜单')
|
||||
})
|
||||
|
||||
it('点另一个菜单:选中切到该菜单', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
|
||||
// 「用户管理」在收起的「系统管理」下:先展开目录再点
|
||||
await findRow(wrapper, '系统管理').find('.tree-node-switcher').trigger('click')
|
||||
await nextTick()
|
||||
await findRow(wrapper, '用户管理').find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.selectedMenuId).toBe('user')
|
||||
expect(moduleNames(wrapper)).toEqual(['用户管理'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:右侧结构', () => {
|
||||
it('两个分区:菜单访问 + 模块权限;入口与模块使用各一个开关', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
expect(sectionTitles(wrapper)).toEqual(['菜单访问', '模块权限'])
|
||||
expect(wrapper.findAll('.power-section')[0].find('.power-row__name').text()).toBe('合作伙伴')
|
||||
expect(moduleNames(wrapper)).toEqual(['合作伙伴'])
|
||||
// 开关只有两处:菜单访问 + 模块使用
|
||||
expect(wrapper.findAll('[role="switch"]')).toHaveLength(2)
|
||||
// chip 只剩业务动作,模块使用不再混在里面
|
||||
expect(wrapper.findAll('.power-chip').map((el) => el.text())).toEqual(['新增'])
|
||||
})
|
||||
|
||||
it('已授权的入口渲染为打开态', async () => {
|
||||
const wrapper = await mountWithMenu('partner', { modelValue: ['menu.partner'] })
|
||||
|
||||
expect(wrapper.vm.menuPowerCode).toBe('menu.partner')
|
||||
expect(entrySwitch(wrapper).attributes('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('目录:不渲染入口开关(没有 menu.* 权限点),只给一行说明', async () => {
|
||||
const wrapper = await mountWithMenu('base')
|
||||
|
||||
expect(sectionTitles(wrapper)).toEqual(['菜单访问'])
|
||||
expect(wrapper.findAll('[role="switch"]')).toHaveLength(0)
|
||||
expect(wrapper.find('.power-panel__hint').text()).toContain('目录只是分组节点')
|
||||
})
|
||||
|
||||
it('外链菜单:没有可授权模块,入口开关独立可用', async () => {
|
||||
const wrapper = await mountWithMenu('portal')
|
||||
|
||||
expect(sectionTitles(wrapper)).toEqual(['菜单访问'])
|
||||
expect(moduleNames(wrapper)).toEqual([])
|
||||
|
||||
await entrySwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['menu.portal'])
|
||||
})
|
||||
|
||||
it('页面没绑模块(自定义页):只剩入口开关,勾选即全部权限', async () => {
|
||||
const wrapper = await mountWithMenu('partner', { menuModules: {} })
|
||||
|
||||
expect(sectionTitles(wrapper)).toEqual(['菜单访问'])
|
||||
expect(wrapper.findAll('[role="switch"]')).toHaveLength(1)
|
||||
|
||||
await entrySwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['menu.partner'])
|
||||
})
|
||||
|
||||
it('未配置权限点的模块不渲染,也不显示空态文案', async () => {
|
||||
const wrapper = await mountWithMenu('user')
|
||||
|
||||
// 「币种」绑定了但没配权限点:不渲染成行,也不出任何提示
|
||||
expect(moduleNames(wrapper)).toEqual(['用户管理'])
|
||||
expect(wrapper.find('.power-panel__main').text()).not.toContain('未配置权限点')
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:全授权 / 全禁用', () => {
|
||||
it('工具栏在面板顶部:说明 + 两个按钮', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.power-toolbar__label').text()).toBe('该账号的全部功能权限')
|
||||
expect(toolbarButtons(wrapper).map((el) => el.text())).toEqual(['全授权', '全禁用'])
|
||||
})
|
||||
|
||||
it('全授权:一次勾满 s_power 的全部权限点', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await toolbarButtons(wrapper)[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(ALL_POWER_IDS)
|
||||
})
|
||||
|
||||
it('全禁用:清空该账号的全部功能权限', async () => {
|
||||
const wrapper = await mountWithMenu('partner', { modelValue: ALL_POWER_IDS })
|
||||
|
||||
await toolbarButtons(wrapper)[1].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual([])
|
||||
})
|
||||
|
||||
it('工具栏不依赖选中菜单:未选中菜单时同样可用', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
expect(wrapper.find('.power-panel__main').text()).toContain('请选择左侧菜单')
|
||||
|
||||
await toolbarButtons(wrapper)[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(ALL_POWER_IDS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:三层联动', () => {
|
||||
it('勾模块使用:自动补上菜单入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await moduleSwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['module.v_b_othercompany', 'menu.partner'])
|
||||
})
|
||||
|
||||
it('勾操作权限:自动补上模块使用与菜单入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await wrapper.findAll('.power-chip')[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual([
|
||||
'action.v_b_othercompany.create',
|
||||
'module.v_b_othercompany',
|
||||
'menu.partner',
|
||||
])
|
||||
})
|
||||
|
||||
it('关模块使用:只清该模块的操作权限,不动菜单入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner', {
|
||||
modelValue: ['menu.partner', 'module.v_b_othercompany', 'action.v_b_othercompany.create'],
|
||||
})
|
||||
|
||||
await moduleSwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['menu.partner'])
|
||||
})
|
||||
|
||||
it('开菜单入口:只补入口本身,不给任何模块权限', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await entrySwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['menu.partner'])
|
||||
})
|
||||
|
||||
it('关菜单入口:连带收回该菜单下的全部模块与动作授权', async () => {
|
||||
const wrapper = await mountWithMenu('partner', {
|
||||
modelValue: ['menu.partner', 'module.v_b_othercompany', 'action.v_b_othercompany.create'],
|
||||
})
|
||||
|
||||
await entrySwitch(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual([])
|
||||
})
|
||||
|
||||
it('取消单个操作权限:不动模块使用与入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner', {
|
||||
modelValue: ['menu.partner', 'module.v_b_othercompany', 'action.v_b_othercompany.create'],
|
||||
})
|
||||
|
||||
await wrapper.findAll('.power-chip')[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(['menu.partner', 'module.v_b_othercompany'])
|
||||
})
|
||||
|
||||
it('勾选态回显:操作权限 chip 与模块使用开关各管各的', async () => {
|
||||
const wrapper = await mountWithMenu('partner', {
|
||||
modelValue: ['action.v_b_othercompany.create'],
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.power-chip')[0].classes()).toContain('is-checked')
|
||||
expect(wrapper.findAll('.power-chip')[0].attributes('aria-checked')).toBe('true')
|
||||
// 模块使用未授权 → 开关是关的
|
||||
expect(moduleSwitch(wrapper).attributes('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('行尾「全选」:补齐该模块的权限点并联动入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await wrapper.find('.power-row__toggle').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual([
|
||||
'module.v_b_othercompany',
|
||||
'action.v_b_othercompany.create',
|
||||
'menu.partner',
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user