| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- #!/usr/bin/env node
- /**
- * scripts/verify-textbox.mjs —— v11b 一、P0-a:text-box 行盒修剪实测。
- *
- * 上一轮写成 `text-box: trim-both cap alphabetic` 简写,Chrome 把 trim-both 丢了:
- * computed text-box-trim = none、行盒 = 32px(font-size × line-height 1)。
- * 本轮全部换成两个独立长属性 `text-box-trim: trim-both; text-box-edge: cap alphabetic;`。
- *
- * 量三档字号 × 加 / 不加 trim 的行盒高度(同一个浏览器、同一字体、同一个元素高度算法:
- * inline-block 元素 getBoundingClientRect().height,就是它的行盒高度),
- * 再量页面上真实元素:
- * 1. `.b`(主数值 / 阶段徽标)——任务书点名的那个类:fontSize / lineHeight / computed text-box-trim / boxHeight
- * 2. 32px 主数值:`.kpi b`(工作台)、`.kpi b`(报告类型页)
- * 3. 达标线:32px 档 boxHeight ≤ 26px
- *
- * 用法:node scripts/verify-textbox.mjs [--url http://127.0.0.1:4173/]
- * 退出码:全部达标 0;任何一项不达标 1(并逐条打印失败项)。
- */
- import { startServer, launch, evalIn, openView, table } 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(4196);
- 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 fails = [];
- const rec = [];
- const row = (item, measured, line, pass) => { rec.push({ item, measured, line, pass }); if (!pass) fails.push(item); };
- console.log(`\n# 浏览器 ${browser.version()} ${base}\n`);
- /* ============================================================
- * A. 三档字号 × 加 / 不加 trim 的对照实验
- * 元素:display:inline-block; white-space:nowrap; line-height:1;文本「1284」
- * 不加 trim = 浏览器默认行盒;加 trim = 两条长属性(与页面同写法)。
- * ============================================================ */
- await page.goto(new URL('/', base).href, { waitUntil: 'networkidle' });
- const probe = await page.evaluate(async () => {
- // 等页面自己的字体(Inter latin subset)生效,三档对照用同一套字体
- try { await document.fonts.ready; } catch { /* ignore */ }
- const host = document.createElement('div');
- host.style.cssText = 'position:fixed;left:-9999px;top:0;font-family:var(--kf-font, system-ui);';
- document.body.appendChild(host);
- const size = (css) => {
- const el = document.createElement('span');
- el.textContent = '1284';
- el.style.cssText = `display:inline-block;white-space:nowrap;line-height:1;${css}`;
- host.appendChild(el);
- const h = el.getBoundingClientRect().height;
- const cs = getComputedStyle(el);
- const out = { h, trim: cs.textBoxTrim, edge: cs.textBoxEdge, lh: parseFloat(cs.lineHeight) };
- el.remove();
- return out;
- };
- const TRIM = 'text-box-trim:trim-both;text-box-edge:cap alphabetic;';
- /* 现场「不生效的写法」对照组:display:flex 的值容器 —— text-box 计算值是 trim-both,但行盒量起来还是 32px */
- const sizeFlex = (css) => {
- const el = document.createElement('span');
- el.style.cssText = `display:flex;font-size:32px;line-height:1;${css}`;
- el.innerHTML = '<span>1284</span>';
- host.appendChild(el);
- const h = el.getBoundingClientRect().height;
- const cs = getComputedStyle(el);
- const out = { h, trim: cs.textBoxTrim, edge: cs.textBoxEdge, lh: parseFloat(cs.lineHeight) };
- el.remove();
- return out;
- };
- const sizes = [12, 20, 32];
- const res = {};
- for (const px of sizes) {
- const fs = `font-size:${px}px;`;
- res[px] = { raw: size(fs), trim: size(fs + TRIM), shorthand: size(fs + 'text-box:trim-both cap alphabetic;') };
- }
- res.flex = sizeFlex(TRIM);
- host.remove();
- return { v: (navigator.userAgent.match(/Chrome\/[\d.]+/) || [''])[0], sizes, res, supports: CSS.supports('text-box-trim', 'trim-both') && CSS.supports('text-box-edge', 'cap alphabetic') };
- });
- /* ============================================================
- * 2. 页面真实元素:.b / 32px 主数值
- * ============================================================ */
- // 工作台:预置「我是谁」让「我的推进」也渲染出来;.b 与 .kpi b 都量
- await openView(page, base, '/', { me: '赵磊', wait: 1000 });
- const wb = await evalIn(page, `
- const pick = (sel, n = 6) => {
- const out = [];
- deepAll((el) => { if (out.length < n && el.matches?.(sel) && visible(el)) out.push(el); });
- return out.map((el) => {
- const cs = getComputedStyle(el);
- const r = el.getBoundingClientRect();
- return { text: el.textContent.trim().slice(0, 12), font: parseFloat(cs.fontSize), lh: parseFloat(cs.lineHeight),
- trim: cs.textBoxTrim, edge: cs.textBoxEdge, h: Math.round(r.height * 100) / 100 };
- });
- };
- return { b: pick('.b'), kpi: pick('.kpi b', 4) };
- `);
- // 报告类型页也有一排 32px KPI
- await openView(page, base, '/reports/voc', { wait: 900 });
- const typeKpi = await evalIn(page, `
- const out = [];
- deepAll((el) => { if (out.length < 4 && el.matches?.('.kpi b') && visible(el)) {
- const cs = getComputedStyle(el); const r = el.getBoundingClientRect();
- out.push({ text: el.textContent.trim().slice(0, 12), font: parseFloat(cs.fontSize), lh: parseFloat(cs.lineHeight), trim: cs.textBoxTrim, edge: cs.textBoxEdge, h: Math.round(r.height * 100) / 100 });
- } });
- return out;
- `);
- /* ============================================================
- * 输出:对照表
- * ============================================================ */
- console.log('## 三档字号:加 / 不加 trim 的行盒高度(inline-block,line-height 1,文本「1284」)\n');
- const ctable = probe.sizes.map((px) => {
- const r = probe.res[px];
- const pct = (a, b) => `${(((b - a) / a) * 100).toFixed(1)}%`;
- return {
- size: `${px}px`,
- raw: `${r.raw.h.toFixed(2)}px`,
- trim: `${r.trim.h.toFixed(2)}px (${r.trim.trim})`,
- delta: pct(r.raw.h, r.trim.h),
- css: `${(r.raw.h / px).toFixed(2)}em → ${(r.trim.h / px).toFixed(2)}em`,
- sh: `${r.shorthand.h.toFixed(2)}px`,
- };
- });
- table(ctable, [['字号', 'size'], ['不加 trim', 'raw'], ['加独立属性', 'trim'], ['变化', 'delta'], ['行盒 / 字号', 'css'], ['对照:简写写法', 'sh']]);
- const fx = probe.res.flex;
- console.log(`\n浏览器(${probe.v})支持独立长属性:${probe.supports ? '是' : '否'};` +
- `简写 text-box: trim-both cap alphabetic 在本浏览器 computed 是 text-box-trim=${probe.res[32].shorthand.trim}(上一轮实测它被丢弃 → 行盒 32px)。`);
- console.log(`对照组:32px + trim-both 但容器 display:flex → computed text-box-trim=${fx.trim},行盒实测 ${fx.h.toFixed(2)}px(修剪对 flex 容器不生效;本轮把值容器改成 block + height:max-content 后才有上面 32px→23.28px 的效果)。`);
- console.log('\n## 页面真实元素\n');
- const fmtEl = (e) => `font ${e.font}px / line-height ${e.lh}px / text-box-trim ${e.trim} / text-box-edge ${e.edge} / 行盒 ${e.h}px`;
- for (const e of wb.kpi) row(`工作台 32px KPI「${e.text}」`, `${e.h}px`, fmtEl(e), e.font === 32 && e.trim === 'trim-both' && e.h <= 26);
- for (const e of typeKpi) row(`报告类型页 32px KPI「${e.text}」`, `${e.h}px`, fmtEl(e), e.font === 32 && e.trim === 'trim-both' && e.h <= 26);
- for (const e of wb.b) {
- const big = e.font >= 28;
- const pass = e.trim === 'trim-both' && e.edge === 'cap alphabetic' && (!big || e.h <= 26);
- row(`.b「${e.text}」${big ? '(主数值)' : '(徽标)'}`, `${e.h}px`, fmtEl(e), pass);
- }
- /* ============================================================
- * 判分
- * ============================================================ */
- console.log('\n## 达标判定(32px 档:trim 后行盒 ≤ 26px)\n');
- table(rec.map((r) => ({ item: r.item, m: r.measured, ok: r.pass ? '✓' : '✗' })), [['项', 'item'], ['实测行盒', 'm'], ['通过', 'ok']]);
- // 对照实验本身也要落进判定:32px 加 trim 必须 ≤ 26,且明显低于不加 trim
- const p32 = probe.res[32];
- const probeOk = p32.trim.h <= 26 && p32.trim.h < p32.raw.h * 0.75;
- console.log(`\n对照实验 32px:不加 trim ${p32.raw.h.toFixed(2)}px → 加 trim ${p32.trim.h.toFixed(2)}px(达标线 ≤ 26px)${probeOk ? ' ✓' : ' ✗'}`);
- await browser.close();
- srv?.kill();
- const bad = fails.length + (probeOk ? 0 : 1);
- if (bad) {
- console.log(`\n失败项 ${bad} 个:`);
- for (const f of fails) console.log(` ✗ ${f}`);
- if (!probeOk) console.log(' ✗ 对照实验 32px 行盒未压到 ≤ 26px');
- process.exit(1);
- }
- console.log('\n全部达标(exit 0)');
|