- 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 配置收敛。
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
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');
|
||
}
|