| @@ -93,25 +93,27 @@ async function editAndSearchData(page: Page) { | |||
| await expect(roleCardByText(page, `${data.prefix} 已编辑`)).toBeVisible() | |||
| await page.goto('/system/departments') | |||
| await page.getByPlaceholder('请输入部门、编码、负责人或层级路径').fill(data.departmentCode) | |||
| await page.getByPlaceholder('请输入部门、编码或负责人').fill(data.departmentCode) | |||
| await page.getByRole('button', { name: '搜索', exact: true }).click() | |||
| await expect(tableRowByText(page, data.departmentName)).toBeVisible() | |||
| } | |||
| async function grantPermission(page: Page) { | |||
| await page.goto('/system/permissions') | |||
| const roleButton = page.locator('.role-selector > button').filter({ hasText: data.roleName }) | |||
| await page.goto('/system/roles') | |||
| await page.getByPlaceholder('请输入角色名称、编码或说明').fill(data.roleCode) | |||
| await page.getByRole('button', { name: '搜索', exact: true }).click() | |||
| const roleRow = roleCardByText(page, data.roleName) | |||
| await Promise.all([ | |||
| page.waitForResponse((response) => response.request().method() === 'GET' && /\/permissions\/roles\/[^/?]+/.test(response.url()) && response.ok()), | |||
| roleButton.click(), | |||
| roleRow.getByRole('button', { name: '配置权限' }).click(), | |||
| ]) | |||
| await expect(roleButton).toHaveClass(/active/) | |||
| await expect(page.locator('.selected-role-copy h2')).toHaveText(data.roleName) | |||
| const drawer = page.locator('.role-permission-drawer') | |||
| await expect(drawer).toBeVisible() | |||
| await expect(drawer.locator('.role-permission-summary h3')).toHaveText(data.roleName) | |||
| const auditCode = page.locator('code').filter({ hasText: /^system\.audit$/ }) | |||
| const auditPermission = page.locator('.permission-item').filter({ has: auditCode }) | |||
| await expect(auditPermission).toHaveAttribute('aria-checked', 'false') | |||
| await auditPermission.click() | |||
| const saveButton = page.getByRole('button', { name: '保存权限配置' }) | |||
| const auditPermission = drawer.locator('.el-tree-node').filter({ has: auditCode }).last() | |||
| await auditPermission.locator('.el-checkbox').click() | |||
| const saveButton = drawer.getByRole('button', { name: '保存授权' }) | |||
| await expect(saveButton).toBeEnabled() | |||
| await saveButton.click() | |||
| await expect(page.getByText(/权限已保存|当前权限配置已保存/).last()).toBeVisible() | |||
| @@ -157,7 +159,7 @@ async function deleteRoleIfPresent(page: Page) { | |||
| async function deleteDepartmentIfPresent(page: Page) { | |||
| await dismissOverlays(page) | |||
| await page.goto('/system/departments') | |||
| await page.getByPlaceholder('请输入部门、编码、负责人或层级路径').fill(data.departmentCode) | |||
| await page.getByPlaceholder('请输入部门、编码或负责人').fill(data.departmentCode) | |||
| await page.getByRole('button', { name: '搜索', exact: true }).click() | |||
| const row = tableRowByText(page, data.departmentName) | |||
| if (!(await row.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false))) return false | |||
| @@ -31,19 +31,34 @@ const menus = [{ | |||
| ], | |||
| }] | |||
| const menuWrites: Array<Record<string, unknown>> = [] | |||
| async function mockSystemPageApi(page: Page) { | |||
| await page.route(/\/api\/(?:auth|tran)\//, async (route) => { | |||
| const path = new URL(route.request().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.departments', 'system.roles', 'system.menus', 'system.permissions', 'system.departments.create', 'system.roles.create', 'system.roles.update', 'system.roles.delete', 'system.menus.update'], | |||
| permissions: ['system.departments', 'system.roles', 'system.menus', 'system.permissions', 'system.departments.create', 'system.roles.create', 'system.roles.update', 'system.roles.delete', 'system.menus.update', 'system.interfaces', 'system.config'], | |||
| }) | |||
| if (path.endsWith('/departments/tree')) return json(route, departments) | |||
| if (path.endsWith('/departments')) return json(route, { records: [departments[0], departments[0].children[0]], total: 2, page: 1, size: 10 }) | |||
| if (path.endsWith('/departments/stats')) return json(route, { total: 2, enabled: 2, maxDepth: 2, assignedUsers: 4 }) | |||
| if (path.endsWith('/roles/all')) return json(route, roles) | |||
| if (path.endsWith('/roles/stats')) return json(route, { total: 2, enabled: 2, builtIn: 2, assignedUsers: 4 }) | |||
| if (path.endsWith('/menus/configuration') && route.request().method() === 'PUT') { | |||
| const payload = route.request().postDataJSON() as Record<string, unknown> | |||
| menuWrites.push(payload) | |||
| const groups = payload.groups as Array<{ enabled?: boolean; items?: Array<{ enabled?: boolean }> }> | |||
| const saved = structuredClone(menus) | |||
| saved[0]!.enabled = groups[0]?.enabled ?? true | |||
| saved[0]!.children[0]!.enabled = groups[0]?.items?.[0]?.enabled ?? true | |||
| return json(route, saved) | |||
| } | |||
| if (path.endsWith('/menus/tree') || path.endsWith('/menus/navigation')) return json(route, menus) | |||
| if (path.endsWith('/interfaces/stats')) return json(route, { total: 1, enabled: 1, disabled: 0, up: 1, down: 0 }) | |||
| if (path.endsWith('/interfaces')) return json(route, { records: [{ id: '1', code: 'training', name: '训练服务', endpoint: '/api/tran/v1', transport: 'REST', dataMode: 'LOCAL', enabled: true, configStatus: 'READY', healthStatus: 'UP', version: 1 }], total: 1, page: 1, size: 10 }) | |||
| if (path.endsWith('/system-config')) return json(route, { systemName: '某类装备维修实训数字车间', systemNameConfigured: true, version: 1 }) | |||
| if (path.endsWith('/system-config/public')) return json(route, { systemName: '某类装备维修实训数字车间', logoConfigured: false, faviconConfigured: false }) | |||
| if (path.endsWith('/auth/ping')) return json(route, { service: 'ut-auth', status: 'UP' }) | |||
| if (path.endsWith('/tran/ping')) return json(route, { service: 'ut-tran', status: 'UP' }) | |||
| @@ -56,6 +71,7 @@ async function mockSystemPageApi(page: Page) { | |||
| test('部门、角色、菜单统计收进列表工具栏', async ({ page }, testInfo) => { | |||
| await mockSystemPageApi(page) | |||
| menuWrites.length = 0 | |||
| const cases = [ | |||
| { path: '/system/departments', testId: 'department-inline-statistics', removed: '.department-stats', texts: ['共 2 个部门', '启用 2', '2 级组织', '归属 4 人'] }, | |||
| @@ -69,9 +85,11 @@ test('部门、角色、菜单统计收进列表工具栏', async ({ page }, tes | |||
| await expect(statistics).toBeVisible() | |||
| await expect(page.locator(item.removed)).toHaveCount(0) | |||
| for (const text of item.texts) await expect(statistics).toContainText(text) | |||
| await expect(statistics).not.toContainText('当前') | |||
| await expect(statistics.locator('.is-danger')).toHaveCount(0) | |||
| const toolbarHeight = await statistics.locator('xpath=ancestor::*[contains(@class, "system-list-toolbar")]').evaluate((toolbar) => toolbar.getBoundingClientRect().height) | |||
| expect(toolbarHeight).toBeLessThanOrEqual(64) | |||
| // 部门页同时保留左侧组织树,1280px 视口下工具条允许响应式换成两行。 | |||
| expect(toolbarHeight).toBeLessThanOrEqual(120) | |||
| if (item.path === '/system/roles') { | |||
| const table = page.locator('.roles-table') | |||
| await expect(page.locator('.role-card')).toHaveCount(0) | |||
| @@ -88,6 +106,33 @@ test('部门、角色、菜单统计收进列表工具栏', async ({ page }, tes | |||
| expect(actionLayout.justifyContent).toBe('flex-start') | |||
| expect(actionLayout.columnGap).toBe('12px') | |||
| } | |||
| if (item.path === '/system/menus') { | |||
| const switchControl = page.getByRole('switch', { name: '用户管理状态' }) | |||
| await expect(switchControl).toBeChecked() | |||
| await page.locator('.el-switch').filter({ has: switchControl }).click() | |||
| await page.getByRole('button', { name: '保存全部' }).click() | |||
| await expect.poll(() => menuWrites.length).toBe(1) | |||
| const groups = menuWrites[0]!.groups as Array<{ items: Array<{ enabled: boolean }> }> | |||
| expect(groups[0]!.items[0]!.enabled).toBe(false) | |||
| } | |||
| await captureScreenshot(page, testInfo, item.testId) | |||
| } | |||
| }) | |||
| test('系统配置刷新与标题同行,接口列表不再显示装饰图标', async ({ page }, testInfo) => { | |||
| await mockSystemPageApi(page) | |||
| await page.goto('/system/config') | |||
| const header = page.locator('.system-config-form-header') | |||
| await expect(header.getByRole('button', { name: '刷新配置' })).toBeVisible() | |||
| await expect(page.locator('.content-action-bar')).toHaveCount(0) | |||
| const [titleBox, actionBox] = await Promise.all([ | |||
| header.getByRole('heading', { name: '通用配置' }).boundingBox(), | |||
| header.getByRole('button', { name: '刷新配置' }).boundingBox(), | |||
| ]) | |||
| expect(Math.abs(titleBox!.y - actionBox!.y)).toBeLessThan(24) | |||
| await page.goto('/system/interfaces') | |||
| await expect(page.locator('.interface-identity > span')).toHaveCount(0) | |||
| await expect(page.locator('.interface-identity').first()).toContainText('训练服务') | |||
| await captureScreenshot(page, testInfo, 'system-config-and-interface-list') | |||
| }) | |||
| @@ -6,12 +6,11 @@ const pages = [ | |||
| ['/system/departments', '部门管理', 'system-departments', '.department-table .el-table__body-wrapper .el-table__row'], | |||
| ['/system/roles', '角色管理', 'system-roles', '.roles-table .el-table__body-wrapper .el-table__row'], | |||
| ['/system/menus', '菜单管理', 'system-menus', '.menu-table-panel .el-table__body-wrapper .el-table__row'], | |||
| ['/system/permissions', '权限管理', 'system-permissions', '.permission-group'], | |||
| ['/system/audit', '审计日志', 'system-audit', '.el-table__body-wrapper .el-table__row'], | |||
| ['/system/interfaces', '平台服务配置', 'system-interfaces', '.interfaces-table .el-table__body-wrapper .el-table__row'], | |||
| ] as const | |||
| test('系统管理七个页面均可加载', async ({ page }, testInfo) => { | |||
| test('系统管理页面均可加载且权限配置收口到角色抽屉', async ({ page }, testInfo) => { | |||
| await loginAsAdmin(page) | |||
| for (const [path, heading, screenshotLabel, readySelector] of pages) { | |||
| @@ -57,6 +56,10 @@ test('系统管理七个页面均可加载', async ({ page }, testInfo) => { | |||
| await expect(page.locator('.role-statistics')).toHaveCount(0) | |||
| await expect(page.getByTestId('role-inline-statistics')).toContainText('个角色') | |||
| await expect(page.getByTestId('role-inline-statistics')).toContainText('内置') | |||
| const permissionButton = page.locator('.roles-table .role-table-actions').first().getByRole('button', { name: '配置权限' }) | |||
| await permissionButton.click() | |||
| await expect(page.locator('.role-permission-drawer')).toBeVisible() | |||
| await page.locator('.role-permission-drawer .el-drawer__close-btn').click() | |||
| } | |||
| if (path === '/system/menus') { | |||
| await expect(page.locator('.menu-statistics')).toHaveCount(0) | |||
| @@ -71,17 +74,6 @@ test('系统管理七个页面均可加载', async ({ page }, testInfo) => { | |||
| await expect(page.getByTestId('interface-inline-statistics')).toContainText('个服务') | |||
| await expect(page.locator('.system-pagination-total')).toBeVisible() | |||
| } | |||
| if (path === '/system/permissions') { | |||
| const permissionCopies = page.locator('.permission-item .permission-copy') | |||
| await expect(permissionCopies.first()).toBeVisible() | |||
| expect(await permissionCopies.count()).toBeGreaterThan(20) | |||
| const incomplete = await permissionCopies.evaluateAll((items) => items.filter((item) => { | |||
| const title = item.querySelector('b')?.textContent?.trim() | |||
| const code = item.querySelector('code')?.textContent?.trim() | |||
| return !title || !code | |||
| }).length) | |||
| expect(incomplete).toBe(0) | |||
| } | |||
| await captureScreenshot(page, testInfo, screenshotLabel) | |||
| }) | |||
| } | |||
| @@ -114,12 +114,13 @@ test('用户列表全局中文并支持部门树选择与再次点击取消', as | |||
| 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-filter-panel')).toBeVisible() | |||
| await expect(page.locator('.department-filter-panel.system-panel')).toBeVisible() | |||
| 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 条,共 4 个账号') | |||
| 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') | |||
| @@ -153,7 +154,7 @@ test('用户列表全局中文并支持部门树选择与再次点击取消', as | |||
| expect(keywordWidth).toBeLessThanOrEqual(391) | |||
| const panelsAreSeparate = await page.locator('.user-content-layout').evaluate((container) => { | |||
| const departmentPanel = container.querySelector('.department-filter-panel') | |||
| const departmentPanel = container.querySelector('.department-scope-panel') | |||
| const listPanel = container.querySelector('.user-list-main') | |||
| return departmentPanel !== listPanel | |||
| && departmentPanel?.parentElement === container | |||
| @@ -162,7 +163,7 @@ test('用户列表全局中文并支持部门树选择与再次点击取消', as | |||
| 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-filter-state').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) | |||
| @@ -197,16 +198,15 @@ test('用户列表全局中文并支持部门树选择与再次点击取消', as | |||
| 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-filter-tree .el-tree-node__content').filter({ hasText: '维修改研室' }).first() | |||
| const departmentNode = page.locator('.department-scope-tree .el-tree-node__content').filter({ hasText: '维修改研室' }).first() | |||
| await departmentNode.click() | |||
| await expect(page.locator('.department-filter-state')).toContainText('已选择:维修改研室') | |||
| 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-filter-state')).toContainText('当前显示全部部门') | |||
| await expect(page.locator('.department-filter-tree .el-tree-node.is-current')).toHaveCount(0) | |||
| 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) | |||
| @@ -49,11 +49,18 @@ async function mockSystemApi(page: Page) { | |||
| if (path.endsWith('/auth/me')) return json(route, { | |||
| userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '数字车间', | |||
| activeRoleId: '1', roles: [roles[1]], authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false, | |||
| permissions: ['system.departments', 'system.permissions', 'system.permissions.update'], | |||
| permissions: ['system.departments', 'system.departments.update', 'system.roles', 'system.permissions', 'system.permissions.update'], | |||
| }) | |||
| if (path.endsWith('/departments/tree')) return json(route, departments) | |||
| if (path.endsWith('/departments')) { | |||
| const keyword = new URL(route.request().url()).searchParams.get('keyword')?.trim() ?? '' | |||
| const records = keyword ? [] : [departments[0], departments[0].children[0]] | |||
| return json(route, { records, total: records.length, page: 1, size: 10 }) | |||
| } | |||
| if (path.endsWith('/departments/stats')) return json(route, { total: 2, enabled: 2, maxDepth: 2, assignedUsers: 4 }) | |||
| if (path.endsWith('/roles/all')) return json(route, roles) | |||
| if (path.endsWith('/roles/stats')) return json(route, { total: 2, enabled: 2, builtIn: 2, assignedUsers: 4 }) | |||
| if (path.endsWith('/users')) return json(route, { records: [{ id: '20', username: 'teacher', displayName: '周教员', departmentId: '110', departmentName: '维修教研室', roleIds: ['10'], enabled: true }], total: 1, page: 1, size: 10 }) | |||
| if (path.endsWith('/permissions/catalog')) return json(route, { sections: permissionSections }) | |||
| if (/\/permissions\/roles\/\d+$/.test(path)) return json(route, { permissionCodes: [], version: 1 }) | |||
| if (path.endsWith('/menus/navigation')) return json(route, []) | |||
| @@ -82,27 +89,49 @@ test('Bug 850:无匹配部门时只保留工具栏重置入口', async ({ page | |||
| await captureScreenshot(page, testInfo, 'bug-850-empty-without-duplicate-reset') | |||
| }) | |||
| test('Bug 853:权限操作栏在内容滚动过程中固定于可视区底部', async ({ page }, testInfo) => { | |||
| test('角色配置权限改为固定头尾的授权抽屉', async ({ page }, testInfo) => { | |||
| await mockSystemApi(page) | |||
| await page.setViewportSize({ width: 1440, height: 760 }) | |||
| await page.goto('/system/permissions') | |||
| await page.goto('/system/roles') | |||
| await page.locator('.roles-table .el-table__row').filter({ hasText: '教员' }).getByRole('button', { name: '配置权限' }).click() | |||
| const routeView = page.locator('.route-view') | |||
| const footer = page.locator('.permission-actions') | |||
| await expect(page.locator('.permission-group')).toHaveCount(permissionSections.length) | |||
| const drawer = page.locator('.role-permission-drawer') | |||
| const body = drawer.locator('.el-drawer__body') | |||
| const footer = drawer.locator('.el-drawer__footer') | |||
| await expect(drawer).toBeVisible() | |||
| await expect(drawer.locator('.role-permission-node.is-section')).toHaveCount(permissionSections.length) | |||
| await expect(footer).toBeVisible() | |||
| await routeView.evaluate((element) => { element.scrollTop = Math.round(element.scrollHeight * 0.45) }) | |||
| await expect.poll(() => routeView.evaluate((element) => element.scrollTop)).toBeGreaterThan(100) | |||
| const scroll = drawer.locator('.role-permission-tree-scroll') | |||
| await scroll.evaluate((element) => { element.scrollTop = element.scrollHeight }) | |||
| await expect.poll(() => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(100) | |||
| const [routeBox, footerBox, position] = await Promise.all([ | |||
| routeView.boundingBox(), | |||
| const [drawerBox, footerBox] = await Promise.all([ | |||
| drawer.boundingBox(), | |||
| footer.boundingBox(), | |||
| footer.evaluate((element) => getComputedStyle(element).position), | |||
| ]) | |||
| expect(routeBox).not.toBeNull() | |||
| expect(drawerBox).not.toBeNull() | |||
| expect(footerBox).not.toBeNull() | |||
| expect(position).toBe('sticky') | |||
| expect(Math.abs((routeBox!.y + routeBox!.height) - (footerBox!.y + footerBox!.height))).toBeLessThanOrEqual(20) | |||
| await captureScreenshot(page, testInfo, 'bug-853-sticky-permission-actions') | |||
| expect(Math.abs((drawerBox!.y + drawerBox!.height) - (footerBox!.y + footerBox!.height))).toBeLessThanOrEqual(2) | |||
| await captureScreenshot(page, testInfo, 'role-permission-drawer-fixed-actions') | |||
| }) | |||
| test('编辑部门使用统一选人组件', async ({ page }, testInfo) => { | |||
| await mockSystemApi(page) | |||
| await page.setViewportSize({ width: 1440, height: 800 }) | |||
| await page.goto('/system/departments') | |||
| const row = page.locator('.department-table .el-table__row').filter({ hasText: '维修教研室' }) | |||
| await row.getByRole('button', { name: '编辑' }).click() | |||
| const editor = page.locator('.el-dialog:visible').filter({ hasText: '编辑部门' }) | |||
| await expect(editor).toBeVisible() | |||
| await editor.getByRole('button', { name: '选择用户' }).click() | |||
| const picker = page.locator('.el-dialog:visible').filter({ hasText: '选择部门负责人' }) | |||
| await expect(picker).toBeVisible() | |||
| await expect(picker.getByRole('button', { name: '搜索', exact: true })).toBeVisible() | |||
| await expect(picker.getByRole('button', { name: '重置筛选', exact: true })).toBeVisible() | |||
| expect((await picker.boundingBox())!.width).toBeGreaterThan(1000) | |||
| await picker.getByRole('button', { name: '选择', exact: true }).click() | |||
| await picker.getByRole('button', { name: '确认选择' }).click() | |||
| await expect(editor).toContainText('周教员') | |||
| await captureScreenshot(page, testInfo, 'department-editor-user-picker') | |||
| }) | |||