|
|
@@ -0,0 +1,197 @@ |
|
|
|
|
|
import type { Locator, Page, Route } from '@playwright/test' |
|
|
|
|
|
|
|
|
|
|
|
import { captureScreenshot, expect, test } from './fixtures' |
|
|
|
|
|
|
|
|
|
|
|
type RoleRecord = { |
|
|
|
|
|
id: string |
|
|
|
|
|
code: string |
|
|
|
|
|
name: string |
|
|
|
|
|
shortName: string |
|
|
|
|
|
description: string |
|
|
|
|
|
enabled: boolean |
|
|
|
|
|
builtIn: boolean |
|
|
|
|
|
superAdmin: boolean |
|
|
|
|
|
dataScope: string |
|
|
|
|
|
departmentScopeIds: string[] |
|
|
|
|
|
includeChildDepartments: boolean |
|
|
|
|
|
userCount: number |
|
|
|
|
|
permissions: string[] |
|
|
|
|
|
sortOrder: number |
|
|
|
|
|
dataVersion: number |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const json = (route: Route, data: unknown) => route.fulfill({ |
|
|
|
|
|
status: 200, |
|
|
|
|
|
contentType: 'application/json', |
|
|
|
|
|
body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'system-basic-regression' }), |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const roles: RoleRecord[] = [ |
|
|
|
|
|
{ |
|
|
|
|
|
id: '10', code: 'teacher', name: '教员', shortName: '教', description: '教员业务角色', enabled: true, |
|
|
|
|
|
builtIn: true, superAdmin: false, dataScope: 'SYSTEM', departmentScopeIds: [], includeChildDepartments: true, |
|
|
|
|
|
userCount: 3, permissions: [], sortOrder: 10, dataVersion: 1, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '1', code: 'admin', name: '管理员', shortName: '管', description: '系统管理员', enabled: true, |
|
|
|
|
|
builtIn: true, superAdmin: true, dataScope: 'ALL', departmentScopeIds: [], includeChildDepartments: true, |
|
|
|
|
|
userCount: 1, permissions: [], sortOrder: 20, dataVersion: 1, |
|
|
|
|
|
}, |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
const departments = [{ |
|
|
|
|
|
id: '100', code: 'ORG-100', name: '虚拟教员平台', parentId: null, enabled: true, |
|
|
|
|
|
builtIn: true, isRoot: true, sortOrder: 10, directUserCount: 1, directChildCount: 1, |
|
|
|
|
|
descendantCount: 1, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00', |
|
|
|
|
|
children: [{ |
|
|
|
|
|
id: '110', code: 'ORG-110', name: '维修教研室', parentId: '100', enabled: true, |
|
|
|
|
|
builtIn: false, isRoot: false, sortOrder: 10, directUserCount: 3, directChildCount: 0, |
|
|
|
|
|
descendantCount: 0, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00', children: [], |
|
|
|
|
|
}], |
|
|
|
|
|
}] |
|
|
|
|
|
|
|
|
|
|
|
const permissionSections = Array.from({ length: 6 }, (_, sectionIndex) => ({ |
|
|
|
|
|
id: `section-${sectionIndex}`, |
|
|
|
|
|
label: `权限分组 ${sectionIndex + 1}`, |
|
|
|
|
|
items: Array.from({ length: 8 }, (_, itemIndex) => ({ |
|
|
|
|
|
code: `mock.section${sectionIndex}.action${itemIndex}`, |
|
|
|
|
|
label: `业务权限 ${sectionIndex + 1}-${itemIndex + 1}`, |
|
|
|
|
|
description: '用于验证长权限目录中的底部操作栏', |
|
|
|
|
|
type: 'ACTION', |
|
|
|
|
|
})), |
|
|
|
|
|
})) |
|
|
|
|
|
|
|
|
|
|
|
const fillFormItem = (dialog: Locator, label: string, value: string) => dialog |
|
|
|
|
|
.locator('.el-form-item') |
|
|
|
|
|
.filter({ hasText: label }) |
|
|
|
|
|
.first() |
|
|
|
|
|
.locator('input, textarea') |
|
|
|
|
|
.first() |
|
|
|
|
|
.fill(value) |
|
|
|
|
|
|
|
|
|
|
|
async function mockSystemApi(page: Page, submitted: Array<Record<string, unknown>> = []) { |
|
|
|
|
|
const mutableRoles = roles.map((role) => ({ ...role })) |
|
|
|
|
|
await page.route(/\/api\//, async (route) => { |
|
|
|
|
|
const url = new URL(route.request().url()) |
|
|
|
|
|
const path = url.pathname |
|
|
|
|
|
const method = route.request().method() |
|
|
|
|
|
if (!path.startsWith('/api/')) return route.fallback() |
|
|
|
|
|
|
|
|
|
|
|
if (path.endsWith('/auth/me')) return json(route, { |
|
|
|
|
|
userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '虚拟教员平台', |
|
|
|
|
|
activeRoleId: '1', roles: [mutableRoles[1]], authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false, |
|
|
|
|
|
permissions: [ |
|
|
|
|
|
'system.departments', 'system.roles', 'system.roles.create', 'system.roles.update', |
|
|
|
|
|
'system.permissions', 'system.permissions.update', |
|
|
|
|
|
], |
|
|
|
|
|
}) |
|
|
|
|
|
if (path.endsWith('/departments/tree')) return json(route, departments) |
|
|
|
|
|
if (path.endsWith('/departments/stats')) return json(route, { total: 2, enabled: 2, maxDepth: 2, assignedUsers: 4 }) |
|
|
|
|
|
if (path.endsWith('/roles/all')) return json(route, mutableRoles) |
|
|
|
|
|
if (path.endsWith('/roles/stats')) return json(route, { |
|
|
|
|
|
total: mutableRoles.length, |
|
|
|
|
|
enabled: mutableRoles.filter((role) => role.enabled).length, |
|
|
|
|
|
builtIn: mutableRoles.filter((role) => role.builtIn).length, |
|
|
|
|
|
assignedUsers: mutableRoles.reduce((total, role) => total + role.userCount, 0), |
|
|
|
|
|
}) |
|
|
|
|
|
if (path.endsWith('/roles') && method === 'POST') { |
|
|
|
|
|
const body = route.request().postDataJSON() as Record<string, unknown> |
|
|
|
|
|
submitted.push(body) |
|
|
|
|
|
const created: RoleRecord = { |
|
|
|
|
|
id: '20', code: String(body.code), name: String(body.name), shortName: String(body.shortName ?? ''), |
|
|
|
|
|
description: String(body.description ?? ''), enabled: true, builtIn: false, superAdmin: false, |
|
|
|
|
|
dataScope: String(body.dataScope), departmentScopeIds: [], includeChildDepartments: true, |
|
|
|
|
|
userCount: 0, permissions: [], sortOrder: Number(body.sortOrder), dataVersion: 1, |
|
|
|
|
|
} |
|
|
|
|
|
mutableRoles.push(created) |
|
|
|
|
|
return json(route, created) |
|
|
|
|
|
} |
|
|
|
|
|
if (path.endsWith('/roles/20') && method === 'PUT') { |
|
|
|
|
|
const body = route.request().postDataJSON() as Record<string, unknown> |
|
|
|
|
|
submitted.push(body) |
|
|
|
|
|
Object.assign(mutableRoles[2]!, { |
|
|
|
|
|
name: String(body.name), shortName: String(body.shortName ?? ''), description: String(body.description ?? ''), |
|
|
|
|
|
dataScope: String(body.dataScope), dataVersion: mutableRoles[2]!.dataVersion + 1, |
|
|
|
|
|
}) |
|
|
|
|
|
return json(route, mutableRoles[2]) |
|
|
|
|
|
} |
|
|
|
|
|
if (path.endsWith('/permissions/catalog')) return json(route, { sections: permissionSections }) |
|
|
|
|
|
if (/\/permissions\/roles\/\d+$/.test(path)) return json(route, { permissionCodes: [], dataVersion: 1 }) |
|
|
|
|
|
if (path.endsWith('/menus/navigation')) return json(route, []) |
|
|
|
|
|
if (path.endsWith('/system-config/public')) return json(route, { systemName: '虚拟教员系统(试用)' }) |
|
|
|
|
|
return json(route, {}) |
|
|
|
|
|
}) |
|
|
|
|
|
await page.addInitScript(() => { |
|
|
|
|
|
window.sessionStorage.setItem('ai-person:web:access-token:v1', 'system-basic-regression-token') |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
test('Bug 850:无匹配部门时不重复显示清除筛选按钮', async ({ page }, testInfo) => { |
|
|
|
|
|
await mockSystemApi(page) |
|
|
|
|
|
await page.goto('/organization/departments') |
|
|
|
|
|
|
|
|
|
|
|
await page.getByPlaceholder('请输入部门、编码或负责人').fill('绝对不存在的部门') |
|
|
|
|
|
await page.getByRole('button', { name: '搜索', exact: true }).click() |
|
|
|
|
|
|
|
|
|
|
|
const empty = page.locator('.department-table-body .el-empty') |
|
|
|
|
|
await expect(empty).toBeVisible() |
|
|
|
|
|
await expect(empty).toContainText('没有符合当前筛选条件的部门') |
|
|
|
|
|
await expect(empty.getByRole('button', { name: '清除筛选' })).toHaveCount(0) |
|
|
|
|
|
await expect(page.locator('.department-toolbar').getByRole('button', { name: '重置', exact: true })).toBeVisible() |
|
|
|
|
|
await captureScreenshot(page, testInfo, 'bug-850-empty-without-duplicate-reset') |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
test('Bug 851、852、854:角色简称提示清晰、空值原样保存且校验失败有提示', async ({ page }, testInfo) => { |
|
|
|
|
|
const submitted: Array<Record<string, unknown>> = [] |
|
|
|
|
|
await mockSystemApi(page, submitted) |
|
|
|
|
|
await page.goto('/organization/roles') |
|
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: '新增角色', exact: true }).click() |
|
|
|
|
|
let dialog = page.locator('.el-dialog:visible') |
|
|
|
|
|
const shortInput = dialog.getByPlaceholder('请输入界面简称,如:审核') |
|
|
|
|
|
await expect(shortInput).toBeVisible() |
|
|
|
|
|
|
|
|
|
|
|
await fillFormItem(dialog, '角色名称', '测试角色') |
|
|
|
|
|
await fillFormItem(dialog, '角色编码', `a${'1'.repeat(48)}`) |
|
|
|
|
|
await dialog.getByRole('button', { name: '创建角色' }).click() |
|
|
|
|
|
await expect(page.locator('.el-message').filter({ hasText: '请检查表单中的错误项' })).toBeVisible() |
|
|
|
|
|
expect(submitted).toHaveLength(0) |
|
|
|
|
|
|
|
|
|
|
|
await fillFormItem(dialog, '角色编码', 'test_role') |
|
|
|
|
|
await dialog.getByRole('button', { name: '创建角色' }).click() |
|
|
|
|
|
await expect(dialog).toBeHidden() |
|
|
|
|
|
expect(submitted[0]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' }) |
|
|
|
|
|
|
|
|
|
|
|
const createdRow = page.locator('.roles-table .el-table__row').filter({ hasText: '测试角色' }) |
|
|
|
|
|
await createdRow.getByRole('button', { name: '编辑' }).click() |
|
|
|
|
|
dialog = page.locator('.el-dialog:visible') |
|
|
|
|
|
await expect(dialog.getByPlaceholder('请输入界面简称,如:审核')).toHaveValue('') |
|
|
|
|
|
await captureScreenshot(page, testInfo, 'bugs-851-852-854-role-form-regression') |
|
|
|
|
|
await dialog.getByRole('button', { name: '保存修改' }).click() |
|
|
|
|
|
await expect(dialog).toBeHidden() |
|
|
|
|
|
expect(submitted[1]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' }) |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
test('Bug 853:权限操作栏在长内容滚动时固定于视口底部', async ({ page }, testInfo) => { |
|
|
|
|
|
await mockSystemApi(page) |
|
|
|
|
|
await page.setViewportSize({ width: 1440, height: 760 }) |
|
|
|
|
|
await page.goto('/organization/permissions') |
|
|
|
|
|
|
|
|
|
|
|
const footer = page.locator('.permission-actions') |
|
|
|
|
|
await expect(page.locator('.permission-group')).toHaveCount(permissionSections.length) |
|
|
|
|
|
await expect.poll(() => page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeGreaterThan(500) |
|
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => window.scrollTo(0, Math.round((document.documentElement.scrollHeight - window.innerHeight) * 0.45))) |
|
|
|
|
|
await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(100) |
|
|
|
|
|
await expect(footer).toBeVisible() |
|
|
|
|
|
|
|
|
|
|
|
const [footerBox, viewportHeight, position] = await Promise.all([ |
|
|
|
|
|
footer.boundingBox(), |
|
|
|
|
|
page.evaluate(() => window.innerHeight), |
|
|
|
|
|
footer.evaluate((element) => getComputedStyle(element).position), |
|
|
|
|
|
]) |
|
|
|
|
|
expect(footerBox).not.toBeNull() |
|
|
|
|
|
expect(position).toBe('sticky') |
|
|
|
|
|
expect(Math.abs(viewportHeight - (footerBox!.y + footerBox!.height))).toBeLessThanOrEqual(20) |
|
|
|
|
|
await captureScreenshot(page, testInfo, 'bug-853-sticky-permission-actions') |
|
|
|
|
|
}) |