Skip to content

feat(web): jsx-a11y + vitest-axe route-smoke CI gate (JEF-499)#267

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-499-web-a11y-gate
Jul 24, 2026
Merged

feat(web): jsx-a11y + vitest-axe route-smoke CI gate (JEF-499)#267
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-499-web-a11y-gate

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

engine/web/ (the Preact dashboard client) shipped with no accessibility tooling — no eslint, no eslint-plugin-jsx-a11y, no npm run lint, no axe. JEF-489/490 fell back to manual a11y passes because the assumed tooling wasn't there. This stands up an enforced gate so a PR that introduces an a11y regression goes red.

Changes

1. eslint 9 flat config + jsx-a11y (engine/web/eslint.config.js, npm run lint)

  • eslint-plugin-jsx-a11y recommended rules mapped onto Preact JSX (lints .jsx via the stock espree parser — no React runtime), layered on @eslint/js recommended.
  • eslint-plugin-react registered for the single react/jsx-uses-vars rule so no-unused-vars treats a component referenced only in JSX (<CoverageRow/>) as used. No full react ruleset — the engine is Preact (automatic JSX runtime).
  • Per-area globals (browser / vitest / node) so no-undef never false-fires.
  • Zero jsx-a11y rules disabled — jsx-a11y found no violations in the current tree (the components were already a11y-conscious: real <button aria-expanded aria-controls>, aria-hidden glyphs, <th scope>, labelled landmarks). The one lint fix was a genuine dead prop (heading) in signing_detail.jsx surfaced by no-unused-vars.

2. vitest-axe route-smoke (engine/web/test/a11y-routes.test.jsx)

  • Mounts every top-level view (findings / alerts / action / readiness / admission / access — each populated + key empty states), the AuthGate 401/403 interstitials, the live app shell, and the status strip, with test/fixtures.js + mocked poll.
  • Runs axe-core and asserts no violation of impact serious or critical. Filters by impact (the best-practice landmark/heading noise from mounting one view outside a full <html> document isn't a real defect); color-contrast disabled because jsdom paints no pixels for axe to evaluate. Both rationales documented in the file header.

3. CI wiring (.github/workflows/rust.yml)

  • The lint job now runs npm run lint (a11y authoring regression → red).
  • The test job's npm run test now includes the axe route-smoke (a11y runtime regression → red).
  • Both gate PRs. Web deps install with npm ci --ignore-scripts, consistent with the repo.

Pinned devDeps (exact, installed --ignore-scripts)

eslint 9.39.5 · @eslint/js 9.39.5 · eslint-plugin-jsx-a11y 6.10.2 · eslint-plugin-react 7.37.5 · globals 17.7.0 · vitest-axe 0.1.0 · axe-core 4.12.1

eslint pinned to 9.x (not 10) because eslint-plugin-jsx-a11y@6.10.2's peer range is ^9.

Gates (local, from engine/web/)

  • npm run lint — clean
  • npm test111 passed (17 files), incl. the 12 new axe tests; existing tests unregressed
  • npm run build — green (dist/dashboard.js produced)
  • No file >1000 lines in web/src (largest 402)

Closes JEF-499

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

engine/web/ (the Preact dashboard client) had no accessibility tooling —
no eslint, no jsx-a11y, no axe — so JEF-489/490 fell back to manual a11y
passes. Stand up an enforced gate.

- eslint 9 flat config (eslint.config.js): eslint-plugin-jsx-a11y
  recommended rules mapped onto Preact JSX (lints .jsx via the stock
  espree parser), layered on @eslint/js recommended. eslint-plugin-react
  is registered for the single `react/jsx-uses-vars` rule so no-unused-vars
  sees a component referenced only in JSX as used (no full react ruleset —
  the engine is Preact, automatic runtime). Per-area globals (browser /
  vitest / node) so no-undef never false-fires. `npm run lint` script.
  Zero jsx-a11y rules disabled; jsx-a11y found no violations in the tree.
  The one lint fix was a genuine dead prop (`heading`) in signing_detail.jsx
  surfaced by no-unused-vars.

- vitest-axe route-smoke (test/a11y-routes.test.jsx): mounts every
  top-level view (findings/alerts/action/readiness/admission/access, each
  populated + key empty states), the AuthGate 401/403 interstitials, the
  live app shell, and the status strip with fixtures/mocked poll, runs
  axe-core, and asserts no violation of impact serious/critical. Filters by
  impact (best-practice landmark/heading noise from mounting one view
  outside a full document is not a real defect); color-contrast disabled
  (jsdom paints no pixels, so axe can't evaluate it).

- CI (rust.yml): the `lint` job now runs `npm run lint` (a11y authoring
  regression → red); the `test` job's `npm run test` now includes the axe
  route-smoke (runtime a11y regression → red). Both gate PRs.

New devDeps pinned exact, installed --ignore-scripts: eslint 9.39.5,
@eslint/js 9.39.5, eslint-plugin-jsx-a11y 6.10.2, eslint-plugin-react
7.37.5, globals 17.7.0, vitest-axe 0.1.0, axe-core 4.12.1.

Closes JEF-499

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson
thejefflarson merged commit a03b2ea into main Jul 24, 2026
5 checks passed
@thejefflarson
thejefflarson deleted the thejefflarson/jef-499-web-a11y-gate branch July 24, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant