intent/tests/a11y.py

68 lines
2.5 KiB
Python
Raw Permalink Normal View History

"""Structural accessibility checks for rendered pages.
Not a substitute for axe or a screen reader, but cheap enough to run on every page in unit tests.
"""
from html.parser import HTMLParser
_UNLABELLED_INPUT_TYPES = {"hidden", "submit", "button", "reset", "image"}
class _Collector(HTMLParser):
def __init__(self):
super().__init__()
self.html_lang = None
self.headings = []
self.ids = []
self.label_fors = set()
self.controls = []
self.images_without_alt = 0
self._label_depth = 0
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == "html":
self.html_lang = attrs.get("lang")
if "id" in attrs:
self.ids.append(attrs["id"])
if tag in {"h1", "h2", "h3", "h4", "h5", "h6"}:
self.headings.append(int(tag[1]))
if tag == "label":
self._label_depth += 1
if attrs.get("for"):
self.label_fors.add(attrs["for"])
if tag == "img" and "alt" not in attrs:
self.images_without_alt += 1
if tag in {"input", "select", "textarea"}:
if tag == "input" and attrs.get("type", "text") in _UNLABELLED_INPUT_TYPES:
return
wrapped = self._label_depth > 0
named = bool(attrs.get("aria-label") or attrs.get("aria-labelledby"))
self.controls.append((attrs.get("id"), attrs.get("name"), wrapped or named))
def handle_endtag(self, tag):
if tag == "label":
self._label_depth -= 1
def assert_accessible_html(html: str) -> None:
page = _Collector()
page.feed(html)
problems = []
if not page.html_lang:
problems.append("<html> has no lang")
if page.headings.count(1) != 1:
problems.append(f"expected one <h1>, found {page.headings.count(1)}")
for before, after in zip(page.headings, page.headings[1:], strict=False):
if after > before + 1:
problems.append(f"heading level skips from h{before} to h{after}")
duplicates = {i for i in page.ids if page.ids.count(i) > 1}
if duplicates:
problems.append(f"duplicate ids: {sorted(duplicates)}")
for control_id, name, labelled in page.controls:
if not (labelled or control_id in page.label_fors):
problems.append(f"form control without a label: id={control_id} name={name}")
if page.images_without_alt:
problems.append(f"{page.images_without_alt} <img> without alt")
assert not problems, "\n".join(problems)