Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bca5977
Fix axe SCSS compilation for revealjs format
cderv Dec 23, 2025
9b23ea7
Add comprehensive axe accessibility test coverage
cderv Feb 24, 2026
a2b6a03
Await reporter.report() in axe-check.js for future-proofing
cderv Feb 24, 2026
b48bf01
Load axe-check.js as standalone module for all HTML formats
cderv Feb 24, 2026
ccf29e3
Add smoke-all tests for axe conditional inclusion
cderv Feb 25, 2026
39a6387
Add idempotency guard to axe init and use violation text lookup map
cderv Feb 25, 2026
8c11659
Guard violationText lookup against missing keys in axe tests
cderv Feb 25, 2026
c4fa15a
Fix axe report CSS: z-index, background, scroll for all HTML formats
cderv Feb 26, 2026
23ea4e0
Add Sass theming documentation for cross-format styling
cderv Feb 26, 2026
526b932
Verify dark theme uses CSS bridge, not Sass fallback
cderv Feb 26, 2026
9ade887
Update revealjs axe test to expect report as dedicated slide
cderv Feb 26, 2026
a69266a
Add revealjs axe report as dedicated slide with Reveal API navigation
cderv Feb 26, 2026
03ff36a
Scan all RevealJS slides for axe violations and improve report UX
cderv Feb 26, 2026
7df6262
Fix cross-slide scanning test and minor axe-check.js issues
cderv Feb 26, 2026
5e26dac
Fix deferred report timing, vertical slide scanning, and axe error sa…
cderv Feb 26, 2026
95a1bba
Use web-first Playwright assertions throughout axe tests
cderv Feb 26, 2026
41f173f
Use Bootstrap offcanvas sidebar for dashboard axe report
cderv Feb 27, 2026
d864c68
Add aria-label to toggle button and scope toggle styles to dashboard
cderv Feb 27, 2026
7d90076
Extract highlight/unhighlight helpers in axe-check.js
cderv Feb 27, 2026
b7f18c9
Gate dashboard SCSS rules behind isDashboard conditional
cderv Feb 27, 2026
202a3e6
Add usage comment to isHtmlDashboardOutput
cderv Feb 27, 2026
3748350
Re-trigger axe scan on dashboard tab/page/sidebar change
cderv Feb 27, 2026
52c7fc1
Add card tabset rescan test and violations to dashboard fixture
cderv Feb 27, 2026
d9baa8c
Use role-based Playwright selectors in dashboard rescan tests
cderv Feb 27, 2026
d85d4ca
Add error handling to rescanDashboard() for axe.run() failures
cderv Feb 27, 2026
7bc2a3a
Add role="alert" to rescan error message for screen reader announcement
cderv Feb 27, 2026
2e36845
Add comment explaining popstate 50ms delay in setupDashboardRescan
cderv Feb 27, 2026
4de8964
Harden axe-check.js init(), fix multi-node event bug, normalize options
cderv Feb 27, 2026
98d5687
Refactor axe to use FormatDependency head field
cderv Feb 27, 2026
8042384
Add gitignore for accessibility test artifacts
cderv Feb 27, 2026
692a2aa
Add changelog entry for revealjs axe accessibility fix (#13781)
cderv Feb 27, 2026
0282b84
Add changelog entries for axe report in revealjs and dashboard (#14125)
cderv Feb 27, 2026
a00be0c
Add comprehensive axe accessibility test coverage
cderv Feb 27, 2026
4affb36
Clarify hover test implementation comments
cderv Feb 27, 2026
bfd181f
Document Playwright best practices from axe test development
cderv Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .claude/rules/formats/html-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
paths:
- "src/format/html/**/*"
---

# HTML FormatDependency Pattern

**Reference:** `axeHtmlDependency()` in `src/format/html/format-html-axe.ts`

## Use `head` Field for Dynamic HTML

```typescript
function myHtmlDependency(config: unknown): FormatDependency {
return {
name: "my-feature",
head: `<script type="text/plain">${encodeBase64(JSON.stringify(config))}</script>`,
scripts: [{ name: "file.js", path: formatResourcePath(...) }],
};
}
```

- Use `head` for inline/dynamic content (config scripts, meta tags)
- Use `scripts`/`stylesheets` fields for external files
- Don't create temp files manually with `temp.createFileFromString()`
- Base64-encode JSON in script tags (prevents `</script>` parser issues)
22 changes: 22 additions & 0 deletions .claude/rules/formats/sass-theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
paths:
- "src/core/sass*"
- "src/format/html/format-html-scss*"
- "src/format/html/format-html-axe*"
- "src/format/reveal/format-reveal-theme*"
- "src/format/dashboard/format-dashboard-shared*"
- "src/resources/formats/**/*.scss"
---

# Sass Theming

RevealJS sass-bundles compile separately from the theme (`format-reveal-theme.ts`),
so theme variables aren't in scope. Use CSS custom properties from `exposer.scss`:
`--r-background-color`, `--r-main-color`, `--r-heading-color`, etc.

For cross-format CSS (works in both Bootstrap and RevealJS):
```scss
background-color: var(--r-background-color, $body-bg);
```

Read `llm-docs/sass-theming-architecture.md` for full compilation pipeline details.
45 changes: 45 additions & 0 deletions .claude/rules/testing/playwright-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,40 @@ test("Feature description", async ({ page }) => {
});
```

### Parameterized Tests

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.

```typescript
const testCases = [
{ format: 'html', url: '/html/feature.html' },
{ format: 'revealjs', url: '/revealjs/feature.html', shouldFail: 'reason (#issue)' },
];

test.describe('Feature across formats', () => {
for (const { format, url, shouldFail } of testCases) {
test(`${format} — feature works`, async ({ page }) => {
if (shouldFail) test.fail();
// shared test logic
});
}
});
```

**When to use:** Same assertion logic applied to multiple formats, output modes, or configurations. Reduces file count and centralizes shared helpers.

### Expected Failures

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).

```typescript
test('Feature that is known broken', async ({ page }) => {
// Brief explanation of why this fails and issue reference
test.fail();
// ... normal test logic
});
```

## Configuration

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

## Best Practices

**For detailed examples and patterns, see [llm-docs/playwright-best-practices.md](../../llm-docs/playwright-best-practices.md)**

Key patterns for reliable tests:

- **Web-first assertions:** Use `expect(el).toContainText()`, `toBeAttached()`, `toHaveCSS()` instead of imperative DOM queries
- **Role-based selectors:** Prefer `getByRole('tab', { name: 'Page 2' })` over `locator('a[data-bs-target]')`
- **Non-unique selectors:** When using `.first()`, add comment explaining why selector may be non-unique and what you're testing
- **Completion signals:** Use `data-feature-complete` attributes in finally blocks instead of arbitrary delays

## Utilities

From `src/utils.ts`:
Expand Down
102 changes: 102 additions & 0 deletions llm-docs/axe-accessibility-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
main_commit: ee0f68be1
analyzed_date: 2026-02-27
key_files:
- src/format/html/format-html.ts
- src/format/html/format-html-axe.ts
- src/resources/formats/html/axe/axe-check.js
- src/resources/formats/html/axe/axe-check.scss
- src/resources/formats/revealjs/axe/axe-check.scss
- src/resources/formats/dashboard/axe/axe-check.scss
---

# Axe Accessibility Checking Architecture

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.

## Three-Layer Overview

### Layer 1: Build-time TypeScript (format detection + dependency injection)

**File:** `src/format/html/format-html.ts`

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`).

Key responsibilities:
- Read `axe` option from document metadata
- Inject axe files as `FormatDependency` (copies JS/CSS into output)
- Encode options as base64 JSON into a `<script>` tag

The base64 encoding is a defensive pattern that prevents JSON containing `</script>` from breaking the HTML parser.

### Layer 2: Compile-time SCSS (format-specific styling)

**Files:**
- `src/resources/formats/html/axe/axe-check.scss` — HTML/dashboard styles
- `src/resources/formats/revealjs/axe/axe-check.scss` — RevealJS-specific styles

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.

### Layer 3: Runtime JavaScript (scanning + reporting)

**File:** `src/resources/formats/html/axe/axe-check.js`

Single JS file handles all formats at runtime. Format detection uses DOM inspection:
- `typeof Reveal !== "undefined"` → RevealJS
- `document.body.classList.contains("quarto-dashboard")` → Dashboard
- Otherwise → standard HTML

Key classes:
- `QuartoAxeChecker` — Orchestrates scanning. Loads axe-core from CDN, runs scans, creates reporters.
- `QuartoAxeDocumentReporter` — Format-specific DOM report (overlay, slide, or offcanvas)
- `QuartoAxeConsoleReporter` — Logs violations to browser console
- `QuartoAxeJsonReporter` — Dumps full axe result as JSON to console

## Format-Specific Behavior

### HTML (standard)
- Report: overlay appended to `<main>` (or `<body>`)
- Interaction: hover highlights violation elements
- Rescan: none (static page)

### RevealJS
- Report: dedicated `<section>` slide appended to `.reveal .slides`
- Interaction: click navigates to the slide containing the violation
- Scan prep: temporarily removes `hidden`/`aria-hidden` from all slides so axe can inspect them
- Rescan: none (slides are static)

### Dashboard
- Report: Bootstrap offcanvas sidebar with toggle button
- Interaction: hover highlights violation elements
- Rescan: triggered by `shown.bs.tab` (page/card tabs), `popstate` (back/forward), `bslib.sidebar` (sidebar toggle)
- Generation counter prevents stale scan results from overwriting newer ones

## Adding a New HTML Format

If a new HTML-based format is added that needs axe support:

1. **TypeScript** (`format-html.ts`): Add format detection to the axe dependency injection logic
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.
3. **JavaScript** (`axe-check.js`):
- Add format detection in `QuartoAxeDocumentReporter.report()` — this determines which `createReport*()` method is called
- Implement a `createReport*()` method for the format's DOM structure
- If the format has dynamic content changes, add rescan triggers in `setupDashboardRescan()` (or create a format-specific equivalent)
4. **Tests**: Add Playwright test fixtures (`.qmd` files in `tests/docs/playwright/<format>/`) and parameterized test cases in `axe-accessibility.spec.ts`

## Key Design Decisions

### CDN loading
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.

### Options encoding
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()`.

### Generation counter (dashboard rescan)
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.

## Known Limitations

- **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.
- **CDN dependency**: No offline fallback. See `quarto-cli-2u4f` for documentation task.
- **Popstate delay**: 50ms `setTimeout` for dashboard back/forward navigation. See `quarto-cli-1fdf` for improvement task.
- **Multi-main elements**: `createReportOverlay()` uses `document.querySelector('main')` which returns the first match. Multiple `<main>` elements is invalid HTML, so this is acceptable.
Loading
Loading