25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

71 lines
8.8 KiB

  1. import fs from 'node:fs/promises';
  2. import path from 'node:path';
  3. import assert from 'node:assert/strict';
  4. import { pathToFileURL } from 'node:url';
  5. import { chromium } from '@playwright/test';
  6. const folder = path.resolve('reports/bridge-inspection-20260915');
  7. const read = async file => JSON.parse(await fs.readFile(path.join(folder, file), 'utf8'));
  8. const tasks = [
  9. { id: 86, code: '005', title: '液压泵应急切换', steps: 5 },
  10. { id: 87, code: '006', title: '液压泵应急替代', steps: 7 },
  11. { id: 88, code: '007', title: '前泵换件修复', steps: 11 },
  12. { id: 89, code: '008', title: '平衡阀拆拼互换', steps: 16 },
  13. ];
  14. for (const task of tasks) {
  15. const result = await read(`task-${task.id}-verification.json`);
  16. assert.equal(result.pass, true);
  17. assert.equal(result.errors.length, 0);
  18. assert.ok(result.writes.every(url => url.endsWith(`/assignments/${task.id}/preview/validate`)));
  19. task.actions = result.checks.filter(check => check.target).length;
  20. task.validationRequests = result.writes.length;
  21. }
  22. const lan = '../bridge-inspection-lan-20260915';
  23. for (const file of ['task-87-click-verification.json', `${lan}/click-flow-1280.json`, `${lan}/controls-1024.json`]) {
  24. assert.equal((await read(file)).pass, true);
  25. }
  26. const validation = await read('validation.json');
  27. assert.equal(validation.build.pass, true);
  28. const summary = {
  29. pass: true, generatedAt: new Date().toISOString(), tasks,
  30. steps: tasks.reduce((sum, task) => sum + task.steps, 0),
  31. actions: tasks.reduce((sum, task) => sum + task.actions, 0),
  32. validationRequests: tasks.reduce((sum, task) => sum + task.validationRequests, 0),
  33. environments: ['http://127.0.0.1:6180', 'http://192.168.31.168:6180'],
  34. resolutions: ['1366×768', '1280×720', '1024×768'],
  35. accountType: '管理员 / 教员预览',
  36. residualData: '未新增教学任务;未写入学员成绩。仅调用现有任务的教员预览校验接口。',
  37. validation,
  38. };
  39. await fs.writeFile(path.join(folder, 'summary.json'), JSON.stringify(summary, null, 2));
  40. const requirements = [
  41. ['逐步聚焦', '每个步骤和子动作自动对准对应部件,移出的螺栓、泵体随动作跟随。相机取景避开教员和面板。'],
  42. ['稳定距离与 HDR', '四套脚本使用统一的相机观察距离(约 2.5322),按部件尺寸调整光学缩放;曝光 1、环境光 0.8、主光 1.4 在步骤间不变。Venice Sunset HDR 已随项目静态交付。'],
  43. ['实体与轮廓', '物理网格使用双面材质,OutlinePass 绘制轮廓辉光。聚焦内部部件时局部隐藏遮挡外壳;复位后恢复。实体透明度不因选中或高亮而变化。'],
  44. ['小屏与全屏', '步骤栏、右侧面板可收起,小屏默认收起;操作区支持全屏、按钮退出和 Esc 退出。1024×768、1280×720 均无页面横向溢出。'],
  45. ['模型动作', '旋转、拆卸、移动和连接采用模型动画;拖动时模型实时跟随。纯旋转开关的端部铰点在动作中保持固定。点击模式也完成实际动画后才提交校验。'],
  46. ['标签', '去掉前导圆点,动态避让部件、浮层及其他标签;无法安放的额外标签隐藏。点击只聚焦放大,不产生步骤校验或成绩写入。'],
  47. ['高亮恢复', '选中部件后反复开关问题高亮,全部材质透明度与保存的基线一致,不残留整车半透明状态。'],
  48. ['隐藏其他', '隐藏其他模型时同时隐藏其标签;点标签后会同步查看目标,避免沿用之前的整车选择。'],
  49. ['操作范围', '流程目标继续按步骤顺序判断;非流程整车网格可点击查看、拖动旋转视角,不增加操作错误、不调用步骤校验。'],
  50. ];
  51. const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>四套桥梁脚本 · 3D 交互验收</title>
  52. <style>body{margin:0;background:#eef4f2;color:#163d37;font:15px/1.7 system-ui,sans-serif}main{max-width:1180px;margin:auto;padding:32px}h1{font-size:28px;margin:0}h2{font-size:21px;margin-top:32px}p{margin:8px 0 18px}.status{padding:16px 20px;border-left:5px solid #168c72;background:white;border-radius:10px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:18px}.card{background:white;border:1px solid #c7dcd6;border-radius:10px;padding:16px}img{width:100%;height:auto;border-radius:6px}table{width:100%;border-collapse:collapse;background:white}td,th{padding:10px 14px;border:1px solid #c7dcd6;text-align:left}a{color:#007a67}details{margin-top:10px}summary{cursor:pointer;font-weight:600}.steps{display:flex;flex-wrap:wrap;gap:6px}.steps a{padding:2px 9px;border:1px solid #bbd7cd;border-radius:5px}code{background:#e4eeea;padding:2px 4px}small{color:#58776e}@media(max-width:700px){main{padding:16px}.grid{grid-template-columns:1fr}h1{font-size:23px}}</style>
  53. <main><h1>四套桥梁脚本 · 3D 交互验收</h1><p>2026-09-15 · 正式 HTML + JavaScript 工作区 · 管理员账号 / 教员预览</p>
  54. <div class="status"><b>通过:${summary.steps} 个步骤、${summary.actions} 个模型操作、${summary.validationRequests} 次真实服务端预览校验。</b><br>另完成 005、006 点击动画回归,以及局域网小屏、自由查看、材质恢复和全屏退出检查。构建、正式路由契约及 16 项模型/脚本/讲解视频测试通过。</div>
  55. <h2>检查范围与结果</h2><table><thead><tr><th>需求</th><th>实现及验证</th></tr></thead><tbody>${requirements.map(([name, detail]) => `<tr><th>${name}</th><td>${detail}</td></tr>`).join('')}</tbody></table>
  56. <h2>各任务实际截图</h2><div class="grid">${tasks.map(task => `<article class="card"><b>TASK-VIRTUAL-${task.code} · ${task.title}</b><p>${task.steps} 步 / ${task.actions} 次模型操作 / ${task.validationRequests} 次预览校验</p><a href="task-${task.id}-initial.png"><img src="task-${task.id}-initial.png" alt="${task.title}初始聚焦"></a><details><summary>查看各步完成截图与记录</summary><div class="steps">${Array.from({length:task.steps},(_,i)=>`<a href="task-${task.id}-step-${i+1}.png">第 ${i+1} 步</a>`).join('')}</div><p><a href="task-${task.id}-fullscreen.png">全屏截图</a> · <a href="task-${task.id}-verification.json">完整 JSON</a></p></details></article>`).join('')}</div>
  57. <h2>局域网与小屏</h2><p>入口:<code>127.0.0.1:6180</code>、<code>192.168.31.168:6180</code>,页面 API 经 6100 Gateway。分辨率:1366×768、1280×720、1024×768。以下为 1280×720 的截图。</p><div class="grid"><article class="card"><a href="${lan}/free-inspection-controls.png"><img src="${lan}/free-inspection-controls.png" alt="小屏自由查看"></a><p>整车选择、拖动视角、切换高亮与 Esc 退出后。</p></article><article class="card"><a href="${lan}/task-86-fullscreen.png"><img src="${lan}/task-86-fullscreen.png" alt="全屏聚焦"></a><p>全屏内仍可使用工具、讲解和退出按钮。</p></article></div><p><a href="${lan}/controls-1024.json">1024 小屏记录</a> · <a href="${lan}/click-flow-1280.json">1280 点击流程记录</a> · <a href="task-87-click-verification.json">006 点击动画记录</a></p>
  58. <h2>验证边界</h2><p>${summary.residualData} 008 中需要观察记录的管路步骤仍要求记录并保留教员复核,未用界面操作绕过该条件。本轮验证的是三维交互与预览校验,未新增实装尺寸、受力或扭矩测量能力。截图中的补充模型沿用任务原有资源。</p>
  59. <p>运行方式:Edge / Playwright;检查脚本 <code>tools/bridge-inspection-live.mjs</code> 仅注入只读诊断,模型操作全部由真实鼠标点击或拖动完成。</p>
  60. <p>HDR 来自 <a href="https://raw.githubusercontent.com/mrdoob/three.js/r185/examples/textures/equirectangular/venice_sunset_1k.hdr">Three.js r185 示例</a>,原资源 <a href="https://polyhaven.com/a/venice_sunset">Poly Haven / Greg Zaal / Venice Sunset</a>(CC0),静态文件已纳入构建,无运行时外网请求。</p><p><a href="summary.json">汇总 JSON</a> · <a href="validation.json">命令与构建结果</a></p><small>本报告保留关键截图及通过记录;较早调试文件可能包含失败尝试,以本报告链接记录为准。</small></main></html>`;
  61. await fs.writeFile(path.join(folder, 'index.html'), html);
  62. const browser = await chromium.launch({channel:'msedge',headless:true});
  63. try {
  64. const page = await browser.newPage({viewport:{width:1440,height:1000}});
  65. await page.goto(pathToFileURL(path.join(folder,'index.html')).href);
  66. assert.ok(await page.locator('img').evaluateAll(images => images.every(image => image.complete && image.naturalWidth > 0)), 'all report screenshots load');
  67. await page.screenshot({path:path.join(folder,'report-overview.png')});
  68. } finally { await browser.close(); }
  69. console.log(JSON.stringify({pass:true,steps:summary.steps,actions:summary.actions,report:path.join(folder,'index.html')}));