feat: add script to audit guides for unique WCAG 2.1 AA issues using pa11y
Co-authored-by: aider (o3) <aider@aider.chat>
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Audit each guide URL in guides.csv for WCAG 2.1 AA accessibility issues
|
||||||
|
(using pa11y). Writes a report that omits issues appearing in *every*
|
||||||
|
guide.
|
||||||
|
|
||||||
|
Output file: output2.txt
|
||||||
|
Usage:
|
||||||
|
python check_wcag_unique.py
|
||||||
|
Prerequisite:
|
||||||
|
npm install -g pa11y # or have pa11y available via npx
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Set
|
||||||
|
|
||||||
|
GUIDES_CSV = Path(__file__).with_name("guides.csv")
|
||||||
|
OUTPUT_FILE = Path("output2.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def run_pa11y(url: str) -> List[dict]:
|
||||||
|
"""Run pa11y on the URL and return its JSON‐parsed list of issues."""
|
||||||
|
cmd_base = ["pa11y"] if shutil.which("pa11y") else ["npx", "pa11y"]
|
||||||
|
result = subprocess.run(
|
||||||
|
[*cmd_base, url, "--standard", "WCAG2AA", "--reporter", "json"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# pa11y exits 0 (no issues) or 1 (issues found). Treat both as success.
|
||||||
|
if result.returncode not in (0, 1):
|
||||||
|
print(f"[ERROR] pa11y failed with code {result.returncode} for {url}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[WARN] Could not parse pa11y JSON output for {url}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def read_guides(csv_path: Path) -> List[Dict[str, str]]:
|
||||||
|
with csv_path.open(newline="", encoding="utf-8-sig") as fh:
|
||||||
|
return list(csv.DictReader(fh))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_common_codes(issues_by_guide: Dict[str, List[dict]]) -> Set[str]:
|
||||||
|
"""Return set of issue codes present in *every* guide."""
|
||||||
|
common: Set[str] | None = None
|
||||||
|
for issues in issues_by_guide.values():
|
||||||
|
codes = {i["code"] for i in issues}
|
||||||
|
common = codes if common is None else common & codes
|
||||||
|
return common or set()
|
||||||
|
|
||||||
|
|
||||||
|
def format_issue(issue: dict) -> str:
|
||||||
|
selector = (issue.get("selector") or "").strip() or "N/A"
|
||||||
|
return f"[{issue['type']}] {issue['code']}: {issue['message']} (selector: {selector})"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
guides = read_guides(GUIDES_CSV)
|
||||||
|
issues_by_guide: Dict[str, List[dict]] = {}
|
||||||
|
|
||||||
|
print("Auditing guides with pa11y…\n")
|
||||||
|
for guide in guides:
|
||||||
|
name, url = guide["Name"], guide["URL"]
|
||||||
|
print(f"• {name} — {url}")
|
||||||
|
issues_by_guide[name] = run_pa11y(url)
|
||||||
|
|
||||||
|
common_codes = compute_common_codes(issues_by_guide)
|
||||||
|
|
||||||
|
lines: List[str] = []
|
||||||
|
lines.append("Accessibility report (WCAG 2.1 AA — unique issues only)")
|
||||||
|
lines.append("=" * 50)
|
||||||
|
|
||||||
|
for name, issues in issues_by_guide.items():
|
||||||
|
unique = [i for i in issues if i["code"] not in common_codes]
|
||||||
|
lines.append(f"\nGuide: {name}")
|
||||||
|
if not unique:
|
||||||
|
lines.append(" ✔ No unique issues found.")
|
||||||
|
continue
|
||||||
|
for issue in unique:
|
||||||
|
lines.append(f" • {format_issue(issue)}")
|
||||||
|
|
||||||
|
report = "\n".join(lines)
|
||||||
|
print("\n" + report)
|
||||||
|
OUTPUT_FILE.write_text(report, encoding="utf-8")
|
||||||
|
print(f"\nReport written to {OUTPUT_FILE.resolve()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user