Skip to content

feat(vibe-tests): ground the a11y score in runtime axe results (#4145)#4229

Open
AKnassa wants to merge 2 commits into
facebook:mainfrom
AKnassa:ak-4145-vibe-a11y-eval
Open

feat(vibe-tests): ground the a11y score in runtime axe results (#4145)#4229
AKnassa wants to merge 2 commits into
facebook:mainfrom
AKnassa:ak-4145-vibe-a11y-eval

Conversation

@AKnassa

@AKnassa AKnassa commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #4145

The accessibility dimension was a static regex scan of consumer code, so component-composed output scores ~100 by construction. This adds a runtime basis and stops the score from overclaiming.

  • New axe-previews.ts — axe-core over each built preview in headless Chromium (light + dark) → axe-results.json sidecar per iteration, same pattern as build-errors.json. Wired into vibe-screenshots.yml after screenshots; zero new deps (@axe-core/playwright was already a root devDep).
  • universal-eval.ts folds violations in by impact (critical −15 / serious −10 / moderate −8 / minor −3 per rule). The four static rules axe re-verifies on the rendered DOM (image-alt, labels, button-name, heading-order) stop double-penalizing; click-non-interactive stays static-only — React handlers are invisible to the DOM. No sidecar → static-only scoring as before; old iterations and the stored accessibility key are untouched.
  • Honest labels + visible ceiling — "A11y Hygiene (composition)" vs "Accessibility (runtime + hygiene)" in the CLI tables, compare markdown, and the report app; new A11yMetrics records eligible-site counts so a 100 earned on zero signal reads as "100 by construction".
  • Not done here: scoring the a11y-manifests/ diff (proposal 2) — the manifests use different component vocabularies and only 2 of 4 targets have one, so naive wiring breaks the Fair Evaluators invariant. Documented as a non-goal in the README.

Proof (demo iteration, two html-target variants with identical static profiles):

static-only with axe
accessible variant 100 100
broken variant (aria-required-attr + color-contrast) 100 75

Testing: 49 new tests including a real-Chromium integration test (auto-skipped on browserless runners); scoring rules mutation-checked; malformed sidecar/manifest entries degrade instead of crashing the aggregate. Full suite 7260/7273 — the 13 are the known pre-existing packages/cli load flakes, green in isolation.

Pre-existing, noticed but untouched: analyzeMaintainability omits darkModeSupport from its metrics (darkModeRate always computes 0), and the baseline a11y manifest is hardcoded despite its header saying it fetches Radix docs.

AKnassa added 2 commits July 22, 2026 22:30
The accessibility dimension was a 5-rule static regex scan of consumer
code that can only fire on raw-HTML footguns, so component-composed
output scored ~100 by construction — it measured composition hygiene,
not accessibility, and never moved during the APG hardening work.

Implements proposals 1, 3, and 4 from the issue:

- axe-previews.ts: headless-Chromium axe-core scan of built previews
  (light + dark), writing an axe-results.json sidecar per iteration —
  the same pattern as build-errors.json. Wired into vibe-screenshots.yml
  after the screenshot step; zero new dependencies (root already ships
  @axe-core/playwright + playwright).
- universal-eval.ts folds sidecar violations into the score by impact
  (critical -15, serious -10, moderate -8, minor -3 per rule). Static
  rules axe verifies on the rendered DOM (image-alt, labels,
  button-name, heading-order) stop double-penalizing when runtime data
  exists; click-non-interactive stays static-only since React handlers
  are invisible to the DOM. No sidecar -> static-only, old iterations
  unaffected; the 'accessibility' key is unchanged (stored universal.json
  files key on it).
- Honest labels: "A11y Hygiene (composition)" without runtime data,
  "Accessibility (runtime + hygiene)" with it — CLI tables, markdown
  compare footnote, and the report app.
- A11yMetrics surfaces the ceiling: eligible-site counts per static
  rule, rules fired, runtime availability, axe stats. Zero eligible
  sites now reads as "100 by construction" instead of a perfect score.

Proposal 2 (scoring the a11y-manifests diff) is documented as a
non-goal: the astryx/baseline manifests use different component
vocabularies and only 2 of 4 targets have one, so naive wiring would
break the Fair Evaluators invariant.

Verification: 32 new tests including a real-Chromium integration test
(skipIf-guarded for runners without a browser); suppression set,
unknown-impact default, and floor mutation-checked. Demo iteration:
static-only scores two variants 100/100; with the axe pass the broken
variant (aria-required-attr, color-contrast in both themes) drops to
75 while the accessible one stays 100.

Pre-existing, untouched: analyzeMaintainability omits darkModeSupport
from its metrics (strict tsc error; darkModeRate always computes 0).

Closes facebook#4145
Edge-case pass over the axe fold-in, all found red-first:

- A truncated or hand-edited axe-results.json entry (missing violations
  array, or a violation without a themes list) crashed evaluate() and
  took down the whole aggregate run. Malformed records now degrade to
  "no violations" / empty theme list.
- A malformed previews/manifest.json entry (string instead of a target
  map) iterated as characters in enumeratePreviews and could leak the
  iteration directory itself as a preview path. Non-object entries are
  now skipped.

Pins added while in there: clean runtime scans keep their metrics,
explicitly-null axe results stay static-basis, rule-eligible sites that
do not fire count as eligible only, missing previews directory returns
an empty list, and the report app's hasRuntimeA11y/dimensionLabel
handle old-format results with no a11y metrics.

Part of facebook#4145
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 23, 2026 2:34am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 23, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | View full report

@AKnassa
AKnassa marked this pull request as ready for review July 23, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vibe test: Accessibility dimension is a false positive — scores ~100 by construction, invisible to component-level a11y quality

1 participant