full_diff.mjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. // P58 整页外壳对比:v2.9.2 原文页(工作台)↔ 新版 `/detail/v2#workbench`
  2. // 比整个 <body> 的元素骨架(tag.class 序列),忽略 svg/canvas/script/style 与 .portal-full/iframe。
  3. import { readFileSync } from 'node:fs'
  4. import { fileURLToPath, pathToFileURL } from 'node:url'
  5. import { dirname, resolve } from 'node:path'
  6. import { JSDOM, VirtualConsole } from 'jsdom'
  7. const HERE = dirname(fileURLToPath(import.meta.url))
  8. const GATEWAY = process.argv[2] || 'http://106.120.102.238:28084'
  9. const dist = resolve(HERE, '../../../release/web')
  10. function skeleton(el) {
  11. const out = []
  12. const walk = (n) => {
  13. for (const c of Array.from(n.children)) {
  14. const tag = c.tagName.toLowerCase()
  15. if (['svg', 'canvas', 'script', 'style', 'iframe'].includes(tag)) continue
  16. const cls = (c.getAttribute('class') || '').split(/\s+/).filter(Boolean).sort().join('.')
  17. out.push(tag + (cls ? '.' + cls : ''))
  18. walk(c)
  19. }
  20. }
  21. walk(el)
  22. return out
  23. }
  24. // ① 基线:v2.9.2 原文页(其工作台 = 默认页签的 SPA)
  25. const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8')
  26. const baseState = { cache: new Map() }
  27. function stub(window, state) {
  28. window.fetch = async (input) => {
  29. const raw = typeof input === 'string' ? input : (input && input.url) || String(input)
  30. const url = raw.startsWith('http') ? raw : GATEWAY + (raw.startsWith('/') ? raw : '/' + raw)
  31. if (/\/api\/fleet/.test(url)) return { ok: true, status: 200, headers: new Map(), text: async () => fleet, json: async () => JSON.parse(fleet) }
  32. if (state.cache.has(url)) { const c = state.cache.get(url); return { ok: true, status: 200, headers: new Map(), text: async () => c, json: async () => JSON.parse(c) } }
  33. const r = await fetch(url); const b = await r.text(); if (r.ok) state.cache.set(url, b)
  34. return { ok: r.ok, status: r.status, headers: r.headers, text: async () => b, json: async () => JSON.parse(b) }
  35. }
  36. }
  37. const bdom = new JSDOM(readFileSync(dist + '/detail_v2.html', 'utf8'),
  38. { url: GATEWAY + '/detail/v2', runScripts: 'dangerously', pretendToBeVisual: true,
  39. virtualConsole: new VirtualConsole(), beforeParse: (w) => stub(w, baseState) })
  40. {
  41. const d = bdom.window.document
  42. for (let i = 0; i < 120; i++) { const r = d.getElementById('root'); if (r && r.querySelector('.page-head')) break; await new Promise((x) => setTimeout(x, 500)) }
  43. let last = -1, same = 0
  44. for (let i = 0; i < 40; i++) { const n = d.body.querySelectorAll('*').length; if (n === last) { if (++same >= 3) break } else same = 0; last = n; await new Promise((x) => setTimeout(x, 700)) }
  45. }
  46. const base = skeleton(bdom.window.document.body)
  47. // ② 新版:/detail/v2#workbench(构建产物真跑)
  48. const idx = readFileSync(dist + '/index.html', 'utf8')
  49. const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '')
  50. const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '')
  51. const code = readFileSync(dist + '/assets/' + jsFile, 'utf8')
  52. const css = readFileSync(dist + '/assets/' + cssFile, 'utf8')
  53. const ndom = new JSDOM(idx.replace('</head>', '<style>' + css + '</style></head>'),
  54. { url: GATEWAY + '/detail/v2#workbench', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: new VirtualConsole() })
  55. {
  56. const w = ndom.window
  57. w.fetch = async (u) => { const s = String(u); const body = s.includes('fleet') ? fleet : '{}'
  58. return { ok: true, status: 200, json: async () => JSON.parse(body), text: async () => body } }
  59. try { w.eval(code) } catch (e) { console.log('EVAL_ERR ' + String(e).slice(0, 120)) }
  60. for (let i = 0; i < 24; i++) { await new Promise((x) => setTimeout(x, 500)); if (w.document.querySelector('.wb-tabs')) break }
  61. }
  62. const now = skeleton(ndom.window.document.body)
  63. console.log('BASE_TOTAL ' + base.length + ' · NEW_TOTAL ' + now.length)
  64. const n = Math.max(base.length, now.length)
  65. let shown = 0
  66. for (let i = 0; i < n && shown < 24; i++) {
  67. if (base[i] !== now[i]) { console.log('#' + i + ' 基线=' + (base[i] || '∅') + ' · 新版=' + (now[i] || '∅')); shown++ }
  68. }
  69. // 关键件核验
  70. const has = (arr, s) => arr.some((x) => x === s || x.startsWith(s + '.'))
  71. for (const k of ['div.logo', 'div.brand', 'div.right', 'button.reselect', 'select', 'span.winrng', 'a.back-portal', 'nav.tabs', 'nav.wb-tabs', 'div.page-head', 'main', 'footer']) {
  72. console.log('件 ' + k + ' 基线=' + has(base, k) + ' 新版=' + has(now, k))
  73. }