71 lines
5.6 KiB
JavaScript
71 lines
5.6 KiB
JavaScript
// R68b 验证:视频关键信息卡 板块标题→内容卡间距 30→22(标题与卡更靠近),原断言保持
|
|
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, 200) }; return r.result.value; };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable');
|
|
await send('Network.setCacheDisabled', { cacheDisabled: true });
|
|
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' };
|
|
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 cards = rects.filter((r) => (r.fill || '').startsWith('#F') && r.w > 550 && r.h > 40);
|
|
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 gaps = Object.entries(cardByTitle).map(([t, r]) => ({ t, gap: Math.round((r.y - titleYs.find((x) => x.t === t).y) * 10) / 10 }));
|
|
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 { firstTxt: first ? first.txt : '', gap: first ? Math.round((first.y - r.y) * 10) / 10 : null };
|
|
});
|
|
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'));
|
|
const maxTextY = Math.max(...texts.map((t) => t.y));
|
|
return { vbH, gaps, audits, secBoard, bgStops, over: maxTextY > vbH - 4 };
|
|
})()`;
|
|
|
|
const VIDS = [93, 190];
|
|
const out = {};
|
|
for (const id of VIDS) {
|
|
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}#/video/${id}` });
|
|
if (!(await waitFor(`location.hash === '#/video/${id}' && !!document.querySelector('#view .video-grid') && [...document.scripts].some((s) => (s.src || '').includes('data-pages.js?v=20260904s'))`, 15000))) fail(`v${id} route`);
|
|
await sleep(400);
|
|
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
|
if (!(await waitFor(`!!document.querySelector('#view .vk-view svg')`, 12000))) { out['v' + id] = { __err: 'no svg' }; continue; }
|
|
await sleep(500);
|
|
out['v' + id] = await ev(AUDIT);
|
|
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) {
|
|
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, 2600), scale: 1 } });
|
|
fs.writeFileSync(`tmp/r68b_v${id}.png`, Buffer.from(shot.data, 'base64'));
|
|
}
|
|
}
|
|
console.log(JSON.stringify(out, null, 1));
|
|
const v93 = out.v93, v190 = out.v190;
|
|
const ok = v93 && !v93.__err && v190 && !v190.__err
|
|
&& v93.gaps.length && v93.gaps.every((g) => g.gap === 22) && v190.gaps.length && v190.gaps.every((g) => g.gap === 22)
|
|
&& v93.secBoard.some((b) => b.includes('开场钩子')) && v190.secBoard.some((b) => b.includes('开场钩子'))
|
|
&& v93.bgStops.every((c) => c === '#FFFFFF') && v190.bgStops.every((c) => c === '#FFFFFF')
|
|
&& 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);
|