Sfoglia il codice sorgente

test: 覆盖虚拟教员知识库集成

main
leiyun 3 settimane fa
parent
commit
0b3dec83d1
3 ha cambiato i file con 90 aggiunte e 15 eliminazioni
  1. +48
    -4
      tests/teaching-contract-mock.ts
  2. +38
    -8
      tests/teaching-module.spec.ts
  3. +4
    -3
      tests/teaching-preview-workbench.spec.ts

+ 48
- 4
tests/teaching-contract-mock.ts Vedi File

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


+ 38
- 8
tests/teaching-module.spec.ts Vedi File

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


+ 4
- 3
tests/teaching-preview-workbench.spec.ts Vedi File

@@ -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('虚拟教员在线答疑')


Caricamento…
Annulla
Salva