Skip to content

feat(a11y): WCAG 2.2 AA scan harness, Accessible theme, and name/role fixes#404

Merged
therealbrad merged 25 commits into
mainfrom
a11y-audit
Jun 5, 2026
Merged

feat(a11y): WCAG 2.2 AA scan harness, Accessible theme, and name/role fixes#404
therealbrad merged 25 commits into
mainfrom
a11y-audit

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

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/)

  • axe-core driven Playwright scan over 80 application routes, authenticated as admin against seeded data. Reuses the existing e2e auth/seed/server wiring — no new infrastructure.
  • pnpm a11y:scan runs in report mode; A11Y_STRICT=on (or CI=strict) fails the run on serious/critical findings. Scan a single route with --route=<name>, or a specific theme with A11Y_THEME=<theme>.
  • Output is a Markdown report grouped by WCAG success criterion (gitignored). A CI workflow draft is included at e2e/a11y/ci-workflow.draft.yml.

2. Opt-in "Accessible" theme

  • A new, user-selectable theme scoped to a .accessible CSS class. The five existing themes (Light/Dark/Green/Orange/Purple) are visually untouched — verified in the scan.
  • Carries a presentation-fix override layer addressing several criteria app-wide without editing individual components: neutralized muted-foreground/primary opacity (1.4.3 Contrast), ≥24px target sizing (2.5.8), a high-contrast focus ring (2.4.7), and strengthened borders/inputs (1.4.11).
  • Added to the Theme enum and persisted via the existing user-preferences picker.

3. "Name, Role, Value" (4.1.2) markup sweep

  • Accessible names added to icon-only controls across the app: row-action menus, avatars, the project sidebar toggle, all admin tables' edit/delete buttons and status toggle switches, shared column-filter operator selects, result-expand, and more.
  • These are aria-label/sr-only/asChild changes 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update

How Has This Been Tested?

  • E2E tests — the new axe/Playwright harness scans 79/80 routes (2 routes need seedable fixtures not yet available). Run against a production build per the project's E2E process.
  • Manual testing — verified the Accessible theme renders correctly and selectably from all three theme pickers (user menu, profile Preferences page, onboarding dialog), and that the existing themes are visually unchanged.

Test Configuration:

  • OS: macOS
  • Browser (if applicable): Chromium (Playwright)
  • Node version: 22

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

  • The Theme enum gains an Accessible value. This project uses prisma db push (no migrations directory), so deploying requires the Postgres enum value to be added the same way (pnpm generate then push) — no migration file is included by design.
  • The scan harness does not modify application code; the markup/theme fixes are separate, reviewable commits.
  • Two routes are skipped by the scan because they need seeded fixtures that lack an ApiHelper creator (a SAML provider id, and a dataset id).

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.
…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.
Comment thread testplanit/e2e/a11y/aggregate.ts Fixed
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.
@therealbrad therealbrad merged commit ae18e3e into main Jun 5, 2026
5 checks passed
@therealbrad therealbrad deleted the a11y-audit branch June 5, 2026 15:29
@therealbrad
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.35.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants