Skip to content

Course Map Export Accessibility Audit (#1153)

The two HTML documents a user can download from a course map — the interactive standalone page (export.html) and the source document Puppeteer prints to PDF (export.pdf) — are audited for WCAG 2.1 AA by the existing converter audit engine, not a second axe-core pipeline. The converter API already runs in the accessible-pdf-converter-api-node-* containers on the shared 10.1.1.4 host; this gate POSTs each rendered document to its /api/audit endpoint as a build-artifact audit and asserts a clean AA pass.

How it works

  • src/services/export-a11y.ts
    • buildExportArtifacts(map, opts) — renders both downloadable HTML documents (pure; no network). CSV/JSON exports are data, not documents, so they are out of scope.
    • auditCourseMapExports(client, map, opts) — audits both concurrently and aggregates the verdicts (passed is true only when every artifact passes).
  • src/services/audit-client.ts — thin client for /api/audit: submit a build-artifact audit, poll the async job, and read a pass/fail verdict off audit_v1.summary (severity counts + passes). The summary is populated on both the free and pro tiers, so a pass/fail gate works with a free-tier key; a pro-tier key additionally fills the per-violation detail used to explain a failure.
  • The gate is the vitest spec src/__tests__/services/export-a11y.test.ts. Its live describe block is skipIf the audit env is unset, so it runs only where the engine is reachable and skips cleanly in plain GitHub CI (which has no route to the internal container).

Configuration

All optional — when COURSE_MAP_AUDIT_URL/COURSE_MAP_AUDIT_KEY are unset the worker boots and serves exports normally and the gate is skipped.

Env varExampleNotes
COURSE_MAP_AUDIT_URLhttp://api-node-1:8790Converter API base URL. On 10.1.1.4 the engine is reachable internally at http://api-node-1:8790; the public route is https://api.theaccessible.org.
COURSE_MAP_AUDIT_KEYcmap_…Org-scoped audit key (see below). Shown once at mint time; only its SHA-256 hash is stored.
COURSE_MAP_AUDIT_REPOtheaccessible/course-mapMust match the key’s repo_scope, or be anything for an org-level key. Defaults to theaccessible/course-map.

Provisioning a key

The converter authenticates /api/audit with a key stored as a SHA-256 hash in the converter Supabase’s audit_api_keys table. Mint one without touching any database:

Terminal window
cd workers/course-map-api
npm run audit:mint-key -- --org <converter-org-uuid> --tier pro
# org-level key (accepts any repo): add --repo ""

The script prints (1) the secret to set as COURSE_MAP_AUDIT_KEY and (2) a ready-to-run INSERT for the converter Supabase SQL editor. Run the INSERT there (DB changes are applied manually), then set the secret on the course-map worker.

  • --tier free is enough for the pass/fail gate (severity counts only).
  • --tier pro adds per-violation detail (rule, WCAG criterion, selector) that makes a failing gate actionable.
  • Each audit run costs 1 credit against the org; the gate runs two audits per invocation.

Running the gate

Terminal window
cd workers/course-map-api
COURSE_MAP_AUDIT_URL=http://api-node-1:8790 \
COURSE_MAP_AUDIT_KEY=cmap_… \
npx vitest run src/__tests__/services/export-a11y.test.ts

Run it from a container/host on 10.1.1.4 (or against the public URL) so the engine is reachable. Without the env vars, the live block is skipped and a named placeholder test records why.

Why a skippable test, not a hard GitHub CI gate

The engine runs async (SQS → batch worker) and its internal URL is only reachable from the docker host. A hard gate in GitHub Actions would either break the test job (no network to the container) or hit the public prod engine on every push (credits + nondeterminism). A skipIf integration test runs wherever the engine is reachable — locally, on the server, or in a future self-hosted CI runner on 10.1.1.4 — and no-ops elsewhere. The network-free unit tests of the artifact builder and client verdict logic give the gate real coverage in plain CI regardless.