|
| 1 | +--- |
| 2 | +main_commit: 10946d28a |
| 3 | +analyzed_date: 2026-04-17 |
| 4 | +key_files: |
| 5 | + - src/format/html/format-html-axe.ts |
| 6 | + - src/resources/formats/html/axe/axe-check.js |
| 7 | + - src/resources/schema/document-a11y.yml |
| 8 | +--- |
| 9 | + |
| 10 | +# Testing Axe Accessibility Checks |
| 11 | + |
| 12 | +Workflow for exercising Quarto's `axe:` feature against real rendered output when triaging accessibility reports or validating fixes. For the internal architecture (dependency injection, reporters, sass bundles), see `axe-accessibility-architecture.md`. |
| 13 | + |
| 14 | +## Quarto's Scan Configuration |
| 15 | + |
| 16 | +`axe-check.js` calls `axe.run()` with only these options: |
| 17 | + |
| 18 | +- `exclude: ["[data-tabster-dummy]"]` — works around microsoft/tabster#288 |
| 19 | +- `preload: { assets: ["cssom"], timeout: 50000 }` |
| 20 | + |
| 21 | +No `runOnly` filter — Quarto surfaces the full default rule set (WCAG 2.0 A/AA + WCAG 2.1 + best-practice). This matters when comparing Quarto's output against an external axe scan: they should agree rule-for-rule. |
| 22 | + |
| 23 | +## Runtime Signals |
| 24 | + |
| 25 | +Two DOM markers are useful for automated testing: |
| 26 | + |
| 27 | +- **`<script id="quarto-axe-checker-options" type="text/plain">...`** — base64-encoded JSON of the `axe:` options, injected at build time by `format-html-axe.ts`. Patching this string switches output mode in a pre-rendered HTML file without re-rendering: |
| 28 | + |
| 29 | + ```bash |
| 30 | + # Switch output: document → json in place |
| 31 | + sed -i 's|eyJvdXRwdXQiOiJkb2N1bWVudCJ9|eyJvdXRwdXQiOiJqc29uIn0=|g' output/index.html |
| 32 | + # ({"output":"document"} ↔ {"output":"json"}) |
| 33 | + ``` |
| 34 | + |
| 35 | +- **`document.body.dataset.quartoAxeComplete === "true"`** — set by `axe-check.js` once the scan finishes. Use as a wait condition in headless-browser tests. |
| 36 | + |
| 37 | +## Output Modes Recap |
| 38 | + |
| 39 | +- `json` — one `console.log(JSON.stringify(result, null, 2))` call with the full axe result. Not stdout. Needs a browser to capture. |
| 40 | +- `console` — one `console.log` per violation with targets. |
| 41 | +- `document` — visual overlay (HTML), slide (RevealJS), or offcanvas (dashboard). |
| 42 | + |
| 43 | +## Testing Pipeline |
| 44 | + |
| 45 | +For a11y triage against a user's repro repo: |
| 46 | + |
| 47 | +1. Clone the repro into a scratch dir (not inside `quarto-cli`). |
| 48 | +2. Render with the dev Quarto build: `package/dist/bin/quarto.cmd render`. |
| 49 | +3. Either run with `axe: output: json` in `_quarto.yml`, or patch the base64 config in the rendered HTML as above. |
| 50 | +4. Serve the output locally — `simple-http-server.exe --nocache -i -p PORT path/`. |
| 51 | +5. Scan with `agent-browser` (uses Chrome via CDP): |
| 52 | + |
| 53 | + ```bash |
| 54 | + agent-browser --session scan open "http://localhost:PORT/index.html" |
| 55 | + agent-browser --session scan wait --fn \ |
| 56 | + 'document.body.getAttribute("data-quarto-axe-complete") === "true"' |
| 57 | + agent-browser --session scan console > result.txt |
| 58 | + # extract the last [log] entry and feed to jq |
| 59 | + ``` |
| 60 | + |
| 61 | +## Running Axe Manually (Full Control) |
| 62 | + |
| 63 | +Quarto's built-in scan fires soon after `DOMContentLoaded`. For pages with late-arriving content (htmlwidgets, profvis, late-mounted callouts), the built-in scan can miss violations that a later scan would catch. Run axe directly via `agent-browser eval`: |
| 64 | + |
| 65 | +```bash |
| 66 | +agent-browser --session scan eval --stdin <<'EOF' |
| 67 | +(async () => { |
| 68 | + const axe = await import("https://cdn.jsdelivr.net/npm/[email protected]/+esm"); |
| 69 | + const results = await axe.default.run(document, { resultTypes: ["violations"] }); |
| 70 | + return results.violations.map(v => ({ |
| 71 | + id: v.id, impact: v.impact, nodes: v.nodes.length, |
| 72 | + targets: v.nodes.map(n => n.target), tags: v.tags |
| 73 | + })); |
| 74 | +})() |
| 75 | +EOF |
| 76 | +``` |
| 77 | + |
| 78 | +Use the same axe-core version (`4.10.3` at the time of writing) as bundled in `axe-check.js` to keep rule parity with Quarto's own scan. |
| 79 | + |
| 80 | +## Comparing With / Without Fixes |
| 81 | + |
| 82 | +To identify which violations a proposed patch resolves vs. which remain, render twice — once with the patch disabled, once with it enabled — and diff the rule IDs. `include-after-body:` is the right hook for injecting a JS fix without touching Quarto internals during triage. |
| 83 | + |
| 84 | +## Tips |
| 85 | + |
| 86 | +- The axe-core bundled version is loaded from `cdn.skypack.dev` at runtime. Offline environments will silently fail; note this when triaging reports that blame a "missing" scan. |
| 87 | +- If a scan only surfaces 1–2 violations where you expected many, the built-in scan likely ran before the page finished rendering. Run axe manually via eval to confirm. |
| 88 | +- Some violations change with viewport size (sidebar visibility, scrollable regions). Note the viewport when recording findings — `agent-browser` defaults to 1262×568 in headless mode. |
0 commit comments