import type { APIRequestContext, APIResponse } from '@playwright/test' import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures' 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(() => ({})) function configRoot(value: unknown) { const root = asRecord(envelopeData(value)) return asRecord(root.configuration ?? root.config ?? root.settings ?? root) } function generalFields(value: unknown) { const root = configRoot(value) const general = asRecord(root.general ?? root.generalSettings ?? root) return { systemName: String(general.systemName ?? root.systemName ?? ''), shortName: String(general.shortName ?? root.shortName ?? ''), subtitle: String(general.subtitle ?? general.description ?? root.subtitle ?? root.description ?? ''), defaultTheme: String(general.defaultTheme ?? root.defaultTheme ?? 'light') === 'dark' ? 'dark' : 'light', allowThemeSwitch: Boolean(general.allowThemeSwitch ?? root.allowThemeSwitch ?? true), knowledgeMenuVisible: Boolean(general.knowledgeMenuVisible ?? general.localKnowledgeMenuVisible ?? root.knowledgeMenuVisible ?? true), } } function versionMap(value: unknown) { const root = configRoot(value) const versions = asRecord(root.versions ?? root.dataVersions ?? root.versionMap) return { systemName: Number(versions.systemName ?? 0), shortName: Number(versions.shortName ?? 0), subtitle: Number(versions.subtitle ?? 0), defaultTheme: Number(versions.defaultTheme ?? 0), allowThemeSwitch: Number(versions.allowThemeSwitch ?? 0), knowledgeMenuVisible: Number(versions.knowledgeMenuVisible ?? 0), } } 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 description: string dataVersion: number version?: number code?: string children: MenuNode[] } function menuTree(value: unknown): MenuNode[] { const data = envelopeData(value) const root = asRecord(data) const values = Array.isArray(data) ? data : Array.isArray(root.items) ? root.items : Array.isArray(root.tree) ? root.tree : [] const normalize = (item: unknown): MenuNode => { const node = asRecord(item) return { id: String(node.id ?? node.menuId ?? ''), name: String(node.name ?? node.menuName ?? ''), description: String(node.description ?? ''), dataVersion: Number(node.dataVersion ?? node.version ?? 0), code: String(node.code ?? node.menuCode ?? ''), children: (Array.isArray(node.children) ? node.children : []).map(normalize), } } return values.map(normalize) } function currentVersionById(tree: MenuNode[]) { const map = new Map() const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { map.set(node.id, node.dataVersion); walk(node.children) }) walk(tree) return map } function restoreMenuCommand(original: MenuNode[], current: MenuNode[]) { const versions = currentVersionById(current) const mapNode = (node: MenuNode): AnyRecord => ({ id: node.id, name: node.name, description: node.description, version: versions.get(node.id) ?? node.dataVersion, children: node.children.map(mapNode), }) return { items: original.map(mapNode) } } function menuDisplaySummary(nodes: MenuNode[]): unknown[] { return nodes.map((node) => ({ id: node.id, name: node.name, description: node.description, children: menuDisplaySummary(node.children), })) } async function getSystemConfig(request: APIRequestContext, token: string) { const response = await request.get('/api/auth/v1/system-config', { headers: headers(token) }) expect(response.ok()).toBeTruthy() return apiJson(response) } async function restoreSystemConfig(request: APIRequestContext, token: string, original: unknown) { const current = await getSystemConfig(request, token) return request.put('/api/auth/v1/system-config/general', { headers: headers(token), data: { ...restorableGeneralFields(original), versions: versionMap(current) }, }) } async function getMenus(request: APIRequestContext, token: string) { const response = await request.get('/api/auth/v1/menus/tree', { headers: headers(token) }) expect(response.ok()).toBeTruthy() return menuTree(await apiJson(response)) } test('基础设置真实保存后立即应用,并精确恢复原值', async ({ page, request }, testInfo) => { const token = await adminAccessToken(request) const original = await getSystemConfig(request, token) const originalFields = generalFields(original) const temporaryName = `APE2E配置-${runPrefix.slice(-8)}` let primaryError: unknown let restoreStatus = 0 try { await loginAsAdmin(page) await page.goto('/settings/general') await page.getByPlaceholder('请输入系统名称').fill(temporaryName) await page.getByPlaceholder('请输入系统简称').fill('E2E数字人') await page.getByPlaceholder('请输入系统副标题').fill(`${runPrefix} 配置提交测试`) await page.getByRole('button', { name: '保存基础设置' }).click() await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible() await expect(page.locator('body')).toContainText(temporaryName) await captureScreenshot(page, testInfo, '01-system-config-saved') await page.reload() await expect(page.getByPlaceholder('请输入系统名称')).toHaveValue(temporaryName) const persisted = generalFields(await getSystemConfig(request, token)) expect(persisted.systemName).toBe(temporaryName) expect(persisted.shortName).toBe('E2E数字人') await attachJson(testInfo, 'system-config-persistence', { persisted: true, fields: ['systemName', 'shortName', 'subtitle'] }) } catch (error) { primaryError = error } finally { const response = await restoreSystemConfig(request, token, original) restoreStatus = response.status() await attachJson(testInfo, 'system-config-restore', { status: restoreStatus }) } if (primaryError) throw primaryError expect(restoreStatus).toBe(200) const restored = generalFields(await getSystemConfig(request, token)) 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) const changedName = `E2E概览${runPrefix.slice(-4)}`.slice(0, 16) let primaryError: unknown let restoreStatus = 0 try { await loginAsAdmin(page) await page.goto('/settings/menus') const search = page.getByPlaceholder('请输入菜单名称、权限码或说明') await search.fill('ai.overview.view') await page.getByRole('button', { name: '搜索', exact: true }).click() const row = tableRowByText(page, 'ai.overview.view') await expect(row).toBeVisible() await row.getByRole('button', { name: '编辑' }).click() const drawer = page.locator('.el-drawer:visible').last() await fillFormItem(drawer, '菜单名称', changedName) await fillFormItem(drawer, '菜单说明', `${runPrefix} 菜单提交并恢复测试`) await drawer.getByRole('button', { name: '应用到草稿' }).click() const save = page.getByRole('button', { name: '保存全部' }) await expect(save).toBeEnabled() await save.click() await expect(page.getByText('菜单配置已保存并同步到导航').last()).toBeVisible() await captureScreenshot(page, testInfo, '02-menu-saved') const persisted = await getMenus(request, token) const flattened: MenuNode[] = [] const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { flattened.push(node); walk(node.children) }) walk(persisted) expect(flattened.some((node) => node.name === changedName && node.description.includes(runPrefix))).toBeTruthy() } catch (error) { primaryError = error } finally { const current = await getMenus(request, token) const response = await request.put('/api/auth/v1/menus/configuration', { headers: headers(token), data: restoreMenuCommand(original, current), }) restoreStatus = response.status() await attachJson(testInfo, 'menu-restore', { status: restoreStatus, restoredNodes: currentVersionById(original).size }) } if (primaryError) throw primaryError expect(restoreStatus).toBe(200) const restored = await getMenus(request, token) const originalSummary = JSON.stringify(menuDisplaySummary(original)) const restoredSummary = JSON.stringify(menuDisplaySummary(restored)) expect(restoredSummary).toBe(originalSummary) })