Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

302 Zeilen
17 KiB

  1. import type { Page, Route } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. const departments = [
  4. {
  5. id: '100', code: 'ORG-100', name: '某类装备维修实训数字车间', parentId: null,
  6. enabled: true, isRoot: true, sortOrder: 10, version: 1,
  7. children: [
  8. { id: '110', code: 'ORG-110', name: '信息中心', parentId: '100', enabled: true, sortOrder: 10, version: 1, children: [] },
  9. {
  10. id: '200', code: 'ORG-200', name: '维修改研室', parentId: '100', enabled: true, sortOrder: 20, version: 1,
  11. children: [
  12. { id: '210', code: 'ORG-210', name: '维修三组', parentId: '200', enabled: true, sortOrder: 10, version: 1, children: [] },
  13. ],
  14. },
  15. ],
  16. },
  17. ]
  18. const roles = [
  19. { id: '1', code: 'ADMIN', name: '管理员', shortName: '管', enabled: true, builtIn: true, isSuperAdmin: true, dataScopeCode: 'ALL', version: 1 },
  20. { id: '2', code: 'TEACHER', name: '教员', shortName: '教', enabled: true, builtIn: true, isSuperAdmin: false, dataScopeCode: 'SYSTEM', version: 1 },
  21. { id: '3', code: 'STUDENT', name: '学员', shortName: '学', enabled: true, builtIn: true, isSuperAdmin: false, dataScopeCode: 'SELF', version: 1 },
  22. ]
  23. const users = [
  24. { id: '1', username: 'admin', displayName: '系统管理员', departmentId: '110', departmentName: '信息中心', roleIds: ['1'], defaultRoleId: '1', enabled: true, isProtected: true, credentialConfigured: true, mustChangePassword: false, version: 1 },
  25. { id: '2', username: 'teacher', displayName: '周教员', departmentId: '200', departmentName: '维修改研室', roleIds: ['2'], defaultRoleId: '2', enabled: true, isProtected: false, credentialConfigured: true, mustChangePassword: true, version: 1 },
  26. { id: '3', username: 'student', displayName: '张伟', departmentId: '210', departmentName: '维修三组', roleIds: ['3'], defaultRoleId: '3', enabled: true, isProtected: false, credentialConfigured: false, mustChangePassword: true, version: 1 },
  27. { id: '4', username: 'operator', displayName: '李教员', departmentId: '110', departmentName: '信息中心', roleIds: ['2'], defaultRoleId: '2', enabled: false, isProtected: false, credentialConfigured: false, mustChangePassword: true, version: 1 },
  28. ]
  29. const json = (route: Route, data: unknown) => route.fulfill({
  30. status: 200,
  31. contentType: 'application/json',
  32. body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'e2e-mock' }),
  33. })
  34. async function mockUserPageApi(
  35. page: Page,
  36. requestedDepartments: Array<string | null>,
  37. passwordResets: Array<Record<string, unknown>> = [],
  38. requestedAuditKeywords: Array<string | null> = [],
  39. ) {
  40. await page.route(/\/api\/(?:auth|tran)\//, async (route) => {
  41. const url = new URL(route.request().url())
  42. const path = url.pathname
  43. if (path.endsWith('/auth/me')) {
  44. return json(route, {
  45. userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '110', departmentName: '信息中心',
  46. activeRoleId: '1', roles, authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false,
  47. permissions: [
  48. 'system.users', 'system.users.create', 'system.users.update', 'system.users.delete', 'system.users.reset-password',
  49. 'system.audit', 'system.audit.export',
  50. ],
  51. })
  52. }
  53. if (path.endsWith('/departments/tree')) return json(route, departments)
  54. if (path.endsWith('/roles/all')) return json(route, roles)
  55. if (path.endsWith('/users/stats')) return json(route, { total: 4, enabled: 3, disabled: 1, passwordPending: 1 })
  56. if (/\/users\/\d+\/password$/.test(path) && route.request().method() === 'PUT') {
  57. const request = route.request().postDataJSON() as Record<string, unknown>
  58. passwordResets.push(request)
  59. return json(route, {
  60. temporaryPassword: request.password ? null : 'System#Reset8A',
  61. mustChangePassword: true,
  62. })
  63. }
  64. if (path.endsWith('/users')) {
  65. const departmentId = url.searchParams.get('departmentId')
  66. requestedDepartments.push(departmentId)
  67. const records = departmentId === '200'
  68. ? users.filter((user) => ['200', '210'].includes(user.departmentId))
  69. : users
  70. return json(route, { records, total: records.length, page: 1, size: 10 })
  71. }
  72. if (path.endsWith('/audit-logs')) {
  73. requestedAuditKeywords.push(url.searchParams.get('keyword'))
  74. const size = Number(url.searchParams.get('size') ?? 20)
  75. const records = Array.from({ length: size }, (_, index) => ({
  76. id: String(index + 1),
  77. time: `2026-08-08T18:${String(index).padStart(2, '0')}:00`,
  78. user: index % 2 ? '周教员' : '系统管理员',
  79. moduleCode: index === 0 ? 'SYSTEM' : index === 1 ? 'AUTH' : index === 2 ? 'CONTENT' : index === 3 ? 'CUSTOM_AUDIT' : 'SYSTEM',
  80. action: index % 2 ? '编辑用户' : '登录系统',
  81. ip: '127.0.0.1',
  82. success: true,
  83. requestId: `audit-${index + 1}`,
  84. }))
  85. return json(route, { records, total: 42, page: 1, size })
  86. }
  87. if (path.endsWith('/auth/ping')) return json(route, { service: 'ut-auth', port: 6101, status: 'UP' })
  88. if (path.endsWith('/tran/ping')) return json(route, { service: 'ut-tran', port: 6102, status: 'UP' })
  89. return json(route, {})
  90. })
  91. }
  92. test('用户列表全局中文并支持部门树选择与再次点击取消', async ({ page }, testInfo) => {
  93. const requestedDepartments: Array<string | null> = []
  94. page.on('pageerror', (error) => console.error(`PAGE_ERROR: ${error.message}`))
  95. page.on('console', (message) => {
  96. if (message.type() === 'error') console.error(`CONSOLE_ERROR: ${message.text()}`)
  97. })
  98. await mockUserPageApi(page, requestedDepartments)
  99. await page.addInitScript(() => {
  100. window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
  101. })
  102. await page.goto('/system/users')
  103. // 页头卡已去掉,页面身份改由页面标签栏承担
  104. await expect(page.getByRole('tab', { name: '用户管理', exact: true })).toBeVisible()
  105. await expect(page.locator('.el-pagination__sizes')).toContainText('条/页')
  106. await expect(page.locator('.el-pagination__jump')).toContainText('前往')
  107. await expect(page.locator('.system-pagination-total')).toHaveText('共 4 条记录')
  108. await expect(page.locator('.department-scope-panel')).toBeVisible()
  109. await expect(page.locator('.department-scope-panel.system-panel')).toBeVisible()
  110. await expect(page.locator('.user-list-main.system-panel.system-list-card')).toBeVisible()
  111. await expect(page.locator('.user-stats')).toHaveCount(0)
  112. const inlineStatistics = page.getByTestId('user-inline-statistics')
  113. await expect(inlineStatistics).toContainText('共 4 个账号')
  114. await expect(inlineStatistics).not.toContainText('当前')
  115. await expect(inlineStatistics).toContainText('正常 3')
  116. await expect(inlineStatistics).toContainText('停用 1')
  117. await expect(inlineStatistics).toContainText('待改密 1')
  118. await captureScreenshot(page, testInfo, 'users-inline-statistics')
  119. await expect(page.locator('.users-table')).toHaveClass(/el-table--border/)
  120. await expect(page.locator('.users-table')).toHaveClass(/el-table--striped/)
  121. const pendingRow = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
  122. const unconfiguredRow = page.locator('.users-table .el-table__row').filter({ hasText: '张伟' })
  123. const disabledRow = page.locator('.users-table .el-table__row').filter({ hasText: '李教员' })
  124. await expect(pendingRow.getByText('待修改密码')).toBeVisible()
  125. await expect(unconfiguredRow.getByText('未设置登录密码')).toBeVisible()
  126. await expect(disabledRow.getByText('待修改密码')).toHaveCount(0)
  127. await expect(disabledRow.getByText('未设置登录密码')).toHaveCount(0)
  128. const actionLayout = await page.locator('.table-actions').first().evaluate((element) => {
  129. const style = getComputedStyle(element)
  130. const childMargins = Array.from(element.children).map((child) => getComputedStyle(child).marginLeft)
  131. return { justifyContent: style.justifyContent, columnGap: style.columnGap, childMargins }
  132. })
  133. expect(actionLayout.justifyContent).toBe('flex-start')
  134. expect(actionLayout.columnGap).toBe('12px')
  135. expect(actionLayout.childMargins.every((margin) => margin === '0px')).toBe(true)
  136. const listCardIsUnified = await page.locator('.user-list-main').evaluate((card) => {
  137. const toolbar = card.querySelector('.system-list-toolbar')
  138. const body = card.querySelector('.system-list-body')
  139. return toolbar?.parentElement === card && body?.parentElement === card
  140. })
  141. expect(listCardIsUnified).toBe(true)
  142. const keywordWidth = await page.locator('.user-list-main .system-list-toolbar .el-input').first()
  143. .evaluate((element) => element.getBoundingClientRect().width)
  144. expect(keywordWidth).toBeLessThanOrEqual(391)
  145. const panelsAreSeparate = await page.locator('.user-content-layout').evaluate((container) => {
  146. const departmentPanel = container.querySelector('.department-scope-panel')
  147. const listPanel = container.querySelector('.user-list-main')
  148. return departmentPanel !== listPanel
  149. && departmentPanel?.parentElement === container
  150. && listPanel?.parentElement === container
  151. })
  152. expect(panelsAreSeparate).toBe(true)
  153. const normalFontSize = await page.locator('.users-table .el-table__body .cell').first().evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))
  154. const hintFontSize = await page.locator('.department-scope-node__name').first().evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))
  155. expect(normalFontSize).toBeGreaterThanOrEqual(14)
  156. expect(hintFontSize).toBeGreaterThanOrEqual(13)
  157. await page.getByRole('button', { name: '新增用户' }).click()
  158. const userDialog = page.locator('.el-dialog:visible')
  159. const dialogOverlay = page.locator('.el-overlay:visible')
  160. await expect(userDialog).toBeVisible()
  161. await expect(userDialog).toHaveClass(/is-draggable/)
  162. const dialogBox = await userDialog.boundingBox()
  163. const viewport = page.viewportSize()
  164. expect(dialogBox).not.toBeNull()
  165. expect(viewport).not.toBeNull()
  166. // Element Plus keeps a small safe-area offset while using align-center.
  167. expect(Math.abs((dialogBox!.y + dialogBox!.height / 2) - viewport!.height / 2)).toBeLessThanOrEqual(24)
  168. await dialogOverlay.click({ position: { x: 8, y: 8 } })
  169. await expect(userDialog).toBeVisible()
  170. await captureScreenshot(page, testInfo, 'users-dialog-global-behavior')
  171. await userDialog.getByRole('button', { name: '取消' }).click()
  172. await expect(userDialog).toBeHidden()
  173. const pageSizeRequest = page.waitForRequest((request) => {
  174. const url = new URL(request.url())
  175. return url.pathname.endsWith('/users') && url.searchParams.get('size') === '20'
  176. })
  177. await page.locator('.el-pagination__sizes .el-select').click()
  178. await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: '20' }).click()
  179. await pageSizeRequest
  180. await expect(page.locator('.el-pagination__sizes')).toContainText('20')
  181. const enabledUserRow = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
  182. await expect(enabledUserRow.getByRole('button', { name: '停用' })).toHaveClass(/el-button--warning/)
  183. const disabledUserRow = page.locator('.users-table .el-table__row').filter({ hasText: '李教员' })
  184. await expect(disabledUserRow.getByRole('button', { name: '启用' })).toHaveClass(/el-button--success/)
  185. const departmentNode = page.locator('.department-scope-tree .el-tree-node__content').filter({ hasText: '维修改研室' }).first()
  186. await departmentNode.click()
  187. await expect(departmentNode.locator('xpath=..')).toHaveClass(/is-current/)
  188. await expect.poll(() => requestedDepartments.at(-1)).toBe('200')
  189. await expect(page.locator('.users-table .el-table__body-wrapper .el-table__row')).toHaveCount(2)
  190. await captureScreenshot(page, testInfo, 'users-department-selected')
  191. await departmentNode.click()
  192. await expect(page.locator('.department-scope-tree .el-tree-node.is-current')).toHaveCount(0)
  193. await expect.poll(() => requestedDepartments.at(-1)).toBeNull()
  194. await expect(page.locator('.users-table .el-table__body-wrapper .el-table__row')).toHaveCount(4)
  195. await captureScreenshot(page, testInfo, 'users-department-cleared')
  196. })
  197. test('审计表格与分页遵循系统统一规范', async ({ page }, testInfo) => {
  198. const requestedAuditKeywords: Array<string | null> = []
  199. await mockUserPageApi(page, [], [], requestedAuditKeywords)
  200. await page.addInitScript(() => {
  201. window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
  202. })
  203. await page.goto('/system/audit')
  204. await expect(page.getByRole('tab', { name: '审计日志', exact: true })).toBeVisible()
  205. const table = page.locator('.el-table')
  206. await expect(table).toHaveClass(/el-table--border/)
  207. await expect(table).toHaveClass(/el-table--striped/)
  208. await expect(page.locator('.system-pagination-total')).toHaveText('共 42 条记录')
  209. await expect(page.locator('.el-pagination__sizes')).toContainText('条/页')
  210. await expect(page.locator('.el-pagination__jump')).toContainText('前往')
  211. await expect(page.locator('.el-pagination .number')).toHaveCount(3)
  212. const rows = page.locator('.el-table__body-wrapper .el-table__row')
  213. await expect(rows.nth(0).locator('td').nth(2)).toContainText('系统管理')
  214. await expect(rows.nth(1).locator('td').nth(2)).toContainText('身份认证')
  215. await expect(rows.nth(2).locator('td').nth(2)).toContainText('内容制作')
  216. await expect(rows.nth(3).locator('td').nth(2)).toContainText('CUSTOM_AUDIT')
  217. const keyword = page.locator('.system-list-toolbar .el-input input').first()
  218. await keyword.fill('/system/config')
  219. await page.getByRole('button', { name: '搜索', exact: true }).click()
  220. await expect.poll(() => requestedAuditKeywords.at(-1)).toBe('/system/config')
  221. await expect(rows).toHaveCount(20)
  222. await captureScreenshot(page, testInfo, 'audit-module-labels-and-uri-search')
  223. const auditCardIsUnified = await page.locator('.system-list-card').evaluate((card) => {
  224. const toolbar = card.querySelector('.system-list-toolbar')
  225. const body = card.querySelector('.system-list-body')
  226. return toolbar?.parentElement === card && body?.parentElement === card
  227. })
  228. expect(auditCardIsUnified).toBe(true)
  229. const auditKeywordWidth = await page.locator('.system-list-toolbar .el-input').first()
  230. .evaluate((element) => element.getBoundingClientRect().width)
  231. expect(auditKeywordWidth).toBeLessThanOrEqual(391)
  232. await page.locator('.system-pagination').scrollIntoViewIfNeeded()
  233. await captureScreenshot(page, testInfo, 'audit-table-pagination')
  234. })
  235. test('密码重置支持系统生成和手动输入并实时展示统一规则', async ({ page }, testInfo) => {
  236. const passwordResets: Array<Record<string, unknown>> = []
  237. await mockUserPageApi(page, [], passwordResets)
  238. await page.addInitScript(() => {
  239. window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
  240. })
  241. await page.goto('/system/users')
  242. const row = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
  243. await row.getByRole('button', { name: '重置密码' }).click()
  244. const resetDialog = page.locator('.password-reset-dialog:visible')
  245. await expect(resetDialog).toBeVisible()
  246. await expect(resetDialog.getByRole('radio', { name: '系统重置' })).toBeChecked()
  247. await resetDialog.getByTestId('confirm-password-reset').click()
  248. const resultDialog = page.locator('.password-result-dialog:visible')
  249. await expect(resultDialog).toBeVisible()
  250. await expect(resultDialog.locator('input')).toHaveCount(0)
  251. await expect(resultDialog.getByTestId('password-result-mode')).toHaveText('系统生成')
  252. await expect(resultDialog.getByTestId('generated-password')).toHaveText('System#Reset8A')
  253. expect(passwordResets[0]).toEqual({ version: 1 })
  254. await resultDialog.getByRole('button', { name: '我已妥善保存' }).click()
  255. await row.getByRole('button', { name: '重置密码' }).click()
  256. await resetDialog.locator('.el-radio-button').filter({ hasText: '手动重置' }).click()
  257. const passwordInput = resetDialog.getByTestId('manual-reset-password-input')
  258. const confirm = resetDialog.getByTestId('confirm-password-reset')
  259. await expect(resetDialog.locator('.password-requirements .is-unmet')).toHaveCount(5)
  260. await passwordInput.fill('weak')
  261. await expect(confirm).toBeDisabled()
  262. await passwordInput.fill('Manual#Reset8')
  263. await expect(resetDialog.locator('.password-requirements .is-met')).toHaveCount(5)
  264. await expect(confirm).toBeEnabled()
  265. await captureScreenshot(page, testInfo, 'password-reset-manual-requirements')
  266. await confirm.click()
  267. await expect(resetDialog).toBeHidden()
  268. expect(passwordResets[1]).toEqual({ password: 'Manual#Reset8', version: 1 })
  269. await expect(resultDialog).toBeVisible()
  270. await expect(resultDialog.getByTestId('password-result-mode')).toHaveText('手动设置')
  271. await expect(resultDialog.getByTestId('generated-password')).toHaveText('Manual#Reset8')
  272. await captureScreenshot(page, testInfo, 'password-reset-manual-result')
  273. await resultDialog.getByRole('button', { name: '我已妥善保存' }).click()
  274. })