選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

179 行
8.1 KiB

  1. import { captureScreenshot, expect, test } from './fixtures'
  2. import type { Page, Route } from '@playwright/test'
  3. const navigationModes = [
  4. { label: '顶部一级 + 左侧二级', value: 'top-side' },
  5. { label: '左侧分组菜单', value: 'side' },
  6. { label: '顶部下拉菜单', value: 'top' },
  7. ] as const
  8. const waitForDashboard = async (page: import('@playwright/test').Page) => {
  9. await expect(page.locator('.platform-shell')).toBeVisible()
  10. }
  11. const envelope = (data: unknown) => JSON.stringify({
  12. code: 200,
  13. message: '成功',
  14. data,
  15. timestamp: '2026-08-30T10:00:00+08:00',
  16. requestId: 'ute2e-preferences',
  17. })
  18. const fulfillJson = (route: Route, data: unknown) => route.fulfill({
  19. status: 200,
  20. contentType: 'application/json',
  21. body: envelope(data),
  22. })
  23. const installMockedSession = async (page: Page) => {
  24. await page.addInitScript(() => {
  25. sessionStorage.setItem('unreal-tran:web:access-token:v1', 'ute2e-preferences-token')
  26. })
  27. await page.route('**/api/auth/v1/**', (route) => {
  28. const path = new URL(route.request().url()).pathname
  29. if (path === '/api/auth/v1/system-config/public') {
  30. return fulfillJson(route, {
  31. systemName: '装备维修实训数字车间',
  32. loginDescription: '统一承载内容制作、虚实训练、训练考核与资源管理。',
  33. defaultTheme: 'military',
  34. logoConfigured: false,
  35. faviconConfigured: false,
  36. loginLogoConfigured: false,
  37. })
  38. }
  39. if (path === '/api/auth/v1/auth/me') {
  40. return fulfillJson(route, {
  41. user: {
  42. id: '1', username: 'admin', displayName: '系统管理员', departmentId: '10',
  43. departmentName: '信息中心', mustChangePassword: false, version: 1,
  44. },
  45. activeRoleId: '100',
  46. roles: [{
  47. id: '100', code: 'admin', name: '管理员', shortName: '管', status: 1,
  48. builtIn: 1, isSuperAdmin: 1, dataScopeCode: 'ALL',
  49. }],
  50. permissions: ['dashboard.view'],
  51. authorizationMode: 'SINGLE_ACTIVE',
  52. loginTime: 1788064800,
  53. })
  54. }
  55. if (path === '/api/auth/v1/menus/navigation') return fulfillJson(route, [])
  56. return fulfillJson(route, {})
  57. })
  58. await page.route('**/api/tran/v1/**', (route) => fulfillJson(route, {}))
  59. await page.goto('/dashboard')
  60. await waitForDashboard(page)
  61. }
  62. const contrastRatio = (foreground: string, background: string) => {
  63. const channels = (value: string) => (value.match(/[\d.]+/g) ?? []).slice(0, 3).map(Number)
  64. const luminance = (value: string) => channels(value).reduce((sum, channel, index) => {
  65. const normalized = channel / 255
  66. const linear = normalized <= .03928 ? normalized / 12.92 : ((normalized + .055) / 1.055) ** 2.4
  67. return sum + linear * [.2126, .7152, .0722][index]!
  68. }, 0)
  69. const [lighter, darker] = [luminance(foreground), luminance(background)].sort((left, right) => right - left)
  70. return (lighter! + .05) / (darker! + .05)
  71. }
  72. test('三种导航模式与四种主题可以持久化', async ({ page }, testInfo) => {
  73. await page.emulateMedia({ colorScheme: 'dark' })
  74. await installMockedSession(page)
  75. await expect(page.locator('.platform-shell')).toHaveAttribute('data-navigation-mode', 'top-side')
  76. await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
  77. await expect(page.locator('html')).toHaveAttribute('data-color-theme', 'military')
  78. expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe('#168a72')
  79. await expect(page.getByText('本地服务正常', { exact: true })).toHaveCount(0)
  80. await expect(page.getByText('平台服务正常', { exact: true })).toHaveCount(0)
  81. for (const mode of navigationModes) {
  82. await test.step(`导航模式:${mode.label}`, async () => {
  83. await page.getByRole('button', { name: '界面与导航设置' }).click()
  84. const drawer = page.locator('.el-drawer:visible')
  85. await drawer.getByRole('button', { name: new RegExp(mode.label.replace('+', '\\+')) }).click()
  86. await expect(page.locator('.platform-shell')).toHaveAttribute('data-navigation-mode', mode.value)
  87. await page.keyboard.press('Escape')
  88. await page.reload()
  89. await expect(page.locator('.platform-shell')).toHaveAttribute('data-navigation-mode', mode.value)
  90. await waitForDashboard(page)
  91. await captureScreenshot(page, testInfo, `navigation-${mode.value}`)
  92. })
  93. }
  94. await test.step('桌面侧栏收起、刷新持久化与展开恢复', async () => {
  95. await page.getByRole('button', { name: '界面与导航设置' }).click()
  96. const drawer = page.locator('.el-drawer:visible')
  97. await drawer.getByRole('button', { name: /顶部一级 \+ 左侧二级/ }).click()
  98. await page.keyboard.press('Escape')
  99. await expect(drawer).toBeHidden()
  100. const shell = page.locator('.platform-shell')
  101. await expect(shell).toHaveAttribute('data-navigation-mode', 'top-side')
  102. const collapseButton = page.getByRole('button', { name: '收起左侧导航' })
  103. await expect(collapseButton).toBeVisible()
  104. await collapseButton.click()
  105. await expect(shell).toHaveClass(/sidebar-collapsed/)
  106. await expect(page.getByRole('button', { name: '展开左侧导航' })).toBeVisible()
  107. await captureScreenshot(page, testInfo, 'sidebar-collapsed')
  108. await page.reload()
  109. await waitForDashboard(page)
  110. await expect(shell).toHaveClass(/sidebar-collapsed/)
  111. const expandButton = page.getByRole('button', { name: '展开左侧导航' })
  112. await expect(expandButton).toBeVisible()
  113. await expandButton.click()
  114. await expect(shell).not.toHaveClass(/sidebar-collapsed/)
  115. await expect(page.getByRole('button', { name: '收起左侧导航' })).toBeVisible()
  116. await captureScreenshot(page, testInfo, 'sidebar-expanded')
  117. await page.reload()
  118. await waitForDashboard(page)
  119. await expect(shell).not.toHaveClass(/sidebar-collapsed/)
  120. })
  121. const themes = [
  122. { label: '军工绿', value: 'military', expected: 'light', primary: '#168a72' },
  123. { label: '科技蓝', value: 'technology', expected: 'light', primary: '#197fc8' },
  124. { label: '深空灰', value: 'graphite', expected: 'light', primary: '#4f8995' },
  125. { label: '暗色', value: 'dark', expected: 'dark', primary: '#55d0bd' },
  126. ] as const
  127. for (const theme of themes) {
  128. await test.step(`主题模式:${theme.label}`, async () => {
  129. await page.getByRole('button', { name: '界面与导航设置' }).click()
  130. const drawer = page.locator('.el-drawer:visible')
  131. await drawer.getByRole('button', { name: new RegExp(theme.label) }).click()
  132. await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected)
  133. await expect(page.locator('html')).toHaveAttribute('data-color-theme', theme.value)
  134. await page.keyboard.press('Escape')
  135. await page.reload()
  136. await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected)
  137. await expect(page.locator('html')).toHaveAttribute('data-color-theme', theme.value)
  138. await waitForDashboard(page)
  139. expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe(theme.primary)
  140. if (theme.expected === 'dark') {
  141. const colors = await page.locator('html').evaluate(() => {
  142. const probe = document.createElement('div')
  143. probe.style.color = 'var(--text-primary)'
  144. probe.style.backgroundColor = 'var(--surface)'
  145. document.body.appendChild(probe)
  146. const result = {
  147. foreground: getComputedStyle(probe).color,
  148. background: getComputedStyle(probe).backgroundColor,
  149. }
  150. probe.remove()
  151. return result
  152. })
  153. expect(contrastRatio(colors.foreground, colors.background)).toBeGreaterThanOrEqual(4.5)
  154. }
  155. await captureScreenshot(page, testInfo, `theme-${theme.value}`)
  156. })
  157. }
  158. // 保持默认值,避免 headed 模式下人工继续检查时继承最后一次设置。
  159. await page.getByRole('button', { name: '界面与导航设置' }).click()
  160. const drawer = page.locator('.el-drawer:visible')
  161. await drawer.getByRole('button', { name: /军工绿/ }).click()
  162. await drawer.getByRole('button', { name: /顶部一级 \+ 左侧二级/ }).click()
  163. })