Parcourir la source

test(system): 补充基础功能回归测试

main
leiyun il y a 1 mois
Parent
révision
fc7563cc35
3 fichiers modifiés avec 308 ajouts et 2 suppressions
  1. +3
    -0
      tests/auth-navigation.spec.ts
  2. +108
    -2
      tests/settings-menu.spec.ts
  3. +197
    -0
      tests/system-basic-regression.spec.ts

+ 3
- 0
tests/auth-navigation.spec.ts Voir le fichier

@@ -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')


+ 108
- 2
tests/settings-menu.spec.ts Voir le fichier

@@ -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<string, unknown>
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)


+ 197
- 0
tests/system-basic-regression.spec.ts Voir le fichier

@@ -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<Record<string, unknown>> = []) {
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<string, unknown>
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<string, unknown>
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<Record<string, unknown>> = []
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')
})

Chargement…
Annuler
Enregistrer