25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

57 satır
4.7 KiB

  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import ts from 'typescript'
  5. import { sceneGroups } from './scene-editor-feature-catalog.mjs'
  6. import { sceneEvidenceBindings, sceneUnitPlan } from './scene-editor-evidence-bindings.mjs'
  7. // Static test-plan extraction, never executes a business test or accesses API.
  8. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
  9. const report = path.join(root, 'reports/scene-editor-closure-20260906')
  10. const catalog = sceneGroups.flatMap(([group, rows]) => rows.map(([id, name]) => ({ id, name, group })))
  11. const browser = []
  12. const literal = node => node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : null
  13. function property(node, key) {
  14. return node && ts.isObjectLiteralExpression(node)
  15. ? node.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText().replace(/['"]/g, '') === key)?.initializer : null
  16. }
  17. for (const basename of fs.readdirSync(path.join(root, 'tests')).filter(f => /^scene-editor-.*closure\.spec\.ts$/.test(f))) {
  18. const file = `tests/${basename}`, text = fs.readFileSync(path.join(root, file), 'utf8')
  19. const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)
  20. function visit(node, title = '') {
  21. if (ts.isCallExpression(node) && node.expression.getText(source) === 'test') title = literal(node.arguments[0]) || node.arguments[0].getText(source)
  22. if (ts.isCallExpression(node) && node.expression.getText(source) === 'evidence') {
  23. const name = literal(node.arguments[0]), data = node.arguments[1]
  24. const idsNode = property(data, 'featureIds')
  25. const explicitIds = idsNode && ts.isArrayLiteralExpression(idsNode) ? idsNode.elements.map(literal).filter(Boolean) : []
  26. const binding = name && sceneEvidenceBindings[name]
  27. const ids = explicitIds.length ? explicitIds : binding?.featureIds || []
  28. const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1
  29. if (name) {
  30. const featureIds = name === 'basic-assets'
  31. ? [...ids, ...['diagnostic','equipment','workbench','shelf','safety-zone','camera','worker','light'].map(v => `asset.${v}`)] : ids
  32. browser.push({ file, line, evidenceName: name, testTitle: title, kind: 'browser', featureIds: [...new Set(featureIds)], scope: literal(property(data, 'scope')) || binding?.scope || '以源测试实际断言为准;计划关联不代表最终通过。' })
  33. } else if (basename === 'scene-editor-templates-closure.spec.ts') {
  34. for (const item of catalog.filter(item => item.id.startsWith('template.') && item.id !== 'template.switch-cancel')) {
  35. browser.push({ file, line, evidenceName: item.id.replace('template.', 'template-'), testTitle: `场景模板实际加载、保存重开:${item.name}`, kind: 'browser', featureIds: [item.id], scope: '循环参数化模板用例,每种独立副本、真实点击、保存、刷新与截图。' })
  36. }
  37. } else if (basename === 'scene-editor-downstream-closure.spec.ts') {
  38. for (const key of ['fox','truck']) browser.push({ file, line, evidenceName: `downstream-${key}`, testTitle: `${key}:场景发布后在真实训练编排选择、保存并重开静态模型`, kind: 'browser', featureIds: ids, scope: '真实训练编排静态还原;不计来源模型动画/蓝图自动执行。' })
  39. }
  40. }
  41. ts.forEachChild(node, child => visit(child, title))
  42. }
  43. visit(source)
  44. }
  45. const unit = sceneUnitPlan.map(([file, testTitle, featureIds]) => ({ file, testTitle, featureIds, kind: 'unit', scope: '聚焦合同/真实DOM/Three对象计算;不称为浏览器鼠标交互。' }))
  46. const rows = catalog.map(item => ({ ...item, verification: 'PENDING', plannedEvidence: [...browser, ...unit].filter(proof => proof.featureIds.includes(item.id)) }))
  47. const unknownIds = [...new Set([...browser, ...unit].flatMap(p => p.featureIds).filter(id => !catalog.some(row => row.id === id)))]
  48. const data = { generatedAt: new Date().toISOString(), status: 'PENDING', scope: '只读解析当前spec与明确unit计划;不执行测试,不推断最终PASS。',
  49. count: { total: rows.length, withBrowserPlan: rows.filter(r => r.plannedEvidence.some(p => p.kind === 'browser')).length,
  50. onlyUnitPlan: rows.filter(r => r.plannedEvidence.length && r.plannedEvidence.every(p => p.kind === 'unit')).length,
  51. unmapped: rows.filter(r => !r.plannedEvidence.length).length },
  52. unknownIds, gaps: rows.filter(r => !r.plannedEvidence.length).map(({ id, name }) => ({ id, name })), features: rows }
  53. fs.mkdirSync(path.join(report, 'data'), { recursive: true })
  54. fs.writeFileSync(path.join(report, 'data/coverage-test-plan.json'), JSON.stringify(data, null, 2))
  55. console.log(JSON.stringify({ count: data.count, gaps: data.gaps, unknownIds }, null, 2))