|
- import { test, expect, type Page, type TestInfo } from '@playwright/test'
- import fs from 'node:fs'
- import path from 'node:path'
- // Local helper reads credentials in memory. Reports and Playwright traces never contain them.
- // @ts-ignore JS helper is also executable independently for API checks.
- import { call, login, work } from '../tools/role-permissions-live.mjs'
-
- const shots = path.resolve('reports/role-permissions-20260906/screenshots')
- fs.mkdirSync(shots, { recursive: true })
- const fixture = () => JSON.parse(fs.readFileSync(path.join(work, 'fixtures.json'), 'utf8'))
- const networkFailures = new WeakMap<Page, Array<{ path: string; status?: number; error?: string }>>()
- test.beforeEach(async ({ page }) => {
- const failures: Array<{ path: string; status?: number; error?: string }> = []
- networkFailures.set(page, failures)
- page.on('requestfailed', request => {
- if (new URL(request.url()).pathname.startsWith('/api/')) failures.push({ path: new URL(request.url()).pathname, error: request.failure()?.errorText })
- })
- page.on('response', response => {
- if (response.status() >= 400 && new URL(response.url()).pathname.startsWith('/api/')) failures.push({ path: new URL(response.url()).pathname, status: response.status() })
- })
- })
- test.afterEach(async ({ page }, info) => {
- await info.attach('sanitized-api-failures', { body: JSON.stringify(networkFailures.get(page) ?? []), contentType: 'application/json' })
- const token = await page.evaluate(() => sessionStorage.getItem('unreal-tran:web:access-token:v1') || localStorage.getItem('unreal-tran:web:access-token:v1')).catch(() => null)
- if (token) await call('auth/v1/auth/logout', token, {}).catch(() => undefined)
- })
- const shot = async (page: Page, info: TestInfo, name: string) => {
- const filename = path.join(shots, `${name}.png`)
- await expect(page.locator('.el-loading-mask:visible')).toHaveCount(0)
- await page.screenshot({ path: filename, fullPage: true, animations: 'disabled' })
- await info.attach(name, { path: filename, contentType: 'image/png' })
- }
- async function signIn(page: Page, role: 'admin' | 'reader' | 'editor' | 'teacher' = 'admin') {
- let session
- if (role === 'admin') session = await login()
- else {
- const data = fixture()
- const result = await call('auth/v1/auth/login', null, { userId: data.user.id, password: data.user.password, roleCode: 'teacher', departmentId: data.user.departmentId, rememberMe: false })
- expect(result.status).toBe(200)
- const selected = await call('auth/v1/auth/active-role', result.data.accessToken, { roleId: role === 'teacher' ? data.teacherRoleId : data[role].id }, 'PUT')
- expect(selected.status).toBe(200)
- session = selected.data?.accessToken ? selected.data : result.data
- }
- await page.addInitScript(({ accessToken, refreshToken }) => {
- // Only initialize once: a role switch may rotate tokens before a full app reload.
- if (!sessionStorage.getItem('role-permissions-test-initialized')) {
- sessionStorage.setItem('unreal-tran:web:access-token:v1', accessToken)
- if (refreshToken) sessionStorage.setItem('unreal-tran:web:refresh-token:v1', refreshToken)
- sessionStorage.setItem('role-permissions-test-initialized', '1')
- }
- }, { accessToken: session.accessToken, refreshToken: session.refreshToken })
- }
- async function openSwitcher(page: Page) {
- await page.getByRole('button', { name: /^账号菜单:/ }).click()
- await page.getByRole('menuitem', { name: '切换角色', exact: true }).click()
- const dialog = page.getByRole('dialog', { name: '切换角色', exact: true })
- await expect(dialog).toBeVisible()
- return dialog
- }
- const treeNode = (container: ReturnType<Page['locator']>, code: string) => container.locator('.el-tree-node__content').filter({ has: container.page().locator('code').filter({ hasText: new RegExp(`^${code.replaceAll('.', '\\.')}$$`) }) })
-
- test('管理员授权固定,权限树细分到页面操作', async ({ page }, info) => {
- await signIn(page)
- await page.goto('/system/roles')
- const adminRow = page.locator('.el-table__row').filter({ has: page.locator('.role-identity code').filter({ hasText: /^admin$/ }) })
- await adminRow.getByRole('button', { name: '配置权限', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: '配置角色权限' })
- await expect(drawer).toContainText('该角色采用固定授权')
- await drawer.getByPlaceholder('搜索权限名称或编码').fill('content.model')
- await expect(drawer.locator('code').filter({ hasText: /^content\.model\.create$/ })).toBeVisible()
- await expect(drawer.getByRole('button', { name: '保存授权' })).toBeDisabled()
- await expect(treeNode(drawer, 'content.model.create').getByRole('checkbox')).toBeDisabled()
- await shot(page, info, '01-admin-fixed-permissions')
- })
-
- test('权限依赖:勾选操作自动补齐查看,取消查看需要确认', async ({ page }, info) => {
- await signIn(page)
- const data = fixture()
- await page.goto('/system/roles')
- const row = page.locator('.el-table__row').filter({ has: page.locator('.role-identity code').filter({ hasText: data.reader.code }) })
- await row.getByRole('button', { name: '配置权限', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: '配置角色权限' })
- const editor = drawer.locator('.permission-tree-editor')
- await editor.getByPlaceholder('搜索权限名称或编码').fill('content.model')
- const read = treeNode(editor, 'content.model').getByRole('checkbox')
- const create = treeNode(editor, 'content.model.create').getByRole('checkbox')
- await expect(read).toBeChecked()
- await expect(create).not.toBeChecked()
- await treeNode(editor, 'content.model').locator('.el-checkbox').click()
- await expect(read).not.toBeChecked()
- await treeNode(editor, 'content.model.create').locator('.el-checkbox').click()
- await expect(read).toBeChecked()
- await expect(create).toBeChecked()
- await shot(page, info, '02-action-adds-page-permission')
- await treeNode(editor, 'content.model').locator('.el-checkbox').click()
- const confirm = page.getByRole('dialog', { name: '确认取消关联权限' })
- await expect(confirm).toBeVisible()
- await shot(page, info, '03-removing-page-confirms-dependent-actions')
- await confirm.getByRole('button', { name: '保留权限' }).click()
- await expect(read).toBeChecked()
- await expect(create).toBeChecked()
- await drawer.getByRole('button', { name: '保存授权' }).click()
- await expect(drawer).not.toBeVisible()
- await row.getByRole('button', { name: '配置权限', exact: true }).click()
- await editor.getByPlaceholder('搜索权限名称或编码').fill('content.model')
- await expect(create).toBeChecked()
- await treeNode(editor, 'content.model.create').locator('.el-checkbox').click()
- await expect(create).not.toBeChecked()
- await drawer.getByRole('button', { name: '保存授权' }).click()
- await expect(drawer).not.toBeVisible()
- })
-
- test('头像弹窗切换自定义角色,清理旧缓存并改变操作能力', async ({ page }, info) => {
- await signIn(page, 'reader')
- await page.goto('/content/models')
- await expect(page.getByText('只读权限', { exact: true })).toBeVisible()
- await shot(page, info, '04-custom-reader-model-list')
- const dialog = await openSwitcher(page)
- await expect(dialog.getByRole('button', { name: /权限实测·模型只读/ })).toContainText('当前角色')
- await shot(page, info, '05-avatar-role-switch-dialog')
- await dialog.getByRole('button', { name: /权限实测·模型编辑/ }).click()
- await expect(page).toHaveURL(/\/dashboard$/)
- await expect(page.getByRole('button', { name: /^账号菜单:/ })).toContainText('权限实测·模型编辑')
- await page.goto('/content/models')
- await expect(page.getByText('只读权限', { exact: true })).toHaveCount(0)
- await expect(page.getByRole('button', { name: /新建模型|新增模型/ }).first()).toBeVisible()
- await shot(page, info, '06-custom-editor-model-list')
- const scopes = await page.evaluate(() => Object.keys(JSON.parse(localStorage.getItem('unreal-tran:web:tabs:v2') || '{}')))
- const data = fixture()
- expect(scopes).toContain(`${data.user.id}:${data.reader.id}`)
- expect(scopes).toContain(`${data.user.id}:${data.editor.id}`)
- await page.goto('/system/roles')
- await expect(page).not.toHaveURL(/\/system\/roles$/)
- })
-
- test('角色切换接口失败保留原身份与页面', async ({ page }, info) => {
- await signIn(page, 'reader')
- await page.goto('/content/models')
- await expect(page.getByText('只读权限', { exact: true })).toBeVisible()
- await page.route('**/api/auth/v1/auth/active-role', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'UNAVAILABLE', message: '角色服务临时不可用', data: null }) }))
- const dialog = await openSwitcher(page)
- await dialog.getByRole('button', { name: /权限实测·模型编辑/ }).click()
- await expect(page.locator('.el-message--error')).toContainText('角色服务临时不可用')
- await expect(page.getByRole('button', { name: /^账号菜单:/ })).toContainText('权限实测·模型只读')
- await expect(page).toHaveURL(/\/content\/models$/)
- await shot(page, info, '07-failed-switch-keeps-current-role')
- })
-
- test('切换已提交但身份加载失败,不展示旧角色业务页面', async ({ page }, info) => {
- await signIn(page, 'reader')
- await page.goto('/content/models')
- await expect(page.getByText('只读权限', { exact: true })).toBeVisible()
- await page.route('**/api/auth/v1/auth/me', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'UNAVAILABLE', message: '身份信息临时不可用', data: null }) }))
- const dialog = await openSwitcher(page)
- await dialog.getByRole('button', { name: /权限实测·模型编辑/ }).click()
- await expect(page).toHaveURL(/\/login/)
- await expect(page.getByRole('button', { name: /^账号菜单:/ })).toHaveCount(0)
- await shot(page, info, '08-role-refresh-failure-clears-old-view')
- })
-
- test('训练编排未保存时,取消切换保留当前内容与身份', async ({ page }, info) => {
- await signIn(page, 'teacher')
- await page.goto('/content/training-projects/164/legacy')
- await page.getByRole('button', { name: '项目设置', exact: true }).click()
- const name = page.locator('[data-field="name"][data-scope="project"]')
- await expect(name).toBeVisible()
- await name.fill('权限切换未保存验证')
- await name.blur()
- await page.locator('[data-action="close-dialog"]').filter({ hasText: '完成' }).click()
- const switcher = await openSwitcher(page)
- let submitted = 0
- page.on('request', request => { if (request.url().endsWith('/auth/active-role') && request.method() === 'PUT') submitted++ })
- await switcher.getByRole('button', { name: /权限实测·模型只读/ }).click()
- const warning = page.getByRole('dialog', { name: '离开训练编排?', exact: true })
- await expect(warning).toBeVisible()
- await shot(page, info, '09-unsaved-training-blocks-role-switch')
- await warning.getByRole('button', { name: /close|关闭/i }).click()
- await expect(warning).not.toBeVisible()
- await expect(page.getByRole('button', { name: /^账号菜单:/ })).toContainText('教员')
- expect(submitted).toBe(0)
- await switcher.getByRole('button', { name: '关闭', exact: true }).click()
- await page.getByRole('button', { name: '项目设置', exact: true }).click()
- await expect(name).toHaveValue('权限切换未保存验证')
- })
-
- test('未保存训练的保存请求失败时,阻止提交角色切换', async ({ page }, info) => {
- await signIn(page, 'teacher')
- await page.goto('/content/training-projects/164/legacy')
- await page.getByRole('button', { name: '项目设置', exact: true }).click()
- const name = page.locator('[data-field="name"][data-scope="project"]')
- await name.fill('保存失败角色切换验证')
- await name.blur()
- await page.locator('[data-action="close-dialog"]').filter({ hasText: '完成' }).click()
- let roleWrites = 0
- let failedSaves = 0
- page.on('request', request => { if (request.url().endsWith('/auth/active-role') && request.method() === 'PUT') roleWrites++ })
- await page.route('**/api/tran/v1/content/projects/164', async route => {
- if (route.request().method() !== 'PUT') return route.continue()
- failedSaves++
- return route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 'UNAVAILABLE', message: '实测保存失败,请稍后重试', data: null }) })
- })
- const switcher = await openSwitcher(page)
- await switcher.getByRole('button', { name: /权限实测·模型只读/ }).click()
- await page.getByRole('dialog', { name: '离开训练编排?', exact: true }).getByRole('button', { name: '保存并离开' }).click()
- await expect.poll(() => failedSaves).toBe(1)
- await expect(page.locator('.el-message--error')).toContainText('实测保存失败')
- await expect(page.getByRole('button', { name: /^账号菜单:/ })).toContainText('教员')
- expect(roleWrites).toBe(0)
- await shot(page, info, '09b-save-failure-blocks-role-switch')
- await switcher.getByRole('button', { name: '关闭', exact: true }).click()
- await page.getByRole('button', { name: '项目设置', exact: true }).click()
- await expect(name).toHaveValue('保存失败角色切换验证')
- })
|