Files
book-comic-library/frontend/e2e/helpers/axe.ts
T
2026-09-16 19:27:25 +08:00

50 lines
1.5 KiB
TypeScript

import { expect, type Page } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
interface Waiver {
page: string;
rule: string;
reason: string;
}
/**
* critical/serious 违规的逐条豁免清单(④ 基建):
* ② 阅读器改版后旧 chrome 重写完毕,本期清空;新增豁免必须写明 reason 并控制在一次性条件。
*/
const WAIVERS: Waiver[] = [];
export async function scanA11y(page: Page, name: string): Promise<void> {
// 等 CSS 动画/过渡收尾(如 fx-rise 入场淡入)再扫:动画途中采样会因不透明度混色造成 contrast 误报;2s 上限兜底长动画
await page.evaluate(() =>
Promise.race([
Promise.allSettled(document.getAnimations().map((a) => a.finished)),
new Promise((r) => setTimeout(r, 2000)),
]),
);
const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();
if (results.violations.length > 0) {
console.log(
`[axe:${name}]`,
JSON.stringify(
results.violations.map((v) => ({
id: v.id,
impact: v.impact,
nodes: v.nodes.map((n) => n.target.join(" ")),
})),
null,
2,
),
);
}
const blocking = results.violations.filter(
(v) =>
(v.impact === "critical" || v.impact === "serious") && !WAIVERS.some((w) => w.page === name && w.rule === v.id),
);
expect(
blocking.map((v) => ({ id: v.id, impact: v.impact })),
`${name} 存在未豁免的 critical/serious a11y 违规`,
).toEqual([]);
}