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

531 行
24 KiB

  1. import type { Page, Route } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. const DEFAULT_SYSTEM_NAME = '某类装备维修实训数字车间'
  4. const PNG_BYTES = Buffer.from(
  5. 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
  6. 'base64',
  7. )
  8. type BrandAsset = 'logo' | 'favicon'
  9. interface ConfigurationState {
  10. systemName: string
  11. systemNameConfigured: boolean
  12. systemNameDataVersion: number
  13. defaultTheme: 'military' | 'technology' | 'graphite' | 'dark'
  14. defaultThemeConfigured: boolean
  15. defaultThemeDataVersion: number
  16. digitalHumanWidgetConfigured: boolean
  17. digitalHumanWidget: {
  18. baseUrl: string
  19. agentSlug: string
  20. position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
  21. label: string
  22. } | null
  23. digitalHumanWidgetDataVersion: number
  24. logoConfigured: boolean
  25. logoUrl: string | null
  26. logoMediaType: string | null
  27. logoOriginalName: string | null
  28. logoSize: number | null
  29. logoSha256: string | null
  30. logoDataVersion: number
  31. faviconConfigured: boolean
  32. faviconUrl: string | null
  33. faviconMediaType: string | null
  34. faviconOriginalName: string | null
  35. faviconSize: number | null
  36. faviconSha256: string | null
  37. faviconDataVersion: number
  38. }
  39. const initialConfiguration = (): ConfigurationState => ({
  40. systemName: DEFAULT_SYSTEM_NAME,
  41. systemNameConfigured: false,
  42. systemNameDataVersion: 0,
  43. defaultTheme: 'military',
  44. defaultThemeConfigured: true,
  45. defaultThemeDataVersion: 1,
  46. digitalHumanWidgetConfigured: false,
  47. digitalHumanWidget: null,
  48. digitalHumanWidgetDataVersion: 0,
  49. logoConfigured: false,
  50. logoUrl: null,
  51. logoMediaType: null,
  52. logoOriginalName: null,
  53. logoSize: null,
  54. logoSha256: null,
  55. logoDataVersion: 0,
  56. faviconConfigured: false,
  57. faviconUrl: null,
  58. faviconMediaType: null,
  59. faviconOriginalName: null,
  60. faviconSize: null,
  61. faviconSha256: null,
  62. faviconDataVersion: 0,
  63. })
  64. const profile = (canUpdate: boolean) => ({
  65. user: {
  66. id: '1',
  67. username: 'admin',
  68. displayName: '系统管理员',
  69. departmentId: '10',
  70. departmentName: '信息中心',
  71. mustChangePassword: false,
  72. version: 1,
  73. },
  74. activeRoleId: '100',
  75. activeRole: { id: '100', code: 'admin', name: '管理员' },
  76. roles: [{
  77. id: '100', code: 'admin', name: '管理员', shortName: '管', status: 1,
  78. builtIn: 1, isSuperAdmin: 1, dataScopeCode: 'ALL',
  79. }],
  80. permissions: ['system.config', ...(canUpdate ? ['system.config.update'] : [])],
  81. authorizationMode: 'SINGLE_ACTIVE',
  82. loginTime: 1786387200,
  83. })
  84. const loginContext = {
  85. defaultRoleCode: 'admin',
  86. roles: [{ code: 'admin', label: '管理员', accountLabel: '登录账号', organizationMode: 'NONE', organizationLabels: [] }],
  87. departments: [],
  88. }
  89. const envelope = (data: unknown, message = '成功', code: number | string = 200) => JSON.stringify({
  90. code,
  91. message,
  92. data,
  93. timestamp: '2026-08-11T12:00:00+08:00',
  94. requestId: 'ute2e-system-config',
  95. })
  96. const fulfillJson = (route: Route, data: unknown, status = 200, message = '成功', code: number | string = status) => route.fulfill({
  97. status,
  98. contentType: 'application/json',
  99. body: envelope(data, message, code),
  100. })
  101. class SystemConfigurationMock {
  102. readonly state = initialConfiguration()
  103. readonly mutations: string[] = []
  104. readonly canUpdate: boolean
  105. conflictNextNameSave = false
  106. managementReadCount = 0
  107. constructor(canUpdate = true) {
  108. this.canUpdate = canUpdate
  109. }
  110. async install(page: Page, authenticated = true) {
  111. if (authenticated) {
  112. await page.addInitScript(() => {
  113. sessionStorage.setItem('unreal-tran:web:access-token:v1', 'ute2e-system-config-token')
  114. })
  115. }
  116. await page.route('**/api/auth/v1/**', (route) => this.handle(route))
  117. await page.route('https://human.example.test/**', (route) => route.fulfill({
  118. status: 200,
  119. headers: { 'Content-Type': 'text/html; charset=utf-8' },
  120. body: `<!doctype html><html><body style="margin:0;background:transparent">
  121. <button style="width:188px;height:56px">问数字教员</button>
  122. <script>
  123. addEventListener('message', function (event) {
  124. var data = event.data || {};
  125. if (data.source !== 'virtual-instructor-widget-host' || data.type !== 'theme') return;
  126. document.body.dataset.hostTheme = data.theme && data.theme.scheme;
  127. document.body.dataset.hostAccent = data.theme && data.theme.accent;
  128. });
  129. parent.postMessage({source:'virtual-instructor-widget',type:'ready',protocolVersion:2,capabilities:['resize','drag','theme']}, '*');
  130. parent.postMessage({source:'virtual-instructor-widget',type:'resize',width:208,height:92}, '*');
  131. </script>
  132. </body></html>`,
  133. }))
  134. }
  135. private publicState() {
  136. return {
  137. systemName: this.state.systemName,
  138. defaultTheme: this.state.defaultTheme,
  139. digitalHumanWidget: this.state.digitalHumanWidget,
  140. logoConfigured: this.state.logoConfigured,
  141. logoUrl: this.state.logoUrl,
  142. logoMediaType: this.state.logoMediaType,
  143. faviconConfigured: this.state.faviconConfigured,
  144. faviconUrl: this.state.faviconUrl,
  145. faviconMediaType: this.state.faviconMediaType,
  146. }
  147. }
  148. private configureAsset(asset: BrandAsset) {
  149. const nextVersion = this.state[`${asset}DataVersion`] + 1
  150. this.state[`${asset}Configured`] = true
  151. this.state[`${asset}Url`] = `/api/auth/v1/system-config/assets/${asset}?v=${nextVersion}`
  152. this.state[`${asset}MediaType`] = 'image/png'
  153. this.state[`${asset}OriginalName`] = asset === 'logo' ? 'brand-logo.png' : 'site-icon.png'
  154. this.state[`${asset}Size`] = PNG_BYTES.length
  155. this.state[`${asset}Sha256`] = `mock-${asset}-sha256`
  156. this.state[`${asset}DataVersion`] = nextVersion
  157. }
  158. private removeAsset(asset: BrandAsset) {
  159. this.state[`${asset}Configured`] = false
  160. this.state[`${asset}Url`] = null
  161. this.state[`${asset}MediaType`] = null
  162. this.state[`${asset}OriginalName`] = null
  163. this.state[`${asset}Size`] = null
  164. this.state[`${asset}Sha256`] = null
  165. this.state[`${asset}DataVersion`] += 1
  166. }
  167. private async handle(route: Route) {
  168. const request = route.request()
  169. const url = new URL(request.url())
  170. const path = url.pathname
  171. const method = request.method()
  172. if (method === 'GET' && path === '/api/auth/v1/system-config/public') {
  173. return fulfillJson(route, this.publicState())
  174. }
  175. if (method === 'GET' && path === '/api/auth/v1/auth/login-context') {
  176. return fulfillJson(route, loginContext)
  177. }
  178. if (method === 'GET' && path === '/api/auth/v1/auth/me') {
  179. return fulfillJson(route, profile(this.canUpdate))
  180. }
  181. if (method === 'GET' && path === '/api/auth/v1/menus/navigation') {
  182. return fulfillJson(route, [])
  183. }
  184. if (method === 'GET' && path === '/api/auth/v1/system-config') {
  185. this.managementReadCount += 1
  186. return fulfillJson(route, this.state)
  187. }
  188. if (method === 'PUT' && path === '/api/auth/v1/system-config/name') {
  189. this.mutations.push('name')
  190. if (this.conflictNextNameSave) {
  191. this.conflictNextNameSave = false
  192. this.state.systemName = '其他管理员刚保存的名称'
  193. this.state.systemNameConfigured = true
  194. this.state.systemNameDataVersion += 1
  195. return fulfillJson(route, null, 409, '系统名称版本冲突', 'CONFIG_VERSION_CONFLICT')
  196. }
  197. const body = request.postDataJSON() as { systemName?: string }
  198. const nextName = String(body.systemName ?? '').trim()
  199. this.state.systemName = nextName || DEFAULT_SYSTEM_NAME
  200. this.state.systemNameConfigured = Boolean(nextName)
  201. this.state.systemNameDataVersion += 1
  202. return fulfillJson(route, this.state)
  203. }
  204. if (method === 'PUT' && path === '/api/auth/v1/system-config/default-theme') {
  205. this.mutations.push('default-theme')
  206. const body = request.postDataJSON() as { defaultTheme?: ConfigurationState['defaultTheme'] }
  207. this.state.defaultTheme = body.defaultTheme ?? 'military'
  208. this.state.defaultThemeConfigured = true
  209. this.state.defaultThemeDataVersion += 1
  210. return fulfillJson(route, this.state)
  211. }
  212. if (method === 'POST' && path === '/api/auth/v1/system-config/digital-human-widget/parse') {
  213. const body = request.postDataJSON() as { embedCode?: string }
  214. const code = String(body.embedCode ?? '')
  215. if (!code.includes('/embed/') || !code.includes('virtual-instructor-widget')) {
  216. return fulfillJson(route, null, 400, '不是数字人系统生成的有效悬浮图标代码', 'VALIDATION_FAILED')
  217. }
  218. return fulfillJson(route, {
  219. baseUrl: 'https://human.example.test',
  220. agentSlug: 'dh-global-demo',
  221. position: 'bottom-right',
  222. label: '问数字教员',
  223. })
  224. }
  225. if (method === 'PUT' && path === '/api/auth/v1/system-config/digital-human-widget') {
  226. this.mutations.push('digital-human-widget')
  227. this.state.digitalHumanWidgetConfigured = true
  228. this.state.digitalHumanWidget = {
  229. baseUrl: 'https://human.example.test',
  230. agentSlug: 'dh-global-demo',
  231. position: 'bottom-right',
  232. label: '问数字教员',
  233. }
  234. this.state.digitalHumanWidgetDataVersion += 1
  235. return fulfillJson(route, this.state)
  236. }
  237. if (method === 'DELETE' && path === '/api/auth/v1/system-config/digital-human-widget') {
  238. this.mutations.push('remove-digital-human-widget')
  239. this.state.digitalHumanWidgetConfigured = false
  240. this.state.digitalHumanWidget = null
  241. this.state.digitalHumanWidgetDataVersion += 1
  242. return fulfillJson(route, this.state)
  243. }
  244. if (method === 'POST' && /^\/api\/auth\/v1\/system-config\/(logo|favicon)$/.test(path)) {
  245. const asset = path.endsWith('/logo') ? 'logo' : 'favicon'
  246. this.mutations.push(`upload-${asset}`)
  247. this.configureAsset(asset)
  248. return fulfillJson(route, this.state)
  249. }
  250. if (method === 'DELETE' && /^\/api\/auth\/v1\/system-config\/(logo|favicon)$/.test(path)) {
  251. const asset = path.endsWith('/logo') ? 'logo' : 'favicon'
  252. this.mutations.push(`remove-${asset}`)
  253. this.removeAsset(asset)
  254. return fulfillJson(route, this.state)
  255. }
  256. if (method === 'GET' && /^\/api\/auth\/v1\/system-config\/assets\/(logo|favicon)$/.test(path)) {
  257. return route.fulfill({
  258. status: 200,
  259. contentType: 'image/png',
  260. headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
  261. body: PNG_BYTES,
  262. })
  263. }
  264. return fulfillJson(route, {})
  265. }
  266. }
  267. const configuredBrandImage = (page: Page) => page.locator('.platform-brand .brand-mark.configured img')
  268. const faviconLink = (page: Page) => page.locator('link[rel~="icon"]')
  269. test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)', () => {
  270. test('公开配置为空时登录页使用当前内置品牌', async ({ page }) => {
  271. const mock = new SystemConfigurationMock()
  272. await mock.install(page, false)
  273. await page.goto('/login')
  274. await expect(page.locator('.login-brand')).toContainText(DEFAULT_SYSTEM_NAME)
  275. await expect(page).toHaveTitle(`登录 - ${DEFAULT_SYSTEM_NAME}`)
  276. await expect(faviconLink(page)).toHaveAttribute('href', '/favicon.svg')
  277. await expect(faviconLink(page)).toHaveAttribute('type', 'image/svg+xml')
  278. expect(mock.mutations).toEqual([])
  279. })
  280. test('已配置的悬浮数字人只挂载到登录后的业务外壳', async ({ page }) => {
  281. const mock = new SystemConfigurationMock()
  282. mock.state.digitalHumanWidgetConfigured = true
  283. mock.state.digitalHumanWidget = {
  284. baseUrl: 'https://human.example.test',
  285. agentSlug: 'dh-global-demo',
  286. position: 'bottom-right',
  287. label: '问数字教员',
  288. }
  289. mock.state.digitalHumanWidgetDataVersion = 1
  290. await mock.install(page, false)
  291. await page.goto('/login')
  292. await expect(page.getByTestId('global-digital-human-widget')).toHaveCount(0)
  293. })
  294. test('已配置的平台 LOGO 保持透明且不显示边框', async ({ page }) => {
  295. const mock = new SystemConfigurationMock()
  296. Object.assign(mock.state, {
  297. logoConfigured: true,
  298. logoUrl: '/api/auth/v1/system-config/assets/logo?v=1',
  299. logoMediaType: 'image/png',
  300. logoOriginalName: 'brand-logo.png',
  301. logoSize: PNG_BYTES.length,
  302. logoSha256: 'mock-logo-sha256',
  303. logoDataVersion: 1,
  304. })
  305. await mock.install(page)
  306. await page.goto('/system/config')
  307. await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true')
  308. const configuredBrandMark = configuredBrandImage(page).locator('..')
  309. await expect(configuredBrandMark).toHaveCSS('border-top-width', '0px')
  310. await expect(configuredBrandMark).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
  311. await expect(configuredBrandMark).toHaveCSS('box-shadow', 'none')
  312. })
  313. test('管理员可从菜单进入并保存、清空名称以及上传、移除 PNG 品牌资源', async ({ page }, testInfo) => {
  314. const mock = new SystemConfigurationMock()
  315. await mock.install(page)
  316. await page.setViewportSize({ width: 1920, height: 1080 })
  317. await page.goto('/system/config')
  318. await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true')
  319. await expect(page).toHaveURL(/\/system\/config(?:\?|$)/)
  320. await expect(page.locator('.current-group-navigation')).toContainText('系统配置')
  321. await expect(page.getByRole('link', { name: '系统配置' })).toHaveClass(/active/)
  322. await expect(page.getByTestId('system-config-form')).toBeVisible()
  323. await expect(page.locator('.config-navigation')).toHaveCount(0)
  324. await expect(page.locator('.system-config-form-card')).toHaveCount(1)
  325. await expect(page.locator('.system-config-form-row')).toHaveCount(7)
  326. const pageBox = await page.getByTestId('system-config-page').boundingBox()
  327. const formBox = await page.getByTestId('system-config-form').boundingBox()
  328. expect(pageBox).not.toBeNull()
  329. expect(formBox).not.toBeNull()
  330. expect(formBox!.width / pageBox!.width).toBeGreaterThan(0.95)
  331. const nameInput = page.getByTestId('system-config-name')
  332. await nameInput.fill('装备维修教学测试平台')
  333. await page.getByTestId('system-config-save-name').click()
  334. await expect(page.locator('.platform-brand')).toContainText('装备维修教学测试平台')
  335. await expect(page).toHaveTitle('系统配置 - 装备维修教学测试平台')
  336. await page.getByTestId('system-config-clear-name').click()
  337. const restoreNameDialog = page.locator('.el-message-box:visible')
  338. await restoreNameDialog.getByRole('button', { name: '恢复默认' }).click()
  339. await expect(page.locator('.platform-brand')).toContainText(DEFAULT_SYSTEM_NAME)
  340. await expect(page).toHaveTitle(`系统配置 - ${DEFAULT_SYSTEM_NAME}`)
  341. await page.getByTestId('system-config-default-theme').getByRole('button', { name: /科技蓝/ }).click()
  342. await page.getByTestId('system-config-save-default-theme').click()
  343. await expect(page.locator('html')).toHaveAttribute('data-color-theme', 'technology')
  344. await expect(page.locator('.el-message')).toHaveCount(0, { timeout: 10_000 })
  345. await page.getByTestId('system-config-open-widget-drawer').click()
  346. await expect(page.getByTestId('system-config-widget-drawer')).toBeVisible()
  347. await page.getByTestId('system-config-widget-code').fill('这不是悬浮数字人代码')
  348. await page.getByTestId('system-config-parse-widget').click()
  349. await expect(page.getByText('不是数字人系统生成的有效悬浮图标代码', { exact: false })).toBeVisible()
  350. await page.getByTestId('system-config-widget-code').fill(`
  351. <script>
  352. (function () {
  353. var BASE = "https://human.example.test";
  354. var AGENT = "dh-global-demo";
  355. var POSITION = "bottom-right";
  356. var LABEL = "问数字教员";
  357. var frame = document.createElement('iframe');
  358. frame.id = 'virtual-instructor-widget';
  359. frame.src = BASE + '/embed/' + AGENT;
  360. window.__unsafePastedScriptWasExecuted = true;
  361. })();
  362. <\/script>
  363. `)
  364. await page.getByTestId('system-config-parse-widget').click()
  365. await expect(page.getByText('解析成功,请确认接入信息')).toBeVisible()
  366. const widgetPreviewFrame = page.getByTestId('system-config-widget-preview').locator('iframe')
  367. await expect(widgetPreviewFrame).toHaveAttribute('src', /human\.example\.test\/embed\/dh-global-demo\?.*open=1/)
  368. expect(await page.evaluate(() => (window as Window & { __unsafePastedScriptWasExecuted?: boolean }).__unsafePastedScriptWasExecuted)).toBeUndefined()
  369. await captureScreenshot(page, testInfo, 'system-config-widget-preview')
  370. await page.getByTestId('system-config-save-widget').click()
  371. await expect(page.getByTestId('system-config-widget-drawer')).toBeHidden()
  372. await expect(page.getByTestId('system-config-digital-human-widget-card')).toContainText('已接入数字教员')
  373. const globalWidget = page.getByTestId('global-digital-human-widget')
  374. await expect(globalWidget).toHaveAttribute('src', /human\.example\.test\/embed\/dh-global-demo/)
  375. const globalWidgetBody = page.frameLocator('[data-testid="global-digital-human-widget"]').locator('body')
  376. await expect(globalWidgetBody).toHaveAttribute('data-host-theme', 'light')
  377. await expect(globalWidgetBody).toHaveAttribute('data-host-accent', '#197fc8')
  378. const widgetBeforeDrag = await globalWidget.boundingBox()
  379. const widgetHandle = await globalWidget.elementHandle()
  380. const widgetContent = await widgetHandle?.contentFrame()
  381. expect(widgetBeforeDrag).not.toBeNull()
  382. expect(widgetContent).not.toBeNull()
  383. await widgetContent!.evaluate(() => {
  384. parent.postMessage({ source: 'virtual-instructor-widget', type: 'drag-start', handle: 'launcher' }, '*')
  385. parent.postMessage({ source: 'virtual-instructor-widget', type: 'drag-move', handle: 'launcher', deltaX: -180, deltaY: -120 }, '*')
  386. parent.postMessage({ source: 'virtual-instructor-widget', type: 'drag-end', handle: 'launcher' }, '*')
  387. })
  388. await expect.poll(async () => (await globalWidget.boundingBox())?.x).toBeLessThan(widgetBeforeDrag!.x - 100)
  389. const storedWidgetPosition = await page.evaluate(() => Object.entries(localStorage)
  390. .find(([key]) => key.startsWith('unreal-tran:web:digital-human-widget-position:v1:'))?.[1] ?? '')
  391. expect(storedWidgetPosition).toContain('offsetX')
  392. await page.getByTestId('system-config-remove-widget').click()
  393. await page.locator('.el-message-box:visible').getByRole('button', { name: '确认移除' }).click()
  394. await expect(page.getByTestId('global-digital-human-widget')).toHaveCount(0)
  395. const logoCard = page.getByTestId('system-config-logo-card')
  396. await logoCard.locator('input[type="file"]').setInputFiles({
  397. name: 'brand-logo.png',
  398. mimeType: 'image/png',
  399. buffer: PNG_BYTES,
  400. })
  401. const logoDialog = page.getByTestId('system-config-upload-dialog')
  402. await expect(logoDialog).toBeVisible()
  403. await logoDialog.getByRole('button', { name: '确认上传' }).click()
  404. await expect(logoCard).toContainText('brand-logo.png')
  405. await expect(configuredBrandImage(page)).toHaveAttribute('src', /\/api\/auth\/v1\/system-config\/assets\/logo\?v=1$/)
  406. await page.getByTestId('system-config-remove-logo').click()
  407. await page.locator('.el-message-box:visible').getByRole('button', { name: '确认移除' }).click()
  408. await expect(configuredBrandImage(page)).toHaveCount(0)
  409. await expect(logoCard).toContainText('使用系统内置 LOGO')
  410. const faviconCard = page.getByTestId('system-config-favicon-card')
  411. await faviconCard.locator('input[type="file"]').setInputFiles({
  412. name: 'site-icon.png',
  413. mimeType: 'image/png',
  414. buffer: PNG_BYTES,
  415. })
  416. const faviconDialog = page.getByTestId('system-config-upload-dialog')
  417. await faviconDialog.getByRole('button', { name: '确认上传' }).click()
  418. await expect(faviconCard).toContainText('site-icon.png')
  419. await expect(faviconLink(page)).toHaveAttribute('type', 'image/png')
  420. await expect(faviconLink(page)).toHaveAttribute('href', /\/api\/auth\/v1\/system-config\/assets\/favicon\?v=1$/)
  421. await page.getByTestId('system-config-remove-favicon').click()
  422. await page.locator('.el-message-box:visible').getByRole('button', { name: '确认移除' }).click()
  423. await expect(faviconLink(page)).toHaveAttribute('type', 'image/svg+xml')
  424. await expect(faviconLink(page)).toHaveAttribute('href', '/favicon.svg')
  425. await expect(faviconCard).toContainText('使用系统内置网站图标')
  426. expect(mock.mutations).toEqual([
  427. 'name', 'name', 'default-theme', 'digital-human-widget', 'remove-digital-human-widget',
  428. 'upload-logo', 'remove-logo', 'upload-favicon', 'remove-favicon',
  429. ])
  430. await expect(page.locator('.el-message')).toHaveCount(0, { timeout: 10_000 })
  431. await captureScreenshot(page, testInfo, 'system-config-defaults-restored')
  432. })
  433. test('保存遇到 409 时刷新服务端配置并提示重新确认', async ({ page }) => {
  434. const mock = new SystemConfigurationMock()
  435. mock.conflictNextNameSave = true
  436. await mock.install(page)
  437. await page.goto('/system/config')
  438. await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true')
  439. const readsBeforeSave = mock.managementReadCount
  440. await page.getByTestId('system-config-name').fill('本次输入会冲突')
  441. await page.getByTestId('system-config-save-name').click()
  442. await expect(page.getByText('配置已被其他管理员修改,页面已刷新,请确认后重新操作', { exact: true })).toBeVisible()
  443. await expect(page.getByTestId('system-config-name')).toHaveValue('其他管理员刚保存的名称')
  444. await expect(page.locator('.platform-brand')).toContainText('其他管理员刚保存的名称')
  445. await expect(page).toHaveTitle('系统配置 - 其他管理员刚保存的名称')
  446. expect(mock.managementReadCount).toBeGreaterThan(readsBeforeSave)
  447. })
  448. test('只有查看权限时页面为只读且不暴露更新操作', async ({ page }, testInfo) => {
  449. const mock = new SystemConfigurationMock(false)
  450. await mock.install(page)
  451. await page.goto('/system/config')
  452. await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true')
  453. await expect(page.getByRole('link', { name: '系统配置' })).toBeVisible()
  454. await expect(page.getByTestId('system-config-name')).toBeDisabled()
  455. await expect(page.getByTestId('system-config-save-name')).toHaveCount(0)
  456. await expect(page.getByTestId('system-config-clear-name')).toHaveCount(0)
  457. await expect(page.getByTestId('system-config-save-default-theme')).toHaveCount(0)
  458. await expect(page.getByTestId('system-config-open-widget-drawer')).toHaveCount(0)
  459. await expect(page.getByTestId('system-config-upload-logo')).toHaveCount(0)
  460. await expect(page.getByTestId('system-config-upload-favicon')).toHaveCount(0)
  461. expect(mock.mutations).toEqual([])
  462. await captureScreenshot(page, testInfo, 'system-config-read-only')
  463. })
  464. test('简化表单在移动端暗色模式下无横向溢出', async ({ page }, testInfo) => {
  465. const mock = new SystemConfigurationMock()
  466. await mock.install(page)
  467. await page.addInitScript(() => {
  468. localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({
  469. navigationMode: 'top-side',
  470. themeMode: 'dark',
  471. sidebarCollapsed: false,
  472. }))
  473. })
  474. await page.setViewportSize({ width: 375, height: 812 })
  475. await page.goto('/system/config')
  476. await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true')
  477. await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
  478. await expect(page.getByTestId('system-config-form')).toBeVisible()
  479. await expect(page.locator('.config-navigation')).toHaveCount(0)
  480. expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
  481. await captureScreenshot(page, testInfo, 'system-config-mobile-dark')
  482. })
  483. })