| @@ -27,7 +27,7 @@ const data = { | |||||
| path: '/content/models', | path: '/content/models', | ||||
| routeSegment: 'models', | routeSegment: 'models', | ||||
| singular: '模型', | singular: '模型', | ||||
| code: `E2E_MODEL_${upperToken}`.slice(0, 64), | |||||
| codePrefix: 'MODEL', | |||||
| name: `UTE2E 挖掘机模型 ${upperToken}`, | name: `UTE2E 挖掘机模型 ${upperToken}`, | ||||
| category: '整机装备', | category: '整机装备', | ||||
| description: `Playwright 三维模型编辑器验收数据 ${upperToken}`, | description: `Playwright 三维模型编辑器验收数据 ${upperToken}`, | ||||
| @@ -36,7 +36,7 @@ const data = { | |||||
| path: '/content/scenes', | path: '/content/scenes', | ||||
| routeSegment: 'scenes', | routeSegment: 'scenes', | ||||
| singular: '场景', | singular: '场景', | ||||
| code: `E2E_SCENE_${upperToken}`.slice(0, 64), | |||||
| codePrefix: 'SCENE', | |||||
| name: `UTE2E 数字车间 ${upperToken}`, | name: `UTE2E 数字车间 ${upperToken}`, | ||||
| category: '维修车间', | category: '维修车间', | ||||
| description: `Playwright 数字车间场景编辑器验收数据 ${upperToken}`, | description: `Playwright 数字车间场景编辑器验收数据 ${upperToken}`, | ||||
| @@ -119,14 +119,15 @@ async function waitForContentList(page: Page): Promise<void> { | |||||
| await expect(page.locator('.content-projects-page > .el-alert--error:visible')).toHaveCount(0) | await expect(page.locator('.content-projects-page > .el-alert--error:visible')).toHaveCount(0) | ||||
| } | } | ||||
| async function createContentProject(page: Page, seed: ProjectSeed): Promise<string> { | |||||
| async function createContentProject(page: Page, seed: ProjectSeed): Promise<{ id: string; code: string }> { | |||||
| await page.goto(seed.path) | await page.goto(seed.path) | ||||
| await waitForContentList(page) | await waitForContentList(page) | ||||
| await page.getByRole('button', { name: `新建${seed.singular}`, exact: true }).click() | await page.getByRole('button', { name: `新建${seed.singular}`, exact: true }).click() | ||||
| const dialog = visibleDialog(page) | const dialog = visibleDialog(page) | ||||
| await expect(dialog).toContainText(`新建${seed.singular}`) | await expect(dialog).toContainText(`新建${seed.singular}`) | ||||
| await fillFormItem(dialog, '项目编码', seed.code) | |||||
| await expect(dialog.getByText('项目编码将在创建后由系统自动生成')).toBeVisible() | |||||
| await expect(dialog.getByText('项目编码', { exact: true })).toHaveCount(0) | |||||
| await fillFormItem(dialog, '项目名称', seed.name) | await fillFormItem(dialog, '项目名称', seed.name) | ||||
| await chooseSelectOption(page, dialog, '业务分类', seed.category) | await chooseSelectOption(page, dialog, '业务分类', seed.category) | ||||
| await fillFormItem(dialog, '项目说明', seed.description) | await fillFormItem(dialog, '项目说明', seed.description) | ||||
| @@ -139,9 +140,12 @@ async function createContentProject(page: Page, seed: ProjectSeed): Promise<stri | |||||
| const createdResponse = await expectSuccessfulResponse(createdResponsePromise, `新建${seed.singular}`) | const createdResponse = await expectSuccessfulResponse(createdResponsePromise, `新建${seed.singular}`) | ||||
| const body = await createdResponse.json() as ApiEnvelope<ContentDetailRecord> | const body = await createdResponse.json() as ApiEnvelope<ContentDetailRecord> | ||||
| const projectId = String(body.data?.project.id ?? '') | const projectId = String(body.data?.project.id ?? '') | ||||
| const projectCode = String(body.data?.project.code ?? '') | |||||
| expect(projectId, `新建${seed.singular}后应返回工程 ID`).not.toBe('') | expect(projectId, `新建${seed.singular}后应返回工程 ID`).not.toBe('') | ||||
| expect(projectCode).toMatch(new RegExp(`^${seed.codePrefix}-[A-F0-9]{16}$`)) | |||||
| expect(createdResponse.request().postDataJSON()).not.toHaveProperty('code') | |||||
| await expect(page).toHaveURL(new RegExp(`/content/${seed.routeSegment}/${projectId}/edit(?:\\?|$)`), { timeout: 20_000 }) | await expect(page).toHaveURL(new RegExp(`/content/${seed.routeSegment}/${projectId}/edit(?:\\?|$)`), { timeout: 20_000 }) | ||||
| return projectId | |||||
| return { id: projectId, code: projectCode } | |||||
| } | } | ||||
| async function waitForModelEditor(page: Page): Promise<Locator> { | async function waitForModelEditor(page: Page): Promise<Locator> { | ||||
| @@ -343,6 +347,8 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 | |||||
| let apiSession: ContentApiSession | null = null | let apiSession: ContentApiSession | null = null | ||||
| let modelProjectId: string | null = null | let modelProjectId: string | null = null | ||||
| let sceneProjectId: string | null = null | let sceneProjectId: string | null = null | ||||
| let modelProjectCode = '' | |||||
| let sceneProjectCode = '' | |||||
| let testFailure: unknown | let testFailure: unknown | ||||
| const completed: string[] = [] | const completed: string[] = [] | ||||
| const cleanup: CleanupRecord[] = [] | const cleanup: CleanupRecord[] = [] | ||||
| @@ -357,7 +363,9 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 | |||||
| await loginAsAdmin(page) | await loginAsAdmin(page) | ||||
| await test.step('新建模型工程并确认原生 Three.js 画布', async () => { | await test.step('新建模型工程并确认原生 Three.js 画布', async () => { | ||||
| modelProjectId = await createContentProject(page, data.model) | |||||
| const created = await createContentProject(page, data.model) | |||||
| modelProjectId = created.id | |||||
| modelProjectCode = created.code | |||||
| const editor = await waitForModelEditor(page) | const editor = await waitForModelEditor(page) | ||||
| await expect(editor.locator('.brand-copy')).toContainText('装备模型编辑工具') | await expect(editor.locator('.brand-copy')).toContainText('装备模型编辑工具') | ||||
| completed.push('模型编辑器无 iframe,开放 Shadow DOM 内原生 canvas 正常显示') | completed.push('模型编辑器无 iframe,开放 Shadow DOM 内原生 canvas 正常显示') | ||||
| @@ -484,7 +492,9 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 | |||||
| let workshopPartName = '' | let workshopPartName = '' | ||||
| await test.step('新建场景,切换真实车间并验证部件软删除刷新恢复', async () => { | await test.step('新建场景,切换真实车间并验证部件软删除刷新恢复', async () => { | ||||
| sceneProjectId = await createContentProject(page, data.scene) | |||||
| const created = await createContentProject(page, data.scene) | |||||
| sceneProjectId = created.id | |||||
| sceneProjectCode = created.code | |||||
| let editor = await waitForSceneEditor(page) | let editor = await waitForSceneEditor(page) | ||||
| completed.push('场景编辑器无 iframe,原生 scene-editor-canvas 正常显示') | completed.push('场景编辑器无 iframe,原生 scene-editor-canvas 正常显示') | ||||
| @@ -605,11 +615,11 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 | |||||
| testFailure = error | testFailure = error | ||||
| } finally { | } finally { | ||||
| if (apiSession) { | if (apiSession) { | ||||
| cleanup.push(await removeContentProject(request, apiSession, '场景工程', sceneProjectId, data.scene.code)) | |||||
| cleanup.push(await removeContentProject(request, apiSession, '模型工程', modelProjectId, data.model.code)) | |||||
| cleanup.push(await removeContentProject(request, apiSession, '场景工程', sceneProjectId, sceneProjectCode)) | |||||
| cleanup.push(await removeContentProject(request, apiSession, '模型工程', modelProjectId, modelProjectCode)) | |||||
| } else { | } else { | ||||
| cleanup.push({ kind: '场景工程', id: sceneProjectId, code: data.scene.code, outcome: sceneProjectId ? 'failed' : 'not-created', message: 'API 会话未建立' }) | |||||
| cleanup.push({ kind: '模型工程', id: modelProjectId, code: data.model.code, outcome: modelProjectId ? 'failed' : 'not-created', message: 'API 会话未建立' }) | |||||
| cleanup.push({ kind: '场景工程', id: sceneProjectId, code: sceneProjectCode, outcome: sceneProjectId ? 'failed' : 'not-created', message: 'API 会话未建立' }) | |||||
| cleanup.push({ kind: '模型工程', id: modelProjectId, code: modelProjectCode, outcome: modelProjectId ? 'failed' : 'not-created', message: 'API 会话未建立' }) | |||||
| } | } | ||||
| await closeContentApiSession(request, apiSession) | await closeContentApiSession(request, apiSession) | ||||
| @@ -376,6 +376,23 @@ test.describe('训练编排工作台', () => { | |||||
| await page.screenshot({ path: path.join(SHOTS, '08-高级编排.png'), fullPage: true }) | await page.screenshot({ path: path.join(SHOTS, '08-高级编排.png'), fullPage: true }) | ||||
| }) | }) | ||||
| test('训练目标支持 Enter 换行并按非空行保存', async ({ page }) => { | |||||
| const mock = new TrainingMock() | |||||
| await mock.install(page) | |||||
| await page.goto('/content/training-projects/901/studio?view=overview') | |||||
| const objectives = page.locator('.el-form-item', { hasText: '训练目标(每行一条)' }).locator('textarea') | |||||
| await expect(objectives).toBeVisible() | |||||
| await objectives.fill('完成停机卸压') | |||||
| await objectives.press('Enter') | |||||
| await objectives.type('采集故障基线') | |||||
| await expect(objectives).toHaveValue('完成停机卸压\n采集故障基线') | |||||
| await page.locator('.content-action-bar').getByRole('button', { name: '保存', exact: true }).click() | |||||
| await expect(page.locator('.el-message--success').filter({ hasText: '训练编排已保存' })).toBeVisible() | |||||
| expect(mock.savedContent().objectives).toEqual(['完成停机卸压', '采集故障基线']) | |||||
| }) | |||||
| test('九个流程视图与工具、故障、评分视图均可渲染', async ({ page }) => { | test('九个流程视图与工具、故障、评分视图均可渲染', async ({ page }) => { | ||||
| const mock = new TrainingMock() | const mock = new TrainingMock() | ||||
| await mock.install(page) | await mock.install(page) | ||||
| @@ -1,5 +1,5 @@ | |||||
| import { captureScreenshot, expect, test } from './fixtures' | import { captureScreenshot, expect, test } from './fixtures' | ||||
| import { loginAsAdmin } from './helpers' | |||||
| import type { Page, Route } from '@playwright/test' | |||||
| const navigationModes = [ | const navigationModes = [ | ||||
| { label: '顶部一级 + 左侧二级', value: 'top-side' }, | { label: '顶部一级 + 左侧二级', value: 'top-side' }, | ||||
| @@ -9,9 +9,60 @@ const navigationModes = [ | |||||
| const waitForDashboard = async (page: import('@playwright/test').Page) => { | const waitForDashboard = async (page: import('@playwright/test').Page) => { | ||||
| await expect(page.locator('.platform-shell')).toBeVisible() | await expect(page.locator('.platform-shell')).toBeVisible() | ||||
| await expect(page.getByRole('heading', { name: '前后端联调状态' })).toBeVisible() | |||||
| await expect(page.getByText('2/2', { exact: true })).toBeVisible({ timeout: 15_000 }) | |||||
| await expect(page.locator('.dashboard-metrics article').first().locator('b')).not.toHaveText('0', { timeout: 15_000 }) | |||||
| } | |||||
| const envelope = (data: unknown) => JSON.stringify({ | |||||
| code: 200, | |||||
| message: '成功', | |||||
| data, | |||||
| timestamp: '2026-08-30T10:00:00+08:00', | |||||
| requestId: 'ute2e-preferences', | |||||
| }) | |||||
| const fulfillJson = (route: Route, data: unknown) => route.fulfill({ | |||||
| status: 200, | |||||
| contentType: 'application/json', | |||||
| body: envelope(data), | |||||
| }) | |||||
| const installMockedSession = async (page: Page) => { | |||||
| await page.addInitScript(() => { | |||||
| sessionStorage.setItem('unreal-tran:web:access-token:v1', 'ute2e-preferences-token') | |||||
| }) | |||||
| await page.route('**/api/auth/v1/**', (route) => { | |||||
| const path = new URL(route.request().url()).pathname | |||||
| if (path === '/api/auth/v1/system-config/public') { | |||||
| return fulfillJson(route, { | |||||
| systemName: '装备维修实训数字车间', | |||||
| loginDescription: '统一承载内容制作、虚实训练、训练考核与资源管理。', | |||||
| defaultTheme: 'military', | |||||
| logoConfigured: false, | |||||
| faviconConfigured: false, | |||||
| loginLogoConfigured: false, | |||||
| }) | |||||
| } | |||||
| if (path === '/api/auth/v1/auth/me') { | |||||
| return fulfillJson(route, { | |||||
| user: { | |||||
| id: '1', username: 'admin', displayName: '系统管理员', departmentId: '10', | |||||
| departmentName: '信息中心', mustChangePassword: false, version: 1, | |||||
| }, | |||||
| activeRoleId: '100', | |||||
| roles: [{ | |||||
| id: '100', code: 'admin', name: '管理员', shortName: '管', status: 1, | |||||
| builtIn: 1, isSuperAdmin: 1, dataScopeCode: 'ALL', | |||||
| }], | |||||
| permissions: ['dashboard.view'], | |||||
| authorizationMode: 'SINGLE_ACTIVE', | |||||
| loginTime: 1788064800, | |||||
| }) | |||||
| } | |||||
| if (path === '/api/auth/v1/menus/navigation') return fulfillJson(route, []) | |||||
| return fulfillJson(route, {}) | |||||
| }) | |||||
| await page.route('**/api/tran/v1/**', (route) => fulfillJson(route, {})) | |||||
| await page.goto('/dashboard') | |||||
| await waitForDashboard(page) | |||||
| } | } | ||||
| const contrastRatio = (foreground: string, background: string) => { | const contrastRatio = (foreground: string, background: string) => { | ||||
| @@ -25,13 +76,15 @@ const contrastRatio = (foreground: string, background: string) => { | |||||
| return (lighter! + .05) / (darker! + .05) | return (lighter! + .05) / (darker! + .05) | ||||
| } | } | ||||
| test('三种导航模式与三种主题可以持久化', async ({ page }, testInfo) => { | |||||
| test('三种导航模式与四种主题可以持久化', async ({ page }, testInfo) => { | |||||
| await page.emulateMedia({ colorScheme: 'dark' }) | await page.emulateMedia({ colorScheme: 'dark' }) | ||||
| await loginAsAdmin(page) | |||||
| await installMockedSession(page) | |||||
| await expect(page.locator('.platform-shell')).toHaveAttribute('data-navigation-mode', 'top-side') | await expect(page.locator('.platform-shell')).toHaveAttribute('data-navigation-mode', 'top-side') | ||||
| await expect(page.locator('html')).toHaveAttribute('data-theme', 'light') | await expect(page.locator('html')).toHaveAttribute('data-theme', 'light') | ||||
| await expect(page.locator('.platform-header')).toHaveCSS('background-color', 'rgb(255, 255, 255)') | |||||
| expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe('#0b7a70') | |||||
| await expect(page.locator('html')).toHaveAttribute('data-color-theme', 'military') | |||||
| expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe('#168a72') | |||||
| await expect(page.getByText('本地服务正常', { exact: true })).toHaveCount(0) | |||||
| await expect(page.getByText('平台服务正常', { exact: true })).toHaveCount(0) | |||||
| for (const mode of navigationModes) { | for (const mode of navigationModes) { | ||||
| await test.step(`导航模式:${mode.label}`, async () => { | await test.step(`导航模式:${mode.label}`, async () => { | ||||
| @@ -79,9 +132,10 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn | |||||
| }) | }) | ||||
| const themes = [ | const themes = [ | ||||
| { label: '明亮', value: 'light', expected: 'light' }, | |||||
| { label: '暗黑', value: 'dark', expected: 'dark' }, | |||||
| { label: '跟随系统', value: 'system', expected: 'dark' }, | |||||
| { label: '军工绿', value: 'military', expected: 'light', primary: '#168a72' }, | |||||
| { label: '科技蓝', value: 'technology', expected: 'light', primary: '#197fc8' }, | |||||
| { label: '深空灰', value: 'graphite', expected: 'light', primary: '#4f8995' }, | |||||
| { label: '暗色', value: 'dark', expected: 'dark', primary: '#55d0bd' }, | |||||
| ] as const | ] as const | ||||
| for (const theme of themes) { | for (const theme of themes) { | ||||
| @@ -90,17 +144,27 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn | |||||
| const drawer = page.locator('.el-drawer:visible') | const drawer = page.locator('.el-drawer:visible') | ||||
| await drawer.getByRole('button', { name: new RegExp(theme.label) }).click() | await drawer.getByRole('button', { name: new RegExp(theme.label) }).click() | ||||
| await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected) | await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected) | ||||
| await expect(page.locator('html')).toHaveAttribute('data-color-theme', theme.value) | |||||
| await page.keyboard.press('Escape') | await page.keyboard.press('Escape') | ||||
| await page.reload() | await page.reload() | ||||
| await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected) | await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected) | ||||
| await expect(page.locator('html')).toHaveAttribute('data-color-theme', theme.value) | |||||
| await waitForDashboard(page) | await waitForDashboard(page) | ||||
| expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe(theme.primary) | |||||
| if (theme.expected === 'dark') { | if (theme.expected === 'dark') { | ||||
| const colors = await page.locator('.dashboard-metrics article').first().evaluate((element) => ({ | |||||
| foreground: getComputedStyle(element).color, | |||||
| background: getComputedStyle(element).backgroundColor, | |||||
| })) | |||||
| const colors = await page.locator('html').evaluate(() => { | |||||
| const probe = document.createElement('div') | |||||
| probe.style.color = 'var(--text-primary)' | |||||
| probe.style.backgroundColor = 'var(--surface)' | |||||
| document.body.appendChild(probe) | |||||
| const result = { | |||||
| foreground: getComputedStyle(probe).color, | |||||
| background: getComputedStyle(probe).backgroundColor, | |||||
| } | |||||
| probe.remove() | |||||
| return result | |||||
| }) | |||||
| expect(contrastRatio(colors.foreground, colors.background)).toBeGreaterThanOrEqual(4.5) | expect(contrastRatio(colors.foreground, colors.background)).toBeGreaterThanOrEqual(4.5) | ||||
| expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe('#55d0bd') | |||||
| } | } | ||||
| await captureScreenshot(page, testInfo, `theme-${theme.value}`) | await captureScreenshot(page, testInfo, `theme-${theme.value}`) | ||||
| }) | }) | ||||
| @@ -109,6 +173,6 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn | |||||
| // 保持默认值,避免 headed 模式下人工继续检查时继承最后一次设置。 | // 保持默认值,避免 headed 模式下人工继续检查时继承最后一次设置。 | ||||
| await page.getByRole('button', { name: '界面与导航设置' }).click() | await page.getByRole('button', { name: '界面与导航设置' }).click() | ||||
| const drawer = page.locator('.el-drawer:visible') | const drawer = page.locator('.el-drawer:visible') | ||||
| await drawer.getByRole('button', { name: /明亮/ }).click() | |||||
| await drawer.getByRole('button', { name: /军工绿/ }).click() | |||||
| await drawer.getByRole('button', { name: /顶部一级 \+ 左侧二级/ }).click() | await drawer.getByRole('button', { name: /顶部一级 \+ 左侧二级/ }).click() | ||||
| }) | }) | ||||
| @@ -14,6 +14,17 @@ interface ConfigurationState { | |||||
| systemName: string | systemName: string | ||||
| systemNameConfigured: boolean | systemNameConfigured: boolean | ||||
| systemNameDataVersion: number | 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 | logoConfigured: boolean | ||||
| logoUrl: string | null | logoUrl: string | null | ||||
| logoMediaType: string | null | logoMediaType: string | null | ||||
| @@ -34,6 +45,12 @@ const initialConfiguration = (): ConfigurationState => ({ | |||||
| systemName: DEFAULT_SYSTEM_NAME, | systemName: DEFAULT_SYSTEM_NAME, | ||||
| systemNameConfigured: false, | systemNameConfigured: false, | ||||
| systemNameDataVersion: 0, | systemNameDataVersion: 0, | ||||
| defaultTheme: 'military', | |||||
| defaultThemeConfigured: true, | |||||
| defaultThemeDataVersion: 1, | |||||
| digitalHumanWidgetConfigured: false, | |||||
| digitalHumanWidget: null, | |||||
| digitalHumanWidgetDataVersion: 0, | |||||
| logoConfigured: false, | logoConfigured: false, | ||||
| logoUrl: null, | logoUrl: null, | ||||
| logoMediaType: null, | logoMediaType: null, | ||||
| @@ -109,11 +126,21 @@ class SystemConfigurationMock { | |||||
| }) | }) | ||||
| } | } | ||||
| await page.route('**/api/auth/v1/**', (route) => this.handle(route)) | 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: `<!doctype html><html><body style="margin:0;background:transparent"> | |||||
| <button style="width:188px;height:56px">问数字教员</button> | |||||
| <script>parent.postMessage({source:'virtual-instructor-widget',type:'resize',width:208,height:92}, '*')</script> | |||||
| </body></html>`, | |||||
| })) | |||||
| } | } | ||||
| private publicState() { | private publicState() { | ||||
| return { | return { | ||||
| systemName: this.state.systemName, | systemName: this.state.systemName, | ||||
| defaultTheme: this.state.defaultTheme, | |||||
| digitalHumanWidget: this.state.digitalHumanWidget, | |||||
| logoConfigured: this.state.logoConfigured, | logoConfigured: this.state.logoConfigured, | ||||
| logoUrl: this.state.logoUrl, | logoUrl: this.state.logoUrl, | ||||
| logoMediaType: this.state.logoMediaType, | logoMediaType: this.state.logoMediaType, | ||||
| @@ -182,6 +209,46 @@ class SystemConfigurationMock { | |||||
| this.state.systemNameDataVersion += 1 | this.state.systemNameDataVersion += 1 | ||||
| return fulfillJson(route, this.state) | 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)) { | if (method === 'POST' && /^\/api\/auth\/v1\/system-config\/(logo|favicon)$/.test(path)) { | ||||
| const asset = path.endsWith('/logo') ? 'logo' : 'favicon' | const asset = path.endsWith('/logo') ? 'logo' : 'favicon' | ||||
| this.mutations.push(`upload-${asset}`) | this.mutations.push(`upload-${asset}`) | ||||
| @@ -223,6 +290,23 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' | |||||
| expect(mock.mutations).toEqual([]) | 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 }) => { | test('已配置的平台 LOGO 保持透明且不显示边框', async ({ page }) => { | ||||
| const mock = new SystemConfigurationMock() | const mock = new SystemConfigurationMock() | ||||
| Object.assign(mock.state, { | Object.assign(mock.state, { | ||||
| @@ -257,7 +341,7 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' | |||||
| await expect(page.getByTestId('system-config-form')).toBeVisible() | await expect(page.getByTestId('system-config-form')).toBeVisible() | ||||
| await expect(page.locator('.config-navigation')).toHaveCount(0) | 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-card')).toHaveCount(1) | ||||
| await expect(page.locator('.system-config-form-row')).toHaveCount(3) | |||||
| await expect(page.locator('.system-config-form-row')).toHaveCount(7) | |||||
| const pageBox = await page.getByTestId('system-config-page').boundingBox() | const pageBox = await page.getByTestId('system-config-page').boundingBox() | ||||
| const formBox = await page.getByTestId('system-config-form').boundingBox() | const formBox = await page.getByTestId('system-config-form').boundingBox() | ||||
| expect(pageBox).not.toBeNull() | expect(pageBox).not.toBeNull() | ||||
| @@ -276,6 +360,46 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' | |||||
| await expect(page.locator('.platform-brand')).toContainText(DEFAULT_SYSTEM_NAME) | await expect(page.locator('.platform-brand')).toContainText(DEFAULT_SYSTEM_NAME) | ||||
| await expect(page).toHaveTitle(`系统配置 - ${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(` | |||||
| <script> | |||||
| (function () { | |||||
| var BASE = "https://human.example.test"; | |||||
| var AGENT = "dh-global-demo"; | |||||
| var POSITION = "bottom-right"; | |||||
| var LABEL = "问数字教员"; | |||||
| var frame = document.createElement('iframe'); | |||||
| frame.id = 'virtual-instructor-widget'; | |||||
| frame.src = BASE + '/embed/' + AGENT; | |||||
| window.__unsafePastedScriptWasExecuted = true; | |||||
| })(); | |||||
| <\/script> | |||||
| `) | |||||
| await page.getByTestId('system-config-parse-widget').click() | |||||
| await expect(page.getByText('解析成功,请确认接入信息')).toBeVisible() | |||||
| const widgetPreviewFrame = page.getByTestId('system-config-widget-preview').locator('iframe') | |||||
| await expect(widgetPreviewFrame).toHaveAttribute('src', /human\.example\.test\/embed\/dh-global-demo\?.*open=1/) | |||||
| expect(await page.evaluate(() => (window as Window & { __unsafePastedScriptWasExecuted?: boolean }).__unsafePastedScriptWasExecuted)).toBeUndefined() | |||||
| await captureScreenshot(page, testInfo, 'system-config-widget-preview') | |||||
| await page.getByTestId('system-config-save-widget').click() | |||||
| await expect(page.getByTestId('system-config-widget-drawer')).toBeHidden() | |||||
| await expect(page.getByTestId('system-config-digital-human-widget-card')).toContainText('已接入数字教员') | |||||
| await expect(page.getByTestId('global-digital-human-widget')).toHaveAttribute('src', /human\.example\.test\/embed\/dh-global-demo/) | |||||
| await page.getByTestId('system-config-remove-widget').click() | |||||
| await page.locator('.el-message-box:visible').getByRole('button', { name: '确认移除' }).click() | |||||
| await expect(page.getByTestId('global-digital-human-widget')).toHaveCount(0) | |||||
| const logoCard = page.getByTestId('system-config-logo-card') | const logoCard = page.getByTestId('system-config-logo-card') | ||||
| await logoCard.locator('input[type="file"]').setInputFiles({ | await logoCard.locator('input[type="file"]').setInputFiles({ | ||||
| name: 'brand-logo.png', | name: 'brand-logo.png', | ||||
| @@ -312,7 +436,8 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' | |||||
| await expect(faviconCard).toContainText('使用系统内置网站图标') | await expect(faviconCard).toContainText('使用系统内置网站图标') | ||||
| expect(mock.mutations).toEqual([ | expect(mock.mutations).toEqual([ | ||||
| 'name', 'name', 'upload-logo', 'remove-logo', 'upload-favicon', 'remove-favicon', | |||||
| 'name', 'name', 'default-theme', 'digital-human-widget', 'remove-digital-human-widget', | |||||
| 'upload-logo', 'remove-logo', 'upload-favicon', 'remove-favicon', | |||||
| ]) | ]) | ||||
| await expect(page.locator('.el-message')).toHaveCount(0, { timeout: 10_000 }) | await expect(page.locator('.el-message')).toHaveCount(0, { timeout: 10_000 }) | ||||
| await captureScreenshot(page, testInfo, 'system-config-defaults-restored') | await captureScreenshot(page, testInfo, 'system-config-defaults-restored') | ||||
| @@ -346,6 +471,8 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' | |||||
| await expect(page.getByTestId('system-config-name')).toBeDisabled() | await expect(page.getByTestId('system-config-name')).toBeDisabled() | ||||
| await expect(page.getByTestId('system-config-save-name')).toHaveCount(0) | await expect(page.getByTestId('system-config-save-name')).toHaveCount(0) | ||||
| await expect(page.getByTestId('system-config-clear-name')).toHaveCount(0) | await expect(page.getByTestId('system-config-clear-name')).toHaveCount(0) | ||||
| await expect(page.getByTestId('system-config-save-default-theme')).toHaveCount(0) | |||||
| await expect(page.getByTestId('system-config-open-widget-drawer')).toHaveCount(0) | |||||
| await expect(page.getByTestId('system-config-upload-logo')).toHaveCount(0) | await expect(page.getByTestId('system-config-upload-logo')).toHaveCount(0) | ||||
| await expect(page.getByTestId('system-config-upload-favicon')).toHaveCount(0) | await expect(page.getByTestId('system-config-upload-favicon')).toHaveCount(0) | ||||
| expect(mock.mutations).toEqual([]) | expect(mock.mutations).toEqual([]) | ||||