# wcag-audit

A small Express + Playwright service that runs a multi-page **WCAG accessibility audit**
using [axe-core](https://github.com/dequelabs/axe-core), then renders a single
self-contained HTML report per run plus CSV, Excel and PDF exports.

Extracted from the `ui-compare` project as a standalone service — the same feature still
lives there too; this repo is just the accessibility audit on its own, with none of the
visual-diff / post-deploy machinery.

## How it behaves for a visitor

1. They paste up to **3 URLs** and hit Run Audit.
2. When the scan finishes, a **summary dashboard renders on the same page** —
   an overall pass score, counts by severity (critical / high / medium / low /
   needs review), and a per-category breakdown of exactly which checks passed
   and which failed.
3. To see the **detailed report** they enter their name, email and phone in a
   modal. Submitting it unlocks the report and emails the contact details to
   the enquiries inbox.
4. From there they can open the full report or **download it as a PDF**.

The detailed report is genuinely gated: `web-reports/` is not served statically,
and `GET /report/:runId` returns 403 until that run has had a lead submitted
against it.

## What it does

- Scans up to **3 pages** (pasted URLs) or samples up to **60** from an XML sitemap
  (sitemap mode is API-only — the page itself only takes pasted URLs).
- Runs **axe-core** against a selectable WCAG version/level — 2.0/2.1/2.2 × A/AA
  (cumulative tag sets, defaults to **2.2 AA**).
- Adds two live checks axe-core can't do alone: a **Tab-key walk** (reachability,
  keyboard traps, visible focus indicators) and a **320px reflow** check.
- Classifies every violation by **region** — walking up from the failing element to the
  nearest landmark. Hits inside `header`/`nav`/`footer` are grouped into one *site-wide*
  issue per rule (that markup repeats on every page), while `main` hits stay page-specific.
- Exports **HTML, CSV, Excel** (`exceljs`) and **PDF** (a second headless page calling
  `page.pdf()`).

## Requirements

- Node.js >= 18
- Chromium (installed via Playwright — the only browser this service drives)

## Install

```bash
npm install
npx playwright install chromium
```

Or run `./install.sh`, which does both and restarts a PM2 service if one is running.

## Run

```bash
npm start                 # http://localhost:4200 (PORT env var to override)
```

Reports are written to `web-reports/<runId>/`. They are not served statically — see the report gate below.

## Architecture

- `webapp.js` — Express routes only: the run lifecycle (`runs` in-memory map), SSE
  broadcasting, and the Cloudflare-clearance endpoints. No audit logic lives here.
- `src/a11y-checker.js` — the audit itself. Injects axe-core into each Playwright page,
  runs the keyboard/reflow passes, and classifies each violation's region.
  `WCAG_VERSIONS` maps the selectable version/level to axe tag sets.
- `src/a11y-reporter.js` — `generateA11yReport(result, outDir)`; builds the HTML report
  and the CSV/Excel/PDF exports, and does the common-vs-page-specific grouping.
- `src/sitemap-loader.js` — loads a sitemap (recursing into a `<sitemapindex>`'s child
  sitemaps) and samples across page templates so a large site's page types all get
  covered rather than just the first N URLs listed.
- `src/cf-clearance.js` / `src/xvfb.js` — the "solve the Cloudflare challenge once in a
  real headed browser, save the session, replay it" mechanism, streamed to the browser
  over SSE as a live CDP screencast.

## Browser routes

These back the web page. For machine use see the **API** section below.

### `POST /a11y-audit`

Form-urlencoded. Returns `{ runId }` — subscribe to `/stream/:runId` for progress, or
poll `/status/:runId`.

| Field | Notes |
|---|---|
| `mode` | `direct` (default) or `sitemap` |
| `urls` | newline/comma-separated, up to 3 — for `mode=direct` |
| `sitemapUrl` | required for `mode=sitemap` |
| `maxPages` | 1–60, default 20 — for `mode=sitemap` |
| `wcagVersion` | `2.0-A`, `2.0-AA`, `2.1-A`, `2.1-AA`, `2.2-A`, `2.2-AA` (default) |

```bash
curl -X POST http://localhost:4200/a11y-audit \
  --data-urlencode 'urls=https://www.example.com' \
  --data-urlencode 'wcagVersion=2.2-AA'
```

### Other routes

- `GET /result/:runId` — the dashboard payload: counts + the category rollup. Public.
- `POST /lead` — `{ runId, name, email, phone, company }`. Validates, emails the
  enquiries inbox, and unlocks the report for that run.
- `GET /report/:runId` — the detailed HTML report. **403 until a lead is submitted.**
- `GET /report/:runId/pdf` — same, as PDF. **403 until a lead is submitted.**
- `GET /status/:runId` — `{ status, result }`
- `GET /stream/:runId` — SSE: `progress` | `done` | `error`
- `POST /cf-clearance/*` — the interactive Cloudflare clearance flow

Apart from the report gate there is no authentication; anyone who can reach the server
on its port can start an audit.

## API

There is a key-authenticated JSON API for running audits and fetching results
plus the PDF, with no lead-capture step. See **[API.md](API.md)**, also served as a page at **`/api-docs`**.

Credentials go in a `.env` file in the project root (loaded automatically, gitignored):

```bash
# .env
WCAG_API_KEYS="your-key"
WCAG_API_PASSCODE='your-passcode'
```

`WCAG_API_KEYS` is comma-separated so each consumer can hold its own and be
revoked independently. `WCAG_API_PASSCODE` is an optional shared secret — once
set, it is required on every request alongside the key.

Quote any value containing `#`, `$` or spaces. Unquoted, `#` starts a comment
and silently truncates the value, which surfaces as a puzzling `401`.

```bash
npm start

curl -X POST http://localhost:4200/api/v1/audits \
  -H 'X-API-Key: your-key' \
  -H 'X-API-Passcode: your-passcode' \
  -H 'Content-Type: application/json' \
  -d '{"urls":["https://www.example.com/"]}'
```

The API is **disabled unless `WCAG_API_KEYS` is set** — it fails closed.

## Email / SMTP

Lead notifications go out via SMTP, configured entirely through environment variables —
see `.env.example`. Set `SMTP_HOST` (plus port/user/pass as needed) and `LEAD_NOTIFY_TO`.

**Until SMTP is configured the app still works.** Every lead is appended to
`leads.jsonl` in the project root regardless of whether the email sent, so nothing is
lost while the mail server is being set up — and a send failure never surfaces as an
error to the visitor, who has already earned their report. `leads.jsonl` holds personal
data and is gitignored.

With no `LEAD_NOTIFY_TO` set, notifications default to `suresh.v@thecommerceshop.com`.

## Notes

- `web-reports/` accumulates one directory per run and is **not** cleaned up automatically.
- Cloudflare-protected sites may report "blocked by bot-challenge" — use the
  **Solve Cloudflare challenge** button on the page first; the cleared session is saved
  per-host under `cf-clearance-state/` and reused on later runs.
