新增可复用的全站 dogfood 审计工具链(Playwright,31 路由约 2 分钟跑完): - scripts/audit/ui-audit.mjs —— 主审计器。9 维自动化探针:对比度(WCAG 相对亮度 + alpha 合成 + 大文本 3:1 / 小文本 4.5:1)、横向溢出、触控目标 (WCAG 2.2 AA 2.5.8 24x24)、img alt、标题层级、form label、文本截断、 颜色频次(品牌红覆盖)、性能(LCP + CLS)。支持多视口采集与 P1/P2/P3 自动分级。 - scripts/audit/audit-fullpage.mjs —— 核心页 fullPage 长图(首屏看不全叙事结构) - scripts/audit/verify-tabbar.mjs —— 移动端 Tab bar 遮挡修复验证 - scripts/audit/contact-diag.mjs —— 单页诊断(console/pageerror/requestfailed 捕获) 防刷绿机制:对比度探针命中 aria-hidden 祖先时跳过,但累计 decorativeSkipped 让豁免量可见;触控探针把 aria-hidden 内的可聚焦元素计入 ariaHiddenFocusable 单独上报,不静默跳过。 已知盲区(需在报告中人工补判):effectiveBg 遇到渐变或图片背景返回 unknown 并跳过整节点 —— 详情页 hero 的装饰数字因此从未被自动报出。 .gitignore 增加 dogfood-ui-audit/(采集产物单轮可达 39MB,只提交脚本与报告)。 Refs: UI_UX_UE_AUDIT_2026-09-01.md
49 lines
2.0 KiB
JavaScript
49 lines
2.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 补充脚本:对核心页截 fullPage 长图,用于验证四层叙事完整性。
|
|
*/
|
|
import { chromium } from 'playwright';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, '../..');
|
|
const OUT = process.argv[2] || path.join(ROOT, 'dogfood-ui-audit', '2026-09-01T07-07-49', 'screenshots', 'fullpage');
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
const TARGETS = [
|
|
{ route: '/', vp: { width: 1440, height: 900 } },
|
|
{ route: '/products', vp: { width: 1440, height: 900 } },
|
|
{ route: '/products/erp', vp: { width: 1440, height: 900 } },
|
|
{ route: '/solutions/finance', vp: { width: 1440, height: 900 } },
|
|
{ route: '/services/consulting', vp: { width: 1440, height: 900 } },
|
|
{ route: '/about', vp: { width: 1440, height: 900 } },
|
|
{ route: '/contact', vp: { width: 1440, height: 900 } },
|
|
{ route: '/', vp: { width: 390, height: 844 } },
|
|
{ route: '/products', vp: { width: 390, height: 844 } },
|
|
{ route: '/contact', vp: { width: 390, height: 844 } },
|
|
];
|
|
|
|
const BASE = process.env.AUDIT_BASE_URL || 'http://localhost:3000';
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
for (const t of TARGETS) {
|
|
const ctx = await browser.newContext({ viewport: t.vp, deviceScaleFactor: 1 });
|
|
const page = await ctx.newPage();
|
|
const slug = (t.route === '/' ? 'home' : t.route.replace(/^\//, '').replace(/\//g, '__'));
|
|
const vp = t.vp.width < 500 ? 'mobile' : 'desktop';
|
|
const file = path.join(OUT, `${vp}-${slug}.png`);
|
|
try {
|
|
const t0 = Date.now();
|
|
await page.goto(BASE + t.route, { waitUntil: 'load', timeout: 60000 });
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: file, fullPage: true });
|
|
console.log(`✓ ${file} (${Date.now() - t0}ms)`);
|
|
} catch (e) {
|
|
console.log(`✗ ${t.route} ${t.vp.width}x${t.vp.height}: ${e.message.slice(0, 100)}`);
|
|
}
|
|
await ctx.close();
|
|
}
|
|
await browser.close();
|
|
console.log(`\n输出目录:${OUT}`); |