Add standard and best-practice options to axe (#14607)#14682
Draft
cwickham wants to merge 4 commits into
Draft
Conversation
Collaborator
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Commit 4ed3ffb (#14503) added 'Supports markdown formatting.' to several website navigation descriptions in definitions.yml but the regenerated editor artifacts only partially captured the change, so the committed artifacts have been stale since. Any later schema change that reruns dev-call build-artifacts picks the drift up; regenerate it separately so the next commit's artifact diff is only its own change.
The axe accessibility checker always ran axe-core's default rule set.
A new `standard` option scopes checks to a WCAG conformance level by
mapping the level to axe-core tags (STANDARD_TAGS) and passing them to
axe.run's runOnly. Tags aren't cumulative in axe (a rule's version tag
marks where its criterion was introduced), so each level lists its own
tag plus the lower levels and earlier versions it builds on. The lists
follow WCAG conformance logic rather than the bundled axe-core's rule
inventory: tags with no rules yet (2.1 AAA, 2.2 A/AAA) are listed
anyway — axe's unknown-tag warning explicitly exempts wcag2x level
tags, and a future axe that adds such rules scopes them in with no
change here. A `best-practice` option controls whether axe's
best-practice rules run.
Because runOnly by tag matches on tags alone (ignoring axe's
enabled:false defaults), a standard reaches the default-disabled rules
for its level (AAA rules, target-size) but would also resurrect
deprecated rules tagged wcag2a; those are explicitly disabled via the
documented rules override.
Also defaults output to console for object-form options, so
`axe: {standard: wcag21aa}` doesn't silently fail on the reporter
lookup, and regenerates the schema editor artifacts.
Unit tests cover the STANDARD_TAGS mapping (structural recurrence: each level covers its own tag, lower levels of its version, and the previous version's same level) and axeScopeOptions with hand-written rule catalogs (immune to axe-core re-tagging). Playwright fixtures share one body violating image-alt (A), color-contrast (AA), and heading-order (best practice), differing only in axe options, so absence of a violation in one case is provably scoping.
ff7acf0 to
c907f17
Compare
Member
Author
|
Docs PR: quarto-dev/quarto-web#2105 (draft, targets |
axe: {standard: wcag2a} with no output key previously hit
reporters[undefined] and silently did nothing; it now defaults to the
console reporter. The new fixture pins both the console fallback and
that scoping applies through the console path (image-alt reported,
color-contrast and heading-order absent).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14607.
Stacked on #14677 (base:
axe/vendor-axe-core); retargets tomainautomatically when that merges. Docs PR: quarto-dev/quarto-web#2105.What
The
axe:accessibility checker always ran axe-core's default rule set. This PR adds two options:standard— one ofwcag2a,wcag2aa,wcag2aaa,wcag21a,wcag21aa,wcag21aaa,wcag22a,wcag22aa,wcag22aaa. Axe tags aren't cumulative (a rule's version tag marks where its criterion was introduced), so each level maps to its own tag plus those of the lower levels and earlier versions it builds on — theSTANDARD_TAGStable, exported fromaxe-check.jsfor reuse by later tooling.best-practice— withstandard,trueadds axe's best-practice rules to the scope (they're excluded by default once a standard is set). Withoutstandard,falseremoves best-practice rules from the default scan. Unset preserves today's behavior in both cases.No options set ⇒ behavior is unchanged.
Design notes
Behavior claims below were verified against the axe-core 4.10.3 source, not remembered docs.
What we send to axe. For
standard: wcag2aa,axe-check.jscallsaxe.runwith:The one fact that drives the design: when
runOnlyselects by tag, axe ignores each rule'senabled: falsedefault (lib/core/utils/rule-should-run.js). So what runs is: rules matching our tag list, minus rules we explicitly override. Axe's default-disabled rules fall into three groups:target-size(2.2 AA)enabled: falsearia-roledescription,audio-caption)enabled: falsewcag2a)enabled: falseenabled: falsetagExcludefilters theexperimentaltagThe first row is deliberate and user-visible: choosing a standard can report violations the default scan doesn't — all three AAA rules and
target-sizeship disabled and turn on when the chosen standard covers them. "Check me against this level" should mean full coverage of that level; without this,standard: wcag2aaawould add zero AAA checks.Only the override column is ours.
axeScopeOptionsbuilds it: filteraxe.getRules()for deprecated rules the tag list would match, setenabled: falsefor each — rule-level overrides beat tag selection, which is axe's documented "runOnly + rules" combination (not the undocumentedvalues: {include, exclude}form). Rule IDs are never hardcoded; they come out of axe's catalog at scan time, so the list adapts if an axe upgrade deprecates different rules. The same mechanism handlesbest-practice: falsewithout a standard, filtering on thebest-practicetag instead.STANDARD_TAGSlists some tags that match no rules today. The table encodes WCAG conformance logic — each level covers its own tag, the lower levels of its version, and the versions it builds on — rather than axe's current rule inventory (which has no rules yet for criteria introduced at 2.1 AAA, 2.2 A, or 2.2 AAA; true of both 4.10.3 and 4.12.1). Passing those tags is safe: axe's unknown-tag warning explicitly exempts wcag2x level tags (Audit.normalizeOptions). And it's future-proof: an axe upgrade that adds rules for those criteria scopes them in with no Quarto change.Two small fixes ride along:
output: console. Previously an object withoutoutputhitreporters[undefined]and silently did nothing —axe: {standard: wcag21aa}would have been the common way to trip that.preloadmoved fromaxe.run's context argument to its options argument, as sketched in the issue.Commits
definitions.ymldescriptions but the committed editor artifacts only partially captured it, so any PR that rerunsdev-call build-artifactsinherits the drift. Regenerated separately so the feature commit's artifact diff is only its own change.axe-check.js, schema, regenerated artifacts, changelog.Tests
tests/unit/axe-standard-tags.test.ts): STANDARD_TAGS structural recurrence (each level covers its own tag, the lower levels of its version, and the previous version's same level) andaxeScopeOptionsacross all option combinations, using hand-written rule catalogs so an axe-core upgrade that re-tags rules can't break them. 18/18 pass (incl. existing conformance tests).image-alt(A),color-contrast(AA), andheading-order(best practice), differing only in axe options — presence in one case proves absence in another is scoping, not a fixture that stopped violating. Fullaxe-accessibility.spec.tspasses: 90/90 across chromium/firefox/webkit (also verifies theaxe.runrestructuring didn't regress the default scan, and that the rule-lesswcag22atag is accepted warning-free in practice). A fourth case coversaxe: {standard: wcag2a}with nooutput: it falls back to console reporting (thereporters[undefined]fix) and scoping applies through the console path.AI-assisted (Claude Code), grounded in a local clone per CONTRIBUTING.md "Using AI tools to investigate"; axe-core behavior claims verified against the vendored 4.10.3/4.12.1 bundles and the v4.10.3 tag of dequelabs/axe-core.