Files
admin-web/src/modules/iam/pages/RolePermissionDialogs.test.ts
T
2026-10-27 17:44:49 +08:00

208 lines
16 KiB
TypeScript

import { createRequire } from 'node:module'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuthStore } from '@/core/auth/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AppDialog from '@/shared/components/AppDialog.vue'
import RowActions from '@/shared/components/RowActions.vue'
import RolesView from './RolesView.vue'
import PermissionsView from './PermissionsView.vue'
import * as api from '../api/platformIam'
const { default: ElementPlus, ElDialog, ElTree, ElFormItem, ElSelect, ElOption, ElMessageBox } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
vi.mock('../api/platformIam', () => ({ listPlatformRoles: vi.fn(), listPlatformPermissions: vi.fn(), createPlatformRole: vi.fn(), updatePlatformRole: vi.fn(), replacePlatformRolePermissions: vi.fn(), changePlatformRoleStatus: vi.fn(), deletePlatformRole: vi.fn(), createPlatformPermission: vi.fn(), updatePlatformPermission: vi.fn(), changePlatformPermissionStatus: vi.fn(), deletePlatformPermission: vi.fn() }))
const roleId = '9223372036854775807', rootId = '9223372036854775806', childId = '9223372036854775805', hiddenId = '9223372036854775804'
let role: api.PlatformRoleResource
let resources: api.PlatformPermissionResource[]
const table = { props: ['data'], template: '<div><slot name="toolbar-actions"/><div class="real-row"><slot v-if="data?.length" name="actions" :row="data[0]"/></div></div>' }
function button(label: string, selector = 'button') {
const found = Array.from(document.querySelectorAll<HTMLButtonElement>(selector)).find(node => node.textContent?.trim() === label)
expect(found, label).toBeDefined(); return found!
}
async function view(component: typeof RolesView | typeof PermissionsView) {
const wrapper = mount(component, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { DataTable: table, QueryToolbar: true } } }); await flushPromises(); return wrapper
}
async function roleEditor(create = false) {
const wrapper = await view(RolesView); button(create ? '新增角色' : '配置').click(); await flushPromises()
wrapper.findComponent(AppDialog).vm.$emit('opened'); await flushPromises(); return wrapper
}
async function permissionEditor(create = false) {
const wrapper = await view(PermissionsView)
if (create) button('新增权限').click()
else wrapper.findComponent(RowActions).vm.$emit('action', 'edit')
await flushPromises(); return wrapper
}
async function input(placeholder: string, value: string) {
const node = document.querySelector<HTMLInputElement>(`.el-dialog [placeholder="${placeholder}"]`)!
expect(node, placeholder).not.toBeNull(); node.value = value; node.dispatchEvent(new Event('input', { bubbles: true })); node.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises()
}
function select(wrapper: Awaited<ReturnType<typeof view>>, label: string) {
const field = wrapper.findAllComponents(ElFormItem).find(field => field.props('label') === label)
expect(field, label).toBeDefined(); return field!.findComponent(ElSelect)
}
function permissions(codes: string[]) { useAuthStore().permissions = codes; useAccessPreviewStore().initializeAfterLogin() }
enableAutoUnmount(afterEach)
afterEach(() => ElMessageBox.close())
beforeEach(() => {
vi.resetAllMocks(); document.body.innerHTML = ''; setActivePinia(createPinia()); useAuthStore().token = 'test-session'
permissions(['platform:role:list','platform:role:create','platform:role:edit','platform:role:grant','platform:role:status','platform:role:delete','platform:permission:list','platform:permission:create','platform:permission:edit','platform:permission:status','platform:permission:delete'])
role = { id: roleId, tenantId: '0', appCode: 'PLATFORM', roleCode: 'TEST_OPERATOR', roleName: '测试角色', roleType: 'CUSTOM', status: 'ACTIVE', description: '原说明', memberCount: 0, permissionIds: [childId] }
resources = [
{ id: rootId, tenantId: '0', appCode: 'PLATFORM', permissionType: 'MENU', parentId: '0', permissionCode: 'platform:customer:list', permissionName: '测试客户菜单', routePath: '/customers', httpMethod: null, sortNo: 1, status: 'ACTIVE', builtIn: false },
{ id: childId, tenantId: '0', appCode: 'PLATFORM', permissionType: 'BUTTON', parentId: rootId, permissionCode: 'platform:customer:create', permissionName: '测试新增客户', routePath: null, httpMethod: null, sortNo: 2, status: 'ACTIVE', builtIn: false },
{ id: hiddenId, tenantId: '0', appCode: 'PLATFORM', permissionType: 'MENU', parentId: '0', permissionCode: 'platform:express:list', permissionName: '正式隐藏菜单权限', routePath: '/master-data/express-companies', httpMethod: null, sortNo: 3, status: 'ACTIVE', builtIn: false },
]
vi.mocked(api.listPlatformRoles).mockImplementation(async () => [role])
vi.mocked(api.listPlatformPermissions).mockImplementation(async () => resources)
vi.mocked(api.createPlatformRole).mockImplementation(async () => role)
vi.mocked(api.updatePlatformRole).mockImplementation(async () => role)
vi.mocked(api.replacePlatformRolePermissions).mockResolvedValue(undefined)
vi.mocked(api.changePlatformRoleStatus).mockImplementation(async (_id,status) => ({ ...role, status }))
vi.mocked(api.deletePlatformRole).mockResolvedValue(undefined)
vi.mocked(api.createPlatformPermission).mockImplementation(async () => resources[0]!)
vi.mocked(api.updatePlatformPermission).mockImplementation(async () => resources[0]!)
vi.mocked(api.changePlatformPermissionStatus).mockImplementation(async (_id,status) => ({ ...resources[0]!, status }))
vi.mocked(api.deletePlatformPermission).mockResolvedValue(undefined)
})
describe('roles and permission dialogs', () => {
it('keeps saved and hidden formal grants across fullscreen and excludes navigation group IDs', async () => {
const wrapper = await roleEditor(); const dialog = wrapper.findComponent(AppDialog); const tree = wrapper.findComponent(ElTree)
expect(dialog.props()).toMatchObject({ title: '配置角色 · 测试角色', width: 'min(860px, calc(100vw - 28px))', fullscreenable: true })
expect(document.querySelector('.el-dialog [placeholder="如:ORDER_OPERATOR"]')).toBeNull()
expect(tree.vm.getCheckedKeys()).toContain(childId)
expect(tree.vm.getNode(hiddenId)).toBeTruthy()
tree.vm.setCheckedKeys([childId, hiddenId]); tree.vm.$emit('check'); await flushPromises()
expect(tree.vm.getHalfCheckedKeys().length).toBeGreaterThan(0)
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true)
expect(tree.vm.getCheckedKeys()).toContain(hiddenId)
button('保存角色', '.el-dialog__footer button').click(); await flushPromises()
expect(api.replacePlatformRolePermissions).toHaveBeenCalledWith(roleId, expect.arrayContaining([childId, hiddenId]))
const granted = vi.mocked(api.replacePlatformRolePermissions).mock.calls[0]![1]
expect(granted.every(id => !String(id).startsWith('navigation-'))).toBe(true)
})
it('grants permissions with readonly role facts when no profile-edit permission exists', async () => {
permissions(['platform:role:list','platform:role:grant'])
const wrapper = await roleEditor()
expect(document.querySelector('.el-dialog [placeholder="如:订单运营专员"]')).toBeNull()
expect(document.querySelector('[aria-label="角色基本资料"]')?.textContent).toContain('原说明')
button('保存角色', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updatePlatformRole).not.toHaveBeenCalled()
expect(api.replacePlatformRolePermissions).toHaveBeenCalledWith(roleId, [childId])
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
})
it('edits only role facts when grant permission is absent', async () => {
permissions(['platform:role:list','platform:role:edit'])
const wrapper = await roleEditor()
expect(wrapper.findComponent(ElTree).exists()).toBe(false)
await input('如:订单运营专员', '修改角色')
button('保存角色', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updatePlatformRole).toHaveBeenCalledWith(roleId, { roleName: '修改角色', description: '原说明' })
expect(api.replacePlatformRolePermissions).not.toHaveBeenCalled()
})
it('validates create, normalizes its code and attaches checked grants only after creation', async () => {
const wrapper = await roleEditor(true)
button('保存角色', '.el-dialog__footer button').click(); await flushPromises(); expect(api.createPlatformRole).not.toHaveBeenCalled()
await input('如:订单运营专员', '新角色'); await input('如:ORDER_OPERATOR', 'new_operator')
wrapper.findComponent(ElTree).vm.setCheckedKeys([childId]); wrapper.findComponent(ElTree).vm.$emit('check'); await flushPromises()
button('保存角色', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformRole).toHaveBeenCalledWith({ roleCode: 'NEW_OPERATOR', roleName: '新角色', description: '' })
expect(api.replacePlatformRolePermissions).toHaveBeenCalledWith(roleId, [childId])
})
it('locks the tree and close controls during save then preserves failed selection', async () => {
let rejectGrant!: (error: Error) => void
vi.mocked(api.replacePlatformRolePermissions).mockImplementation(() => new Promise((_resolve,reject) => { rejectGrant = reject }))
const wrapper = await roleEditor()
button('保存角色', '.el-dialog__footer button').click(); await flushPromises()
expect(wrapper.findComponent(ElDialog).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
expect(button('取消', '.el-dialog__footer button').disabled).toBe(true)
const checks = Array.from(document.querySelectorAll<HTMLInputElement>('.permission-tree input[type="checkbox"]'))
expect(checks.length).toBeGreaterThan(0); expect(checks.every(input => input.disabled)).toBe(true)
rejectGrant(new Error('授权失败')); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
expect(wrapper.findComponent(ElTree).vm.getCheckedKeys()).toContain(childId)
})
it.each(['停用','启用','删除'] as const)('identifies the role %s confirmation and retains the original command', async verb => {
if (verb === '启用') role.status = 'DISABLED'
await view(RolesView); button(verb, '.real-row button').click(); await flushPromises()
expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}角色 · 测试角色`)
expect(api.changePlatformRoleStatus).not.toHaveBeenCalled(); expect(api.deletePlatformRole).not.toHaveBeenCalled()
button(verb === '删除' ? '删除' : '确认', '.el-message-box button').click(); await flushPromises()
if (verb === '删除') expect(api.deletePlatformRole).toHaveBeenCalledWith(roleId)
else expect(api.changePlatformRoleStatus).toHaveBeenCalledWith(roleId, verb === '启用' ? 'ACTIVE' : 'DISABLED')
})
it('retains built-in role protection for status and delete', async () => {
role.roleType = 'SYSTEM'
await view(RolesView)
expect(button('停用', '.real-row button').disabled).toBe(true)
expect(button('删除', '.real-row button').disabled).toBe(true)
expect(api.changePlatformRoleStatus).not.toHaveBeenCalled(); expect(api.deletePlatformRole).not.toHaveBeenCalled()
})
it.each([['菜单','MENU'],['页面','PAGE'],['按钮','BUTTON'],['接口','API']] as const)('creates %s resources with original conditional payload', async (kind,type) => {
const wrapper = await permissionEditor(true)
await input('用户可理解的中文名称', '新增资源'); await input('如:platform:customer:list', 'platform:new:action')
select(wrapper, '资源类型').vm.$emit('update:modelValue', kind); select(wrapper, '资源类型').vm.$emit('change', kind); await flushPromises()
if (kind !== '菜单') {
button('保存权限', '.el-dialog__footer button').click(); await flushPromises(); expect(api.createPlatformPermission).not.toHaveBeenCalled()
select(wrapper, '上级资源').vm.$emit('update:modelValue', rootId); await flushPromises()
}
if (kind === '菜单' || kind === '页面') await input('如:/customers', '/new')
expect(document.querySelector('.el-dialog [placeholder="如:/customers"]') !== null).toBe(kind === '菜单' || kind === '页面')
button('保存权限', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformPermission).toHaveBeenCalledWith({ type, code: 'platform:new:action', name: '新增资源', parentId: kind === '菜单' ? '0' : rootId, routePath: kind === '菜单' || kind === '页面' ? '/new' : null, httpMethod: kind === '接口' ? 'GET' : null, sortNo: 10 })
})
it('keeps code/type fixed and excludes current resource and descendants from parent choices', async () => {
const wrapper = await permissionEditor()
expect(wrapper.findComponent(AppDialog).props()).toMatchObject({ title: '编辑权限资源 · 测试客户菜单', width: 'min(720px, calc(100vw - 28px))', fullscreenable: true })
expect(document.querySelector('.el-dialog [placeholder="如:platform:customer:list"]')).toBeNull()
expect(select(wrapper, '资源类型').exists()).toBe(false)
const parents = select(wrapper, '上级资源').findAllComponents(ElOption).map(option => option.props('value'))
expect(parents).toContain(hiddenId); expect(parents).not.toContain(rootId); expect(parents).not.toContain(childId)
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
await input('用户可理解的中文名称', '重命名资源')
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updatePlatformPermission).toHaveBeenCalledWith(rootId, { name: '重命名资源', parentId: null, routePath: '/customers', httpMethod: null, sortNo: 1 })
})
it('keeps failed permission input visible after locking close during submission', async () => {
let rejectSave!: (error: Error) => void
vi.mocked(api.updatePlatformPermission).mockImplementation(() => new Promise((_resolve,reject) => { rejectSave = reject }))
const wrapper = await permissionEditor(); await input('用户可理解的中文名称', '失败输入')
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(wrapper.findComponent(ElDialog).props('showClose')).toBe(false)
expect(button('取消', '.el-dialog__footer button').disabled).toBe(true)
rejectSave(new Error('保存失败')); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
expect(document.querySelector<HTMLInputElement>('[placeholder="用户可理解的中文名称"]')!.value).toBe('失败输入')
})
it.each(['停用','启用','删除'] as const)('identifies permission %s confirmation without losing original restrictions', async verb => {
if (verb === '启用') resources[0]!.status = 'DISABLED'
const wrapper = await view(PermissionsView)
wrapper.findComponent(RowActions).vm.$emit('action', verb === '删除' ? 'delete' : 'status'); await flushPromises()
expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}权限 · 测试客户菜单`)
if (verb === '删除') expect(document.querySelector('.el-message-box')?.textContent).toContain('没有下级资源且未分配给角色')
expect(api.deletePlatformPermission).not.toHaveBeenCalled(); expect(api.changePlatformPermissionStatus).not.toHaveBeenCalled()
button(verb === '删除' ? '删除' : '确认', '.el-message-box button').click(); await flushPromises()
if (verb === '删除') expect(api.deletePlatformPermission).toHaveBeenCalledWith(rootId)
else expect(api.changePlatformPermissionStatus).toHaveBeenCalledWith(rootId, verb === '启用' ? 'ACTIVE' : 'DISABLED')
})
it('never opens editing for a built-in permission resource', async () => {
resources[0]!.builtIn = true
const wrapper = await view(PermissionsView)
wrapper.findComponent(RowActions).vm.$emit('action', 'edit'); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
expect(api.updatePlatformPermission).not.toHaveBeenCalled()
})
})