You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

310 rivejä
14 KiB

  1. import type { APIRequestContext, APIResponse } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
  3. import { adminAccessToken, confirmMessageBox, envelopeData, fillFormItem, loginAsAdmin, tableRowByText } from './helpers'
  4. test.describe.configure({ mode: 'serial' })
  5. type AnyRecord = Record<string, unknown>
  6. const DEFAULT_SYSTEM_NAME = '虚拟教员系统(试用)'
  7. const asRecord = (value: unknown): AnyRecord => value && typeof value === 'object' && !Array.isArray(value) ? value as AnyRecord : {}
  8. const headers = (token: string) => ({ Authorization: `Bearer ${token}` })
  9. const apiJson = async (response: APIResponse) => response.json().catch(() => ({}))
  10. function configRoot(value: unknown) {
  11. const root = asRecord(envelopeData(value))
  12. return asRecord(root.configuration ?? root.config ?? root.settings ?? root)
  13. }
  14. function generalFields(value: unknown) {
  15. const root = configRoot(value)
  16. const general = asRecord(root.general ?? root.generalSettings ?? root)
  17. return {
  18. systemName: String(general.systemName ?? root.systemName ?? ''),
  19. shortName: String(general.shortName ?? root.shortName ?? ''),
  20. subtitle: String(general.subtitle ?? general.description ?? root.subtitle ?? root.description ?? ''),
  21. defaultTheme: String(general.defaultTheme ?? root.defaultTheme ?? 'light') === 'dark' ? 'dark' : 'light',
  22. allowThemeSwitch: Boolean(general.allowThemeSwitch ?? root.allowThemeSwitch ?? true),
  23. knowledgeMenuVisible: Boolean(general.knowledgeMenuVisible ?? general.localKnowledgeMenuVisible ?? root.knowledgeMenuVisible ?? true),
  24. }
  25. }
  26. function versionMap(value: unknown) {
  27. const root = configRoot(value)
  28. const versions = asRecord(root.versions ?? root.dataVersions ?? root.versionMap)
  29. return {
  30. systemName: Number(versions.systemName ?? 0),
  31. shortName: Number(versions.shortName ?? 0),
  32. subtitle: Number(versions.subtitle ?? 0),
  33. defaultTheme: Number(versions.defaultTheme ?? 0),
  34. allowThemeSwitch: Number(versions.allowThemeSwitch ?? 0),
  35. knowledgeMenuVisible: Number(versions.knowledgeMenuVisible ?? 0),
  36. }
  37. }
  38. function configuredFields(value: unknown) {
  39. const configured = asRecord(configRoot(value).configured)
  40. return {
  41. systemName: Boolean(configured.systemName),
  42. shortName: Boolean(configured.shortName),
  43. subtitle: Boolean(configured.subtitle),
  44. defaultTheme: Boolean(configured.defaultTheme),
  45. allowThemeSwitch: Boolean(configured.allowThemeSwitch),
  46. knowledgeMenuVisible: Boolean(configured.knowledgeMenuVisible),
  47. }
  48. }
  49. function restorableGeneralFields(value: unknown) {
  50. const fields = generalFields(value)
  51. const configured = configuredFields(value)
  52. return {
  53. systemName: configured.systemName ? fields.systemName : null,
  54. shortName: configured.shortName ? fields.shortName : null,
  55. subtitle: configured.subtitle ? fields.subtitle : null,
  56. defaultTheme: configured.defaultTheme ? fields.defaultTheme : null,
  57. allowThemeSwitch: configured.allowThemeSwitch ? fields.allowThemeSwitch : null,
  58. knowledgeMenuVisible: configured.knowledgeMenuVisible ? fields.knowledgeMenuVisible : null,
  59. }
  60. }
  61. interface MenuNode {
  62. id: string
  63. name: string
  64. description: string
  65. dataVersion: number
  66. version?: number
  67. code?: string
  68. children: MenuNode[]
  69. }
  70. function menuTree(value: unknown): MenuNode[] {
  71. const data = envelopeData<unknown>(value)
  72. const root = asRecord(data)
  73. const values = Array.isArray(data) ? data : Array.isArray(root.items) ? root.items : Array.isArray(root.tree) ? root.tree : []
  74. const normalize = (item: unknown): MenuNode => {
  75. const node = asRecord(item)
  76. return {
  77. id: String(node.id ?? node.menuId ?? ''),
  78. name: String(node.name ?? node.menuName ?? ''),
  79. description: String(node.description ?? ''),
  80. dataVersion: Number(node.dataVersion ?? node.version ?? 0),
  81. code: String(node.code ?? node.menuCode ?? ''),
  82. children: (Array.isArray(node.children) ? node.children : []).map(normalize),
  83. }
  84. }
  85. return values.map(normalize)
  86. }
  87. function currentVersionById(tree: MenuNode[]) {
  88. const map = new Map<string, number>()
  89. const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { map.set(node.id, node.dataVersion); walk(node.children) })
  90. walk(tree)
  91. return map
  92. }
  93. function restoreMenuCommand(original: MenuNode[], current: MenuNode[]) {
  94. const versions = currentVersionById(current)
  95. const mapNode = (node: MenuNode): AnyRecord => ({
  96. id: node.id,
  97. name: node.name,
  98. description: node.description,
  99. version: versions.get(node.id) ?? node.dataVersion,
  100. children: node.children.map(mapNode),
  101. })
  102. return { items: original.map(mapNode) }
  103. }
  104. function menuDisplaySummary(nodes: MenuNode[]): unknown[] {
  105. return nodes.map((node) => ({
  106. id: node.id,
  107. name: node.name,
  108. description: node.description,
  109. children: menuDisplaySummary(node.children),
  110. }))
  111. }
  112. async function getSystemConfig(request: APIRequestContext, token: string) {
  113. const response = await request.get('/api/auth/v1/system-config', { headers: headers(token) })
  114. expect(response.ok()).toBeTruthy()
  115. return apiJson(response)
  116. }
  117. async function restoreSystemConfig(request: APIRequestContext, token: string, original: unknown) {
  118. const current = await getSystemConfig(request, token)
  119. return request.put('/api/auth/v1/system-config/general', {
  120. headers: headers(token),
  121. data: { ...restorableGeneralFields(original), versions: versionMap(current) },
  122. })
  123. }
  124. async function getMenus(request: APIRequestContext, token: string) {
  125. const response = await request.get('/api/auth/v1/menus/tree', { headers: headers(token) })
  126. expect(response.ok()).toBeTruthy()
  127. return menuTree(await apiJson(response))
  128. }
  129. test('基础设置真实保存后立即应用,并精确恢复原值', async ({ page, request }, testInfo) => {
  130. const token = await adminAccessToken(request)
  131. const original = await getSystemConfig(request, token)
  132. const originalFields = generalFields(original)
  133. const temporaryName = `APE2E配置-${runPrefix.slice(-8)}`
  134. let primaryError: unknown
  135. let restoreStatus = 0
  136. try {
  137. await loginAsAdmin(page)
  138. await page.goto('/settings/general')
  139. await page.getByPlaceholder('请输入系统名称').fill(temporaryName)
  140. await page.getByPlaceholder('请输入系统简称').fill('E2E数字人')
  141. await page.getByPlaceholder('请输入系统副标题').fill(`${runPrefix} 配置提交测试`)
  142. await page.getByRole('button', { name: '保存基础设置' }).click()
  143. await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible()
  144. await expect(page.locator('body')).toContainText(temporaryName)
  145. await captureScreenshot(page, testInfo, '01-system-config-saved')
  146. await page.reload()
  147. await expect(page.getByPlaceholder('请输入系统名称')).toHaveValue(temporaryName)
  148. const persisted = generalFields(await getSystemConfig(request, token))
  149. expect(persisted.systemName).toBe(temporaryName)
  150. expect(persisted.shortName).toBe('E2E数字人')
  151. await attachJson(testInfo, 'system-config-persistence', { persisted: true, fields: ['systemName', 'shortName', 'subtitle'] })
  152. } catch (error) {
  153. primaryError = error
  154. } finally {
  155. const response = await restoreSystemConfig(request, token, original)
  156. restoreStatus = response.status()
  157. await attachJson(testInfo, 'system-config-restore', { status: restoreStatus })
  158. }
  159. if (primaryError) throw primaryError
  160. expect(restoreStatus).toBe(200)
  161. const restored = generalFields(await getSystemConfig(request, token))
  162. expect(restored).toEqual(originalFields)
  163. })
  164. test('系统名称可通过页面恢复默认,清空保存也会回落默认值', async ({ page, request }, testInfo) => {
  165. const token = await adminAccessToken(request)
  166. const original = await getSystemConfig(request, token)
  167. const originalFields = generalFields(original)
  168. const originalConfigured = configuredFields(original)
  169. const suffix = runPrefix.slice(-8)
  170. const restoreButtonName = '恢复默认名称'
  171. let primaryError: unknown
  172. let restoreStatus = 0
  173. try {
  174. await loginAsAdmin(page)
  175. await page.goto('/settings/general')
  176. const nameInput = page.getByPlaceholder('请输入系统名称')
  177. const shortNameInput = page.getByPlaceholder('请输入系统简称')
  178. const saveButton = page.getByRole('button', { name: '保存基础设置' })
  179. const restoredByButtonSource = `APE2E恢复按钮-${suffix}`
  180. await nameInput.fill(restoredByButtonSource)
  181. await saveButton.click()
  182. await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible()
  183. await page.reload()
  184. await expect(nameInput).toHaveValue(restoredByButtonSource)
  185. await captureScreenshot(page, testInfo, 'system-name-before-default-restore')
  186. const unsavedShortName = `未保存${suffix}`
  187. await shortNameInput.fill(unsavedShortName)
  188. const restoreButton = page.getByRole('button', { name: restoreButtonName })
  189. await expect(restoreButton).toBeEnabled()
  190. await restoreButton.click()
  191. await expect(page.locator('.el-message-box:visible').last()).toContainText(DEFAULT_SYSTEM_NAME)
  192. await confirmMessageBox(page, '恢复默认')
  193. await expect(page.getByText('系统名称已恢复默认并立即生效').last()).toBeVisible()
  194. await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME)
  195. await expect(shortNameInput).toHaveValue(unsavedShortName)
  196. await page.reload()
  197. await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME)
  198. await expect(shortNameInput).toHaveValue(originalFields.shortName)
  199. const restoredByButton = await getSystemConfig(request, token)
  200. expect(generalFields(restoredByButton).systemName).toBe(DEFAULT_SYSTEM_NAME)
  201. expect(configuredFields(restoredByButton).systemName).toBe(false)
  202. await captureScreenshot(page, testInfo, 'system-name-restored-by-button')
  203. const clearedSource = `APE2E清空保存-${suffix}`
  204. await nameInput.fill(clearedSource)
  205. await saveButton.click()
  206. await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible()
  207. await page.reload()
  208. await expect(nameInput).toHaveValue(clearedSource)
  209. await nameInput.fill('')
  210. await saveButton.click()
  211. await expect(page.getByText('基础设置已保存并立即生效').last()).toBeVisible()
  212. await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME)
  213. await page.reload()
  214. await expect(nameInput).toHaveValue(DEFAULT_SYSTEM_NAME)
  215. const restoredByClearing = await getSystemConfig(request, token)
  216. expect(generalFields(restoredByClearing).systemName).toBe(DEFAULT_SYSTEM_NAME)
  217. expect(configuredFields(restoredByClearing).systemName).toBe(false)
  218. await captureScreenshot(page, testInfo, 'system-name-restored-by-clearing')
  219. await attachJson(testInfo, 'system-name-default-regression', {
  220. restoredByButton: true,
  221. restoredByClearing: true,
  222. persistedAfterReload: true,
  223. })
  224. } catch (error) {
  225. primaryError = error
  226. } finally {
  227. const response = await restoreSystemConfig(request, token, original)
  228. restoreStatus = response.status()
  229. await attachJson(testInfo, 'system-name-original-state-restore', { status: restoreStatus })
  230. }
  231. if (primaryError) throw primaryError
  232. expect(restoreStatus).toBe(200)
  233. const restored = await getSystemConfig(request, token)
  234. expect(generalFields(restored)).toEqual(originalFields)
  235. expect(configuredFields(restored)).toEqual(originalConfigured)
  236. })
  237. test('菜单显示信息经页面保存到数据库并恢复原菜单树', async ({ page, request }, testInfo) => {
  238. const token = await adminAccessToken(request)
  239. const original = await getMenus(request, token)
  240. const changedName = `E2E概览${runPrefix.slice(-4)}`.slice(0, 16)
  241. let primaryError: unknown
  242. let restoreStatus = 0
  243. try {
  244. await loginAsAdmin(page)
  245. await page.goto('/settings/menus')
  246. const search = page.getByPlaceholder('请输入菜单名称、权限码或说明')
  247. await search.fill('ai.overview.view')
  248. await page.getByRole('button', { name: '搜索', exact: true }).click()
  249. const row = tableRowByText(page, 'ai.overview.view')
  250. await expect(row).toBeVisible()
  251. await row.getByRole('button', { name: '编辑' }).click()
  252. const drawer = page.locator('.el-drawer:visible').last()
  253. await fillFormItem(drawer, '菜单名称', changedName)
  254. await fillFormItem(drawer, '菜单说明', `${runPrefix} 菜单提交并恢复测试`)
  255. await drawer.getByRole('button', { name: '应用到草稿' }).click()
  256. const save = page.getByRole('button', { name: '保存全部' })
  257. await expect(save).toBeEnabled()
  258. await save.click()
  259. await expect(page.getByText('菜单配置已保存并同步到导航').last()).toBeVisible()
  260. await captureScreenshot(page, testInfo, '02-menu-saved')
  261. const persisted = await getMenus(request, token)
  262. const flattened: MenuNode[] = []
  263. const walk = (nodes: MenuNode[]) => nodes.forEach((node) => { flattened.push(node); walk(node.children) })
  264. walk(persisted)
  265. expect(flattened.some((node) => node.name === changedName && node.description.includes(runPrefix))).toBeTruthy()
  266. } catch (error) {
  267. primaryError = error
  268. } finally {
  269. const current = await getMenus(request, token)
  270. const response = await request.put('/api/auth/v1/menus/configuration', {
  271. headers: headers(token), data: restoreMenuCommand(original, current),
  272. })
  273. restoreStatus = response.status()
  274. await attachJson(testInfo, 'menu-restore', { status: restoreStatus, restoredNodes: currentVersionById(original).size })
  275. }
  276. if (primaryError) throw primaryError
  277. expect(restoreStatus).toBe(200)
  278. const restored = await getMenus(request, token)
  279. const originalSummary = JSON.stringify(menuDisplaySummary(original))
  280. const restoredSummary = JSON.stringify(menuDisplaySummary(restored))
  281. expect(restoredSummary).toBe(originalSummary)
  282. })