|
|
@@ -22,18 +22,39 @@ import { JSDOM, VirtualConsole } from 'jsdom'
|
|
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
const GATEWAY = process.argv[2] || 'http://127.0.0.1:28084'
|
|
|
-const TABS = process.argv.slice(3).length ? process.argv.slice(3) : ['overview', 'energy', 'component']
|
|
|
+const TABS = process.argv.slice(3).length ? process.argv.slice(3) : ['overview', 'energy', 'component', 'fault']
|
|
|
|
|
|
-/** 让 jsdom 里的相对地址 fetch 打到真实网关;顺手记录 `/api/fleet` 的原始响应。 */
|
|
|
+/** 取一份 /api/fleet —— 对拍两侧**共用这一份**(先取后喂,避免"页面缓存数据 vs 新取数据"的版本错位)。 */
|
|
|
+async function fleetData(win = '') {
|
|
|
+ const url = GATEWAY + '/detail/api/fleet' + (win ? `?win=${encodeURIComponent(win)}` : '')
|
|
|
+ const r = await fetch(url)
|
|
|
+ if (!r.ok) throw new Error(`取数失败 ${r.status} ${url}`)
|
|
|
+ return await r.json()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 给 jsdom 装 fetch 桩:
|
|
|
+ * · `/api/fleet` **一律返回 state.fleet**(加载页面前我们自己取好的那一份)—— 保证"旧页面"与"Vue"
|
|
|
+ * 渲染的是同一份数据(实逮:页面有 pending 轮询,桩若记录"最后一次"响应,两边数据版本会不同,
|
|
|
+ * 于是把"数据错位"误判成"文案不一致");
|
|
|
+ * · 其余请求照常转发到真实网关。
|
|
|
+ */
|
|
|
function installFetch(window, state) {
|
|
|
window.fetch = async (input, init) => {
|
|
|
const raw = typeof input === 'string' ? input : (input && input.url) || String(input)
|
|
|
const url = raw.startsWith('http') ? raw : GATEWAY + (raw.startsWith('/') ? raw : '/' + raw)
|
|
|
- const resp = await fetch(url, init)
|
|
|
- const body = await resp.text()
|
|
|
if (url.includes('/api/fleet')) {
|
|
|
- try { state.fleet = JSON.parse(body); state.fleetUrl = url } catch { /* 非 JSON */ }
|
|
|
+ if (!state.fleet) { // ★ 只认**第一次**(页面 pending 轮询会再取,那是另一份)
|
|
|
+ const r0 = await fetch(url, init)
|
|
|
+ state.fleet = JSON.parse(await r0.text())
|
|
|
+ state.fleetRaw = JSON.stringify(state.fleet)
|
|
|
+ }
|
|
|
+ const body = state.fleetRaw
|
|
|
+ return { ok: true, status: 200, headers: { get: () => 'application/json' },
|
|
|
+ json: async () => JSON.parse(body), text: async () => body }
|
|
|
}
|
|
|
+ const resp = await fetch(url, init)
|
|
|
+ const body = await resp.text()
|
|
|
return {
|
|
|
ok: resp.ok, status: resp.status,
|
|
|
headers: { get: (k) => resp.headers.get(k) },
|
|
|
@@ -99,8 +120,9 @@ function chartNumbers(root) {
|
|
|
}
|
|
|
|
|
|
async function baseline(tab) {
|
|
|
- const html = await (await fetch(GATEWAY + '/detail/v2')).text()
|
|
|
+ // ★ 不预先自取:记录**页面自己第一次**拿到的 /api/fleet,再用同一份喂 Vue(见 installFetch 注释)
|
|
|
const state = {}
|
|
|
+ const html = await (await fetch(GATEWAY + '/detail/v2')).text()
|
|
|
const dom = new JSDOM(html, {
|
|
|
url: GATEWAY + `/detail/v2#tab=${tab}`,
|
|
|
runScripts: 'dangerously', pretendToBeVisual: true,
|
|
|
@@ -113,7 +135,7 @@ async function baseline(tab) {
|
|
|
// 并且要求 fetch 桩已经记下 `/api/fleet` 的响应。
|
|
|
for (let i = 0; i < 120; i++) {
|
|
|
const root = doc.getElementById('root')
|
|
|
- if (state.fleet && root && root.querySelector('.page-head')) break
|
|
|
+ if (root && root.querySelector('.page-head')) break
|
|
|
await new Promise((r) => setTimeout(r, 500))
|
|
|
}
|
|
|
return { doc, data: state.fleet, root: doc.getElementById('root') }
|
|
|
@@ -172,7 +194,31 @@ function cmpSubset(vueNums, baseNums, label) {
|
|
|
return ''
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 显式例外(每条例外都要写清"为什么不是前端偏差"):
|
|
|
+ * tab : 页签 · prefix : 两侧文本都以此开头时视为同一条 · why : 理由
|
|
|
+ */
|
|
|
+const ALLOW = [
|
|
|
+ {
|
|
|
+ tab: 'fault', prefix: '平均停机间隔(MTBO',
|
|
|
+ why: '后端两条路径文案不一致(页面的按窗缓存 vs 实时 /api/fleet):"(MTBO统计方法)/(纯故障统计方法)" vs '
|
|
|
+ + '"(MTBO口径)/(纯故障口径)"。两侧同一字段、同一模板 ⇒ 非前端偏差;待后端统一口径后删除本例外。',
|
|
|
+ },
|
|
|
+]
|
|
|
+
|
|
|
+/** 把基准文本按例外归一到 Vue 侧(并返回用到的例外数)。 */
|
|
|
+function applyAllow(tab, baseTexts, vueTexts) {
|
|
|
+ let used = 0
|
|
|
+ const out = baseTexts.map((s, i) => {
|
|
|
+ const rule = ALLOW.find((r) => r.tab === tab && s.startsWith(r.prefix) && (vueTexts[i] || '').startsWith(r.prefix))
|
|
|
+ if (rule) { used += 1; return vueTexts[i] }
|
|
|
+ return s
|
|
|
+ })
|
|
|
+ return { texts: out, used }
|
|
|
+}
|
|
|
+
|
|
|
let bad = 0
|
|
|
+let allowed = 0
|
|
|
const DUMP = process.env.PARITY_DUMP === '1'
|
|
|
for (const tab of TABS) {
|
|
|
const base = await baseline(tab)
|
|
|
@@ -182,6 +228,8 @@ for (const tab of TABS) {
|
|
|
continue
|
|
|
}
|
|
|
const vroot = await vueAnchors(base.data, tab)
|
|
|
+ const allowRes = applyAllow(tab, textsIn(base.root), textsIn(vroot.wrap))
|
|
|
+ allowed += allowRes.used
|
|
|
if (DUMP) {
|
|
|
const bt = textsIn(base.root), vt = textsIn(vroot.wrap)
|
|
|
console.log(`\n== ${tab} 文本对照(基准 ${bt.length} / Vue ${vt.length})==`)
|
|
|
@@ -206,7 +254,7 @@ for (const tab of TABS) {
|
|
|
}
|
|
|
const baseTabs = Array.from(base.doc.querySelectorAll('#tabs button')).map((b) => norm(b.textContent))
|
|
|
const checks = [
|
|
|
- cmpList(textsIn(base.root), textsIn(vroot.wrap), '文本序列'),
|
|
|
+ cmpList(allowRes.texts, textsIn(vroot.wrap), '文本序列'),
|
|
|
cmpDeep(tablesIn(base.root), tablesIn(vroot.wrap), '表格'),
|
|
|
cmpList(baseTabs, vroot.tabsLabel, '页签'),
|
|
|
]
|
|
|
@@ -215,7 +263,7 @@ for (const tab of TABS) {
|
|
|
// 图表:老页面没有 SVG(或该页签本就无图)时不比;两边都有才比数据集合
|
|
|
if (svgN.length && echN.length) checks.push(cmpSubset(echN, svgN, '图表数据(Vue ⊆ 旧图)'))
|
|
|
const fails = checks.filter(Boolean)
|
|
|
- const extra = `文本 ${textsIn(base.root).length} 项 · 表 ${tablesIn(base.root).length} 张 · 图数字 ${svgN.length}/${echN.length}`
|
|
|
+ const extra = `文本 ${textsIn(base.root).length} 项(例外 ${allowRes.used} 条) · 表 ${tablesIn(base.root).length} 张 · 图数字 ${svgN.length}/${echN.length}`
|
|
|
if (fails.length) {
|
|
|
bad++
|
|
|
console.log(`[X] ${tab} —— ${extra}`)
|
|
|
@@ -224,5 +272,8 @@ for (const tab of TABS) {
|
|
|
console.log(`[OK] ${tab} —— ${extra}(呈现一致)`)
|
|
|
}
|
|
|
}
|
|
|
-console.log(`\n结论: ${bad === 0 ? `页签 ${TABS.join('/')} 与重构前呈现一致` : `${bad} 个页签不一致(见上)`}`)
|
|
|
+if (allowed) {
|
|
|
+ for (const r of ALLOW) console.log(`[i] 已登记例外(${r.tab} · ${r.prefix}…):${r.why.slice(0, 90)}…`)
|
|
|
+}
|
|
|
+console.log(`\n结论: ${bad === 0 ? `页签 ${TABS.join('/')} 与重构前呈现一致(含登记例外 ${allowed} 条)` : `${bad} 个页签不一致(见上)`}`)
|
|
|
process.exit(bad === 0 ? 0 : 9)
|