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: '
' } function button(label: string, selector = 'button') { const found = Array.from(document.querySelectorAll(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(`.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>, 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: '配置角色 · TEST_OPERATOR', 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('[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('.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('[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('[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() }) })