Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

187 rindas
10 KiB

  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFileSync } from 'node:fs'
  4. import { createRequire } from 'node:module'
  5. const requireWeb = createRequire(new URL('../../unreal_tran_web/package.json', import.meta.url))
  6. const ts = requireWeb('typescript')
  7. const features = new URL('../../unreal_tran_web/src/features/', import.meta.url)
  8. const compileText = source => ts.transpileModule(source, {
  9. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
  10. }).outputText
  11. const compile = path => compileText(readFileSync(new URL(path, features), 'utf8'))
  12. const dataUrl = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
  13. const documentUrl = dataUrl(compile('training/trainingDocument.ts'))
  14. const bridgeUrl = dataUrl(compile('training-legacy/bridge.ts').replace(/from ['"]\.\.\/training\/trainingDocument['"]/, `from '${documentUrl}'`))
  15. const { toFormalContent, toLegacyProject } = await import(bridgeUrl)
  16. const { normalizeTrainingContent } = await import(documentUrl)
  17. const project = content => ({ id: 'compat-test', name: 'Compatibility', type: 'TRAINING', trainingMode: 'VIRTUAL', status: 'DRAFT', content })
  18. const save = (legacy, previous) => toFormalContent(legacy, 'VIRTUAL', previous).content
  19. const sceneObjects = ['A', 'B'].map(id => ({
  20. id, sceneId: id, name: `Target ${id}`, visible: true,
  21. semantic: { operable: true, interactionId: `scene:${id}`, capabilities: ['inspect'], deviceId: `DEV-${id}` },
  22. }))
  23. for (const runtime of ['guide-legacy/demo-src', 'virtual-training-legacy/upstream']) {
  24. const { createTrainingProject, normalizeTrainingProject } = await import(new URL(`${runtime}/training-project-io.js`, features))
  25. const fixture = () => {
  26. const value = createTrainingProject({ trainingMode: 'virtual' })
  27. value.steps = value.steps.slice(0, 1)
  28. return value
  29. }
  30. const reopen = content => normalizeTrainingProject(toLegacyProject(project(content)))
  31. test(`${runtime}: legacy operation text is visible to the formal editor and survives reopen`, () => {
  32. const value = fixture()
  33. value.steps[0].instruction = '确认狐狸目标位置并记录观察结果'
  34. value.steps[0].guidance = '使用当前镜头观察目标'
  35. const content = save(value)
  36. assert.equal(content.steps[0].summary, value.steps[0].instruction)
  37. assert.equal(content.steps[0].guidance, value.steps[0].guidance)
  38. assert.equal(reopen(content).steps[0].instruction, value.steps[0].instruction)
  39. })
  40. test(`${runtime}: distinct existing summary and instruction survive an unchanged roundtrip`, () => {
  41. const content = save(fixture())
  42. Object.assign(content.steps[0], { summary: '正式已有说明', instruction: '已有详细说明', guidance: '已有引导' })
  43. const original = structuredClone(content)
  44. const next = save(reopen(content), content)
  45. for (const key of ['summary', 'instruction', 'guidance']) assert.equal(next.steps[0][key], content.steps[0][key])
  46. assert.deepEqual(content, original)
  47. })
  48. test(`${runtime}: summary-only historical documents open with their real operation text`, () => {
  49. const content = save(fixture())
  50. content.steps[0].summary = '正式说明用于旧编辑器显示'
  51. for (const instruction of [undefined, '']) {
  52. if (instruction === undefined) delete content.steps[0].instruction
  53. else content.steps[0].instruction = instruction
  54. const reopened = reopen(content)
  55. assert.equal(reopened.steps[0].instruction, content.steps[0].summary)
  56. assert.equal(save(reopened, content).steps[0].summary, content.steps[0].summary)
  57. }
  58. })
  59. test(`${runtime}: clearing operation and optional guidance does not restore demonstration text`, () => {
  60. const content = save(fixture())
  61. const fields = ['summary', 'instruction', 'guidance', 'warning', 'destination', 'success']
  62. for (const key of fields) content.steps[0][key] = ''
  63. const reopened = reopen(content)
  64. const next = save(reopened, content)
  65. for (const key of fields) {
  66. assert.equal(reopened.steps[0][key], '', key)
  67. assert.equal(next.steps[0][key], '', key)
  68. }
  69. delete content.steps[0].instruction
  70. assert.equal(reopen(content).steps[0].instruction, '')
  71. })
  72. test(`${runtime}: formally selected target wins over stale legacy references`, () => {
  73. const source = fixture()
  74. source.scenario.publishedSceneId = 'scene-test'
  75. source.scenario.sceneResource = { sceneId: 'scene-test', snapshot: { objects: sceneObjects } }
  76. Object.assign(source.steps[0], { targetId: 'scene:A', targetRef: { interactionId: 'scene:A' }, mode: 'inspect', targetNodeId: 'scene:A' })
  77. const content = save(normalizeTrainingProject(source))
  78. Object.assign(content.steps[0], { targetId: 'scene:B', targetName: 'Target B' })
  79. content.steps[0].action.targetId = 'scene:B'
  80. const next = save(reopen(content), content)
  81. assert.equal(next.steps[0].targetId, 'scene:B')
  82. assert.equal(next.steps[0].action.targetId, 'scene:B')
  83. assert.equal(next.steps[0].targetRef.interactionId, 'scene:B')
  84. assert.equal(next.steps[0].targetRef.sceneObjectId, 'B')
  85. assert.equal(next.steps[0].targetRef.deviceId, 'DEV-B')
  86. assert.equal(next.steps[0].targetNodeId, 'scene:B')
  87. assert.equal(next.steps[0].targetNodeName, 'Target B')
  88. assert.equal(content.steps[0].targetRef.interactionId, 'scene:A')
  89. })
  90. test(`${runtime}: an explicitly cleared or missing target is not replaced by its stale reference`, () => {
  91. const source = fixture()
  92. source.scenario.publishedSceneId = 'scene-test'
  93. source.scenario.sceneResource = { sceneId: 'scene-test', snapshot: { objects: sceneObjects } }
  94. for (const id of ['', 'scene:missing']) {
  95. Object.assign(source.steps[0], { targetId: id, targetRef: { interactionId: 'scene:A' }, mode: 'inspect' })
  96. const normalized = normalizeTrainingProject(source)
  97. assert.equal(normalized.steps[0].targetId, id)
  98. assert.equal(normalized.steps[0].targetRef, null)
  99. assert.equal(normalized.steps[0].action.targetId, id)
  100. }
  101. })
  102. }
  103. test('formal normalizer migrates old operation text while preserving explicit clears and distinct text', () => {
  104. const content = { steps: [
  105. { id: 'instruction-only', instruction: '旧说明' },
  106. { id: 'empty-summary', summary: '', instruction: '旧说明二' },
  107. { id: 'both-empty', summary: '', instruction: '' },
  108. { id: 'distinct', summary: '简述', instruction: '详情' },
  109. ] }
  110. const next = normalizeTrainingContent(content, 'VIRTUAL')
  111. assert.deepEqual(next.steps.map(step => [step.summary, step.instruction]), [
  112. ['旧说明', '旧说明'], ['旧说明二', '旧说明二'], ['', ''], ['简述', '详情'],
  113. ])
  114. })
  115. test('old local buffers retain saved summary unless the operation instruction was edited', () => {
  116. const content = normalizeTrainingContent({ steps: [{ id: 'step-a', summary: '正式说明', instruction: '详细说明' }] }, 'VIRTUAL')
  117. const unchanged = { steps: [{ id: 'step-a', instruction: '详细说明' }] }
  118. assert.equal(save(unchanged, content).steps[0].summary, '正式说明')
  119. const changed = { steps: [{ id: 'step-a', instruction: '实际编辑新说明' }] }
  120. assert.equal(save(changed, content).steps[0].summary, '实际编辑新说明')
  121. })
  122. test('actual legacy input handler synchronizes operation text only when that field is edited', () => {
  123. const source = readFileSync(new URL('guide-legacy/demo-src/training-editor.js', features), 'utf8')
  124. const start = source.indexOf(' updateField(input, { render = false } = {}) {')
  125. const end = source.indexOf('\n onDragStart(', start)
  126. assert.ok(start >= 0 && end > start)
  127. const editor = new Function('setPath', `return ({${source.slice(start, end)}})`)((object, key, value) => { object[key] = value })
  128. editor.selectedStep = { id: 'test', summary: '原正式说明', instruction: '原详细说明' }
  129. editor.project = { id: 'test' }
  130. editor.markDirty = () => {}
  131. editor.updateField({ dataset: { scope: 'step', field: 'guidance' }, type: 'text', value: '新引导' })
  132. assert.equal(editor.selectedStep.summary, '原正式说明')
  133. for (const value of ['新的操作说明', '']) {
  134. editor.updateField({ dataset: { scope: 'step', field: 'instruction' }, type: 'text', value })
  135. assert.equal(editor.selectedStep.summary, value)
  136. assert.equal(editor.selectedStep.instruction, value)
  137. }
  138. })
  139. const studio = readFileSync(new URL('../views/content/TrainingStudioView.vue', features), 'utf8')
  140. test('actual studio description handler synchronizes both document fields including clear', () => {
  141. const start = studio.indexOf('const setStepSummary =')
  142. const end = studio.indexOf('const selectTarget =', start)
  143. assert.ok(start >= 0 && end > start)
  144. const selected = { value: { summary: 'old', instruction: 'different' } }
  145. const handler = new Function('selectedStep', 'markDirty', `${compileText(studio.slice(start, end))}; return setStepSummary`)(selected, () => {})
  146. for (const value of ['正式新操作说明', '']) {
  147. handler(value)
  148. assert.equal(selected.value.summary, value)
  149. assert.equal(selected.value.instruction, value)
  150. }
  151. assert.match(studio, /@update:model-value="setStepSummary"/)
  152. })
  153. test('actual studio target selection synchronizes semantic, node and action references', () => {
  154. const start = studio.indexOf('const selectTarget =')
  155. const end = studio.indexOf('const selectMode =', start)
  156. assert.ok(start >= 0 && end > start)
  157. const selected = { value: { targetId: 'scene:A', targetRef: { interactionId: 'scene:A' }, mode: 'inspect', action: { targetId: 'scene:A' },
  158. toolPlacement: { attachedTargetId: 'scene:A', pose: 'target', position: [1, 2, 3] } } }
  159. const catalog = { value: new Map([['scene:B', { id: 'scene:B', label: 'Target B', modes: ['inspect'] }]]) }
  160. const handler = new Function('selectedStep', 'targetById', 'sceneContent', 'markDirty', 'selectMode', `${compileText(studio.slice(start, end))}; return selectTarget`)(
  161. selected, catalog, { value: { objects: sceneObjects } }, () => {}, () => { throw new Error('Unexpected mode change') },
  162. )
  163. handler('scene:B')
  164. assert.equal(selected.value.targetId, 'scene:B')
  165. assert.equal(selected.value.action.targetId, 'scene:B')
  166. assert.equal(selected.value.targetRef.interactionId, 'scene:B')
  167. assert.equal(selected.value.targetRef.sceneObjectId, 'B')
  168. assert.equal(selected.value.targetRef.deviceId, 'DEV-B')
  169. assert.equal(selected.value.targetNodeId, 'scene:B')
  170. assert.equal(selected.value.targetNodeName, 'Target B')
  171. assert.equal(selected.value.toolPlacement.attachedTargetId, '')
  172. assert.deepEqual(selected.value.toolPlacement.position, [1, 2, 3])
  173. })