Skip to content

Commit 6758c5a

Browse files
authored
Fix axe accessibility checks for revealjs and dashboard formats (#14125)
Adding `axe: true` to revealjs or dashboard formats failed or did nothing. Root Cause axe-check.js was only loaded via quarto.js (bootstrap HTML only), and axe SCSS used Bootstrap variables that don't exist in revealjs compilation. Fix Made axe-check.js load independently for all HTML formats and added SCSS fallbacks with CSS custom property bridge for cross-format theming. Format-specific report UX needed different approaches: - **RevealJS**: Report as dedicated slide. Temporarily unhides all slides (including vertical) for scanning since RevealJS hides non-visible slides. - **Dashboard**: Bootstrap offcanvas sidebar so content stays interactive. Rescans on page/tab/sidebar changes since dashboard hides inactive content. - **HTML**: Fixed overlay (unchanged). Fixes #13781
1 parent c1df44a commit 6758c5a

39 files changed

Lines changed: 2207 additions & 78 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
paths:
3+
- "src/format/html/**/*"
4+
---
5+
6+
# HTML FormatDependency Pattern
7+
8+
**Reference:** `axeHtmlDependency()` in `src/format/html/format-html-axe.ts`
9+
10+
## Use `head` Field for Dynamic HTML
11+
12+
```typescript
13+
function myHtmlDependency(config: unknown): FormatDependency {
14+
return {
15+
name: "my-feature",
16+
head: `<script type="text/plain">${encodeBase64(JSON.stringify(config))}</script>`,
17+
scripts: [{ name: "file.js", path: formatResourcePath(...) }],
18+
};
19+
}
20+
```
21+
22+
- Use `head` for inline/dynamic content (config scripts, meta tags)
23+
- Use `scripts`/`stylesheets` fields for external files
24+
- Don't create temp files manually with `temp.createFileFromString()`
25+
- Base64-encode JSON in script tags (prevents `</script>` parser issues)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
paths:
3+
- "src/core/sass*"
4+
- "src/format/html/format-html-scss*"
5+
- "src/format/html/format-html-axe*"
6+
- "src/format/reveal/format-reveal-theme*"
7+
- "src/format/dashboard/format-dashboard-shared*"
8+
- "src/resources/formats/**/*.scss"
9+
---
10+
11+
# Sass Theming
12+
13+
RevealJS sass-bundles compile separately from the theme (`format-reveal-theme.ts`),
14+
so theme variables aren't in scope. Use CSS custom properties from `exposer.scss`:
15+
`--r-background-color`, `--r-main-color`, `--r-heading-color`, etc.
16+
17+
For cross-format CSS (works in both Bootstrap and RevealJS):
18+
```scss
19+
background-color: var(--r-background-color, $body-bg);
20+
```
21+
22+
Read `llm-docs/sass-theming-architecture.md` for full compilation pipeline details.

.claude/rules/testing/playwright-tests.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,40 @@ test("Feature description", async ({ page }) => {
6161
});
6262
```
6363

64+
### Parameterized Tests
65+
66+
When testing the same behavior across multiple formats or configurations, use `test.describe` with a test cases array instead of separate spec files. See `html-math-katex.spec.ts` and `axe-accessibility.spec.ts` for examples.
67+
68+
```typescript
69+
const testCases = [
70+
{ format: 'html', url: '/html/feature.html' },
71+
{ format: 'revealjs', url: '/revealjs/feature.html', shouldFail: 'reason (#issue)' },
72+
];
73+
74+
test.describe('Feature across formats', () => {
75+
for (const { format, url, shouldFail } of testCases) {
76+
test(`${format} — feature works`, async ({ page }) => {
77+
if (shouldFail) test.fail();
78+
// shared test logic
79+
});
80+
}
81+
});
82+
```
83+
84+
**When to use:** Same assertion logic applied to multiple formats, output modes, or configurations. Reduces file count and centralizes shared helpers.
85+
86+
### Expected Failures
87+
88+
Use `test.fail()` to document known failures. Playwright inverts the result: the test passes if it fails, and flags if it unexpectedly passes (signaling the fix landed).
89+
90+
```typescript
91+
test('Feature that is known broken', async ({ page }) => {
92+
// Brief explanation of why this fails and issue reference
93+
test.fail();
94+
// ... normal test logic
95+
});
96+
```
97+
6498
## Configuration
6599

66100
- **Config file:** `playwright.config.ts`
@@ -77,6 +111,17 @@ uv run python -m http.server 8080
77111
# Serves from tests/docs/playwright/
78112
```
79113

114+
## Best Practices
115+
116+
**For detailed examples and patterns, see [llm-docs/playwright-best-practices.md](../../llm-docs/playwright-best-practices.md)**
117+
118+
Key patterns for reliable tests:
119+
120+
- **Web-first assertions:** Use `expect(el).toContainText()`, `toBeAttached()`, `toHaveCSS()` instead of imperative DOM queries
121+
- **Role-based selectors:** Prefer `getByRole('tab', { name: 'Page 2' })` over `locator('a[data-bs-target]')`
122+
- **Non-unique selectors:** When using `.first()`, add comment explaining why selector may be non-unique and what you're testing
123+
- **Completion signals:** Use `data-feature-complete` attributes in finally blocks instead of arbitrary delays
124+
80125
## Utilities
81126

82127
From `src/utils.ts`:
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
main_commit: ee0f68be1
3+
analyzed_date: 2026-02-27
4+
key_files:
5+
- src/format/html/format-html.ts
6+
- src/format/html/format-html-axe.ts
7+
- src/resources/formats/html/axe/axe-check.js
8+
- src/resources/formats/html/axe/axe-check.scss
9+
- src/resources/formats/revealjs/axe/axe-check.scss
10+
- src/resources/formats/dashboard/axe/axe-check.scss
11+
---
12+
13+
# Axe Accessibility Checking Architecture
14+
15+
Quarto's axe-core integration spans three layers: build-time TypeScript, compile-time SCSS, and runtime JavaScript. Each layer operates at a fundamentally different stage, which is why they detect formats differently.
16+
17+
## Three-Layer Overview
18+
19+
### Layer 1: Build-time TypeScript (format detection + dependency injection)
20+
21+
**File:** `src/format/html/format-html.ts`
22+
23+
TypeScript code detects the output format and conditionally injects `axe-check.js` + `axe-check.css` as a `FormatDependency`. The format detection uses the Quarto format system (`isHtmlOutput`, `isRevealJsOutput`, `isDashboardOutput`).
24+
25+
Key responsibilities:
26+
- Read `axe` option from document metadata
27+
- Inject axe files as `FormatDependency` (copies JS/CSS into output)
28+
- Encode options as base64 JSON into a `<script>` tag
29+
30+
The base64 encoding is a defensive pattern that prevents JSON containing `</script>` from breaking the HTML parser.
31+
32+
### Layer 2: Compile-time SCSS (format-specific styling)
33+
34+
**Files:**
35+
- `src/resources/formats/html/axe/axe-check.scss` — HTML/dashboard styles
36+
- `src/resources/formats/revealjs/axe/axe-check.scss` — RevealJS-specific styles
37+
38+
SCSS files provide format-specific visual styling. RevealJS compiles its sass-bundles separately from the theme, so theme variables aren't in scope. The CSS custom property bridge (`--quarto-axe-*` variables set in HTML themes, consumed in RevealJS via `var()` with `!default` fallbacks) works around this architectural constraint.
39+
40+
### Layer 3: Runtime JavaScript (scanning + reporting)
41+
42+
**File:** `src/resources/formats/html/axe/axe-check.js`
43+
44+
Single JS file handles all formats at runtime. Format detection uses DOM inspection:
45+
- `typeof Reveal !== "undefined"` → RevealJS
46+
- `document.body.classList.contains("quarto-dashboard")` → Dashboard
47+
- Otherwise → standard HTML
48+
49+
Key classes:
50+
- `QuartoAxeChecker` — Orchestrates scanning. Loads axe-core from CDN, runs scans, creates reporters.
51+
- `QuartoAxeDocumentReporter` — Format-specific DOM report (overlay, slide, or offcanvas)
52+
- `QuartoAxeConsoleReporter` — Logs violations to browser console
53+
- `QuartoAxeJsonReporter` — Dumps full axe result as JSON to console
54+
55+
## Format-Specific Behavior
56+
57+
### HTML (standard)
58+
- Report: overlay appended to `<main>` (or `<body>`)
59+
- Interaction: hover highlights violation elements
60+
- Rescan: none (static page)
61+
62+
### RevealJS
63+
- Report: dedicated `<section>` slide appended to `.reveal .slides`
64+
- Interaction: click navigates to the slide containing the violation
65+
- Scan prep: temporarily removes `hidden`/`aria-hidden` from all slides so axe can inspect them
66+
- Rescan: none (slides are static)
67+
68+
### Dashboard
69+
- Report: Bootstrap offcanvas sidebar with toggle button
70+
- Interaction: hover highlights violation elements
71+
- Rescan: triggered by `shown.bs.tab` (page/card tabs), `popstate` (back/forward), `bslib.sidebar` (sidebar toggle)
72+
- Generation counter prevents stale scan results from overwriting newer ones
73+
74+
## Adding a New HTML Format
75+
76+
If a new HTML-based format is added that needs axe support:
77+
78+
1. **TypeScript** (`format-html.ts`): Add format detection to the axe dependency injection logic
79+
2. **SCSS**: Create format-specific styles if the report placement differs. If the format uses Bootstrap themes, the CSS custom property bridge handles colors automatically.
80+
3. **JavaScript** (`axe-check.js`):
81+
- Add format detection in `QuartoAxeDocumentReporter.report()` — this determines which `createReport*()` method is called
82+
- Implement a `createReport*()` method for the format's DOM structure
83+
- If the format has dynamic content changes, add rescan triggers in `setupDashboardRescan()` (or create a format-specific equivalent)
84+
4. **Tests**: Add Playwright test fixtures (`.qmd` files in `tests/docs/playwright/<format>/`) and parameterized test cases in `axe-accessibility.spec.ts`
85+
86+
## Key Design Decisions
87+
88+
### CDN loading
89+
axe-core (~600KB) is loaded from `cdn.skypack.dev` at runtime rather than bundled. This keeps the Quarto distribution small since axe is a dev-only feature. Tradeoff: requires internet, fails silently in offline/CSP environments.
90+
91+
### Options encoding
92+
Options are base64-encoded in a `<script id="quarto-axe-checker-options">` tag. This prevents JSON containing `</script>` from breaking HTML parsing. The runtime decodes with `atob()`.
93+
94+
### Generation counter (dashboard rescan)
95+
Rapid tab switches can queue multiple `axe.run()` calls. The `scanGeneration` counter ensures only the latest scan's results are displayed. Earlier scans complete but their results are discarded if a newer scan was started.
96+
97+
## Known Limitations
98+
99+
- **SCSS compilation**: RevealJS sass-bundles compile separately from themes. CSS custom properties bridge this gap, but direct SCSS variable sharing isn't possible without pipeline changes.
100+
- **CDN dependency**: No offline fallback. See `quarto-cli-2u4f` for documentation task.
101+
- **Popstate delay**: 50ms `setTimeout` for dashboard back/forward navigation. See `quarto-cli-1fdf` for improvement task.
102+
- **Multi-main elements**: `createReportOverlay()` uses `document.querySelector('main')` which returns the first match. Multiple `<main>` elements is invalid HTML, so this is acceptable.

0 commit comments

Comments
 (0)