| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- #!/usr/bin/env node
- /**
- * scripts/verify-v14.mjs —— v14 验收(只量任务书「一」的四条;「二」是查证,见 docs/v14-report.md)。
- *
- * node scripts/verify-v14.mjs --tag before 只打印、不判分
- * node scripts/verify-v14.mjs 判分:任一项不达标 exit 1
- * --json out.json
- *
- * 量什么(/pool 每张 kf-opportunity-card):
- * - 含「未填写」字样的元素数(跨 shadow,取文本节点所在元素)
- * - 卡高(host 高)、行内极差
- * - 卡内最大连续空白:叶子文字矩形按 top 排,相邻两块竖直间隙的最大值(同 v13 算法)
- * - 有描述的卡 .one 文本仍在
- */
- import { writeFileSync } from 'node:fs';
- import { join, dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
- const argv = process.argv.slice(2);
- const arg = (k, d) => (argv.includes(k) ? argv[argv.indexOf(k) + 1] : d);
- const TAG = arg('--tag', 'after');
- const JUDGE = TAG !== 'before';
- const JSON_OUT = arg('--json', '');
- if (!process.env.CHROME_PATH) {
- const pinned = '/opt/hermes/.playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell';
- const { existsSync } = await import('node:fs');
- if (existsSync(pinned)) process.env.CHROME_PATH = pinned;
- }
- const { startServer, launch, DEEP } = await import('./v11b-lib.mjs');
- const srv = await startServer(Number(process.env.VERIFY_PORT || 4195));
- const browser = await launch();
- const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
- const page = await ctx.newPage();
- const errors = [];
- page.on('pageerror', (e) => errors.push(e.message));
- await page.goto(new URL('/pool', srv.base).href, { waitUntil: 'networkidle' });
- await page.waitForFunction(() => {
- const a = document.querySelector('kf-app');
- const v = a?.shadowRoot?.querySelector('view-pool');
- return v && v.shadowRoot && v.shadowRoot.children.length > 0;
- }, null, { timeout: 15000 });
- await page.waitForTimeout(400);
- const r = await page.evaluate(`(() => { ${DEEP}
- const root = appR().querySelector('view-pool').shadowRoot;
- const cards = [...root.querySelectorAll('kf-opportunity-card')].filter((c) => c.getBoundingClientRect().height > 0);
- const allTextEls = (node, acc = []) => {
- for (const el of node.querySelectorAll ? node.querySelectorAll('*') : []) {
- if ([...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.includes('未填写'))) acc.push(el);
- if (el.shadowRoot) allTextEls(el.shadowRoot, acc);
- }
- return acc;
- };
- const info = cards.map((card) => {
- const sr = card.shadowRoot;
- const face = sr.querySelector('kf-glass-card')?.shadowRoot?.querySelector('.face') || sr.querySelector('.face');
- const c = face || sr.querySelector('.wrap');
- const one = sr.querySelector('.one');
- const textAll = [...c.querySelectorAll('*')].filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()));
- const inner = textAll.filter((e) => !textAll.some((o) => o !== e && e.contains(o)));
- const boxes = inner.map((e) => { const rr = e.getBoundingClientRect(); return { top: Math.round(rr.top), bottom: Math.round(rr.bottom) }; })
- .filter((b) => b.bottom > b.top).sort((a, b) => a.top - b.top);
- let maxGap = 0, gapAt = '', lo = null, hi = null;
- for (const b of boxes) {
- if (hi == null) { lo = b.top; hi = b.bottom; continue; }
- if (b.bottom <= lo) { const g = lo - b.bottom; if (g > maxGap) { maxGap = g; gapAt = b.bottom + '→' + lo; } }
- lo = Math.min(lo, b.top); hi = Math.max(hi, b.bottom);
- }
- return { title: (sr.querySelector('h3')?.textContent || '').trim().slice(0, 12),
- h: Math.round(card.getBoundingClientRect().height), top: Math.round(card.getBoundingClientRect().top),
- hasOne: !!one, oneTxt: one ? one.textContent.trim().slice(0, 20) : null, maxGap, gapAt };
- });
- const placeholderEls = allTextEls(root).length;
- return { n: cards.length, cards: info, placeholderEls };
- })()`);
- const hs = r.cards.map((c) => c.h);
- const rowSpread = new Map();
- for (const c of r.cards) { const k = c.top; rowSpread.set(k, [...(rowSpread.get(k) || []), c.h]); }
- let maxRowDiff = 0;
- for (const v of rowSpread.values()) maxRowDiff = Math.max(maxRowDiff, Math.max(...v) - Math.min(...v));
- const withOne = r.cards.filter((c) => c.hasOne), withoutOne = r.cards.filter((c) => !c.hasOne);
- const maxGap = Math.max(0, ...r.cards.map((c) => c.maxGap));
- console.log(`· /pool 卡片数 ${r.n};卡高 [${hs.join(',')}];行内极差 ${maxRowDiff}px;全部极差 ${Math.max(...hs) - Math.min(...hs)}px`);
- console.log(`· 含「未填写」元素数 ${r.placeholderEls}`);
- console.log(`· 有描述 ${withOne.length} 张(.one 文本:${withOne.map((c) => c.oneTxt).join(' | ') || '无'});无描述 ${withoutOne.length} 张 ${withoutOne.map((c) => c.title).join(',')}`);
- console.log(`· 卡内最大连续空白:${r.cards.map((c) => c.title + '=' + c.maxGap + (c.gapAt ? '@' + c.gapAt : '')).join(', ')}`);
- if (errors.length) console.log(`· 页面错误 ${errors.length}:${errors.slice(0, 3).join(' / ')}`);
- const ok = r.placeholderEls === 0 && maxRowDiff <= 2 && maxGap <= 24 && withOne.length + withoutOne.length === r.n && errors.length === 0;
- const out = { tag: TAG, at: new Date().toISOString(), ...r, maxRowDiff, maxGap, ok };
- if (JSON_OUT) writeFileSync(JSON_OUT, JSON.stringify(out, null, 2));
- console.log(JUDGE ? (ok ? '判分:4/4 达标' : `判分:不达标(placeholder=${r.placeholderEls} rowDiff=${maxRowDiff} maxGap=${maxGap})`) : '基线(不判分)');
- await browser.close();
- srv.kill();
- process.exit(JUDGE && !ok ? 1 : 0);
|