From 0b3dec83d14835ff285c8fea87e40c7acd876fc8 Mon Sep 17 00:00:00 2001 From: leiyun Date: Fri, 28 Aug 2026 15:02:37 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A6=86=E7=9B=96=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E6=95=99=E5=91=98=E7=9F=A5=E8=AF=86=E5=BA=93=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/teaching-contract-mock.ts | 52 ++++++++++++++++++++++-- tests/teaching-module.spec.ts | 46 +++++++++++++++++---- tests/teaching-preview-workbench.spec.ts | 7 ++-- 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/tests/teaching-contract-mock.ts b/tests/teaching-contract-mock.ts index 532b811..e6d32fe 100644 --- a/tests/teaching-contract-mock.ts +++ b/tests/teaching-contract-mock.ts @@ -155,7 +155,11 @@ const assignment = (id: string, channel: Channel, kind: AssignmentKind, audience executionMode: kind === 'FORMAL_EXAM' ? 'EXAM' : 'PRACTICE', audienceType, collaborationMode, contentProjectId: 'training-project-1', contentVersionId: 'training-version-1', definitionSnapshot: twoStepDefinition(channel), definitionChecksum: `checksum-${id}`, - digitalHumanAllowed: kind !== 'FORMAL_EXAM', ownerUserId: '2', ownerUsername: 'teacher', ownerName: '周教员', + digitalHumanAllowed: kind !== 'FORMAL_EXAM', + digitalInstructorAgentSlug: channel === 'VIRTUAL' && kind !== 'FORMAL_EXAM' ? 'demo-equipment-instructor' : '', + digitalInstructorAgentName: channel === 'VIRTUAL' && kind !== 'FORMAL_EXAM' ? '装备维修虚拟教员' : '', + digitalInstructorKnowledgeSource: 'AGENT', + ownerUserId: '2', ownerUsername: 'teacher', ownerName: '周教员', departmentId: '110', departmentName: '维修教研室', status: 'PUBLISHED', scheduleStartAt: 1786383600, dueAt: 1786473600, publishedAt: 1786380000, withdrawnAt: null, archivedAt: null, lifecycleReason: '', version: 3, @@ -171,7 +175,11 @@ const assignment = (id: string, channel: Channel, kind: AssignmentKind, audience const assignmentBrief = (item: JsonRecord) => ({ id: item.id, code: item.code, name: item.name, channel: item.channel, assignmentKind: item.assignmentKind, executionMode: item.executionMode, audienceType: item.audienceType, collaborationMode: item.collaborationMode, - digitalHumanAllowed: item.digitalHumanAllowed, status: item.status, + digitalHumanAllowed: item.digitalHumanAllowed, + digitalInstructorAgentSlug: item.digitalInstructorAgentSlug, + digitalInstructorAgentName: item.digitalInstructorAgentName, + digitalInstructorKnowledgeSource: item.digitalInstructorKnowledgeSource, + status: item.status, scheduleStartAt: item.scheduleStartAt, dueAt: item.dueAt, serverNow: item.serverNow, }) @@ -232,6 +240,9 @@ export class TeachingContractMock { name: '支腿液压缸中修训练(虚拟仿真)', description: '面向液压密封件检查与更换的八步虚拟训练任务。', definitionSnapshot: virtualPreviewDefinition(), + digitalInstructorAgentSlug: 'demo-equipment-instructor', + digitalInstructorAgentName: '装备维修虚拟教员', + digitalInstructorKnowledgeSource: 'WORKSHOP', runCount: 0, pendingReviewCount: 0, listed: false, @@ -454,7 +465,7 @@ export class TeachingContractMock { await page.route('**/api/tran/v1/content/catalog**', (route) => fulfill(route, pageResult([{ projectId: 'training-project-1', versionId: 'training-version-1', type: 'TRAINING', code: 'TRAIN-001', name: '挖掘机液压系统训练', categoryCode: 'VIRTUAL', coverUri: '', versionNumber: 2, versionStatus: 'PUBLISHED', publishedAt: '2026-08-11T08:00:00+08:00', content: { steps: [] }, }]), 'ute2e-teaching-catalog')) - await page.route(/\/embed\/dh-b69424ed0904(?:\?.*)?$/, (route) => { + await page.route(/\/embed\/(?:dh-b69424ed0904|demo-equipment-instructor)(?:\?.*)?$/, (route) => { this.digitalInstructorFrameLoads += 1 return route.fulfill({ status: 200, @@ -478,6 +489,20 @@ export class TeachingContractMock { }]), 'ute2e-guides') if (method === 'GET' && path === '/guides/guide-project-1') return fulfill(route, { projectId: 'guide-project-1', versionId: 'guide-version-1', code: 'GUIDE-001', name: '挖掘机检修作业指导书', description: '标准维修作业流程', categoryCode: '维修作业', coverUri: '', versionNumber: 3, versionCode: 'V3', publishedAt: 1786387200, + editorDocument: null, + resources: {}, + assets: [], + capabilities: { + publishedAssetDownload: false, + serverGeneratesOfd: false, + serverGeneratesOfflineHtml: false, + clientGenerationSupported: false, + availableDeliveryFormats: [], + }, + reading: { + projectId: 'guide-project-1', versionId: 'guide-version-1', status: 'NOT_STARTED', + startedAt: null, lastReadAt: null, completedAt: null, + }, content: { equipment: '履带式挖掘机', specification: '教学训练型', chapters: [ @@ -495,6 +520,12 @@ export class TeachingContractMock { ], }, }, 'ute2e-guide-detail') + if (method === 'GET' && path === '/guides/guide-project-1/deliveries') return fulfill(route, [], 'ute2e-guide-deliveries') + if (method === 'GET' && path === '/guides/guide-project-1/assets') return fulfill(route, [], 'ute2e-guide-assets') + if (method === 'GET' && path === '/guides/guide-project-1/statistics') return fulfill(route, { + projectId: 'guide-project-1', versionId: 'guide-version-1', readerCount: 6, completedCount: 4, + completionRate: 67, feedbackCount: 2, openFeedbackCount: 1, handledFeedbackCount: 1, + }, 'ute2e-guide-statistics') if (method === 'GET' && path === '/assignments/summary') { const channel = url.searchParams.get('channel') @@ -502,6 +533,17 @@ export class TeachingContractMock { const tasks = [...this.assignments.values()].filter((item) => item.listed !== false && (!channel || item.channel === channel) && item.assignmentKind === kind) return fulfill(route, { assignments: tasks.length, draft: 0, published: tasks.length, withdrawn: 0, archived: 0, pendingRuns: 0, activeRuns: this.visibleRuns().filter((item) => item.status === 'IN_PROGRESS').length, submittedRuns: this.visibleRuns().filter((item) => item.status === 'SUBMITTED').length, reviewedRuns: this.visibleRuns().filter((item) => item.status === 'REVIEWED').length, byChannel: Object.fromEntries(tasks.map((item) => [String(item.channel), 1])) }, 'ute2e-summary') } + if (method === 'GET' && path === '/digital-instructors') return fulfill(route, [{ + id: 'agent-equipment-instructor', + slug: 'demo-equipment-instructor', + name: '装备维修虚拟教员', + description: '装备维修训练在线答疑', + accessMode: 'internal', + interactionModes: ['text', 'voice'], + knowledgeSources: ['AGENT', 'WORKSHOP'], + agentKnowledgeBaseCount: 1, + workshopKnowledgeProviderName: '数字车间维修知识', + }], 'ute2e-digital-instructors') if (method === 'GET' && path === '/assignments') { const channel = url.searchParams.get('channel') const kind = url.searchParams.get('assignmentKind') ?? 'TRAINING' @@ -509,7 +551,9 @@ export class TeachingContractMock { return fulfill(route, pageResult(tasks), 'ute2e-assignments') } if (method === 'POST' && path === '/assignments') { - if (typeof body.scheduleStartAt !== 'number' || typeof body.dueAt !== 'number' || body.scheduleStartAt >= 1_000_000_000_000 || body.dueAt >= 1_000_000_000_000) return reject(route, 400, '正式考核时间必须是 Unix 秒') + if (body.assignmentKind === 'FORMAL_EXAM' + && (typeof body.scheduleStartAt !== 'number' || typeof body.dueAt !== 'number' + || body.scheduleStartAt >= 1_000_000_000_000 || body.dueAt >= 1_000_000_000_000)) return reject(route, 400, '正式考核时间必须是 Unix 秒') if (body.assignmentKind === 'FORMAL_EXAM' && (body.executionMode !== 'EXAM' || body.digitalHumanAllowed !== false)) return reject(route, 400, '正式考核必须使用 EXAM 且禁用数字教员') const created = { ...assignment('assignment-created-exam', String(body.channel) as Channel, String(body.assignmentKind) as AssignmentKind, 'ASSIGNED', 'INDIVIDUAL'), ...body, id: 'assignment-created-exam', version: 0, status: 'DRAFT' } this.assignments.set(String(created.id), created) diff --git a/tests/teaching-module.spec.ts b/tests/teaching-module.spec.ts index 2b6bafc..29c8f08 100644 --- a/tests/teaching-module.spec.ts +++ b/tests/teaching-module.spec.ts @@ -91,18 +91,19 @@ test.describe('教学实施模块状态化 UI 契约', () => { await expect(page.getByRole('heading', { name: '虚拟仿真可用指导书', exact: true })).toBeVisible() await expect(page.getByTestId('teaching-guide-library').locator('.guide-card-v2')).toHaveCount(1) await page.getByRole('button', { name: /打开阅读/ }).first().click() - const guideReader = page.locator('.guide-reader-v2') + const guideReader = page.getByTestId('teaching-guide-reader') await expect(guideReader).toBeVisible() - await expect(guideReader).toContainText('01 安全准备') + await guideReader.getByRole('button', { name: /断电隔离/ }).click() + await expect(guideReader).toContainText('安全准备') await expect(guideReader).toContainText('断电隔离') - await guideReader.getByRole('button', { name: '下一步', exact: true }).click() - await expect(guideReader.getByRole('heading', { name: '压力释放', exact: true })).toBeVisible() + await guideReader.getByRole('button', { name: '下一页', exact: true }).click() + await expect(guideReader.locator('.a4-paper')).toHaveAttribute('aria-label', /压力释放/) await expect(guideReader).toContainText('目标压力') - await guideReader.getByRole('button', { name: '下一步', exact: true }).click() - await expect(guideReader.getByRole('heading', { name: '复装与复测', exact: true })).toBeVisible() - await expect(guideReader.getByRole('button', { name: '下一步', exact: true })).toBeDisabled() + await guideReader.getByRole('button', { name: '下一页', exact: true }).click() + await expect(guideReader.locator('.a4-paper')).toHaveAttribute('aria-label', /复装与复测/) + await expect(guideReader.getByRole('button', { name: '下一页', exact: true })).toBeDisabled() await captureScreenshot(page, testInfo, 'teaching-guide-full-reader') - await page.getByRole('button', { name: /返回指导书列表/ }).click() + await page.getByRole('button', { name: /返回列表/ }).click() await expect(page.locator('.guide-list-card')).toBeVisible() await page.getByTestId('teaching-tab-tasks').click() await expect(page).toHaveURL(/\/teaching\/virtual-training$/) @@ -136,6 +137,35 @@ test.describe('教学实施模块状态化 UI 契约', () => { await captureScreenshot(page, testInfo, 'teaching-module-guide-record-roundtrip') }) + test('虚拟任务从数字人目录绑定教员并按任务选择数字车间知识库', async ({ page }) => { + const contract = new TeachingContractMock('admin') + await contract.install(page) + + await page.goto('/teaching/virtual-training') + await page.getByRole('button', { name: '新建虚拟仿真任务', exact: true }).click() + const dialog = visibleDialog(page) + await expect(dialog.getByTestId('digital-instructor-agent-select')).toContainText('装备维修虚拟教员') + await expect(dialog.getByTestId('digital-instructor-knowledge-source')).toContainText('数字人内置知识库') + await expect(dialog.getByTestId('digital-instructor-knowledge-source')).toContainText('数字车间知识库') + await dialog.getByText('数字车间知识库', { exact: true }).click() + await expect(dialog).toContainText('当前授权:数字车间维修知识') + + await dialog.getByRole('textbox', { name: /任务名称/ }).fill('虚拟教员双知识来源契约任务') + await chooseSelectOption(page, dialog, '训练内容', '挖掘机液压系统训练 · V2') + await chooseSelectOption(page, dialog, '参训学员', '红方队长 · 维修教研室') + await page.keyboard.press('Escape') + await dialog.getByRole('button', { name: '保存草稿', exact: true }).click() + await expect(dialog).toBeHidden() + + expect(contract.last('GET', '/digital-instructors')).toBeTruthy() + expect(contract.last('POST', '/assignments')?.body).toMatchObject({ + channel: 'VIRTUAL', + digitalHumanAllowed: true, + digitalInstructorAgentSlug: 'demo-equipment-instructor', + digitalInstructorKnowledgeSource: 'WORKSHOP', + }) + }) + test('对抗任务监控可切换运行、进入三维只读观察并按最终 API 契约注入故障', async ({ page }, testInfo) => { await page.setViewportSize({ width: 1920, height: 1080 }) const contract = new TeachingContractMock('teacher') diff --git a/tests/teaching-preview-workbench.spec.ts b/tests/teaching-preview-workbench.spec.ts index f86eb57..663ae3b 100644 --- a/tests/teaching-preview-workbench.spec.ts +++ b/tests/teaching-preview-workbench.spec.ts @@ -17,7 +17,8 @@ test.describe('教员只读预览工作台', () => { await expect(workbench.locator('.preview-step-panel')).toContainText('卸压确认与故障基线采集') await expect(workbench.locator('.preview-step-panel')).toContainText('密封复验与中修闭环') await expect(workbench.locator('.preview-runtime-viewport canvas')).toBeVisible() - await expect(workbench.locator('.preview-digital-teacher')).toContainText('点火系统维修教研员') + await expect(workbench.locator('.preview-digital-teacher')).toContainText('装备维修虚拟教员') + await expect(workbench.locator('.preview-digital-teacher')).toContainText('数字车间知识库') await expect(workbench.locator('.preview-digital-teacher')).toContainText('确认压力表回零,记录故障压力和泄漏现象。') await expect(workbench.locator('.preview-digital-teacher img')).toHaveJSProperty('complete', true) await expect(workbench.locator('iframe')).toHaveCount(0) @@ -26,11 +27,11 @@ test.describe('教员只读预览工作台', () => { const writesBefore = contract.requests.filter((request) => ['POST', 'PUT', 'DELETE'].includes(request.method)).length await workbench.getByRole('button', { name: '打开在线答疑', exact: true }).click() - const onlineDrawer = page.locator('.el-drawer:visible').filter({ hasText: '点火系统维修教研员 · 在线答疑' }).last() + const onlineDrawer = page.locator('.el-drawer:visible').filter({ hasText: '装备维修虚拟教员 · 数字车间知识库' }).last() const digitalInstructor = onlineDrawer.getByTestId('digital-instructor-embed') await expect(digitalInstructor).toBeVisible() const instructorFrame = digitalInstructor.locator('iframe') - await expect(instructorFrame).toHaveAttribute('src', /\/embed\/dh-b69424ed0904\?open=1$/) + await expect(instructorFrame).toHaveAttribute('src', /\/embed\/demo-equipment-instructor\?open=1&knowledgeSource=workshop$/) await expect(instructorFrame).toHaveAttribute('allow', 'autoplay; microphone') await expect(digitalInstructor.locator('.digital-instructor-embed__status')).toHaveText('页面已加载') await expect(page.frameLocator('[data-testid="digital-instructor-embed"] iframe').getByTestId('mock-ai-person')).toHaveText('虚拟教员在线答疑')