Metadata-Version: 2.4
Name: sightline-a11y
Version: 0.1.0
Summary: The WCAG 2.2 accessibility checks axe-core can't do: focus obscuring, focus appearance, target size, dragging, accessible authentication.
Author: Sightline contributors
License: MIT
Project-URL: Homepage, https://github.com/Ax1zz/sightline
Project-URL: Repository, https://github.com/Ax1zz/sightline
Project-URL: Issues, https://github.com/Ax1zz/sightline/issues
Project-URL: Changelog, https://github.com/Ax1zz/sightline/releases
Keywords: accessibility,a11y,wcag,wcag22,playwright,testing,audit,en301549,european-accessibility-act,ci
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.30
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# Sightline

**axe-core tests 1 of the 9 success criteria WCAG 2.2 added. Sightline tests 8.**

[![PyPI](https://img.shields.io/pypi/v/sightline-a11y?color=1b3a6b&label=pypi)](https://pypi.org/project/sightline-a11y/)
[![License: MIT](https://img.shields.io/badge/license-MIT-1b3a6b.svg)](LICENSE)
[![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-3776ab.svg)](https://www.python.org/)
[![Playwright](https://img.shields.io/badge/engine-Playwright-2ead33.svg)](https://playwright.dev/python/)
[![Tests](https://img.shields.io/badge/tests-55%20passing-1c6b45.svg)](tests/)
[![Status: beta](https://img.shields.io/badge/status-beta-8a5a00.svg)](#roadmap)

🇩🇪 [Deutsche Fassung](README.de.md)

Sightline is an accessibility auditor for the WCAG 2.2 criteria that static DOM
analysis structurally cannot answer: whether a sticky header covers the focused
element, whether a focus indicator is actually *visible*, whether a control is
drag-only, whether a password field silently blocks paste.

It drives a real browser through Playwright, measures pixels and geometry, and
reports the numbers behind every verdict.

![Sightline terminal output](https://raw.githubusercontent.com/Ax1zz/sightline/main/docs/terminal.png)

---

## The problem

Almost every automated accessibility check in the industry runs on
[axe-core](https://github.com/dequelabs/axe-core). Lighthouse depends on it
([`axe-core: ^4.12.1`](https://github.com/GoogleChrome/lighthouse/blob/main/package.json)),
so does [Pa11y](https://github.com/pa11y/pa11y/blob/main/package.json), and so
do `jest-axe`, `@axe-core/playwright` and axe DevTools by construction.

axe-core injects a script into the page and inspects the DOM and computed
styles. That is the right tool for alt text, ARIA validity, heading order and
text contrast — and it is excellent at them.

But WCAG 2.2 ([W3C Recommendation, revised 12 December 2024](https://www.w3.org/TR/WCAG22/))
added nine success criteria, and most of them are not questions the DOM can
answer. They are questions about layout geometry, rendered pixels, which event
handlers a component registered, and how several pages compare to one another.

Check it yourself. Every WCAG tag axe-core emits is listed in
[`doc/rule-descriptions.md`](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md):

```bash
curl -s https://raw.githubusercontent.com/dequelabs/axe-core/develop/doc/rule-descriptions.md \
  | grep -oE "wcag[0-9a-z]+" | sort -u
```

The complete set contains exactly one WCAG 2.2 tag: `wcag258`, on the single
`target-size` rule, marked "needs review". There is no `wcag2411`, no
`wcag2413`, no `wcag257`, no `wcag326`, no `wcag337`, no `wcag338`.

So a team can have a green accessibility pipeline and still fail eight of the
nine criteria the standard added — including four at Level AA, which the
[European Accessibility Act](https://ec.europa.eu/social/main.jsp?catId=1202)
regime and EN 301 549 both point at.

That gap is what Sightline fills. It is not a replacement for axe-core. **Run
both.**

---

## What it checks

| SC | Criterion | Level | In axe-core? | How Sightline decides |
|----|-----------|-------|--------------|------------------------|
| 2.4.11 | Focus Not Obscured (Minimum) | AA | ❌ | Tabs through the page for real; at each stop hit-tests a grid of points with `elementFromPoint` and reports the fraction covered |
| 2.4.12 | Focus Not Obscured (Enhanced) | AAA | ❌ | Same measurement, fails on any real coverage |
| 2.4.13 | Focus Appearance | AAA | ❌ | Screenshots each focus stop unfocused and focused, decodes and diffs the pixels, checks area against a 2px perimeter and contrast against 3:1 |
| 2.5.7 | Dragging Movements | AA | ❌ | Wraps `addEventListener` before navigation to see which elements registered a pointer-down/pointer-move pair with no keyboard path |
| 2.5.8 | Target Size (Minimum) | AA | ⚠️ one rule, "needs review" | 24×24 px, with the spacing exception implemented as real 24px-circle intersection and the inline exception requiring genuine non-target text |
| 3.2.6 | Consistent Help | A | ❌ | Locates help mechanisms in twelve languages across several pages and compares relative order and containing landmark |
| 3.3.8 | Accessible Authentication (Min) | AA | ❌ | Dispatches a real cancelable `paste` event to detect paste blocking; checks credential `autocomplete`; detects CAPTCHAs |
| 3.3.9 | Accessible Authentication (Enhanced) | AAA | ❌ | Same, without the object-recognition exception |
| 3.3.7 | Redundant Entry | A | ❌ | **Deliberately not implemented** — see [Honest limitations](#honest-limitations) |

---

## Install

```bash
pip install sightline-a11y
python -m playwright install chromium
```

Or from source:

```bash
git clone https://github.com/Ax1zz/sightline.git
cd sightline
pip install -e ".[dev]"
python -m playwright install chromium
```

## Quick start

```bash
# Audit a page
sightline https://example.com

# Level AA only, with a standalone HTML report you can attach to an audit
sightline https://example.com --level AA --html report.html

# Several pages: this additionally enables 3.2.6 Consistent Help
sightline https://example.com/ https://example.com/pricing https://example.com/contact

# Machine-readable output for CI
sightline https://example.com --json - | jq '.counts'
```

Exit code is `1` when anything fails, `0` otherwise, so it drops straight into a
pipeline. `--exit-zero` reports without failing the build;
`--fail-on-review` is stricter and also fails on review items.

![HTML report](https://raw.githubusercontent.com/Ax1zz/sightline/main/docs/report.png)

## Use it inside an existing Playwright test

The CLI navigates for you. If you already have a page — behind a login, mid-flow,
three modals deep — hand it over directly:

```python
from playwright.sync_api import sync_playwright
from sightline import audit, instrument

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    instrument(page)                 # before navigating, for 2.5.7
    page.goto("https://app.example.com/login")
    page.fill("#email", "demo@example.com")
    page.fill("#password", "hunter2")
    page.click("button[type=submit]")

    report = audit(page, levels=["A", "AA"])

    for finding in report.violations:
        print(finding.sc, finding.target, finding.message)

    assert not report.violations, report.to_json()
```

`audit()` returns a `Report`; every `Finding` carries `sc`, `level`, `outcome`,
`impact`, `target`, `message`, `help_url` and a `detail` dict holding the raw
measurements — pixel counts, contrast ratios, coverage fractions, dimensions —
so a human can check the tool's arithmetic.

### GitHub Actions

```yaml
- uses: actions/setup-python@v5
  with: { python-version: '3.12' }
- run: pip install sightline-a11y && python -m playwright install --with-deps chromium
- run: sightline ${{ env.PREVIEW_URL }} --level AA --html a11y.html
- uses: actions/upload-artifact@v4
  if: always()
  with: { name: accessibility-report, path: a11y.html }
```

---

## Alternatives

| | Sightline | axe-core / Lighthouse / Pa11y | Manual audit |
|---|---|---|---|
| Focus obscuring (2.4.11/2.4.12) | measured | not covered | yes, slowly |
| Focus appearance (2.4.13) | measured from pixels | not covered | yes, subjective |
| Dragging (2.5.7) | detected, flagged for review | not covered | yes |
| Target size (2.5.8) | full exception logic | one rule, needs review | yes |
| Consistent help (3.2.6) | compared across pages | not covered | yes |
| Accessible auth (3.3.8) | paste actively probed | not covered | yes |
| Alt text, ARIA, contrast, headings | **not covered — use axe-core** | excellent | yes |
| Runs in CI | yes | yes | no |
| Cost per run | free | free | hours of a specialist |

The honest summary: axe-core owns the DOM layer, Sightline owns the
geometry-and-behaviour layer, and neither replaces a human for the criteria that
need judgement.

---

## Honest limitations

Automated testing cannot prove conformance, and a tool that pretends otherwise
is worse than no tool. Sightline is explicit about where it stops:

- **3.3.7 Redundant Entry is not implemented.** It requires modelling a
  multi-step process and knowing what the user already supplied. Any automated
  verdict would be a guess, so there isn't one.
- **Heuristic checks report `review`, never `fail`.** 2.5.7 can tell you a
  component is drag-operated; it cannot know whether an equivalent button exists
  elsewhere on the page.
- **The 2.5.8 "equivalent control" and "essential" exceptions are not
  detectable.** Failures say so.
- **3.2.6 approximates.** Relative order is compared among help mechanisms and
  their landmarks, not against every piece of page content.
- **2.4.13 reads the rendered result.** A focus indicator drawn with a
  background image, a pseudo-element or an SVG is measured the same way as an
  outline — which is the point — but an animated indicator can be captured
  mid-transition. Screenshots are taken with animations disabled to limit this.
- **Only what is reachable by Tab is examined.** Controls behind a menu you have
  to open first need you to drive the page and call `audit(page)`.
- **The 3.3.8 paste probe dispatches a real event** at credential fields, which
  fires the page's own handlers. Use `--no-paste-probe` against production.

## How it works

```
CLI / Python API
      │
   Auditor ──── instrument(page)   init script, must precede navigation
      │
  Check registry ──── each check: run(page, ctx) -> [Finding]
      │
  Primitives:  png.py     dependency-free PNG decoder (zlib only)
               color.py   sRGB luminance, WCAG contrast ratio
               dom.py     hit testing, geometry, listener capture
      │
  Reporters:  text │ json │ standalone HTML
```

The only runtime dependency is Playwright. The PNG decoder is hand-written
rather than pulling in Pillow for one narrow job; there are no network calls, no
telemetry and no model inference anywhere in the audit.

## Development

```bash
pip install -e ".[dev]"
python -m playwright install chromium
pytest -q
```

The test suite runs against two fixtures: `tests/fixtures/failing.html` plants a
defect for each criterion, and `passing.html` is the same UI done accessibly.
Every check is asserted in both directions — a checker that fires on everything
is as useless as one that fires on nothing.

## Roadmap

- [ ] Async Playwright API alongside the sync one
- [ ] `pytest` plugin: `assert_wcag22(page)`
- [ ] Reusable GitHub Action
- [ ] SARIF output for code scanning
- [ ] Baseline files, so existing debt can be accepted and only regressions fail
- [ ] Firefox and WebKit (the checks are engine-agnostic; only capture is tuned)
- [ ] EN 301 549 clause mapping in the HTML report

## Contributing

New checks, better exception logic and real-world false-positive reports are all
welcome — a false positive is a bug here, not a rounding error. See
[CONTRIBUTING.md](CONTRIBUTING.md).

## License

[MIT](LICENSE)
