feat(web): jsx-a11y + vitest-axe route-smoke CI gate (JEF-499)#267
Merged
Conversation
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
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.
What & why
engine/web/(the Preact dashboard client) shipped with no accessibility tooling — no eslint, noeslint-plugin-jsx-a11y, nonpm 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-a11yrecommended rules mapped onto Preact JSX (lints.jsxvia the stock espree parser — no React runtime), layered on@eslint/jsrecommended.eslint-plugin-reactregistered for the singlereact/jsx-uses-varsrule sono-unused-varstreats a component referenced only in JSX (<CoverageRow/>) as used. No full react ruleset — the engine is Preact (automatic JSX runtime).no-undefnever false-fires.<button aria-expanded aria-controls>,aria-hiddenglyphs,<th scope>, labelled landmarks). The one lint fix was a genuine dead prop (heading) insigning_detail.jsxsurfaced byno-unused-vars.2. vitest-axe route-smoke (
engine/web/test/a11y-routes.test.jsx)test/fixtures.js+ mocked poll.seriousorcritical. Filters by impact (the best-practice landmark/heading noise from mounting one view outside a full<html>document isn't a real defect);color-contrastdisabled because jsdom paints no pixels for axe to evaluate. Both rationales documented in the file header.3. CI wiring (
.github/workflows/rust.yml)lintjob now runsnpm run lint(a11y authoring regression → red).testjob'snpm run testnow includes the axe route-smoke (a11y runtime regression → red).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.1Gates (local, from
engine/web/)npm run lint— cleannpm test— 111 passed (17 files), incl. the 12 new axe tests; existing tests unregressednpm run build— green (dist/dashboard.jsproduced)web/src(largest 402)Closes JEF-499
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP