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, passwordResets: Array> = [], requestedAuditKeywords: Array = [], ) { 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 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 = [] 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 = [] 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> = [] 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() })