Files
novalon-website/e2e/touch-targets.ts
T
zhangxiang 6bb7c557ee test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注
  skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets
  等复用桩。
- 集成层:config/test/jest.integration.config.js + tests-integration/ 真库
  一次性 SQLite 用例,teardown 守卫开发库指纹。
- 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、
  check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse
  配置收敛。
2026-09-28 10:48:08 +08:00

79 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<TouchTargetReport> {
return page.evaluate(([aaMin, aaaMin]) => {
const nodes = document.querySelectorAll<HTMLElement>(
'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');
}