// P58 整页外壳对比:v2.9.2 原文页(工作台)↔ 新版 `/detail/v2#workbench` // 比整个 的元素骨架(tag.class 序列),忽略 svg/canvas/script/style 与 .portal-full/iframe。 import { readFileSync } from 'node:fs' import { fileURLToPath, pathToFileURL } from 'node:url' import { dirname, resolve } from 'node:path' import { JSDOM, VirtualConsole } from 'jsdom' const HERE = dirname(fileURLToPath(import.meta.url)) const GATEWAY = process.argv[2] || 'http://106.120.102.238:28084' const dist = resolve(HERE, '../../../release/web') function skeleton(el) { const out = [] const walk = (n) => { for (const c of Array.from(n.children)) { const tag = c.tagName.toLowerCase() if (['svg', 'canvas', 'script', 'style', 'iframe'].includes(tag)) continue const cls = (c.getAttribute('class') || '').split(/\s+/).filter(Boolean).sort().join('.') out.push(tag + (cls ? '.' + cls : '')) walk(c) } } walk(el) return out } // ① 基线:v2.9.2 原文页(其工作台 = 默认页签的 SPA) const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8') const baseState = { cache: new Map() } function stub(window, state) { window.fetch = async (input) => { const raw = typeof input === 'string' ? input : (input && input.url) || String(input) const url = raw.startsWith('http') ? raw : GATEWAY + (raw.startsWith('/') ? raw : '/' + raw) if (/\/api\/fleet/.test(url)) return { ok: true, status: 200, headers: new Map(), text: async () => fleet, json: async () => JSON.parse(fleet) } 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) } } const r = await fetch(url); const b = await r.text(); if (r.ok) state.cache.set(url, b) return { ok: r.ok, status: r.status, headers: r.headers, text: async () => b, json: async () => JSON.parse(b) } } } const bdom = new JSDOM(readFileSync(dist + '/detail_v2.html', 'utf8'), { url: GATEWAY + '/detail/v2', runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: new VirtualConsole(), beforeParse: (w) => stub(w, baseState) }) { const d = bdom.window.document 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)) } let last = -1, same = 0 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)) } } const base = skeleton(bdom.window.document.body) // ② 新版:/detail/v2#workbench(构建产物真跑) const idx = readFileSync(dist + '/index.html', 'utf8') const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '') const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '') const code = readFileSync(dist + '/assets/' + jsFile, 'utf8') const css = readFileSync(dist + '/assets/' + cssFile, 'utf8') const ndom = new JSDOM(idx.replace('', ''), { url: GATEWAY + '/detail/v2#workbench', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: new VirtualConsole() }) { const w = ndom.window w.fetch = async (u) => { const s = String(u); const body = s.includes('fleet') ? fleet : '{}' return { ok: true, status: 200, json: async () => JSON.parse(body), text: async () => body } } try { w.eval(code) } catch (e) { console.log('EVAL_ERR ' + String(e).slice(0, 120)) } for (let i = 0; i < 24; i++) { await new Promise((x) => setTimeout(x, 500)); if (w.document.querySelector('.wb-tabs')) break } } const now = skeleton(ndom.window.document.body) console.log('BASE_TOTAL ' + base.length + ' · NEW_TOTAL ' + now.length) const n = Math.max(base.length, now.length) let shown = 0 for (let i = 0; i < n && shown < 24; i++) { if (base[i] !== now[i]) { console.log('#' + i + ' 基线=' + (base[i] || '∅') + ' · 新版=' + (now[i] || '∅')); shown++ } } // 关键件核验 const has = (arr, s) => arr.some((x) => x === s || x.startsWith(s + '.')) 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']) { console.log('件 ' + k + ' 基线=' + has(base, k) + ' 新版=' + has(now, k)) }