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