import type { Page } from '@playwright/test'; /** * 移动端触摸目标扫描(验收 R-4 / E2E 归零)。 * * 标准出处(两条独立信源): * · WCAG 2.2 SC 2.5.8 Target Size (Minimum) —— Level AA,24×24 CSS px, * 例外:Equivalent / Inline / User Agent Control / Essential / Spacing。 * · WCAG 2.1 SC 2.5.5 Target Size (Enhanced) —— Level AAA,44×44 CSS px。 * 旧断言把 44px 写成「WCAG 2.1 AA」是标准错配:44px 属 AAA,AA 档没有触摸目标条款。 * 因此这里以 AA(24px) 为硬门禁、AAA(44px) 为 advisory 清单供设计侧跟踪。 */ export interface TouchTarget { tag: string; text: string; width: number; height: number; } export interface TouchTargetReport { /** 违反 AA 2.5.8(<24×24)的目标,硬门禁应为 0 */ aaViolations: TouchTarget[]; /** 未达 AAA 2.5.5(<44×44)的目标,仅作跟踪日志 */ aaaShortlist: TouchTarget[]; } const AA_MIN_PX = 24; const AAA_MIN_PX = 44; export async function scanTouchTargets(page: Page): Promise { return page.evaluate(([aaMin, aaaMin]) => { const nodes = document.querySelectorAll( 'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])' ); const aaViolations: TouchTarget[] = []; const aaaShortlist: TouchTarget[] = []; nodes.forEach((el) => { const rect = el.getBoundingClientRect(); const style = window.getComputedStyle(el); const rendered = rect.width > 0 && rect.height > 0 && rect.right > 0 && rect.bottom > 0 && rect.left < window.innerWidth && style.display !== 'none' && style.visibility !== 'hidden' && !el.hasAttribute('hidden'); if (!rendered) {return;} // Inline 例外(2.5.8):段落/列表/标题内的行内链接不按独立目标计量。 if (el.closest('p, li, h1, h2, h3, h4, h5, h6') && style.display === 'inline') {return;} const entry: TouchTarget = { tag: el.tagName, text: (el.textContent || '').trim().slice(0, 30) || el.tagName, width: Math.round(rect.width), height: Math.round(rect.height), }; if (rect.width < aaMin || rect.height < aaMin) { aaViolations.push(entry); } else if (rect.width < aaaMin || rect.height < aaaMin) { aaaShortlist.push(entry); } }); return { aaViolations, aaaShortlist }; }, [AA_MIN_PX, AAA_MIN_PX]); } export function formatTargets(targets: TouchTarget[]): string { return targets .slice(0, 12) .map((t) => ` <${t.tag}> "${t.text}" — ${t.width}x${t.height}px`) .join('\n'); }