Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

204 строки
8.7 KiB

  1. import type { APIRequestContext, APIResponse } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
  3. import { adminAccessToken, envelopeData, fillFormItem, loginAsAdmin, tableRowByText } from './helpers'
  4. test.describe.configure({ mode: 'serial' })
  5. type AnyRecord = Record<string, unknown>
  6. const asRecord = (value: unknown): AnyRecord => value && typeof value === 'object' && !Array.isArray(value) ? value as AnyRecord : {}
  7. const headers = (token: string) => ({ Authorization: `Bearer ${token}` })
  8. const apiJson = async (response: APIResponse) => response.json().catch(() => ({}))
  9. function configRoot(value: unknown) {
  10. const root = asRecord(envelopeData(value))
  11. return asRecord(root.configuration ?? root.config ?? root.settings ?? root)
  12. }
  13. function generalFields(value: unknown) {
  14. const root = configRoot(value)
  15. const general = asRecord(root.general ?? root.generalSettings ?? root)
  16. return {
  17. systemName: String(general.systemName ?? root.systemName ?? ''),
  18. shortName: String(general.shortName ?? root.shortName ?? ''),
  19. subtitle: String(general.subtitle ?? general.description ?? root.subtitle ?? root.description ?? ''),
  20. defaultTheme: String(general.defaultTheme ?? root.defaultTheme ?? 'light') === 'dark' ? 'dark' : 'light',
  21. allowThemeSwitch: Boolean(general.allowThemeSwitch ?? root.allowThemeSwitch ?? true),
  22. knowledgeMenuVisible: Boolean(general.knowledgeMenuVisible ?? general.localKnowledgeMenuVisible ?? root.knowledgeMenuVisible ?? true),
  23. }
  24. }
  25. function versionMap(value: unknown) {
  26. const root = configRoot(value)
  27. const versions = asRecord(root.versions ?? root.dataVersions ?? root.versionMap)
  28. return {
  29. systemName: Number(versions.systemName ?? 0),
  30. shortName: Number(versions.shortName ?? 0),
  31. subtitle: Number(versions.subtitle ?? 0),
  32. defaultTheme: Number(versions.defaultTheme ?? 0),
  33. allowThemeSwitch: Number(versions.allowThemeSwitch ?? 0),
  34. knowledgeMenuVisible: Number(versions.knowledgeMenuVisible ?? 0),
  35. }
  36. }
  37. interface MenuNode {
  38. id: string
  39. name: string
  40. description: string
  41. dataVersion: number
  42. version?: number
  43. code?: string
  44. children: MenuNode[]
  45. }
  46. function menuTree(value: unknown): MenuNode[] {
  47. const data = envelopeData<unknown>(value)
  48. const root = asRecord(data)
  49. const values = Array.isArray(data) ? data : Array.isArray(root.items) ? root.items : Array.isArray(root.tree) ? root.tree : []
  50. const normalize = (item: unknown): MenuNode => {
  51. const node = asRecord(item)
  52. return {
  53. id: String(node.id ?? node.menuId ?? ''),
  54. name: String(node.name ?? node.menuName ?? ''),
  55. description: String(node.description ?? ''),
  56. dataVersion: Number(node.dataVersion ?? node.version ?? 0),
  57. code: String(node.code ?? node.menuCode ?? ''),
  58. children: (Array.isArray(node.children) ? node.children : []).map(normalize),
  59. }
  60. }
  61. return values.map(normalize)
  62. }
  63. function currentVersionById(tree: MenuNode[]) {
  64. const map = new Map<string, number>()
  65. const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { map.set(node.id, node.dataVersion); walk(node.children) })
  66. walk(tree)
  67. return map
  68. }
  69. function restoreMenuCommand(original: MenuNode[], current: MenuNode[]) {
  70. const versions = currentVersionById(current)
  71. const mapNode = (node: MenuNode): AnyRecord => ({
  72. id: node.id,
  73. name: node.name,
  74. description: node.description,
  75. version: versions.get(node.id) ?? node.dataVersion,
  76. children: node.children.map(mapNode),
  77. })
  78. return { items: original.map(mapNode) }
  79. }
  80. function menuDisplaySummary(nodes: MenuNode[]): unknown[] {
  81. return nodes.map((node) => ({
  82. id: node.id,
  83. name: node.name,
  84. description: node.description,
  85. children: menuDisplaySummary(node.children),
  86. }))
  87. }
  88. async function getSystemConfig(request: APIRequestContext, token: string) {
  89. const response = await request.get('/api/auth/v1/system-config', { headers: headers(token) })
  90. expect(response.ok()).toBeTruthy()
  91. return apiJson(response)
  92. }
  93. async function restoreSystemConfig(request: APIRequestContext, token: string, original: unknown) {
  94. const current = await getSystemConfig(request, token)
  95. return request.put('/api/auth/v1/system-config/general', {
  96. headers: headers(token),
  97. data: { ...generalFields(original), versions: versionMap(current) },
  98. })
  99. }
  100. async function getMenus(request: APIRequestContext, token: string) {
  101. const response = await request.get('/api/auth/v1/menus/tree', { headers: headers(token) })
  102. expect(response.ok()).toBeTruthy()
  103. return menuTree(await apiJson(response))
  104. }
  105. test('基础设置真实保存后立即应用,并精确恢复原值', async ({ page, request }, testInfo) => {
  106. const token = await adminAccessToken(request)
  107. const original = await getSystemConfig(request, token)
  108. const originalFields = generalFields(original)
  109. const temporaryName = `APE2E配置-${runPrefix.slice(-8)}`
  110. let primaryError: unknown
  111. let restoreStatus = 0
  112. try {
  113. await loginAsAdmin(page)
  114. await page.goto('/settings/general')
  115. await page.getByPlaceholder('请输入系统名称').fill(temporaryName)
  116. await page.getByPlaceholder('请输入系统简称').fill('E2E数字人')
  117. await page.getByPlaceholder('请输入系统副标题').fill(`${runPrefix} 配置提交测试`)
  118. await page.getByRole('button', { name: '保存基础设置' }).click()
  119. await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible()
  120. await expect(page.locator('body')).toContainText(temporaryName)
  121. await captureScreenshot(page, testInfo, '01-system-config-saved')
  122. await page.reload()
  123. await expect(page.getByPlaceholder('请输入系统名称')).toHaveValue(temporaryName)
  124. const persisted = generalFields(await getSystemConfig(request, token))
  125. expect(persisted.systemName).toBe(temporaryName)
  126. expect(persisted.shortName).toBe('E2E数字人')
  127. await attachJson(testInfo, 'system-config-persistence', { persisted: true, fields: ['systemName', 'shortName', 'subtitle'] })
  128. } catch (error) {
  129. primaryError = error
  130. } finally {
  131. const response = await restoreSystemConfig(request, token, original)
  132. restoreStatus = response.status()
  133. await attachJson(testInfo, 'system-config-restore', { status: restoreStatus })
  134. }
  135. if (primaryError) throw primaryError
  136. expect(restoreStatus).toBe(200)
  137. const restored = generalFields(await getSystemConfig(request, token))
  138. expect(restored).toEqual(originalFields)
  139. })
  140. test('菜单显示信息经页面保存到数据库并恢复原菜单树', async ({ page, request }, testInfo) => {
  141. const token = await adminAccessToken(request)
  142. const original = await getMenus(request, token)
  143. const changedName = `E2E概览${runPrefix.slice(-4)}`.slice(0, 16)
  144. let primaryError: unknown
  145. let restoreStatus = 0
  146. try {
  147. await loginAsAdmin(page)
  148. await page.goto('/settings/menus')
  149. const search = page.getByPlaceholder('请输入菜单名称、权限码或说明')
  150. await search.fill('ai.overview.view')
  151. await page.getByRole('button', { name: '搜索', exact: true }).click()
  152. const row = tableRowByText(page, 'ai.overview.view')
  153. await expect(row).toBeVisible()
  154. await row.getByRole('button', { name: '编辑' }).click()
  155. const drawer = page.locator('.el-drawer:visible').last()
  156. await fillFormItem(drawer, '菜单名称', changedName)
  157. await fillFormItem(drawer, '菜单说明', `${runPrefix} 菜单提交并恢复测试`)
  158. await drawer.getByRole('button', { name: '应用到草稿' }).click()
  159. const save = page.getByRole('button', { name: '保存全部' })
  160. await expect(save).toBeEnabled()
  161. await save.click()
  162. await expect(page.getByText('菜单配置已保存并同步到导航').last()).toBeVisible()
  163. await captureScreenshot(page, testInfo, '02-menu-saved')
  164. const persisted = await getMenus(request, token)
  165. const flattened: MenuNode[] = []
  166. const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { flattened.push(node); walk(node.children) })
  167. walk(persisted)
  168. expect(flattened.some((node) => node.name === changedName && node.description.includes(runPrefix))).toBeTruthy()
  169. } catch (error) {
  170. primaryError = error
  171. } finally {
  172. const current = await getMenus(request, token)
  173. const response = await request.put('/api/auth/v1/menus/configuration', {
  174. headers: headers(token), data: restoreMenuCommand(original, current),
  175. })
  176. restoreStatus = response.status()
  177. await attachJson(testInfo, 'menu-restore', { status: restoreStatus, restoredNodes: currentVersionById(original).size })
  178. }
  179. if (primaryError) throw primaryError
  180. expect(restoreStatus).toBe(200)
  181. const restored = await getMenus(request, token)
  182. const originalSummary = JSON.stringify(menuDisplaySummary(original))
  183. const restoredSummary = JSON.stringify(menuDisplaySummary(restored))
  184. expect(restoredSummary).toBe(originalSummary)
  185. })