2026-10-20 10:11:31 +08:00
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 )
2026-10-27 17:44:49 +08:00
expect ( dialog . props ()). toMatchObject ({ title : '配置角色 · 测试角色' , width : 'min(860px, calc(100vw - 28px))' , fullscreenable : true })
2026-10-20 10:11:31 +08:00
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 ()
})
})