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 { // 等 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([]); }