From dd790eeae8cc5984737d24422a4fb791a006d205 Mon Sep 17 00:00:00 2001 From: leiyun Date: Sun, 30 Aug 2026 12:30:46 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A1=A5=E5=85=85=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8E=E5=86=85=E5=AE=B9=E5=88=B6=E4=BD=9C?= =?UTF-8?q?=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/content-authoring.spec.ts | 32 ++++--- tests/content-training-studio.spec.ts | 17 ++++ tests/preferences.spec.ts | 98 +++++++++++++++---- tests/system-config.spec.ts | 131 +++++++++++++++++++++++++- 4 files changed, 248 insertions(+), 30 deletions(-) diff --git a/tests/content-authoring.spec.ts b/tests/content-authoring.spec.ts index 82e3b61..5ac668d 100644 --- a/tests/content-authoring.spec.ts +++ b/tests/content-authoring.spec.ts @@ -27,7 +27,7 @@ const data = { path: '/content/models', routeSegment: 'models', singular: '模型', - code: `E2E_MODEL_${upperToken}`.slice(0, 64), + codePrefix: 'MODEL', name: `UTE2E 挖掘机模型 ${upperToken}`, category: '整机装备', description: `Playwright 三维模型编辑器验收数据 ${upperToken}`, @@ -36,7 +36,7 @@ const data = { path: '/content/scenes', routeSegment: 'scenes', singular: '场景', - code: `E2E_SCENE_${upperToken}`.slice(0, 64), + codePrefix: 'SCENE', name: `UTE2E 数字车间 ${upperToken}`, category: '维修车间', description: `Playwright 数字车间场景编辑器验收数据 ${upperToken}`, @@ -119,14 +119,15 @@ async function waitForContentList(page: Page): Promise { await expect(page.locator('.content-projects-page > .el-alert--error:visible')).toHaveCount(0) } -async function createContentProject(page: Page, seed: ProjectSeed): Promise { +async function createContentProject(page: Page, seed: ProjectSeed): Promise<{ id: string; code: string }> { await page.goto(seed.path) await waitForContentList(page) await page.getByRole('button', { name: `新建${seed.singular}`, exact: true }).click() const dialog = visibleDialog(page) 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 chooseSelectOption(page, dialog, '业务分类', seed.category) await fillFormItem(dialog, '项目说明', seed.description) @@ -139,9 +140,12 @@ async function createContentProject(page: Page, seed: ProjectSeed): Promise const projectId = String(body.data?.project.id ?? '') + const projectCode = String(body.data?.project.code ?? '') 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 }) - return projectId + return { id: projectId, code: projectCode } } async function waitForModelEditor(page: Page): Promise { @@ -343,6 +347,8 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 let apiSession: ContentApiSession | null = null let modelProjectId: string | null = null let sceneProjectId: string | null = null + let modelProjectCode = '' + let sceneProjectCode = '' let testFailure: unknown const completed: string[] = [] const cleanup: CleanupRecord[] = [] @@ -357,7 +363,9 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 await loginAsAdmin(page) 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) await expect(editor.locator('.brand-copy')).toContainText('装备模型编辑工具') completed.push('模型编辑器无 iframe,开放 Shadow DOM 内原生 canvas 正常显示') @@ -484,7 +492,9 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 let workshopPartName = '' 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) completed.push('场景编辑器无 iframe,原生 scene-editor-canvas 正常显示') @@ -605,11 +615,11 @@ test('装备模型与数字车间编辑器可原生编辑、保存、发布并 testFailure = error } finally { 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 { - 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) diff --git a/tests/content-training-studio.spec.ts b/tests/content-training-studio.spec.ts index e500a2e..c46fad9 100644 --- a/tests/content-training-studio.spec.ts +++ b/tests/content-training-studio.spec.ts @@ -376,6 +376,23 @@ test.describe('训练编排工作台', () => { 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 }) => { const mock = new TrainingMock() await mock.install(page) diff --git a/tests/preferences.spec.ts b/tests/preferences.spec.ts index 9d4aefb..bc39fdd 100644 --- a/tests/preferences.spec.ts +++ b/tests/preferences.spec.ts @@ -1,5 +1,5 @@ import { captureScreenshot, expect, test } from './fixtures' -import { loginAsAdmin } from './helpers' +import type { Page, Route } from '@playwright/test' const navigationModes = [ { label: '顶部一级 + 左侧二级', value: 'top-side' }, @@ -9,9 +9,60 @@ const navigationModes = [ const waitForDashboard = async (page: import('@playwright/test').Page) => { 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) => { @@ -25,13 +76,15 @@ const contrastRatio = (foreground: string, background: string) => { return (lighter! + .05) / (darker! + .05) } -test('三种导航模式与三种主题可以持久化', async ({ page }, testInfo) => { +test('三种导航模式与四种主题可以持久化', async ({ page }, testInfo) => { 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('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) { await test.step(`导航模式:${mode.label}`, async () => { @@ -79,9 +132,10 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn }) 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 for (const theme of themes) { @@ -90,17 +144,27 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn const drawer = page.locator('.el-drawer:visible') 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-color-theme', theme.value) await page.keyboard.press('Escape') await page.reload() await expect(page.locator('html')).toHaveAttribute('data-theme', theme.expected) + await expect(page.locator('html')).toHaveAttribute('data-color-theme', theme.value) await waitForDashboard(page) + expect(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe(theme.primary) 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(await page.locator('html').evaluate((element) => getComputedStyle(element).getPropertyValue('--primary').trim())).toBe('#55d0bd') } await captureScreenshot(page, testInfo, `theme-${theme.value}`) }) @@ -109,6 +173,6 @@ test('三种导航模式与三种主题可以持久化', async ({ page }, testIn // 保持默认值,避免 headed 模式下人工继续检查时继承最后一次设置。 await page.getByRole('button', { name: '界面与导航设置' }).click() 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() }) diff --git a/tests/system-config.spec.ts b/tests/system-config.spec.ts index 8db9f35..573ae9d 100644 --- a/tests/system-config.spec.ts +++ b/tests/system-config.spec.ts @@ -14,6 +14,17 @@ 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 @@ -34,6 +45,12 @@ 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, @@ -109,11 +126,21 @@ class SystemConfigurationMock { }) } 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, @@ -182,6 +209,46 @@ class SystemConfigurationMock { 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}`) @@ -223,6 +290,23 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' 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, { @@ -257,7 +341,7 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' 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(3) + 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() @@ -276,6 +360,46 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)' 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(` +