import type { Page, Route } from '@playwright/test' import { captureScreenshot, expect, test } from './fixtures' const DEFAULT_SYSTEM_NAME = '某类装备维修实训数字车间' const PNG_BYTES = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64', ) type BrandAsset = 'logo' | 'favicon' interface ConfigurationState { systemName: string systemNameConfigured: boolean systemNameDataVersion: number defaultTheme: 'military' | 'technology' | 'graphite' | 'dark' defaultThemeConfigured: boolean defaultThemeDataVersion: number digitalHumanWidgetConfigured: boolean digitalHumanWidget: { baseUrl: string agentSlug: string position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' label: string } | null digitalHumanWidgetDataVersion: number logoConfigured: boolean logoUrl: string | null logoMediaType: string | null logoOriginalName: string | null logoSize: number | null logoSha256: string | null logoDataVersion: number faviconConfigured: boolean faviconUrl: string | null faviconMediaType: string | null faviconOriginalName: string | null faviconSize: number | null faviconSha256: string | null faviconDataVersion: number } const initialConfiguration = (): ConfigurationState => ({ systemName: DEFAULT_SYSTEM_NAME, systemNameConfigured: false, systemNameDataVersion: 0, defaultTheme: 'military', defaultThemeConfigured: true, defaultThemeDataVersion: 1, digitalHumanWidgetConfigured: false, digitalHumanWidget: null, digitalHumanWidgetDataVersion: 0, logoConfigured: false, logoUrl: null, logoMediaType: null, logoOriginalName: null, logoSize: null, logoSha256: null, logoDataVersion: 0, faviconConfigured: false, faviconUrl: null, faviconMediaType: null, faviconOriginalName: null, faviconSize: null, faviconSha256: null, faviconDataVersion: 0, }) const profile = (canUpdate: boolean) => ({ user: { id: '1', username: 'admin', displayName: '系统管理员', departmentId: '10', departmentName: '信息中心', mustChangePassword: false, version: 1, }, activeRoleId: '100', activeRole: { id: '100', code: 'admin', name: '管理员' }, roles: [{ id: '100', code: 'admin', name: '管理员', shortName: '管', status: 1, builtIn: 1, isSuperAdmin: 1, dataScopeCode: 'ALL', }], permissions: ['system.config', ...(canUpdate ? ['system.config.update'] : [])], authorizationMode: 'SINGLE_ACTIVE', loginTime: 1786387200, }) const loginContext = { defaultRoleCode: 'admin', roles: [{ code: 'admin', label: '管理员', accountLabel: '登录账号', organizationMode: 'NONE', organizationLabels: [] }], departments: [], } const envelope = (data: unknown, message = '成功', code: number | string = 200) => JSON.stringify({ code, message, data, timestamp: '2026-08-11T12:00:00+08:00', requestId: 'ute2e-system-config', }) const fulfillJson = (route: Route, data: unknown, status = 200, message = '成功', code: number | string = status) => route.fulfill({ status, contentType: 'application/json', body: envelope(data, message, code), }) class SystemConfigurationMock { readonly state = initialConfiguration() readonly mutations: string[] = [] readonly canUpdate: boolean conflictNextNameSave = false managementReadCount = 0 constructor(canUpdate = true) { this.canUpdate = canUpdate } async install(page: Page, authenticated = true) { if (authenticated) { await page.addInitScript(() => { sessionStorage.setItem('unreal-tran:web:access-token:v1', 'ute2e-system-config-token') }) } await page.route('**/api/auth/v1/**', (route) => this.handle(route)) await page.route('https://human.example.test/**', (route) => route.fulfill({ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: `
`, })) } private publicState() { return { systemName: this.state.systemName, defaultTheme: this.state.defaultTheme, digitalHumanWidget: this.state.digitalHumanWidget, logoConfigured: this.state.logoConfigured, logoUrl: this.state.logoUrl, logoMediaType: this.state.logoMediaType, faviconConfigured: this.state.faviconConfigured, faviconUrl: this.state.faviconUrl, faviconMediaType: this.state.faviconMediaType, } } private configureAsset(asset: BrandAsset) { const nextVersion = this.state[`${asset}DataVersion`] + 1 this.state[`${asset}Configured`] = true this.state[`${asset}Url`] = `/api/auth/v1/system-config/assets/${asset}?v=${nextVersion}` this.state[`${asset}MediaType`] = 'image/png' this.state[`${asset}OriginalName`] = asset === 'logo' ? 'brand-logo.png' : 'site-icon.png' this.state[`${asset}Size`] = PNG_BYTES.length this.state[`${asset}Sha256`] = `mock-${asset}-sha256` this.state[`${asset}DataVersion`] = nextVersion } private removeAsset(asset: BrandAsset) { this.state[`${asset}Configured`] = false this.state[`${asset}Url`] = null this.state[`${asset}MediaType`] = null this.state[`${asset}OriginalName`] = null this.state[`${asset}Size`] = null this.state[`${asset}Sha256`] = null this.state[`${asset}DataVersion`] += 1 } private async handle(route: Route) { const request = route.request() const url = new URL(request.url()) const path = url.pathname const method = request.method() if (method === 'GET' && path === '/api/auth/v1/system-config/public') { return fulfillJson(route, this.publicState()) } if (method === 'GET' && path === '/api/auth/v1/auth/login-context') { return fulfillJson(route, loginContext) } if (method === 'GET' && path === '/api/auth/v1/auth/me') { return fulfillJson(route, profile(this.canUpdate)) } if (method === 'GET' && path === '/api/auth/v1/menus/navigation') { return fulfillJson(route, []) } if (method === 'GET' && path === '/api/auth/v1/system-config') { this.managementReadCount += 1 return fulfillJson(route, this.state) } if (method === 'PUT' && path === '/api/auth/v1/system-config/name') { this.mutations.push('name') if (this.conflictNextNameSave) { this.conflictNextNameSave = false this.state.systemName = '其他管理员刚保存的名称' this.state.systemNameConfigured = true this.state.systemNameDataVersion += 1 return fulfillJson(route, null, 409, '系统名称版本冲突', 'CONFIG_VERSION_CONFLICT') } const body = request.postDataJSON() as { systemName?: string } const nextName = String(body.systemName ?? '').trim() this.state.systemName = nextName || DEFAULT_SYSTEM_NAME this.state.systemNameConfigured = Boolean(nextName) this.state.systemNameDataVersion += 1 return fulfillJson(route, this.state) } if (method === 'PUT' && path === '/api/auth/v1/system-config/default-theme') { this.mutations.push('default-theme') const body = request.postDataJSON() as { defaultTheme?: ConfigurationState['defaultTheme'] } this.state.defaultTheme = body.defaultTheme ?? 'military' this.state.defaultThemeConfigured = true this.state.defaultThemeDataVersion += 1 return fulfillJson(route, this.state) } if (method === 'POST' && path === '/api/auth/v1/system-config/digital-human-widget/parse') { const body = request.postDataJSON() as { embedCode?: string } const code = String(body.embedCode ?? '') if (!code.includes('/embed/') || !code.includes('virtual-instructor-widget')) { return fulfillJson(route, null, 400, '不是数字人系统生成的有效悬浮图标代码', 'VALIDATION_FAILED') } return fulfillJson(route, { baseUrl: 'https://human.example.test', agentSlug: 'dh-global-demo', position: 'bottom-right', label: '问数字教员', }) } if (method === 'PUT' && path === '/api/auth/v1/system-config/digital-human-widget') { this.mutations.push('digital-human-widget') this.state.digitalHumanWidgetConfigured = true this.state.digitalHumanWidget = { baseUrl: 'https://human.example.test', agentSlug: 'dh-global-demo', position: 'bottom-right', label: '问数字教员', } this.state.digitalHumanWidgetDataVersion += 1 return fulfillJson(route, this.state) } if (method === 'DELETE' && path === '/api/auth/v1/system-config/digital-human-widget') { this.mutations.push('remove-digital-human-widget') this.state.digitalHumanWidgetConfigured = false this.state.digitalHumanWidget = null this.state.digitalHumanWidgetDataVersion += 1 return fulfillJson(route, this.state) } if (method === 'POST' && /^\/api\/auth\/v1\/system-config\/(logo|favicon)$/.test(path)) { const asset = path.endsWith('/logo') ? 'logo' : 'favicon' this.mutations.push(`upload-${asset}`) this.configureAsset(asset) return fulfillJson(route, this.state) } if (method === 'DELETE' && /^\/api\/auth\/v1\/system-config\/(logo|favicon)$/.test(path)) { const asset = path.endsWith('/logo') ? 'logo' : 'favicon' this.mutations.push(`remove-${asset}`) this.removeAsset(asset) return fulfillJson(route, this.state) } if (method === 'GET' && /^\/api\/auth\/v1\/system-config\/assets\/(logo|favicon)$/.test(path)) { return route.fulfill({ status: 200, contentType: 'image/png', headers: { 'Cache-Control': 'public, max-age=31536000, immutable' }, body: PNG_BYTES, }) } return fulfillJson(route, {}) } } const configuredBrandImage = (page: Page) => page.locator('.platform-brand .brand-mark.configured img') const faviconLink = (page: Page) => page.locator('link[rel~="icon"]') test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)', () => { test('公开配置为空时登录页使用当前内置品牌', async ({ page }) => { const mock = new SystemConfigurationMock() await mock.install(page, false) await page.goto('/login') await expect(page.locator('.login-brand')).toContainText(DEFAULT_SYSTEM_NAME) await expect(page).toHaveTitle(`登录 - ${DEFAULT_SYSTEM_NAME}`) await expect(faviconLink(page)).toHaveAttribute('href', '/favicon.svg') await expect(faviconLink(page)).toHaveAttribute('type', 'image/svg+xml') expect(mock.mutations).toEqual([]) }) test('已配置的悬浮数字人只挂载到登录后的业务外壳', async ({ page }) => { const mock = new SystemConfigurationMock() mock.state.digitalHumanWidgetConfigured = true mock.state.digitalHumanWidget = { baseUrl: 'https://human.example.test', agentSlug: 'dh-global-demo', position: 'bottom-right', label: '问数字教员', } mock.state.digitalHumanWidgetDataVersion = 1 await mock.install(page, false) await page.goto('/login') await expect(page.getByTestId('global-digital-human-widget')).toHaveCount(0) }) test('已配置的平台 LOGO 保持透明且不显示边框', async ({ page }) => { const mock = new SystemConfigurationMock() Object.assign(mock.state, { logoConfigured: true, logoUrl: '/api/auth/v1/system-config/assets/logo?v=1', logoMediaType: 'image/png', logoOriginalName: 'brand-logo.png', logoSize: PNG_BYTES.length, logoSha256: 'mock-logo-sha256', logoDataVersion: 1, }) await mock.install(page) await page.goto('/system/config') await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true') const configuredBrandMark = configuredBrandImage(page).locator('..') await expect(configuredBrandMark).toHaveCSS('border-top-width', '0px') await expect(configuredBrandMark).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)') await expect(configuredBrandMark).toHaveCSS('box-shadow', 'none') }) test('管理员可从菜单进入并保存、清空名称以及上传、移除 PNG 品牌资源', async ({ page }, testInfo) => { const mock = new SystemConfigurationMock() await mock.install(page) await page.setViewportSize({ width: 1920, height: 1080 }) await page.goto('/system/config') await expect(page.getByTestId('system-config-page')).toHaveAttribute('data-ready', 'true') await expect(page).toHaveURL(/\/system\/config(?:\?|$)/) await expect(page.locator('.current-group-navigation')).toContainText('系统配置') await expect(page.getByRole('link', { name: '系统配置' })).toHaveClass(/active/) await expect(page.getByTestId('system-config-form')).toBeVisible() await expect(page.locator('.config-navigation')).toHaveCount(0) await expect(page.locator('.system-config-form-card')).toHaveCount(1) await expect(page.locator('.system-config-form-row')).toHaveCount(7) const pageBox = await page.getByTestId('system-config-page').boundingBox() const formBox = await page.getByTestId('system-config-form').boundingBox() expect(pageBox).not.toBeNull() expect(formBox).not.toBeNull() expect(formBox!.width / pageBox!.width).toBeGreaterThan(0.95) const nameInput = page.getByTestId('system-config-name') await nameInput.fill('装备维修教学测试平台') await page.getByTestId('system-config-save-name').click() await expect(page.locator('.platform-brand')).toContainText('装备维修教学测试平台') await expect(page).toHaveTitle('系统配置 - 装备维修教学测试平台') await page.getByTestId('system-config-clear-name').click() const restoreNameDialog = page.locator('.el-message-box:visible') await restoreNameDialog.getByRole('button', { name: '恢复默认' }).click() await expect(page.locator('.platform-brand')).toContainText(DEFAULT_SYSTEM_NAME) await expect(page).toHaveTitle(`系统配置 - ${DEFAULT_SYSTEM_NAME}`) await page.getByTestId('system-config-default-theme').getByRole('button', { name: /科技蓝/ }).click() await page.getByTestId('system-config-save-default-theme').click() await expect(page.locator('html')).toHaveAttribute('data-color-theme', 'technology') await expect(page.locator('.el-message')).toHaveCount(0, { timeout: 10_000 }) await page.getByTestId('system-config-open-widget-drawer').click() await expect(page.getByTestId('system-config-widget-drawer')).toBeVisible() await page.getByTestId('system-config-widget-code').fill('这不是悬浮数字人代码') await page.getByTestId('system-config-parse-widget').click() await expect(page.getByText('不是数字人系统生成的有效悬浮图标代码', { exact: false })).toBeVisible() await page.getByTestId('system-config-widget-code').fill(`