PayrollManagementSystem/frontend/e2e/authorization_roles.mjs
liuwenxin a2f9a58de5 feat(前端): 配套授权、监控与同步进度页面
- 角色授权:权限矩阵抽屉与角色创建,按模块分组勾选
- 系统监控:总览、趋势、接口与事件页,15 秒自动刷新,
  请求可中断、窗口切换暂停,详情抽屉支持导出
- T+ 同步:同步进度条组件与工作台状态拆分
- 工资:试用期面板、产品提取弹窗与无权限占位页
- 路由权限:按权限码控制菜单与页面可达性

影响范围:前端全部页面
验证:随本次后端联调本地人工核对,e2e 脚本入库待跑
2026-11-03 11:21:19 +08:00

127 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 角色授权界面验收;合成数据只验证交互,不改真实账号或权限。 */
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() }