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 配置收敛。
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user