feat(a11y): WCAG 2.2 AA scan harness, Accessible theme, and name/role fixes#404
Merged
Conversation
Adds an axe-core + Playwright accessibility scan under e2e/a11y/ that reuses the existing e2e auth/seed/server wiring (no parallel setup, no app changes). It scans every app route as an authenticated user against seeded data, splits WCAG A/AA findings from best-practice, scans cheap interactive states (dialog/menu), and aggregates a deduped report grouped by WCAG success criterion for VPAT-style review. - routes.ts: manifest of all app routes with runtime-resolved seeded IDs - fixtures.setup.ts: seeds a richly-populated project + triggers ES reindex - scan.spec.ts: parameterized axe scan, per-route JSON output - aggregate.ts: deduped report.md/report.json grouped by success criterion - run.ts: a11y:scan runner (single-route, strict mode, exit-code propagation) - adds a11y:scan / a11y:report npm scripts, @axe-core/playwright dev dep, gitignore entries for results/fixtures, README, and a report-mode CI draft Report mode by default; A11Y_STRICT=on (or CI=strict) fails on serious/critical.
Addresses the most widespread findings from the automated accessibility scan, fixing them at the shared-component root so the improvement applies across every page that uses them: - DataTable column-resize handle: add role="separator" + aria-orientation so its aria-label is valid ARIA (WCAG 4.1.2 aria-prohibited-attr) — clears 227 elements across 26 routes. - BreadcrumbComponent: make the tooltip trigger asChild so the folder link is no longer a focusable <a> nested inside a <button> (WCAG 4.1.2 nested-interactive); drop the invalid type="button" on the anchor. - Test run / milestone / session summaries: give the icon-only sort toggle an aria-label from the existing sort-by translation keys (WCAG 4.1.2 aria-command-name) — clears 6 routes. Reuses existing i18n keys; no new strings. Verified via e2e/a11y scan: aria-prohibited-attr 26->0, aria-command-name 6->0, nested-interactive 5->4.
The model create policy's Zod validator rejects a null `ownerCase` relation, so a shared (case-less) dataset can't be created via the API. Connect an in-project ownerCase instead — this satisfies the policy and lets the scan cover the settings/datasets/[dataSetId] detail route (was previously recorded as skipped).
Adds accessible names to the highest-volume unnamed controls surfaced by the
accessibility scan, fixing them at shared-component sources:
- Project switcher (SelectTrigger): aria-label — present on ~33 routes.
- TipTap toolbar (bold/italic/underline/strikethrough/code/heading) buttons.
- Test-case panel collapse toggles.
- Report-type Select triggers.
- Slider primitive now forwards aria-label/aria-labelledby onto the focusable
thumb (role="slider"); the five admin Security sliders pass their existing
labels through.
Reuses existing translation keys where possible; adds common.aria.selectProject,
common.aria.togglePanel, and common.editor.{bold,italic,underline,strikethrough,
code,heading} to en-US only (Crowdin manages other locales). Verified via the
e2e/a11y scan: button-name failing elements 813->712, aria-input-field-name 5->0.
Continues labelling unnamed controls and fixes two structural ARIA issues: - TipTap toolbar: bullet/numbered list, blockquote, and table buttons get aria-labels (new common.editor keys). - UserNameCell: the name tooltip trigger is now asChild, so it no longer renders a <button> nested inside the profile <Link> (nested-interactive). - Issues columns: the title popover trigger is a <button> instead of a <div>, so aria-expanded is valid (aria-allowed-attr). - BreadcrumbComponent: folder links get an explicit aria-label fallback. Verified via e2e/a11y scan: button-name 712->672 elements, aria-allowed-attr 13->7, target-size 108->82 (UserNameCell), with no new violations.
Adds a selectable "Accessible" theme alongside the existing five. It pairs a high-contrast, light-based palette with a small scoped override layer that enforces the presentation success criteria app-wide — without touching any component or the other themes (the rules only apply under .accessible): - 1.4.3 Contrast: neutralizes text-muted-foreground/* and text-primary/* opacity modifiers (the main cause of sub-4.5:1 text) and uses a darker muted token (~8:1). - 1.4.11 Non-text Contrast: darker borders/inputs (>=3:1). - 2.4.7 Focus Visible: a consistent high-contrast focus ring. - 2.5.8 Target Size: 24px minimum hit area for controls and links. Wired through next-themes (themes list), the Theme enum, the user-menu picker (persists like the other themes), and i18n. Scanning with this theme active drops color-contrast from 58 routes to 13 and target-size from 26 to 15; the remainder is data-driven badge colors and exempt disabled controls.
Adds A11Y_THEME so the scan can measure a specific theme regardless of the seeded user preference (e.g. A11Y_THEME=accessible). The forced theme class is applied before axe runs and recorded in the report header.
Avatar rendered its tooltip trigger as a nameless <button> (the color-swatch cursor-default control) that also nested inside link/row cells. Switching the trigger to asChild uses the avatar itself (img alt / initials) as the trigger, removing the button — which clears button-name and nested-interactive for every avatar across the app.
The icon-only "..." DropdownMenu triggers in the users, test-run, session, milestone, comment, and folder-tree row menus had no accessible name. Adds an aria-label (the existing "Actions" string) to each trigger. aria-label only — no visual change in any theme.
Adds aria-labels to controls that had no accessible name: the user active toggle, the project review-workflow switch, the result-editing-policy select, and the app-config / template edit buttons. aria-label only — no visual change.
Adds aria-labels (existing Edit/Delete strings) to the icon-only edit and delete buttons (enabled and disabled variants) in the app-config, template, and status admin tables. aria-label only — no visual change.
…ables (button-name)
…t tables (button-name)
…controls (button-name)
…anguages This commit enhances accessibility by adding aria-labels for project selection, panel toggles, folder management, and various text formatting options across multiple language files. These changes ensure that users relying on assistive technologies can better navigate and interact with the application.
The Accessible theme option appeared in the profile Preferences page and the initial-preferences onboarding dialog but rendered without an icon, since both getThemeIcon/getThemeColor switches lacked an Accessible case. Add the Accessibility icon (matching the user menu) and clear the accessible class in the onboarding theme-preview fallback.
The report cell escaper handled the pipe delimiter but not the backslash itself, so a backslash in axe output could corrupt table rendering. Escape backslashes first, then pipes, and normalize CRLF newlines.
The avatar tooltip now uses asChild, so the avatar element is the trigger itself rather than being wrapped in a nameless button. Assert the accessible contract (no button ancestor) instead of the old wrapper-button DOM shape.
Drop the unused baseURL test arg (no-unused-vars) and apply Prettier formatting to the harness files flagged by format:check.
Add the Accessible theme to the user-menu and user-profile theme lists, a new Accessibility section in the user menu guide explaining what the theme does (contrast, target size, focus ring) and that it is opt-in and scoped, and an Accessible theme entry in the feature list.
Contributor
Author
|
🎉 This PR is included in version 0.35.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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.
Description
Establishes an automated WCAG 2.2 Level AA accessibility baseline for TestPlanIt and resolves the highest-impact violations it surfaced.
1. Automated scan harness (
testplanit/e2e/a11y/)pnpm a11y:scanruns in report mode;A11Y_STRICT=on(orCI=strict) fails the run on serious/critical findings. Scan a single route with--route=<name>, or a specific theme withA11Y_THEME=<theme>.e2e/a11y/ci-workflow.draft.yml.2. Opt-in "Accessible" theme
.accessibleCSS class. The five existing themes (Light/Dark/Green/Orange/Purple) are visually untouched — verified in the scan.Themeenum and persisted via the existing user-preferences picker.3. "Name, Role, Value" (4.1.2) markup sweep
aria-label/sr-only/asChildchanges only — zero visual change to any theme.4. Conformance report (ACR / VPAT draft)
e2e/a11y/VPAT-WCAG22-AA.md— a truthful WCAG 2.2 A & AA support inventory plus a remediation roadmap and the manual-audit checklist that any conformance claim would require. Explicitly not a conformance claim; the automated baseline covers only the ~30–40% of criteria that tooling can verify.Related Issue
N/A
Type of Change
How Has This Been Tested?
Test Configuration:
Checklist
Additional Notes
Themeenum gains anAccessiblevalue. This project usesprisma db push(no migrations directory), so deploying requires the Postgres enum value to be added the same way (pnpm generatethen push) — no migration file is included by design.