- 角色授权:权限矩阵抽屉与角色创建,按模块分组勾选 - 系统监控:总览、趋势、接口与事件页,15 秒自动刷新, 请求可中断、窗口切换暂停,详情抽屉支持导出 - T+ 同步:同步进度条组件与工作台状态拆分 - 工资:试用期面板、产品提取弹窗与无权限占位页 - 路由权限:按权限码控制菜单与页面可达性 影响范围:前端全部页面 验证:随本次后端联调本地人工核对,e2e 脚本入库待跑
127 lines
11 KiB
JavaScript
127 lines
11 KiB
JavaScript
/** 角色授权界面验收;合成数据只验证交互,不改真实账号或权限。 */
|
||
import assert from 'node:assert/strict'
|
||
import fs from 'node:fs'
|
||
import { createRequire } from 'node:module'
|
||
const require = createRequire(import.meta.url)
|
||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||
const base = process.env.AUTH_UI_URL || 'http://127.0.0.1:5173'
|
||
const output = process.env.AUTH_UI_OUTPUT || '.ai'
|
||
const browser = await chromium.launch({ headless: true, channel: 'msedge' })
|
||
const page = await browser.newPage({ viewport: { width: 1366, height: 1000 } })
|
||
const checks = [], errors = [], saves = []
|
||
let revision = 1, failPreview = false, conflict = false
|
||
const names = { view: '查看', create: '新增', update: '编辑', regions: '维护区域', plans: '维护计划', prepare: '准备核算资料', calculate: '核算与重算', allocation: '修改工资分配', generate: '生成工资', export: '导出' }
|
||
const definitions = [
|
||
['system:users', '系统管理', '用户管理', ['view', 'create', 'update']],
|
||
['system:roles', '系统管理', '角色管理', ['view', 'create', 'update']],
|
||
['base:personnel', '基础资料', '人员管理', ['view', 'create', 'update', 'regions', 'plans']],
|
||
['payroll', '绩效工资', '正式核算与分配', ['view', 'prepare', 'calculate', 'allocation']],
|
||
['settlements', '绩效工资', '月度工资', ['view', 'generate']],
|
||
['reports:business', '报表统计', '业务统计', ['view', 'export']],
|
||
]
|
||
const catalog = definitions.flatMap(([module, group, module_name, actions]) => actions.map(action => ({ code: `${module}:${action}`, module, group, module_name, name: names[action], action, view_code: `${module}:view`, admin_only: module.startsWith('system:'), maker_required: false })))
|
||
const roleCodes = { admin: catalog.map(item => item.code), finance: ['payroll:view'], payroll_maker: ['payroll:view', 'payroll:calculate', 'settlements:view', 'settlements:generate'] }
|
||
const roles = [{ id: 1, role_code: 'admin', role_name: '系统管理员', description: '系统管理', status: 1, user_count: 1 }, { id: 2, role_code: 'finance', role_name: '财务', status: 1, user_count: 1 }, { id: 3, role_code: 'payroll_maker', role_name: '工资经办', status: 1, user_count: 0 }]
|
||
let assigned = ['finance']
|
||
function effective(codes) { return [...new Set(codes.flatMap(code => roleCodes[code] ?? []))] }
|
||
function result(codes) { const permissions = effective(codes); return { permissions, roles: codes, sources: Object.fromEntries(catalog.map(item => [item.code, { roles: codes.filter(code => roleCodes[code]?.includes(item.code)), override: null, effective: permissions.includes(item.code) }])) } }
|
||
await page.addInitScript(() => localStorage.setItem('ys_token', 'synthetic-rbac-ui'))
|
||
page.on('pageerror', error => errors.push(error.message))
|
||
await page.route(url => url.pathname.startsWith('/api/'), async route => {
|
||
const request = route.request(), url = new URL(request.url()), path = url.pathname, method = request.method()
|
||
let data = {}, status = 200
|
||
if (path.endsWith('/auth/me')) data = { id: 1, username: 'synthetic-admin', real_name: '授权界面演练', roles: ['admin'], permissions: roleCodes.admin, can_write_payroll: true, is_payroll_maker: true, maker_required_permissions: [] }
|
||
else if (path.endsWith('/permissions/catalog')) data = catalog
|
||
else if (path.endsWith('/system/roles')) data = { list: roles.map(item => ({ ...item, permission_count: roleCodes[item.role_code].length })), total: roles.length, pageNum: 1, pageSize: 200 }
|
||
else if (path.endsWith('/users/meta/roles')) data = roles
|
||
else if (/\/roles\/\d+\/permissions$/.test(path)) {
|
||
const id = Number(path.match(/roles\/(\d+)/)[1]), item = roles.find(role => role.id === id)
|
||
if (method === 'PUT') {
|
||
const body = request.postDataJSON(); saves.push({ path, body })
|
||
if (conflict || body.expected_revision !== revision) status = 409
|
||
else { roleCodes[item.role_code] = body.permission_codes; revision++ }
|
||
}
|
||
data = { role_id: id, role_code: item.role_code, role_name: item.role_name, permission_codes: roleCodes[item.role_code], fixed: id === 1, revision }
|
||
} else if (path.endsWith('/system/users')) data = { list: [{ id: 7, username: 'synthetic-user', real_name: '角色继承演练', roles: assigned, status: 1 }], total: 1, pageNum: 1, pageSize: 20 }
|
||
else if (path.endsWith('/users/7/authorization/preview')) {
|
||
if (failPreview) status = 503
|
||
data = result(request.postDataJSON().role_codes)
|
||
} else if (path.endsWith('/users/7/authorization')) {
|
||
if (method === 'PUT') { const body = request.postDataJSON(); saves.push({ path, body }); assigned = body.role_codes; revision++ }
|
||
data = { ...result(assigned), id: 7, username: 'synthetic-user', role_codes: assigned, overrides: [], revision, is_payroll_maker: true }
|
||
}
|
||
return route.fulfill({ status, json: { code: status, message: status === 409 ? '权限版本已更新' : status === 503 ? '合成预览失败' : 'success', data: status === 200 ? data : null } })
|
||
})
|
||
|
||
try {
|
||
await page.goto(`${base}/system/roles`)
|
||
await page.getByRole('button', { name: '查看权限', exact: true }).click()
|
||
await page.getByText('系统管理员拥有全部权限,仅供查看', { exact: true }).waitFor()
|
||
assert.equal(await page.getByRole('button', { name: '保存角色权限', exact: true }).count(), 0)
|
||
assert.equal(await page.locator('.permission-drawer .el-table').count(), 0)
|
||
for (const width of [1366, 1920, 768]) {
|
||
await page.setViewportSize({ width, height: 1000 })
|
||
await page.screenshot({ path: `${output}/rbac-admin-${width}.png`, fullPage: true })
|
||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
|
||
}
|
||
checks.push('管理员固定只读、模块分组、1366/1920/768 布局无横向溢出')
|
||
await page.getByRole('button', { name: '关闭', exact: true }).click()
|
||
await page.setViewportSize({ width: 1366, height: 1000 })
|
||
await page.getByRole('row').filter({ hasText: '财务' }).getByRole('button', { name: '配置权限', exact: true }).click()
|
||
await page.getByRole('navigation', { name: '权限业务分组' }).getByRole('button', { name: /^绩效工资/ }).click()
|
||
await page.getByText('核算与重算', { exact: true }).click()
|
||
assert.ok(await page.getByRole('checkbox', { name: '正式核算与分配 查看', exact: true }).isChecked())
|
||
assert.ok(await page.getByRole('checkbox', { name: '正式核算与分配 核算与重算', exact: true }).isChecked())
|
||
const moduleAll = page.getByRole('checkbox', { name: '正式核算与分配模块全选', exact: true })
|
||
assert.equal(await moduleAll.evaluate(element => element.indeterminate), true)
|
||
await page.getByLabel('正式核算与分配权限', { exact: true }).getByText('全选', { exact: true }).click()
|
||
assert.ok(await page.getByRole('checkbox', { name: '正式核算与分配 修改工资分配', exact: true }).isChecked())
|
||
await page.getByRole('textbox', { name: '搜索权限', exact: true }).fill('人员')
|
||
await page.getByRole('checkbox', { name: '人员管理模块全选', exact: true }).locator('xpath=ancestor::label[1]').click()
|
||
await page.getByRole('textbox', { name: '搜索权限', exact: true }).fill('不存在的模块')
|
||
await page.getByText('没有匹配的权限', { exact: true }).waitFor()
|
||
await page.getByRole('textbox', { name: '搜索权限', exact: true }).fill('')
|
||
await page.getByRole('textbox', { name: '授权修改原因', exact: true }).fill('合成验收:财务经办职责调整')
|
||
await page.screenshot({ path: `${output}/rbac-role-editor.png`, fullPage: true })
|
||
await page.getByRole('button', { name: '保存角色权限', exact: true }).click()
|
||
await page.getByRole('dialog').waitFor({ state: 'hidden' })
|
||
assert.ok(saves[0].body.permission_codes.includes('payroll:view') && saves[0].body.permission_codes.includes('payroll:calculate'))
|
||
checks.push('操作联动查看、模块全选与半选、搜索空态、原因必填及保存实际载荷')
|
||
await page.goto(`${base}/system/users`)
|
||
await page.getByRole('button', { name: '分配角色', exact: true }).click()
|
||
await page.getByRole('heading', { name: '分配角色', exact: true }).waitFor()
|
||
assert.equal(await page.getByText('个人设置', { exact: true }).count(), 0)
|
||
assert.equal(await page.getByText('额外允许', { exact: true }).count(), 0)
|
||
await page.getByRole('checkbox', { name: '分配工资经办角色', exact: true }).locator('xpath=ancestor::label[1]').click()
|
||
await page.getByText('已选:财务、工资经办', { exact: false }).waitFor()
|
||
await page.getByRole('textbox', { name: '授权修改原因', exact: true }).fill('合成验收:增加工资经办角色')
|
||
await page.screenshot({ path: `${output}/rbac-user-role-assignment.png`, fullPage: true })
|
||
await page.getByRole('button', { name: '保存角色分配', exact: true }).click()
|
||
await page.getByRole('dialog').waitFor({ state: 'hidden' })
|
||
assert.deepEqual(saves.at(-1).body.role_codes, ['finance', 'payroll_maker'])
|
||
assert.deepEqual(saves.at(-1).body.overrides, [])
|
||
checks.push('用户只分配角色、继承权限只读预览、无个人权限覆盖入口')
|
||
await page.getByRole('button', { name: '分配角色', exact: true }).click()
|
||
failPreview = true
|
||
await page.getByRole('checkbox', { name: '分配工资经办角色', exact: true }).locator('xpath=ancestor::label[1]').click()
|
||
await page.getByText('继承权限预览失败,请重试后再保存。', { exact: true }).waitFor()
|
||
assert.ok(await page.getByRole('button', { name: '保存角色分配', exact: true }).isDisabled())
|
||
failPreview = false
|
||
await page.getByRole('button', { name: '重试预览', exact: true }).click()
|
||
await page.getByRole('textbox', { name: '授权修改原因', exact: true }).fill('角色变更演练')
|
||
await page.getByRole('button', { name: '取消', exact: true }).click()
|
||
await page.getByRole('button', { name: '放弃修改', exact: true }).click()
|
||
checks.push('预览失败禁止保存、重试恢复及未保存修改关闭保护')
|
||
await page.goto(`${base}/system/authorization`)
|
||
await page.waitForURL('**/system/roles')
|
||
assert.equal(await page.getByText('权限与工资制作人', { exact: true }).count(), 0)
|
||
checks.push('旧授权页跳转角色管理,导航无重复入口')
|
||
assert.deepEqual(errors, [])
|
||
fs.writeFileSync(`${output}/rbac-browser-result.json`, JSON.stringify({ checks, errors, saves: saves.length }, null, 2))
|
||
console.log(JSON.stringify({ checks, errors, saves: saves.length }))
|
||
} catch (error) {
|
||
await page.screenshot({ path: `${output}/rbac-browser-failure.png`, fullPage: true })
|
||
fs.writeFileSync(`${output}/rbac-browser-failure.txt`, String(error) + '\n' + await page.locator('body').innerText())
|
||
throw error
|
||
} finally { await browser.close() }
|