122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Check each guide URL in guides.csv for WCAG 2.1 AA accessibility
|
|
issues using the pa11y CLI and report results.
|
|
|
|
The script:
|
|
1. Reads guides.csv in the current directory.
|
|
2. Runs `pa11y` for each URL with the WCAG2AA standard, requesting JSON.
|
|
3. Collects the issues for every guide.
|
|
4. Removes any issue codes that appear in *every* guide.
|
|
5. Prints the filtered results to the console and writes them to
|
|
output.txt.
|
|
|
|
Prerequisites:
|
|
npm install -g pa11y
|
|
|
|
Usage:
|
|
python check_wcag.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set
|
|
|
|
GUIDES_CSV = Path(__file__).with_name("guides.csv")
|
|
OUTPUT_FILE = Path("output.txt")
|
|
|
|
|
|
def run_pa11y(url: str) -> List[dict]:
|
|
"""
|
|
Run pa11y on a URL and return the parsed JSON list of issues.
|
|
|
|
Returns an empty list if pa11y fails.
|
|
"""
|
|
try:
|
|
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,
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"[WARN] pa11y returned code {result.returncode} for {url}", file=sys.stderr)
|
|
return []
|
|
return json.loads(result.stdout)
|
|
except FileNotFoundError:
|
|
print("Error: pa11y not found. Install it with `npm install -g pa11y`.", file=sys.stderr)
|
|
sys.exit(1)
|
|
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]]:
|
|
"""Load all guides from the CSV into a list of dicts."""
|
|
with csv_path.open(newline="", encoding="utf-8-sig") as fh:
|
|
reader = csv.DictReader(fh)
|
|
return list(reader)
|
|
|
|
|
|
def compute_common_codes(issues_by_guide: Dict[str, List[dict]]) -> Set[str]:
|
|
"""Return the set of issue codes that appear in every guide."""
|
|
all_codes_iter = (
|
|
{issue["code"] for issue in issues} for issues in issues_by_guide.values()
|
|
)
|
|
common_codes: Set[str] | None = None
|
|
for codes in all_codes_iter:
|
|
if common_codes is None:
|
|
common_codes = codes
|
|
else:
|
|
common_codes &= codes
|
|
return common_codes or set()
|
|
|
|
|
|
def format_issue(issue: dict) -> str:
|
|
"""Return a human-readable one-line representation of a pa11y issue."""
|
|
selector = issue.get("selector", "").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 = guide["Name"]
|
|
url = guide["URL"]
|
|
print(f"• {name} — {url}")
|
|
issues = run_pa11y(url)
|
|
issues_by_guide[name] = issues
|
|
|
|
common_codes = compute_common_codes(issues_by_guide)
|
|
|
|
lines_out: List[str] = []
|
|
lines_out.append("\nAccessibility report (WCAG 2.1 AA)")
|
|
lines_out.append("=" * 40)
|
|
|
|
for name, issues in issues_by_guide.items():
|
|
filtered = [i for i in issues if i["code"] not in common_codes]
|
|
lines_out.append(f"\nGuide: {name}")
|
|
if not filtered:
|
|
lines_out.append(" ✔ No unique issues found.")
|
|
continue
|
|
for issue in filtered:
|
|
lines_out.append(f" • {format_issue(issue)}")
|
|
|
|
report_text = "\n".join(lines_out)
|
|
print("\n" + report_text)
|
|
OUTPUT_FILE.write_text(report_text, encoding="utf-8")
|
|
print(f"\nReport written to {OUTPUT_FILE.resolve()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|