|
- import assert from 'node:assert/strict'
- import test from 'node:test'
- import { readFileSync } from 'node:fs'
- import { createRequire } from 'node:module'
-
- const requireWeb = createRequire(new URL('../../unreal_tran_web/package.json', import.meta.url))
- const ts = requireWeb('typescript')
- const features = new URL('../../unreal_tran_web/src/features/', import.meta.url)
- const compileText = source => ts.transpileModule(source, {
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
- }).outputText
- const compile = path => compileText(readFileSync(new URL(path, features), 'utf8'))
- const dataUrl = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
- const documentUrl = dataUrl(compile('training/trainingDocument.ts'))
- const bridgeUrl = dataUrl(compile('training-legacy/bridge.ts').replace(/from ['"]\.\.\/training\/trainingDocument['"]/, `from '${documentUrl}'`))
- const { toFormalContent, toLegacyProject } = await import(bridgeUrl)
- const { normalizeTrainingContent } = await import(documentUrl)
- const project = content => ({ id: 'compat-test', name: 'Compatibility', type: 'TRAINING', trainingMode: 'VIRTUAL', status: 'DRAFT', content })
- const save = (legacy, previous) => toFormalContent(legacy, 'VIRTUAL', previous).content
- const sceneObjects = ['A', 'B'].map(id => ({
- id, sceneId: id, name: `Target ${id}`, visible: true,
- semantic: { operable: true, interactionId: `scene:${id}`, capabilities: ['inspect'], deviceId: `DEV-${id}` },
- }))
-
- for (const runtime of ['guide-legacy/demo-src', 'virtual-training-legacy/upstream']) {
- const { createTrainingProject, normalizeTrainingProject } = await import(new URL(`${runtime}/training-project-io.js`, features))
- const fixture = () => {
- const value = createTrainingProject({ trainingMode: 'virtual' })
- value.steps = value.steps.slice(0, 1)
- return value
- }
- const reopen = content => normalizeTrainingProject(toLegacyProject(project(content)))
-
- test(`${runtime}: legacy operation text is visible to the formal editor and survives reopen`, () => {
- const value = fixture()
- value.steps[0].instruction = '确认狐狸目标位置并记录观察结果'
- value.steps[0].guidance = '使用当前镜头观察目标'
- const content = save(value)
- assert.equal(content.steps[0].summary, value.steps[0].instruction)
- assert.equal(content.steps[0].guidance, value.steps[0].guidance)
- assert.equal(reopen(content).steps[0].instruction, value.steps[0].instruction)
- })
-
- test(`${runtime}: distinct existing summary and instruction survive an unchanged roundtrip`, () => {
- const content = save(fixture())
- Object.assign(content.steps[0], { summary: '正式已有说明', instruction: '已有详细说明', guidance: '已有引导' })
- const original = structuredClone(content)
- const next = save(reopen(content), content)
- for (const key of ['summary', 'instruction', 'guidance']) assert.equal(next.steps[0][key], content.steps[0][key])
- assert.deepEqual(content, original)
- })
-
- test(`${runtime}: summary-only historical documents open with their real operation text`, () => {
- const content = save(fixture())
- content.steps[0].summary = '正式说明用于旧编辑器显示'
- for (const instruction of [undefined, '']) {
- if (instruction === undefined) delete content.steps[0].instruction
- else content.steps[0].instruction = instruction
- const reopened = reopen(content)
- assert.equal(reopened.steps[0].instruction, content.steps[0].summary)
- assert.equal(save(reopened, content).steps[0].summary, content.steps[0].summary)
- }
- })
-
- test(`${runtime}: clearing operation and optional guidance does not restore demonstration text`, () => {
- const content = save(fixture())
- const fields = ['summary', 'instruction', 'guidance', 'warning', 'destination', 'success']
- for (const key of fields) content.steps[0][key] = ''
- const reopened = reopen(content)
- const next = save(reopened, content)
- for (const key of fields) {
- assert.equal(reopened.steps[0][key], '', key)
- assert.equal(next.steps[0][key], '', key)
- }
- delete content.steps[0].instruction
- assert.equal(reopen(content).steps[0].instruction, '')
- })
-
- test(`${runtime}: formally selected target wins over stale legacy references`, () => {
- const source = fixture()
- source.scenario.publishedSceneId = 'scene-test'
- source.scenario.sceneResource = { sceneId: 'scene-test', snapshot: { objects: sceneObjects } }
- Object.assign(source.steps[0], { targetId: 'scene:A', targetRef: { interactionId: 'scene:A' }, mode: 'inspect', targetNodeId: 'scene:A' })
- const content = save(normalizeTrainingProject(source))
- Object.assign(content.steps[0], { targetId: 'scene:B', targetName: 'Target B' })
- content.steps[0].action.targetId = 'scene:B'
- const next = save(reopen(content), content)
- assert.equal(next.steps[0].targetId, 'scene:B')
- assert.equal(next.steps[0].action.targetId, 'scene:B')
- assert.equal(next.steps[0].targetRef.interactionId, 'scene:B')
- assert.equal(next.steps[0].targetRef.sceneObjectId, 'B')
- assert.equal(next.steps[0].targetRef.deviceId, 'DEV-B')
- assert.equal(next.steps[0].targetNodeId, 'scene:B')
- assert.equal(next.steps[0].targetNodeName, 'Target B')
- assert.equal(content.steps[0].targetRef.interactionId, 'scene:A')
- })
-
- test(`${runtime}: an explicitly cleared or missing target is not replaced by its stale reference`, () => {
- const source = fixture()
- source.scenario.publishedSceneId = 'scene-test'
- source.scenario.sceneResource = { sceneId: 'scene-test', snapshot: { objects: sceneObjects } }
- for (const id of ['', 'scene:missing']) {
- Object.assign(source.steps[0], { targetId: id, targetRef: { interactionId: 'scene:A' }, mode: 'inspect' })
- const normalized = normalizeTrainingProject(source)
- assert.equal(normalized.steps[0].targetId, id)
- assert.equal(normalized.steps[0].targetRef, null)
- assert.equal(normalized.steps[0].action.targetId, id)
- }
- })
- }
-
- test('formal normalizer migrates old operation text while preserving explicit clears and distinct text', () => {
- const content = { steps: [
- { id: 'instruction-only', instruction: '旧说明' },
- { id: 'empty-summary', summary: '', instruction: '旧说明二' },
- { id: 'both-empty', summary: '', instruction: '' },
- { id: 'distinct', summary: '简述', instruction: '详情' },
- ] }
- const next = normalizeTrainingContent(content, 'VIRTUAL')
- assert.deepEqual(next.steps.map(step => [step.summary, step.instruction]), [
- ['旧说明', '旧说明'], ['旧说明二', '旧说明二'], ['', ''], ['简述', '详情'],
- ])
- })
-
- test('old local buffers retain saved summary unless the operation instruction was edited', () => {
- const content = normalizeTrainingContent({ steps: [{ id: 'step-a', summary: '正式说明', instruction: '详细说明' }] }, 'VIRTUAL')
- const unchanged = { steps: [{ id: 'step-a', instruction: '详细说明' }] }
- assert.equal(save(unchanged, content).steps[0].summary, '正式说明')
- const changed = { steps: [{ id: 'step-a', instruction: '实际编辑新说明' }] }
- assert.equal(save(changed, content).steps[0].summary, '实际编辑新说明')
- })
-
- test('actual legacy input handler synchronizes operation text only when that field is edited', () => {
- const source = readFileSync(new URL('guide-legacy/demo-src/training-editor.js', features), 'utf8')
- const start = source.indexOf(' updateField(input, { render = false } = {}) {')
- const end = source.indexOf('\n onDragStart(', start)
- assert.ok(start >= 0 && end > start)
- const editor = new Function('setPath', `return ({${source.slice(start, end)}})`)((object, key, value) => { object[key] = value })
- editor.selectedStep = { id: 'test', summary: '原正式说明', instruction: '原详细说明' }
- editor.project = { id: 'test' }
- editor.markDirty = () => {}
- editor.updateField({ dataset: { scope: 'step', field: 'guidance' }, type: 'text', value: '新引导' })
- assert.equal(editor.selectedStep.summary, '原正式说明')
- for (const value of ['新的操作说明', '']) {
- editor.updateField({ dataset: { scope: 'step', field: 'instruction' }, type: 'text', value })
- assert.equal(editor.selectedStep.summary, value)
- assert.equal(editor.selectedStep.instruction, value)
- }
- })
-
- const studio = readFileSync(new URL('../views/content/TrainingStudioView.vue', features), 'utf8')
- test('actual studio description handler synchronizes both document fields including clear', () => {
- const start = studio.indexOf('const setStepSummary =')
- const end = studio.indexOf('const selectTarget =', start)
- assert.ok(start >= 0 && end > start)
- const selected = { value: { summary: 'old', instruction: 'different' } }
- const handler = new Function('selectedStep', 'markDirty', `${compileText(studio.slice(start, end))}; return setStepSummary`)(selected, () => {})
- for (const value of ['正式新操作说明', '']) {
- handler(value)
- assert.equal(selected.value.summary, value)
- assert.equal(selected.value.instruction, value)
- }
- assert.match(studio, /@update:model-value="setStepSummary"/)
- })
-
- test('actual studio target selection synchronizes semantic, node and action references', () => {
- const start = studio.indexOf('const selectTarget =')
- const end = studio.indexOf('const selectMode =', start)
- assert.ok(start >= 0 && end > start)
- const selected = { value: { targetId: 'scene:A', targetRef: { interactionId: 'scene:A' }, mode: 'inspect', action: { targetId: 'scene:A' },
- toolPlacement: { attachedTargetId: 'scene:A', pose: 'target', position: [1, 2, 3] } } }
- const catalog = { value: new Map([['scene:B', { id: 'scene:B', label: 'Target B', modes: ['inspect'] }]]) }
- const handler = new Function('selectedStep', 'targetById', 'sceneContent', 'markDirty', 'selectMode', `${compileText(studio.slice(start, end))}; return selectTarget`)(
- selected, catalog, { value: { objects: sceneObjects } }, () => {}, () => { throw new Error('Unexpected mode change') },
- )
- handler('scene:B')
- assert.equal(selected.value.targetId, 'scene:B')
- assert.equal(selected.value.action.targetId, 'scene:B')
- assert.equal(selected.value.targetRef.interactionId, 'scene:B')
- assert.equal(selected.value.targetRef.sceneObjectId, 'B')
- assert.equal(selected.value.targetRef.deviceId, 'DEV-B')
- assert.equal(selected.value.targetNodeId, 'scene:B')
- assert.equal(selected.value.targetNodeName, 'Target B')
- assert.equal(selected.value.toolPlacement.attachedTargetId, '')
- assert.deepEqual(selected.value.toolPlacement.position, [1, 2, 3])
- })
|