80 lines
6.3 KiB
JavaScript
80 lines
6.3 KiB
JavaScript
// R59 关键信息卡(白底版)真实渲染验证:拆解 tab 默认卡视图
|
|||
|
|
// 断言:① 卡片 SVG 渲染且含「内容结构」整卡(板块小节齐全)② 卡内首行文字不贴顶(gap≥20)
|
||
|
|
// ③ 文本不越出 viewBox ④ 深浅两系均出卡;并截图存档
|
||
|
|
const CDP = 9223;
|
||
|
|
const list = await fetch(`http://localhost:${CDP}/json`).then((r) => r.json());
|
||
|
|
const page = list.find((t) => t.type === 'page' && (t.url || '').includes('8900')) || list.find((t) => t.type === 'page');
|
||
|
|
if (!page) { console.log('ERR: no page'); process.exit(1); }
|
||
|
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||
|
|
let id = 0; const pending = new Map();
|
||
|
|
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
|
||
|
|
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
|
||
|
|
await new Promise((r) => (ws.onopen = r));
|
||
|
|
const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; };
|
||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
|
await send('Page.enable'); await send('Runtime.enable');
|
||
|
|
await send('Emulation.setDeviceMetricsOverride', { width: 860, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||
|
|
const waitFor = async (expr, ms = 15000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; };
|
||
|
|
const fail = (m) => { console.log('FAIL@' + m); process.exit(1); };
|
||
|
|
const fs = await import('node:fs');
|
||
|
|
|
||
|
|
const AUDIT = `(() => {
|
||
|
|
const svg = document.querySelector('.vk-view svg');
|
||
|
|
if (!svg) return { err: 'no svg in .vk-view' };
|
||
|
|
const vbH = svg.viewBox.baseVal.height;
|
||
|
|
const rects = [...svg.querySelectorAll('rect')].map((r) => ({ x: Math.round(r.x.baseVal.value * 10) / 10, y: Math.round(r.y.baseVal.value * 10) / 10, w: Math.round(r.width.baseVal.value), h: Math.round(r.height.baseVal.value), fill: r.getAttribute('fill') }));
|
||
|
|
const texts = [...svg.querySelectorAll('text')].map((t) => ({ x: +t.getAttribute('x'), y: +t.getAttribute('y'), size: +t.getAttribute('font-size'), wt: t.getAttribute('font-weight'), txt: (t.textContent || '').slice(0, 42) }));
|
||
|
|
const titles = texts.filter((t) => t.txt.startsWith('▍')).map((t) => t.txt.slice(1));
|
||
|
|
const cards = rects.filter((r) => (r.fill || '').startsWith('#F') && r.w > 500 && r.h > 40); /* 整宽 tint 内容卡 */
|
||
|
|
const audits = cards.map((r) => {
|
||
|
|
const inT = texts.filter((t) => t.y > r.y + 2 && t.y < r.y + r.h && t.x >= r.x - 1 && t.x <= r.x + r.w + 1).sort((a, b) => a.y - b.y);
|
||
|
|
const first = inT[0];
|
||
|
|
return { cardTop: r.y, h: r.h, firstTxt: first ? first.txt : '', gap: first ? Math.round((first.y - r.y) * 10) / 10 : null };
|
||
|
|
});
|
||
|
|
const maxTextY = Math.max(...texts.map((t) => t.y));
|
||
|
|
/* 内容结构整卡内应出现的板块名(struct 板块头 text 加粗、位于该卡内且不重复) */
|
||
|
|
const structCard = cards[1]; /* 第二张卡(内容定位卡之后)可能为内容结构;改用标题定位 */
|
||
|
|
const titleYs = texts.filter((t) => t.txt.startsWith('▍')).map((t) => ({ t: t.txt.slice(1), y: t.y }));
|
||
|
|
const cardByTitle = {};
|
||
|
|
cards.forEach((r) => { const t = titleYs.find((x) => x.y < r.y && r.y - x.y < 60); if (t) cardByTitle[t.t] = r; });
|
||
|
|
const secCard = cardByTitle['内容结构'];
|
||
|
|
const secBoard = secCard ? texts.filter((t) => t.y > secCard.y && t.y < secCard.y + secCard.h && (t.wt || '') === '700' && t.x > secCard.x && t.x < secCard.x + 120).map((t) => t.txt) : [];
|
||
|
|
const bgStops = [...svg.querySelectorAll('#bgGrad stop')].map((s) => s.getAttribute('stop-color'));
|
||
|
|
return { vbH, titles, nCards: cards.length, audits, cardByTitle: Object.keys(cardByTitle), secBoard, maxTextY, over: maxTextY > vbH - 4, bgStops };
|
||
|
|
})()`;
|
||
|
|
|
||
|
|
const VIDS = [93, 190];
|
||
|
|
const out = {};
|
||
|
|
for (const id of VIDS) {
|
||
|
|
await send('Page.navigate', { url: `http://127.0.0.1:8900/#/video/${id}` });
|
||
|
|
/* 等路由渲染完成:hash 正确 + 视频信息卡渲染(旧 DOM 残留会误判,需等新内容) */
|
||
|
|
if (!(await waitFor(`location.hash === '#/video/${id}' && !!document.querySelector('#view .video-grid')`, 15000))) fail(`v${id} route`);
|
||
|
|
await sleep(400);
|
||
|
|
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
||
|
|
const hasCard = await waitFor(`!!document.querySelector('#view .vk-view svg')`, 12000);
|
||
|
|
if (!hasCard) { out['v' + id] = { __err: 'no keycard svg (纯 md fallback?)', hasMd: await ev(`!!document.querySelector('.vk-view .md-body, .vk-text')`) }; continue; }
|
||
|
|
await sleep(500);
|
||
|
|
const audit = await ev(AUDIT);
|
||
|
|
out['v' + id] = audit;
|
||
|
|
/* 截图:滚动 svg 到顶部后按元素区域截图 */
|
||
|
|
await ev(`document.querySelector('.vk-view svg').scrollIntoView({ block: 'start' })`); await sleep(250);
|
||
|
|
const rect = await ev(`(() => { const r = document.querySelector('.vk-view svg').getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; })()`);
|
||
|
|
if (rect && !rect.__err && rect.w > 100) {
|
||
|
|
const shot = await send('Page.captureScreenshot', { format: 'png', clip: { x: Math.max(0, rect.x - 4), y: Math.max(0, rect.y - 4), width: Math.min(rect.w + 8, 1200), height: Math.min(rect.h + 8, 4000), scale: 1 } });
|
||
|
|
fs.writeFileSync(`tmp/r59_card_v${id}.png`, Buffer.from(shot.data, 'base64'));
|
||
|
|
out['v' + id].shot = `tmp/r59_card_v${id}.png`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(JSON.stringify(out, null, 1));
|
||
|
|
const v93 = out.v93, v190 = out.v190;
|
||
|
|
const ok = v93 && !v93.__err && v190 && !v190.__err
|
||
|
|
&& v93.bgStops && v93.bgStops.length === 2 && v93.bgStops.every((c) => c === '#FFFFFF')
|
||
|
|
&& v190.bgStops && v190.bgStops.length === 2 && v190.bgStops.every((c) => c === '#FFFFFF')
|
||
|
|
&& v93.titles.includes('内容结构') && v190.titles.includes('内容结构')
|
||
|
|
&& v93.secBoard && v93.secBoard.some((b) => b.includes('开场钩子')) && v93.secBoard.some((b) => b.includes('中段叙事'))
|
||
|
|
&& v93.audits.every((a) => a.gap >= 20) && v190.audits.every((a) => a.gap >= 20)
|
||
|
|
&& !v93.over && !v190.over;
|
||
|
|
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
|
||
|
|
ws.close();
|
||
|
|
process.exit(ok ? 0 : 1);
|