spa_smoke.mjs 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. /**
  2. * 灰度切换冒烟:确认 **Vue 产物经网关可达**、且这份产物确实是"10 页签"的那一份。
  3. *
  4. * 说明(如实):本机没有浏览器自动化,jsdom 也不能执行 Vite 打出的 ESM 包,
  5. * 所以这里做的是**产物级**验证:
  6. * ① `/web/index.html` 200 且引用构建产物;
  7. * ② 该产物里含全部 10 个页签标签(取自语言包)与 10 个视图组件名;
  8. * ③ SPA 依赖的接口面(`/api/fleet` 等)经网关可达。
  9. * 浏览器内的真实交互回归不在此覆盖 —— 由 SSR 对拍(10 页签全绿)承担"呈现一致"的证明。
  10. *
  11. * 用法:node tools/spa_smoke.mjs http://127.0.0.1:28084
  12. */
  13. const GATEWAY = (process.argv[2] || 'http://127.0.0.1:28084').replace(/\/$/, '')
  14. // 页签标签**以现网语言包为准**(`window.__L` 的 `tab.<id>`)—— 不硬编码,避免拿猜测值当判据
  15. const TAB_IDS = ['overview', 'energy', 'component', 'fault', 'vibration', 'generation', 'decision', 'assistant', 'report', 'system', 'documents', 'recalc']
  16. const pageHtml = await (await fetch(`${GATEWAY}/detail/v2`)).text()
  17. const langJson = pageHtml.slice(pageHtml.indexOf('window.__L'), pageHtml.indexOf('window.__L') + 400000)
  18. const TABS10 = TAB_IDS.map((id) => {
  19. const m = langJson.match(new RegExp(`"tab\\.${id}":\\s*"([^"]+)"`))
  20. return m ? m[1] : id
  21. })
  22. const COMPONENTS = ['Overview', 'Energy', 'Component', 'Fault', 'Vibration', 'Generation', 'Decision', 'Assistant', 'Report', 'System']
  23. let bad = 0
  24. const ok = (cond, msg, extra = '') => {
  25. console.log(`${cond ? '[OK]' : '[X] '} ${msg}${extra ? ' —— ' + extra : ''}`)
  26. if (!cond) bad++
  27. }
  28. // ① index.html
  29. const idx = await fetch(`${GATEWAY}/web/index.html`)
  30. ok(idx.ok, `/web/index.html 可达`, `HTTP ${idx.status}`)
  31. const html = await idx.text()
  32. const srcs = [...html.matchAll(/<script[^>]+src="([^"]+)"/g)].map((m) => m[1])
  33. const links = [...html.matchAll(/<link[^>]+href="([^"]+)"/g)].map((m) => m[1])
  34. ok(srcs.length > 0, 'index.html 引用了脚本', srcs.join(', ').slice(0, 120))
  35. // ② 产物内容:10 个页签 + 10 个视图
  36. let bundleText = ''
  37. for (const s of srcs) {
  38. const u = s.startsWith('http') ? s : `${GATEWAY}/web/${s.replace(/^\.?\//, '')}`
  39. const r = await fetch(u)
  40. ok(r.ok, `脚本可达 ${s.slice(0, 48)}`, `HTTP ${r.status}`)
  41. if (r.ok) bundleText += await r.text()
  42. }
  43. for (const l of links) {
  44. const u = l.startsWith('http') ? l : `${GATEWAY}/web/${l.replace(/^\.?\//, '')}`
  45. const r = await fetch(u)
  46. ok(r.ok, `样式可达 ${l.slice(0, 40)}`, `HTTP ${r.status}`)
  47. }
  48. const missingTabs = TABS10.filter((x) => !bundleText.includes(x))
  49. ok(missingTabs.length === 0, '产物含全部 10 个页签标签(取自语言包)', missingTabs.length ? '缺: ' + missingTabs.join('/') : `${TABS10.length}/10 · ${TABS10.join('/')}`)
  50. const missingCmp = COMPONENTS.filter((x) => !bundleText.includes(x))
  51. ok(missingCmp.length === 0, '产物含全部 10 个视图组件', missingCmp.length ? '缺: ' + missingCmp.join('/') : `${COMPONENTS.length}/10`)
  52. // ③ SPA 依赖的接口面
  53. for (const p of ['/api/fleet', '/algorithm/healthz']) {
  54. const r = await fetch(`${GATEWAY}${p}`)
  55. ok(r.ok, `接口可达 ${p}`, `HTTP ${r.status}`)
  56. }
  57. console.log(`\n结论: ${bad === 0 ? 'Vue 产物经网关可作并列入口(灰度第一步就绪)' : bad + ' 项未通过'}`)
  58. process.exit(bad === 0 ? 0 : 1)