50 lines
1.5 KiB
TypeScript
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([]);
|
|
}
|