| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- #!/usr/bin/env node
- /**
- * scripts/verify-cjk.mjs —— v11b 二、P0-b:中英混排 U+2009(THIN SPACE)实测。
- *
- * 验收项(任务书第二节):
- * 1. 遍历页面所有可见文本节点,统计 U+2009 → 必须 > 0 且累计 ≥ 20 处(8 个路由合计;逐页也打印)
- * 2. 抽 3 个具体元素,输出 textContent 的完整**字符码数组**(位置上的 8201 就是证据)
- * 3. 反向检查:汉字与英文/数字之间的普通空格(/[一-龥] [A-Za-z0-9]/、/ [一-龥]/ 前一位是拉丁)
- * 命中为 0;<code> / .kf-mono / URL / 文件名例外单独列出
- * 4. 不达标 exit 1
- *
- * 口径:「可见文本节点」= 在文档扁平树里、不在 SCRIPT/STYLE/NOSCRIPT/TEMPLATE、不在 svg 或 aria-hidden 子树、
- * 元素与所有祖先 display/visibility/opacity 正常(同一套 visible 判定)。
- * 不数 <input value>、不数 aria-label(那是属性不是文本节点)。
- *
- * 用法:node scripts/verify-cjk.mjs [--url http://127.0.0.1:4173/]
- */
- import { startServer, launch, evalIn, openView } from './v11b-lib.mjs';
- const argv = process.argv.slice(2);
- const arg = (k, d) => (argv.includes(k) ? argv[argv.indexOf(k) + 1] : d);
- const srv = arg('--url', '') ? null : await startServer(4197);
- const base = arg('--url', '') || srv.base;
- const browser = await launch();
- const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 1 });
- const cdp = await page.context().newCDPSession(page);
- await cdp.send('Network.setCacheDisabled', { cacheDisabled: true });
- const ROUTES = ['/', '/pool', '/stages', '/reports', '/reports/competitor', '/reports/voc', '/members', '/ledger'];
- const THIN = 0x2009;
- const fails = [];
- const countIn = `
- let thin = 0, nodes = 0, total = 0;
- const withThin = [];
- deepAll((hostEl) => {
- if (!hostEl.shadowRoot) return;
- for (const t of textNodesIn(hostEl.shadowRoot)) {
- const p = t.parentNode;
- if (!visible(t)) continue;
- nodes++;
- const n = [...t.data].filter((c) => c.codePointAt(0) === 0x2009).length;
- if (n) { thin += n; withThin.push(t.data.trim().slice(0, 40)); }
- total += t.data.length;
- }
- });
- return { thin, nodes, total, samples: withThin.slice(0, 6) };
- `;
- const REVERSE = String.raw`
- const plain = []; // 正文里的残留
- const except = []; // <code> / .kf-mono / URL 里的(允许)
- deepAll((hostEl) => {
- if (!hostEl.shadowRoot) return;
- for (const t of textNodesIn(hostEl.shadowRoot)) {
- const p = t.parentNode;
- if (!visible(t)) continue;
- const s = t.data;
- // CJK + 普通空格 + 拉丁/数字;拉丁/数字 + 普通空格 + CJK
- /* 只认真正的普通空格 / 制表符:U+2009 本身是 \s,用 \s 会把正确答案也数进来 */
- const hits = (s.match(/[\u4e00-\u9fa5][ \t]+[A-Za-z0-9]/g) || []).concat(s.match(/[A-Za-z0-9.,][ \t]+[\u4e00-\u9fa5]/g) || []);
- if (!hits.length) continue;
- const mono = p.closest?.('code, pre, .kf-mono, .mono') || /https?:\/\/|www\.|[\w-]+\.(js|mjs|json|md|css|png)\b/.test(s);
- (mono ? except : plain).push(s.trim().slice(0, 50));
- }
- });
- return { plain, except };
- `;
- const per = {};
- for (const r of ROUTES) {
- await openView(page, base, r, { me: '赵磊', wait: 800 });
- per[r] = await evalIn(page, countIn);
- per[r].rev = await evalIn(page, REVERSE);
- console.log(`${r.padEnd(20)} 可见文本节点 ${String(per[r].nodes).padStart(4)} · U+2009 ${String(per[r].thin).padStart(3)} · 普通空格残留 ${per[r].rev.plain.length}`);
- }
- const totalThin = Object.values(per).reduce((n, x) => n + x.thin, 0);
- const wbThin = per['/'].thin;
- const residue = Object.values(per).reduce((n, x) => n + x.rev.plain.length, 0);
- const excepts = [...new Set(Object.values(per).flatMap((x) => x.rev.except))];
- console.log(`\n全页(8 个路由合计)U+2009 计数 = ${totalThin}(工作台单页 ${wbThin})`);
- console.log(`不该有的「汉字 空格 拉丁」残留 = ${residue} 处` + (excepts.length ? `;例外(code / 等宽 / URL)${excepts.length} 处:` : ''));
- for (const e of excepts.slice(0, 5)) console.log(` · 例外:${e}`);
- /* ---------- 抽 3 个元素:完整字符码数组 ---------- */
- await openView(page, base, '/', { me: '赵磊', wait: 900 });
- const samples = await evalIn(page, `
- const want = [
- ['工作台 KPI 单位旁(近 7 天)', '.kpi .lbl'],
- ['工作台「现场」卡标题', '.who h3'],
- ['工作台 机会卡池 区块标题旁的计数链接', '.hd .go'],
- ['工作台 徽标(A 级)', '.b'],
- ['工作台 阶段块数字', '.stg'],
- ];
- const out = [];
- for (const [name, sel] of want) {
- let hit = null;
- deepAll((el) => { if (!hit && el.matches?.(sel) && visible(el) && [...el.textContent].some((c) => c.codePointAt(0) === 8201)) hit = el; });
- if (hit) out.push({ name, sel, text: hit.textContent.trim(), codes: [...hit.textContent.trim()].map((c) => c.codePointAt(0)) });
- }
- return out;
- `);
- // 选中「S4 机会验证评估」这类阶段标签(任务书点名的字符串):在成员/台账或卡牌上找
- if (samples.length < 3) {
- const more = await evalIn(page, `
- const out = [];
- deepAll((el) => { if (out.length < 2 && el.children.length === 0 && visible(el) && /S\\d/.test(el.textContent) && [...el.textContent].includes('\\u2009')) out.push({ name: '阶段标签 ' + elLabel(el), text: el.textContent.trim(), codes: [...el.textContent.trim()].map((c) => c.codePointAt(0)) }); });
- return out;
- `);
- samples.push(...more);
- }
- console.log('\n抽取元素的完整字符码数组(8201 = U+2009):');
- for (const s of samples.slice(0, 3)) {
- console.log(` ${s.name}「${s.text}」`);
- console.log(` [${s.codes.join(', ')}]`);
- }
- if (samples.length < 3) fails.push(`只抽到 ${samples.length} 个带 U+2009 的样本元素(要求 3)`);
- else if (!samples.slice(0, 3).every((s) => s.codes.includes(8201))) fails.push('抽到的样本里有的没有 8201');
- // 任务书名句「S4 机会验证评估 · 13 条」:工作台「现场」卡(何雨卡住)上的它必须含 8201、且 S4 后不是 32
- const sentence = await evalIn(page, `
- let found = null;
- // 先找最小的、以「S4」开头并含「机会验证评估」的元素(避免命中整张卡的祖先)
- deepAll((el) => {
- if (found || !visible(el) || el.children.length) return;
- const t = el.textContent.trim();
- if (t.startsWith('S4') && t.includes('机会验证评估')) found = t;
- });
- return found;
- `);
- console.log(`\n阶段标签实测:${sentence ? `「${sentence}」→ [${[...sentence].map((c) => c.codePointAt(0)).join(', ')}]` : '本页未出现「S4 机会验证评估」'}`);
- if (!(totalThin > 0 && wbThin >= 20)) fails.push(`U+2009 数量不足:工作台 ${wbThin}(要求 ≥ 20)`);
- if (residue > 0) fails.push(`汉字与拉丁之间有 ${residue} 处普通空格残留`);
- if (sentence) {
- const codes = [...sentence].map((c) => c.codePointAt(0));
- if (!codes.includes(8201)) fails.push('「S4 机会验证评估」里没有 8201');
- if (codes[2] === 32) fails.push('「S4 机会验证评估」第 3 位仍是普通空格 32');
- } else fails.push('工作台没有出现「S4 机会验证评估」这段文本(任务书点名的样本)');
- await browser.close();
- srv?.kill();
- if (fails.length) {
- console.log('\n失败项:');
- for (const f of fails) console.log(` ✗ ${f}`);
- process.exit(1);
- }
- console.log('\n全部达标(exit 0)');
|