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.
 
 
 
 

162 regels
6.2 KiB

  1. import type { Page, Route } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. const envelope = (data: unknown) => ({
  4. code: 0,
  5. message: 'ok',
  6. data,
  7. timestamp: Date.now(),
  8. requestId: 'sidebar-bootstrap-regression',
  9. })
  10. const profile = {
  11. user: {
  12. id: 'sidebar-admin',
  13. username: 'sidebar-admin',
  14. displayName: '系统管理员',
  15. departmentId: 'system',
  16. departmentName: '系统管理',
  17. enabled: true,
  18. mustChangePassword: false,
  19. version: 1,
  20. },
  21. roles: [{
  22. id: 'admin-role',
  23. code: 'ADMIN',
  24. name: '系统管理员',
  25. shortName: '系统',
  26. enabled: true,
  27. builtIn: true,
  28. isSuperAdmin: true,
  29. dataScope: 'ALL',
  30. }],
  31. activeRoleId: 'admin-role',
  32. permissions: ['*'],
  33. authorizationMode: 'SINGLE_ACTIVE',
  34. loginTime: new Date().toISOString(),
  35. }
  36. const navigation = [
  37. {
  38. id: 'agents', code: 'agents', name: '数字人智能体', type: 'GROUP', sortOrder: 10, enabled: true,
  39. path: '/agents/manage', icon: 'Platform', permissionCodes: [],
  40. children: [
  41. { id: 'agent-management', code: 'agent-management', name: '虚拟教员智能体', type: 'PAGE', sortOrder: 10, enabled: true, path: '/agents/manage', icon: 'Management', permissionCodes: [], children: [] },
  42. { id: 'agent-projects', code: 'agent-projects', name: '智能体组别管理', type: 'PAGE', sortOrder: 20, enabled: true, path: '/agents/projects', icon: 'Files', permissionCodes: [], children: [] },
  43. ],
  44. },
  45. {
  46. id: 'assets', code: 'assets', name: '资源中心', type: 'GROUP', sortOrder: 20, enabled: true,
  47. path: '/assets/avatars', icon: 'Collection', permissionCodes: [],
  48. children: [
  49. { id: 'avatars', code: 'avatars', name: '数字形象', type: 'PAGE', sortOrder: 10, enabled: true, path: '/assets/avatars', icon: 'Avatar', permissionCodes: [], children: [] },
  50. { id: 'ai-models', code: 'ai-models', name: 'AI模型', type: 'PAGE', sortOrder: 20, enabled: true, path: '/assets/ai-models', icon: 'MagicStick', permissionCodes: [], children: [] },
  51. ],
  52. },
  53. {
  54. id: 'settings', code: 'settings', name: '系统设置', type: 'GROUP', sortOrder: 30, enabled: true,
  55. path: '/settings/general', icon: 'Setting', permissionCodes: [],
  56. children: [
  57. { id: 'settings-general', code: 'settings-general', name: '基础设置', type: 'PAGE', sortOrder: 10, enabled: true, path: '/settings/general', icon: 'Setting', permissionCodes: [], children: [] },
  58. { id: 'settings-menus', code: 'settings-menus', name: '菜单管理', type: 'PAGE', sortOrder: 20, enabled: true, path: '/settings/menus', icon: 'Menu', permissionCodes: [], children: [] },
  59. ],
  60. },
  61. ]
  62. async function installShellMocks(page: Page, holdBootstrap = false) {
  63. let releaseBootstrap = () => {}
  64. const bootstrapGate = holdBootstrap
  65. ? new Promise<void>((resolve) => { releaseBootstrap = resolve })
  66. : Promise.resolve()
  67. await page.addInitScript(() => {
  68. window.localStorage.setItem('ai-person:web:access-token:v1', 'sidebar-bootstrap-token')
  69. window.localStorage.setItem('ai-person:web:remember:v1', '1')
  70. })
  71. const fulfillApi = async (route: Route) => {
  72. const pathname = new URL(route.request().url()).pathname
  73. let data: unknown = {}
  74. if (pathname.endsWith('/system-config/public') || pathname.endsWith('/auth/me')) {
  75. await bootstrapGate
  76. }
  77. if (pathname.endsWith('/auth/me')) data = profile
  78. else if (pathname.endsWith('/menus/navigation')) data = navigation
  79. else if (pathname.endsWith('/overview')) {
  80. data = { stats: {}, agents: [], videos: [], jobs: [] }
  81. }
  82. await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(envelope(data)) })
  83. }
  84. await page.route('**/api/auth/v1/**', fulfillApi)
  85. await page.route('**/api/v1/**', fulfillApi)
  86. return releaseBootstrap
  87. }
  88. test('App 模块尚未加载时由 HTML 直接展示启动状态', async ({ page }) => {
  89. let releaseApp = () => {}
  90. const appGate = new Promise<void>((resolve) => { releaseApp = resolve })
  91. await page.route('**/src/App.vue*', async (route) => {
  92. await appGate
  93. await route.continue()
  94. })
  95. try {
  96. await page.goto('/overview', { waitUntil: 'commit' })
  97. await expect(page.locator('.static-bootstrap-screen')).toBeVisible()
  98. await expect(page.getByText('正在启动系统')).toBeVisible()
  99. await expect(page.getByText('正在加载应用资源,请稍候…')).toBeVisible()
  100. } finally {
  101. releaseApp()
  102. }
  103. await expect(page.locator('.login-shell')).toBeVisible()
  104. await expect(page.locator('.static-bootstrap-screen')).toHaveCount(0)
  105. })
  106. test('认证与公开配置请求未完成时展示品牌启动状态', async ({ page }, testInfo) => {
  107. const releaseBootstrap = await installShellMocks(page, true)
  108. await page.goto('/overview')
  109. await expect(page.locator('.app-bootstrap-screen')).toBeVisible()
  110. await expect(page.getByText('正在进入系统')).toBeVisible()
  111. await expect(page.getByText('正在验证登录状态并加载工作台,请稍候…')).toBeVisible()
  112. await captureScreenshot(page, testInfo, 'bootstrap-loading-not-blank')
  113. releaseBootstrap()
  114. await expect(page.locator('.app-shell')).toBeVisible()
  115. await expect(page.locator('.app-bootstrap-screen')).toHaveCount(0)
  116. })
  117. test('侧栏使用 Element Plus 菜单并保持手风琴展开', async ({ page }, testInfo) => {
  118. await installShellMocks(page)
  119. await page.goto('/overview')
  120. await expect(page.locator('.app-shell')).toBeVisible()
  121. const menu = page.locator('#main-sidebar .sidebar-menu')
  122. const groups = menu.locator(':scope > .el-sub-menu')
  123. await expect(menu).toHaveClass(/el-menu/)
  124. await expect(groups).toHaveCount(3)
  125. const first = groups.nth(0)
  126. const second = groups.nth(1)
  127. const third = groups.nth(2)
  128. await first.locator(':scope > .el-sub-menu__title').click()
  129. await expect(first).toHaveClass(/is-opened/)
  130. await second.locator(':scope > .el-sub-menu__title').click()
  131. await expect(second).toHaveClass(/is-opened/)
  132. await expect(first).not.toHaveClass(/is-opened/)
  133. await third.locator(':scope > .el-sub-menu__title').click()
  134. await expect(third).toHaveClass(/is-opened/)
  135. await expect(second).not.toHaveClass(/is-opened/)
  136. await expect(menu.locator(':scope > .el-sub-menu.is-opened')).toHaveCount(1)
  137. await captureScreenshot(page, testInfo, 'element-menu-accordion')
  138. })