Quellcode durchsuchen

test: 补充系统配置与内容制作回归

main
leiyun vor 2 Wochen
Ursprung
Commit
dd790eeae8
4 geänderte Dateien mit 248 neuen und 30 gelöschten Zeilen
  1. +21
    -11
      tests/content-authoring.spec.ts
  2. +17
    -0
      tests/content-training-studio.spec.ts
  3. +81
    -17
      tests/preferences.spec.ts
  4. +129
    -2
      tests/system-config.spec.ts

+ 21
- 11
tests/content-authoring.spec.ts Datei anzeigen

@@ -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<void> {
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 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<stri
const createdResponse = await expectSuccessfulResponse(createdResponsePromise, `新建${seed.singular}`)
const body = await createdResponse.json() as ApiEnvelope<ContentDetailRecord>
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<Locator> {
@@ -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)



+ 17
- 0
tests/content-training-studio.spec.ts Datei anzeigen

@@ -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)


+ 81
- 17
tests/preferences.spec.ts Datei anzeigen

@@ -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()
})

+ 129
- 2
tests/system-config.spec.ts Datei anzeigen

@@ -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: `<!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() {
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(`
<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')
await logoCard.locator('input[type="file"]').setInputFiles({
name: 'brand-logo.png',
@@ -312,7 +436,8 @@ test.describe('系统配置(状态化 Mock,不访问真实 API/数据库)'
await expect(faviconCard).toContainText('使用系统内置网站图标')

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 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-save-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-favicon')).toHaveCount(0)
expect(mock.mutations).toEqual([])


Laden…
Abbrechen
Speichern