|
- import type { Page, Route } from '@playwright/test'
-
- import { captureScreenshot, expect, test } from './fixtures'
-
- const departments = [
- {
- id: '100', code: 'ORG-100', name: '某类装备维修实训数字车间', parentId: null,
- enabled: true, isRoot: true, sortOrder: 10, version: 1,
- children: [
- { id: '110', code: 'ORG-110', name: '信息中心', parentId: '100', enabled: true, sortOrder: 10, version: 1, children: [] },
- {
- id: '200', code: 'ORG-200', name: '维修改研室', parentId: '100', enabled: true, sortOrder: 20, version: 1,
- children: [
- { id: '210', code: 'ORG-210', name: '维修三组', parentId: '200', enabled: true, sortOrder: 10, version: 1, children: [] },
- ],
- },
- ],
- },
- ]
-
- const roles = [
- { id: '1', code: 'ADMIN', name: '管理员', shortName: '管', enabled: true, builtIn: true, isSuperAdmin: true, dataScopeCode: 'ALL', version: 1 },
- { id: '2', code: 'TEACHER', name: '教员', shortName: '教', enabled: true, builtIn: true, isSuperAdmin: false, dataScopeCode: 'SYSTEM', version: 1 },
- { id: '3', code: 'STUDENT', name: '学员', shortName: '学', enabled: true, builtIn: true, isSuperAdmin: false, dataScopeCode: 'SELF', version: 1 },
- ]
-
- const users = [
- { id: '1', username: 'admin', displayName: '系统管理员', departmentId: '110', departmentName: '信息中心', roleIds: ['1'], defaultRoleId: '1', enabled: true, isProtected: true, credentialConfigured: true, mustChangePassword: false, version: 1 },
- { id: '2', username: 'teacher', displayName: '周教员', departmentId: '200', departmentName: '维修改研室', roleIds: ['2'], defaultRoleId: '2', enabled: true, isProtected: false, credentialConfigured: true, mustChangePassword: true, version: 1 },
- { id: '3', username: 'student', displayName: '张伟', departmentId: '210', departmentName: '维修三组', roleIds: ['3'], defaultRoleId: '3', enabled: true, isProtected: false, credentialConfigured: false, mustChangePassword: true, version: 1 },
- { id: '4', username: 'operator', displayName: '李教员', departmentId: '110', departmentName: '信息中心', roleIds: ['2'], defaultRoleId: '2', enabled: false, isProtected: false, credentialConfigured: false, mustChangePassword: true, version: 1 },
- ]
-
- const json = (route: Route, data: unknown) => route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'e2e-mock' }),
- })
-
- async function mockUserPageApi(
- page: Page,
- requestedDepartments: Array<string | null>,
- passwordResets: Array<Record<string, unknown>> = [],
- requestedAuditKeywords: Array<string | null> = [],
- ) {
- await page.route(/\/api\/(?:auth|tran)\//, async (route) => {
- const url = new URL(route.request().url())
- const path = url.pathname
-
- if (path.endsWith('/auth/me')) {
- return json(route, {
- userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '110', departmentName: '信息中心',
- activeRoleId: '1', roles, authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false,
- permissions: [
- 'system.users', 'system.users.create', 'system.users.update', 'system.users.delete', 'system.users.reset-password',
- 'system.audit', 'system.audit.export',
- ],
- })
- }
- if (path.endsWith('/departments/tree')) return json(route, departments)
- if (path.endsWith('/roles/all')) return json(route, roles)
- if (path.endsWith('/users/stats')) return json(route, { total: 4, enabled: 3, disabled: 1, passwordPending: 1 })
- if (/\/users\/\d+\/password$/.test(path) && route.request().method() === 'PUT') {
- const request = route.request().postDataJSON() as Record<string, unknown>
- passwordResets.push(request)
- return json(route, {
- temporaryPassword: request.password ? null : 'System#Reset8A',
- mustChangePassword: true,
- })
- }
- if (path.endsWith('/users')) {
- const departmentId = url.searchParams.get('departmentId')
- requestedDepartments.push(departmentId)
- const records = departmentId === '200'
- ? users.filter((user) => ['200', '210'].includes(user.departmentId))
- : users
- return json(route, { records, total: records.length, page: 1, size: 10 })
- }
- if (path.endsWith('/audit-logs')) {
- requestedAuditKeywords.push(url.searchParams.get('keyword'))
- const size = Number(url.searchParams.get('size') ?? 20)
- const records = Array.from({ length: size }, (_, index) => ({
- id: String(index + 1),
- time: `2026-08-08T18:${String(index).padStart(2, '0')}:00`,
- user: index % 2 ? '周教员' : '系统管理员',
- moduleCode: index === 0 ? 'SYSTEM' : index === 1 ? 'AUTH' : index === 2 ? 'CONTENT' : index === 3 ? 'CUSTOM_AUDIT' : 'SYSTEM',
- action: index % 2 ? '编辑用户' : '登录系统',
- ip: '127.0.0.1',
- success: true,
- requestId: `audit-${index + 1}`,
- }))
- return json(route, { records, total: 42, page: 1, size })
- }
- if (path.endsWith('/auth/ping')) return json(route, { service: 'ut-auth', port: 6101, status: 'UP' })
- if (path.endsWith('/tran/ping')) return json(route, { service: 'ut-tran', port: 6102, status: 'UP' })
- return json(route, {})
- })
- }
-
- test('用户列表全局中文并支持部门树选择与再次点击取消', async ({ page }, testInfo) => {
- const requestedDepartments: Array<string | null> = []
- page.on('pageerror', (error) => console.error(`PAGE_ERROR: ${error.message}`))
- page.on('console', (message) => {
- if (message.type() === 'error') console.error(`CONSOLE_ERROR: ${message.text()}`)
- })
- await mockUserPageApi(page, requestedDepartments)
- await page.addInitScript(() => {
- window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
- })
-
- await page.goto('/system/users')
- // 页头卡已去掉,页面身份改由页面标签栏承担
- await expect(page.getByRole('tab', { name: '用户管理', exact: true })).toBeVisible()
- await expect(page.locator('.el-pagination__sizes')).toContainText('条/页')
- await expect(page.locator('.el-pagination__jump')).toContainText('前往')
- await expect(page.locator('.system-pagination-total')).toHaveText('共 4 条记录')
- await expect(page.locator('.department-scope-panel')).toBeVisible()
- await expect(page.locator('.department-scope-panel.system-panel')).toBeVisible()
- await expect(page.locator('.user-list-main.system-panel.system-list-card')).toBeVisible()
- await expect(page.locator('.user-stats')).toHaveCount(0)
- const inlineStatistics = page.getByTestId('user-inline-statistics')
- await expect(inlineStatistics).toContainText('共 4 个账号')
- await expect(inlineStatistics).not.toContainText('当前')
- await expect(inlineStatistics).toContainText('正常 3')
- await expect(inlineStatistics).toContainText('停用 1')
- await expect(inlineStatistics).toContainText('待改密 1')
- await captureScreenshot(page, testInfo, 'users-inline-statistics')
- await expect(page.locator('.users-table')).toHaveClass(/el-table--border/)
- await expect(page.locator('.users-table')).toHaveClass(/el-table--striped/)
- const pendingRow = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
- const unconfiguredRow = page.locator('.users-table .el-table__row').filter({ hasText: '张伟' })
- const disabledRow = page.locator('.users-table .el-table__row').filter({ hasText: '李教员' })
- await expect(pendingRow.getByText('待修改密码')).toBeVisible()
- await expect(unconfiguredRow.getByText('未设置登录密码')).toBeVisible()
- await expect(disabledRow.getByText('待修改密码')).toHaveCount(0)
- await expect(disabledRow.getByText('未设置登录密码')).toHaveCount(0)
- const actionLayout = await page.locator('.table-actions').first().evaluate((element) => {
- const style = getComputedStyle(element)
- const childMargins = Array.from(element.children).map((child) => getComputedStyle(child).marginLeft)
- return { justifyContent: style.justifyContent, columnGap: style.columnGap, childMargins }
- })
- expect(actionLayout.justifyContent).toBe('flex-start')
- expect(actionLayout.columnGap).toBe('12px')
- expect(actionLayout.childMargins.every((margin) => margin === '0px')).toBe(true)
-
- const listCardIsUnified = await page.locator('.user-list-main').evaluate((card) => {
- const toolbar = card.querySelector('.system-list-toolbar')
- const body = card.querySelector('.system-list-body')
- return toolbar?.parentElement === card && body?.parentElement === card
- })
- expect(listCardIsUnified).toBe(true)
- const keywordWidth = await page.locator('.user-list-main .system-list-toolbar .el-input').first()
- .evaluate((element) => element.getBoundingClientRect().width)
- expect(keywordWidth).toBeLessThanOrEqual(391)
-
- const panelsAreSeparate = await page.locator('.user-content-layout').evaluate((container) => {
- const departmentPanel = container.querySelector('.department-scope-panel')
- const listPanel = container.querySelector('.user-list-main')
- return departmentPanel !== listPanel
- && departmentPanel?.parentElement === container
- && listPanel?.parentElement === container
- })
- expect(panelsAreSeparate).toBe(true)
-
- const normalFontSize = await page.locator('.users-table .el-table__body .cell').first().evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))
- const hintFontSize = await page.locator('.department-scope-node__name').first().evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))
- expect(normalFontSize).toBeGreaterThanOrEqual(14)
- expect(hintFontSize).toBeGreaterThanOrEqual(13)
-
- await page.getByRole('button', { name: '新增用户' }).click()
- const userDialog = page.locator('.el-dialog:visible')
- const dialogOverlay = page.locator('.el-overlay:visible')
- await expect(userDialog).toBeVisible()
- await expect(userDialog).toHaveClass(/is-draggable/)
- const dialogBox = await userDialog.boundingBox()
- const viewport = page.viewportSize()
- expect(dialogBox).not.toBeNull()
- expect(viewport).not.toBeNull()
- // Element Plus keeps a small safe-area offset while using align-center.
- expect(Math.abs((dialogBox!.y + dialogBox!.height / 2) - viewport!.height / 2)).toBeLessThanOrEqual(24)
- await dialogOverlay.click({ position: { x: 8, y: 8 } })
- await expect(userDialog).toBeVisible()
- await captureScreenshot(page, testInfo, 'users-dialog-global-behavior')
- await userDialog.getByRole('button', { name: '取消' }).click()
- await expect(userDialog).toBeHidden()
-
- const pageSizeRequest = page.waitForRequest((request) => {
- const url = new URL(request.url())
- return url.pathname.endsWith('/users') && url.searchParams.get('size') === '20'
- })
- await page.locator('.el-pagination__sizes .el-select').click()
- await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: '20' }).click()
- await pageSizeRequest
- await expect(page.locator('.el-pagination__sizes')).toContainText('20')
-
- const enabledUserRow = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
- await expect(enabledUserRow.getByRole('button', { name: '停用' })).toHaveClass(/el-button--warning/)
- const disabledUserRow = page.locator('.users-table .el-table__row').filter({ hasText: '李教员' })
- await expect(disabledUserRow.getByRole('button', { name: '启用' })).toHaveClass(/el-button--success/)
-
- const departmentNode = page.locator('.department-scope-tree .el-tree-node__content').filter({ hasText: '维修改研室' }).first()
- await departmentNode.click()
- await expect(departmentNode.locator('xpath=..')).toHaveClass(/is-current/)
- await expect.poll(() => requestedDepartments.at(-1)).toBe('200')
- await expect(page.locator('.users-table .el-table__body-wrapper .el-table__row')).toHaveCount(2)
- await captureScreenshot(page, testInfo, 'users-department-selected')
-
- await departmentNode.click()
- await expect(page.locator('.department-scope-tree .el-tree-node.is-current')).toHaveCount(0)
- await expect.poll(() => requestedDepartments.at(-1)).toBeNull()
- await expect(page.locator('.users-table .el-table__body-wrapper .el-table__row')).toHaveCount(4)
-
- await captureScreenshot(page, testInfo, 'users-department-cleared')
- })
-
- test('审计表格与分页遵循系统统一规范', async ({ page }, testInfo) => {
- const requestedAuditKeywords: Array<string | null> = []
- await mockUserPageApi(page, [], [], requestedAuditKeywords)
- await page.addInitScript(() => {
- window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
- })
-
- await page.goto('/system/audit')
- await expect(page.getByRole('tab', { name: '审计日志', exact: true })).toBeVisible()
- const table = page.locator('.el-table')
- await expect(table).toHaveClass(/el-table--border/)
- await expect(table).toHaveClass(/el-table--striped/)
- await expect(page.locator('.system-pagination-total')).toHaveText('共 42 条记录')
- await expect(page.locator('.el-pagination__sizes')).toContainText('条/页')
- await expect(page.locator('.el-pagination__jump')).toContainText('前往')
- await expect(page.locator('.el-pagination .number')).toHaveCount(3)
- const rows = page.locator('.el-table__body-wrapper .el-table__row')
- await expect(rows.nth(0).locator('td').nth(2)).toContainText('系统管理')
- await expect(rows.nth(1).locator('td').nth(2)).toContainText('身份认证')
- await expect(rows.nth(2).locator('td').nth(2)).toContainText('内容制作')
- await expect(rows.nth(3).locator('td').nth(2)).toContainText('CUSTOM_AUDIT')
-
- const keyword = page.locator('.system-list-toolbar .el-input input').first()
- await keyword.fill('/system/config')
- await page.getByRole('button', { name: '搜索', exact: true }).click()
- await expect.poll(() => requestedAuditKeywords.at(-1)).toBe('/system/config')
- await expect(rows).toHaveCount(20)
- await captureScreenshot(page, testInfo, 'audit-module-labels-and-uri-search')
-
- const auditCardIsUnified = await page.locator('.system-list-card').evaluate((card) => {
- const toolbar = card.querySelector('.system-list-toolbar')
- const body = card.querySelector('.system-list-body')
- return toolbar?.parentElement === card && body?.parentElement === card
- })
- expect(auditCardIsUnified).toBe(true)
- const auditKeywordWidth = await page.locator('.system-list-toolbar .el-input').first()
- .evaluate((element) => element.getBoundingClientRect().width)
- expect(auditKeywordWidth).toBeLessThanOrEqual(391)
-
- await page.locator('.system-pagination').scrollIntoViewIfNeeded()
- await captureScreenshot(page, testInfo, 'audit-table-pagination')
- })
-
- test('密码重置支持系统生成和手动输入并实时展示统一规则', async ({ page }, testInfo) => {
- const passwordResets: Array<Record<string, unknown>> = []
- await mockUserPageApi(page, [], passwordResets)
- await page.addInitScript(() => {
- window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
- })
-
- await page.goto('/system/users')
- const row = page.locator('.users-table .el-table__row').filter({ hasText: '周教员' })
- await row.getByRole('button', { name: '重置密码' }).click()
- const resetDialog = page.locator('.password-reset-dialog:visible')
- await expect(resetDialog).toBeVisible()
- await expect(resetDialog.getByRole('radio', { name: '系统重置' })).toBeChecked()
- await resetDialog.getByTestId('confirm-password-reset').click()
-
- const resultDialog = page.locator('.password-result-dialog:visible')
- await expect(resultDialog).toBeVisible()
- await expect(resultDialog.locator('input')).toHaveCount(0)
- await expect(resultDialog.getByTestId('password-result-mode')).toHaveText('系统生成')
- await expect(resultDialog.getByTestId('generated-password')).toHaveText('System#Reset8A')
- expect(passwordResets[0]).toEqual({ version: 1 })
- await resultDialog.getByRole('button', { name: '我已妥善保存' }).click()
-
- await row.getByRole('button', { name: '重置密码' }).click()
- await resetDialog.locator('.el-radio-button').filter({ hasText: '手动重置' }).click()
- const passwordInput = resetDialog.getByTestId('manual-reset-password-input')
- const confirm = resetDialog.getByTestId('confirm-password-reset')
- await expect(resetDialog.locator('.password-requirements .is-unmet')).toHaveCount(5)
- await passwordInput.fill('weak')
- await expect(confirm).toBeDisabled()
- await passwordInput.fill('Manual#Reset8')
- await expect(resetDialog.locator('.password-requirements .is-met')).toHaveCount(5)
- await expect(confirm).toBeEnabled()
- await captureScreenshot(page, testInfo, 'password-reset-manual-requirements')
- await confirm.click()
- await expect(resetDialog).toBeHidden()
- expect(passwordResets[1]).toEqual({ password: 'Manual#Reset8', version: 1 })
- await expect(resultDialog).toBeVisible()
- await expect(resultDialog.getByTestId('password-result-mode')).toHaveText('手动设置')
- await expect(resultDialog.getByTestId('generated-password')).toHaveText('Manual#Reset8')
- await captureScreenshot(page, testInfo, 'password-reset-manual-result')
- await resultDialog.getByRole('button', { name: '我已妥善保存' }).click()
- })
|