From fc7563cc35bd87f0473e341e9c90a199db0599c4 Mon Sep 17 00:00:00 2001 From: leiyun Date: Sun, 16 Aug 2026 16:01:51 +0800 Subject: [PATCH] =?UTF-8?q?test(system):=20=E8=A1=A5=E5=85=85=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=8A=9F=E8=83=BD=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/auth-navigation.spec.ts | 3 + tests/settings-menu.spec.ts | 110 +++++++++++++- tests/system-basic-regression.spec.ts | 197 ++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 tests/system-basic-regression.spec.ts diff --git a/tests/auth-navigation.spec.ts b/tests/auth-navigation.spec.ts index d88e31f..aca13a5 100644 --- a/tests/auth-navigation.spec.ts +++ b/tests/auth-navigation.spec.ts @@ -43,6 +43,9 @@ test('登录页按行政、教员、学员、管理员展示真实身份控件', await page.locator('[data-role-code="admin"]').click() const password = page.locator('input[name="password"]') + const passwordToggle = page.locator('.login-password-toggle') + await expect(passwordToggle).toHaveCount(1) + await expect(passwordToggle.locator('svg')).toHaveCount(1) await expect(password).toHaveAttribute('type', 'password') await page.getByRole('button', { name: '显示密码' }).click() await expect(password).toHaveAttribute('type', 'text') diff --git a/tests/settings-menu.spec.ts b/tests/settings-menu.spec.ts index 1c81c0d..ebd7a87 100644 --- a/tests/settings-menu.spec.ts +++ b/tests/settings-menu.spec.ts @@ -1,11 +1,12 @@ import type { APIRequestContext, APIResponse } from '@playwright/test' import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures' -import { adminAccessToken, envelopeData, fillFormItem, loginAsAdmin, tableRowByText } from './helpers' +import { adminAccessToken, confirmMessageBox, envelopeData, fillFormItem, loginAsAdmin, tableRowByText } from './helpers' test.describe.configure({ mode: 'serial' }) type AnyRecord = Record +const DEFAULT_SYSTEM_NAME = '虚拟教员系统(试用)' const asRecord = (value: unknown): AnyRecord => value && typeof value === 'object' && !Array.isArray(value) ? value as AnyRecord : {} const headers = (token: string) => ({ Authorization: `Bearer ${token}` }) const apiJson = async (response: APIResponse) => response.json().catch(() => ({})) @@ -41,6 +42,31 @@ function versionMap(value: unknown) { } } +function configuredFields(value: unknown) { + const configured = asRecord(configRoot(value).configured) + return { + systemName: Boolean(configured.systemName), + shortName: Boolean(configured.shortName), + subtitle: Boolean(configured.subtitle), + defaultTheme: Boolean(configured.defaultTheme), + allowThemeSwitch: Boolean(configured.allowThemeSwitch), + knowledgeMenuVisible: Boolean(configured.knowledgeMenuVisible), + } +} + +function restorableGeneralFields(value: unknown) { + const fields = generalFields(value) + const configured = configuredFields(value) + return { + systemName: configured.systemName ? fields.systemName : null, + shortName: configured.shortName ? fields.shortName : null, + subtitle: configured.subtitle ? fields.subtitle : null, + defaultTheme: configured.defaultTheme ? fields.defaultTheme : null, + allowThemeSwitch: configured.allowThemeSwitch ? fields.allowThemeSwitch : null, + knowledgeMenuVisible: configured.knowledgeMenuVisible ? fields.knowledgeMenuVisible : null, + } +} + interface MenuNode { id: string name: string @@ -107,7 +133,7 @@ async function restoreSystemConfig(request: APIRequestContext, token: string, or const current = await getSystemConfig(request, token) return request.put('/api/auth/v1/system-config/general', { headers: headers(token), - data: { ...generalFields(original), versions: versionMap(current) }, + data: { ...restorableGeneralFields(original), versions: versionMap(current) }, }) } @@ -154,6 +180,86 @@ test('基础设置真实保存后立即应用,并精确恢复原值', async ({ expect(restored).toEqual(originalFields) }) +test('系统名称可通过页面恢复默认,清空保存也会回落默认值', async ({ page, request }, testInfo) => { + const token = await adminAccessToken(request) + const original = await getSystemConfig(request, token) + const originalFields = generalFields(original) + const originalConfigured = configuredFields(original) + const suffix = runPrefix.slice(-8) + const restoreButtonName = '恢复默认名称' + let primaryError: unknown + let restoreStatus = 0 + + try { + await loginAsAdmin(page) + await page.goto('/settings/general') + const nameInput = page.getByPlaceholder('请输入系统名称') + const shortNameInput = page.getByPlaceholder('请输入系统简称') + const saveButton = page.getByRole('button', { name: '保存基础设置' }) + + const restoredByButtonSource = `APE2E恢复按钮-${suffix}` + await nameInput.fill(restoredByButtonSource) + await saveButton.click() + await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible() + await page.reload() + await expect(nameInput).toHaveValue(restoredByButtonSource) + await captureScreenshot(page, testInfo, 'system-name-before-default-restore') + + const unsavedShortName = `未保存${suffix}` + await shortNameInput.fill(unsavedShortName) + const restoreButton = page.getByRole('button', { name: restoreButtonName }) + await expect(restoreButton).toBeEnabled() + await restoreButton.click() + await expect(page.locator('.el-message-box:visible').last()).toContainText(DEFAULT_SYSTEM_NAME) + await confirmMessageBox(page, '恢复默认') + await expect(page.getByText('系统名称已恢复默认并立即生效').last()).toBeVisible() + await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME) + await expect(shortNameInput).toHaveValue(unsavedShortName) + await page.reload() + await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME) + await expect(shortNameInput).toHaveValue(originalFields.shortName) + const restoredByButton = await getSystemConfig(request, token) + expect(generalFields(restoredByButton).systemName).toBe(DEFAULT_SYSTEM_NAME) + expect(configuredFields(restoredByButton).systemName).toBe(false) + await captureScreenshot(page, testInfo, 'system-name-restored-by-button') + + const clearedSource = `APE2E清空保存-${suffix}` + await nameInput.fill(clearedSource) + await saveButton.click() + await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible() + await page.reload() + await expect(nameInput).toHaveValue(clearedSource) + + await nameInput.fill('') + await saveButton.click() + await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible() + await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME) + await page.reload() + await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME) + const restoredByClearing = await getSystemConfig(request, token) + expect(generalFields(restoredByClearing).systemName).toBe(DEFAULT_SYSTEM_NAME) + expect(configuredFields(restoredByClearing).systemName).toBe(false) + await captureScreenshot(page, testInfo, 'system-name-restored-by-clearing') + await attachJson(testInfo, 'system-name-default-regression', { + restoredByButton: true, + restoredByClearing: true, + persistedAfterReload: true, + }) + } catch (error) { + primaryError = error + } finally { + const response = await restoreSystemConfig(request, token, original) + restoreStatus = response.status() + await attachJson(testInfo, 'system-name-original-state-restore', { status: restoreStatus }) + } + + if (primaryError) throw primaryError + expect(restoreStatus).toBe(200) + const restored = await getSystemConfig(request, token) + expect(generalFields(restored)).toEqual(originalFields) + expect(configuredFields(restored)).toEqual(originalConfigured) +}) + test('菜单显示信息经页面保存到数据库并恢复原菜单树', async ({ page, request }, testInfo) => { const token = await adminAccessToken(request) const original = await getMenus(request, token) diff --git a/tests/system-basic-regression.spec.ts b/tests/system-basic-regression.spec.ts new file mode 100644 index 0000000..1d1570f --- /dev/null +++ b/tests/system-basic-regression.spec.ts @@ -0,0 +1,197 @@ +import type { Locator, Page, Route } from '@playwright/test' + +import { captureScreenshot, expect, test } from './fixtures' + +type RoleRecord = { + id: string + code: string + name: string + shortName: string + description: string + enabled: boolean + builtIn: boolean + superAdmin: boolean + dataScope: string + departmentScopeIds: string[] + includeChildDepartments: boolean + userCount: number + permissions: string[] + sortOrder: number + dataVersion: number +} + +const json = (route: Route, data: unknown) => route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'system-basic-regression' }), +}) + +const roles: RoleRecord[] = [ + { + id: '10', code: 'teacher', name: '教员', shortName: '教', description: '教员业务角色', enabled: true, + builtIn: true, superAdmin: false, dataScope: 'SYSTEM', departmentScopeIds: [], includeChildDepartments: true, + userCount: 3, permissions: [], sortOrder: 10, dataVersion: 1, + }, + { + id: '1', code: 'admin', name: '管理员', shortName: '管', description: '系统管理员', enabled: true, + builtIn: true, superAdmin: true, dataScope: 'ALL', departmentScopeIds: [], includeChildDepartments: true, + userCount: 1, permissions: [], sortOrder: 20, dataVersion: 1, + }, +] + +const departments = [{ + id: '100', code: 'ORG-100', name: '虚拟教员平台', parentId: null, enabled: true, + builtIn: true, isRoot: true, sortOrder: 10, directUserCount: 1, directChildCount: 1, + descendantCount: 1, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00', + children: [{ + id: '110', code: 'ORG-110', name: '维修教研室', parentId: '100', enabled: true, + builtIn: false, isRoot: false, sortOrder: 10, directUserCount: 3, directChildCount: 0, + descendantCount: 0, dataVersion: 1, updatedAt: '2026-08-16T10:00:00+08:00', children: [], + }], +}] + +const permissionSections = Array.from({ length: 6 }, (_, sectionIndex) => ({ + id: `section-${sectionIndex}`, + label: `权限分组 ${sectionIndex + 1}`, + items: Array.from({ length: 8 }, (_, itemIndex) => ({ + code: `mock.section${sectionIndex}.action${itemIndex}`, + label: `业务权限 ${sectionIndex + 1}-${itemIndex + 1}`, + description: '用于验证长权限目录中的底部操作栏', + type: 'ACTION', + })), +})) + +const fillFormItem = (dialog: Locator, label: string, value: string) => dialog + .locator('.el-form-item') + .filter({ hasText: label }) + .first() + .locator('input, textarea') + .first() + .fill(value) + +async function mockSystemApi(page: Page, submitted: Array> = []) { + const mutableRoles = roles.map((role) => ({ ...role })) + await page.route(/\/api\//, async (route) => { + const url = new URL(route.request().url()) + const path = url.pathname + const method = route.request().method() + if (!path.startsWith('/api/')) return route.fallback() + + if (path.endsWith('/auth/me')) return json(route, { + userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '虚拟教员平台', + activeRoleId: '1', roles: [mutableRoles[1]], authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false, + permissions: [ + 'system.departments', 'system.roles', 'system.roles.create', 'system.roles.update', + 'system.permissions', 'system.permissions.update', + ], + }) + if (path.endsWith('/departments/tree')) return json(route, departments) + if (path.endsWith('/departments/stats')) return json(route, { total: 2, enabled: 2, maxDepth: 2, assignedUsers: 4 }) + if (path.endsWith('/roles/all')) return json(route, mutableRoles) + if (path.endsWith('/roles/stats')) return json(route, { + total: mutableRoles.length, + enabled: mutableRoles.filter((role) => role.enabled).length, + builtIn: mutableRoles.filter((role) => role.builtIn).length, + assignedUsers: mutableRoles.reduce((total, role) => total + role.userCount, 0), + }) + if (path.endsWith('/roles') && method === 'POST') { + const body = route.request().postDataJSON() as Record + submitted.push(body) + const created: RoleRecord = { + id: '20', code: String(body.code), name: String(body.name), shortName: String(body.shortName ?? ''), + description: String(body.description ?? ''), enabled: true, builtIn: false, superAdmin: false, + dataScope: String(body.dataScope), departmentScopeIds: [], includeChildDepartments: true, + userCount: 0, permissions: [], sortOrder: Number(body.sortOrder), dataVersion: 1, + } + mutableRoles.push(created) + return json(route, created) + } + if (path.endsWith('/roles/20') && method === 'PUT') { + const body = route.request().postDataJSON() as Record + submitted.push(body) + Object.assign(mutableRoles[2]!, { + name: String(body.name), shortName: String(body.shortName ?? ''), description: String(body.description ?? ''), + dataScope: String(body.dataScope), dataVersion: mutableRoles[2]!.dataVersion + 1, + }) + return json(route, mutableRoles[2]) + } + if (path.endsWith('/permissions/catalog')) return json(route, { sections: permissionSections }) + if (/\/permissions\/roles\/\d+$/.test(path)) return json(route, { permissionCodes: [], dataVersion: 1 }) + if (path.endsWith('/menus/navigation')) return json(route, []) + if (path.endsWith('/system-config/public')) return json(route, { systemName: '虚拟教员系统(试用)' }) + return json(route, {}) + }) + await page.addInitScript(() => { + window.sessionStorage.setItem('ai-person:web:access-token:v1', 'system-basic-regression-token') + }) +} + +test('Bug 850:无匹配部门时不重复显示清除筛选按钮', async ({ page }, testInfo) => { + await mockSystemApi(page) + await page.goto('/organization/departments') + + await page.getByPlaceholder('请输入部门、编码或负责人').fill('绝对不存在的部门') + await page.getByRole('button', { name: '搜索', exact: true }).click() + + const empty = page.locator('.department-table-body .el-empty') + await expect(empty).toBeVisible() + await expect(empty).toContainText('没有符合当前筛选条件的部门') + await expect(empty.getByRole('button', { name: '清除筛选' })).toHaveCount(0) + await expect(page.locator('.department-toolbar').getByRole('button', { name: '重置', exact: true })).toBeVisible() + await captureScreenshot(page, testInfo, 'bug-850-empty-without-duplicate-reset') +}) + +test('Bug 851、852、854:角色简称提示清晰、空值原样保存且校验失败有提示', async ({ page }, testInfo) => { + const submitted: Array> = [] + await mockSystemApi(page, submitted) + await page.goto('/organization/roles') + + await page.getByRole('button', { name: '新增角色', exact: true }).click() + let dialog = page.locator('.el-dialog:visible') + const shortInput = dialog.getByPlaceholder('请输入界面简称,如:审核') + await expect(shortInput).toBeVisible() + + await fillFormItem(dialog, '角色名称', '测试角色') + await fillFormItem(dialog, '角色编码', `a${'1'.repeat(48)}`) + await dialog.getByRole('button', { name: '创建角色' }).click() + await expect(page.locator('.el-message').filter({ hasText: '请检查表单中的错误项' })).toBeVisible() + expect(submitted).toHaveLength(0) + + await fillFormItem(dialog, '角色编码', 'test_role') + await dialog.getByRole('button', { name: '创建角色' }).click() + await expect(dialog).toBeHidden() + expect(submitted[0]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' }) + + const createdRow = page.locator('.roles-table .el-table__row').filter({ hasText: '测试角色' }) + await createdRow.getByRole('button', { name: '编辑' }).click() + dialog = page.locator('.el-dialog:visible') + await expect(dialog.getByPlaceholder('请输入界面简称,如:审核')).toHaveValue('') + await captureScreenshot(page, testInfo, 'bugs-851-852-854-role-form-regression') + await dialog.getByRole('button', { name: '保存修改' }).click() + await expect(dialog).toBeHidden() + expect(submitted[1]).toMatchObject({ name: '测试角色', code: 'test_role', shortName: '' }) +}) + +test('Bug 853:权限操作栏在长内容滚动时固定于视口底部', async ({ page }, testInfo) => { + await mockSystemApi(page) + await page.setViewportSize({ width: 1440, height: 760 }) + await page.goto('/organization/permissions') + + const footer = page.locator('.permission-actions') + await expect(page.locator('.permission-group')).toHaveCount(permissionSections.length) + await expect.poll(() => page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeGreaterThan(500) + + await page.evaluate(() => window.scrollTo(0, Math.round((document.documentElement.scrollHeight - window.innerHeight) * 0.45))) + await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(100) + await expect(footer).toBeVisible() + + const [footerBox, viewportHeight, position] = await Promise.all([ + footer.boundingBox(), + page.evaluate(() => window.innerHeight), + footer.evaluate((element) => getComputedStyle(element).position), + ]) + expect(footerBox).not.toBeNull() + expect(position).toBe('sticky') + expect(Math.abs(viewportHeight - (footerBox!.y + footerBox!.height))).toBeLessThanOrEqual(20) + await captureScreenshot(page, testInfo, 'bug-853-sticky-permission-actions') +})