Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

198 řádky
9.3 KiB

  1. import type { Locator, Page, Route } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. type RoleRecord = {
  4. id: string
  5. code: string
  6. name: string
  7. shortName: string
  8. description: string
  9. enabled: boolean
  10. builtIn: boolean
  11. superAdmin: boolean
  12. dataScope: string
  13. departmentScopeIds: string[]
  14. includeChildDepartments: boolean
  15. userCount: number
  16. permissions: string[]
  17. sortOrder: number
  18. dataVersion: number
  19. }
  20. const json = (route: Route, data: unknown) => route.fulfill({
  21. status: 200,
  22. contentType: 'application/json',
  23. body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'system-basic-regression' }),
  24. })
  25. const roles: RoleRecord[] = [
  26. {
  27. id: '10', code: 'teacher', name: '教员', shortName: '教', description: '教员业务角色', enabled: true,
  28. builtIn: true, superAdmin: false, dataScope: 'SYSTEM', departmentScopeIds: [], includeChildDepartments: true,
  29. userCount: 3, permissions: [], sortOrder: 10, dataVersion: 1,
  30. },
  31. {
  32. id: '1', code: 'admin', name: '管理员', shortName: '管', description: '系统管理员', enabled: true,
  33. builtIn: true, superAdmin: true, dataScope: 'ALL', departmentScopeIds: [], includeChildDepartments: true,
  34. userCount: 1, permissions: [], sortOrder: 20, dataVersion: 1,
  35. },
  36. ]
  37. const departments = [{
  38. id: '100', code: 'ORG-100', name: '虚拟教员平台', parentId: null, enabled: true,
  39. builtIn: true, isRoot: true, sortOrder: 10, directUserCount: 1, directChildCount: 1,
  40. descendantCount: 1, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00',
  41. children: [{
  42. id: '110', code: 'ORG-110', name: '维修教研室', parentId: '100', enabled: true,
  43. builtIn: false, isRoot: false, sortOrder: 10, directUserCount: 3, directChildCount: 0,
  44. descendantCount: 0, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00', children: [],
  45. }],
  46. }]
  47. const permissionSections = Array.from({ length: 6 }, (_, sectionIndex) => ({
  48. id: `section-${sectionIndex}`,
  49. label: `权限分组 ${sectionIndex + 1}`,
  50. items: Array.from({ length: 8 }, (_, itemIndex) => ({
  51. code: `mock.section${sectionIndex}.action${itemIndex}`,
  52. label: `业务权限 ${sectionIndex + 1}-${itemIndex + 1}`,
  53. description: '用于验证长权限目录中的底部操作栏',
  54. type: 'ACTION',
  55. })),
  56. }))
  57. const fillFormItem = (dialog: Locator, label: string, value: string) => dialog
  58. .locator('.el-form-item')
  59. .filter({ hasText: label })
  60. .first()
  61. .locator('input, textarea')
  62. .first()
  63. .fill(value)
  64. async function mockSystemApi(page: Page, submitted: Array<Record<string, unknown>> = []) {
  65. const mutableRoles = roles.map((role) => ({ ...role }))
  66. await page.route(/\/api\//, async (route) => {
  67. const url = new URL(route.request().url())
  68. const path = url.pathname
  69. const method = route.request().method()
  70. if (!path.startsWith('/api/')) return route.fallback()
  71. if (path.endsWith('/auth/me')) return json(route, {
  72. userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '虚拟教员平台',
  73. activeRoleId: '1', roles: [mutableRoles[1]], authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false,
  74. permissions: [
  75. 'system.departments', 'system.roles', 'system.roles.create', 'system.roles.update',
  76. 'system.permissions', 'system.permissions.update',
  77. ],
  78. })
  79. if (path.endsWith('/departments/tree')) return json(route, departments)
  80. if (path.endsWith('/departments/stats')) return json(route, { total: 2, enabled: 2, maxDepth: 2, assignedUsers: 4 })
  81. if (path.endsWith('/roles/all')) return json(route, mutableRoles)
  82. if (path.endsWith('/roles/stats')) return json(route, {
  83. total: mutableRoles.length,
  84. enabled: mutableRoles.filter((role) => role.enabled).length,
  85. builtIn: mutableRoles.filter((role) => role.builtIn).length,
  86. assignedUsers: mutableRoles.reduce((total, role) => total + role.userCount, 0),
  87. })
  88. if (path.endsWith('/roles') && method === 'POST') {
  89. const body = route.request().postDataJSON() as Record<string, unknown>
  90. submitted.push(body)
  91. const created: RoleRecord = {
  92. id: '20', code: String(body.code), name: String(body.name), shortName: String(body.shortName ?? ''),
  93. description: String(body.description ?? ''), enabled: true, builtIn: false, superAdmin: false,
  94. dataScope: String(body.dataScope), departmentScopeIds: [], includeChildDepartments: true,
  95. userCount: 0, permissions: [], sortOrder: Number(body.sortOrder), dataVersion: 1,
  96. }
  97. mutableRoles.push(created)
  98. return json(route, created)
  99. }
  100. if (path.endsWith('/roles/20') && method === 'PUT') {
  101. const body = route.request().postDataJSON() as Record<string, unknown>
  102. submitted.push(body)
  103. Object.assign(mutableRoles[2]!, {
  104. name: String(body.name), shortName: String(body.shortName ?? ''), description: String(body.description ?? ''),
  105. dataScope: String(body.dataScope), dataVersion: mutableRoles[2]!.dataVersion + 1,
  106. })
  107. return json(route, mutableRoles[2])
  108. }
  109. if (path.endsWith('/permissions/catalog')) return json(route, { sections: permissionSections })
  110. if (/\/permissions\/roles\/\d+$/.test(path)) return json(route, { permissionCodes: [], dataVersion: 1 })
  111. if (path.endsWith('/menus/navigation')) return json(route, [])
  112. if (path.endsWith('/system-config/public')) return json(route, { systemName: '虚拟教员系统(试用)' })
  113. return json(route, {})
  114. })
  115. await page.addInitScript(() => {
  116. window.sessionStorage.setItem('ai-person:web:access-token:v1', 'system-basic-regression-token')
  117. })
  118. }
  119. test('Bug 850:无匹配部门时不重复显示清除筛选按钮', async ({ page }, testInfo) => {
  120. await mockSystemApi(page)
  121. await page.goto('/organization/departments')
  122. await page.getByPlaceholder('请输入部门、编码或负责人').fill('绝对不存在的部门')
  123. await page.getByRole('button', { name: '搜索', exact: true }).click()
  124. const empty = page.locator('.department-table-body .el-empty')
  125. await expect(empty).toBeVisible()
  126. await expect(empty).toContainText('没有符合当前筛选条件的部门')
  127. await expect(empty.getByRole('button', { name: '清除筛选' })).toHaveCount(0)
  128. await expect(page.locator('.department-toolbar').getByRole('button', { name: '重置', exact: true })).toBeVisible()
  129. await captureScreenshot(page, testInfo, 'bug-850-empty-without-duplicate-reset')
  130. })
  131. test('Bug 851、852、854:角色简称提示清晰、空值原样保存且校验失败有提示', async ({ page }, testInfo) => {
  132. const submitted: Array<Record<string, unknown>> = []
  133. await mockSystemApi(page, submitted)
  134. await page.goto('/organization/roles')
  135. await page.getByRole('button', { name: '新增角色', exact: true }).click()
  136. let dialog = page.locator('.el-dialog:visible')
  137. const shortInput = dialog.getByPlaceholder('请输入界面简称,如:审核')
  138. await expect(shortInput).toBeVisible()
  139. await fillFormItem(dialog, '角色名称', '测试角色')
  140. await fillFormItem(dialog, '角色编码', `a${'1'.repeat(48)}`)
  141. await dialog.getByRole('button', { name: '创建角色' }).click()
  142. await expect(page.locator('.el-message').filter({ hasText: '请检查表单中的错误项' })).toBeVisible()
  143. expect(submitted).toHaveLength(0)
  144. await fillFormItem(dialog, '角色编码', 'test_role')
  145. await dialog.getByRole('button', { name: '创建角色' }).click()
  146. await expect(dialog).toBeHidden()
  147. expect(submitted[0]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' })
  148. const createdRow = page.locator('.roles-table .el-table__row').filter({ hasText: '测试角色' })
  149. await createdRow.getByRole('button', { name: '编辑' }).click()
  150. dialog = page.locator('.el-dialog:visible')
  151. await expect(dialog.getByPlaceholder('请输入界面简称,如:审核')).toHaveValue('')
  152. await captureScreenshot(page, testInfo, 'bugs-851-852-854-role-form-regression')
  153. await dialog.getByRole('button', { name: '保存修改' }).click()
  154. await expect(dialog).toBeHidden()
  155. expect(submitted[1]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' })
  156. })
  157. test('Bug 853:权限操作栏在长内容滚动时固定于视口底部', async ({ page }, testInfo) => {
  158. await mockSystemApi(page)
  159. await page.setViewportSize({ width: 1440, height: 760 })
  160. await page.goto('/organization/permissions')
  161. const footer = page.locator('.permission-actions')
  162. await expect(page.locator('.permission-group')).toHaveCount(permissionSections.length)
  163. await expect.poll(() => page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeGreaterThan(500)
  164. await page.evaluate(() => window.scrollTo(0, Math.round((document.documentElement.scrollHeight - window.innerHeight) * 0.45)))
  165. await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(100)
  166. await expect(footer).toBeVisible()
  167. const [footerBox, viewportHeight, position] = await Promise.all([
  168. footer.boundingBox(),
  169. page.evaluate(() => window.innerHeight),
  170. footer.evaluate((element) => getComputedStyle(element).position),
  171. ])
  172. expect(footerBox).not.toBeNull()
  173. expect(position).toBe('sticky')
  174. expect(Math.abs(viewportHeight - (footerBox!.y + footerBox!.height))).toBeLessThanOrEqual(20)
  175. await captureScreenshot(page, testInfo, 'bug-853-sticky-permission-actions')
  176. })