|
- 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<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(() => ({}))
-
- 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<unknown>(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<string, number>()
- 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)
- })
|