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:
2026-09-28 10:48:08 +08:00
parent a366bd1400
commit 6bb7c557ee
142 changed files with 6597 additions and 2653 deletions
+78
View File
@@ -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');
}