Skip to content

Commit ae18e3e

Browse files
authored
feat(a11y): WCAG 2.2 AA scan harness, Accessible theme, and name/role fixes (#404)
* test(a11y): add automated WCAG 2.2 AA scan harness 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. * fix(a11y): resolve top shared-component WCAG violations 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. * test(a11y): seed a case-owned dataset so the dataset-detail route scans 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). * fix(a11y): give icon-only controls accessible names 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. * fix(a11y): name more icon controls and fix issue-title trigger semantics 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. * feat(theme): add opt-in Accessible (WCAG 2.2 AA) theme 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. * test(a11y): let the scan force a theme (A11Y_THEME) 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. * fix(a11y): make the avatar tooltip non-interactive 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. * fix(a11y): give row-action menus accessible names (button-name) 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. * fix(a11y): name admin toggles, selects, and edit buttons (button-name) 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. * fix(a11y): name edit/delete icon buttons in admin tables (button-name) 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. * fix(a11y): name edit/delete buttons in config, llm, projects, roles tables (button-name) * fix(a11y): name edit/delete buttons in remaining admin tables (button-name) * fix(a11y): name edit/delete buttons in workflow/group/milestone/prompt tables (button-name) * fix(a11y): name the project sidebar toggle and case back button (button-name) * fix(a11y): label enabled/status toggle switches in admin tables (button-name) * fix(a11y): name result-expand, remove-parent-folder, requires-review controls (button-name) * fix(a11y): name the shared column-filter operator selects (button-name) * docs(a11y): add WCAG 2.2 AA conformance report (ACR / VPAT draft) * fix(a11y): add accessible names for various UI elements in multiple languages 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. * fix(a11y): show Accessible theme icon in profile and onboarding pickers 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. * fix(a11y): escape backslashes in report Markdown cells 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. * test(a11y): update Avatar test for the asChild tooltip trigger 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. * chore(a11y): satisfy lint and prettier on the scan harness Drop the unused baseURL test arg (no-unused-vars) and apply Prettier formatting to the harness files flagged by format:check. * docs(a11y): document the opt-in Accessible theme 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.
1 parent 2f2f261 commit ae18e3e

88 files changed

Lines changed: 2883 additions & 66 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ TestPlanIt is a comprehensive test management platform designed to help teams pl
174174
- **Custom statuses** - Create statuses with custom icons and colors
175175
- **Templates** - Create templates for consistent test case structure
176176
- **User preferences** - Configure theme, locale, timezone, and date/time formats; preference selections are visible from the profile view as well as the editor
177+
- **Accessible theme** - Opt-in high-contrast theme tuned for WCAG 2.2 Level AA (stronger contrast, larger interactive targets, visible focus), selectable per user without affecting other themes
177178
- **Configurations** - Author OS / browser / environment configurations scoped to projects, with an admin UX overhaul covering bulk assign and search
178179

179180
### Security & Compliance

docs/docs/user-guide/user-menu.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,24 @@ Once opened, the menu displays:
1414
1. **Your Name and Email**: Shown at the top for identification.
1515
2. **View Profile**: Navigates you directly to your **[User Profile](./user-profile.md)** page.
1616
3. **Theme**: Opens a sub-menu where you can select your preferred visual theme for the application.
17-
- Options include Light, Dark, System (matches your operating system setting), Green, Orange, and Purple.
17+
- Options include Light, Dark, System (matches your operating system setting), Green, Orange, Purple, and **Accessible**.
18+
- **Accessible** is a high-contrast theme tuned for accessibility (WCAG 2.2 AA): it strengthens text and border contrast, enlarges small interactive targets, and adds a clearly visible keyboard focus ring. Choose it if you rely on these aids — see [Accessibility](#accessibility) below.
1819
- A checkmark indicates the currently active theme.
1920
- Selecting a new theme saves the preference to your profile and reloads the application to apply the change.
2021
4. **Language**: Opens a sub-menu to select the display language for the application interface.
2122
- Available languages are listed (e.g., English, Spanish).
2223
- A checkmark indicates the currently active language.
2324
- Selecting a new language saves the preference to your profile and reloads the application to apply the change.
2425
5. **Sign Out**: Logs you out of your current session and redirects you to the Sign In page.
26+
27+
## Accessibility
28+
29+
TestPlanIt includes an opt-in **Accessible** theme designed to meet the [WCAG 2.2](https://www.w3.org/TR/WCAG22/) Level AA presentation requirements. It is one of the regular theme choices in the **Theme** sub-menu (and on your [User Profile](./user-profile.md) preferences), so you can turn it on or off at any time without affecting other users.
30+
31+
Compared to the default themes, the Accessible theme:
32+
33+
- **Increases contrast** for body text, secondary ("muted") text, and UI borders so content meets the minimum contrast ratio.
34+
- **Enlarges small interactive targets** (such as compact icon buttons and toggles) to at least the recommended minimum size.
35+
- **Adds a high-contrast keyboard focus ring** so the currently focused control is always clearly visible.
36+
37+
Your selection is saved to your profile and persists across sessions and devices. The other themes are left unchanged, so teammates who prefer them are unaffected.

docs/docs/user-guide/user-profile.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ When viewing your own profile, you can view and edit these preferences:
6363

6464
#### Display Preferences
6565

66-
- **Theme**: Choose from Light, Dark, System, Green, Orange, or Purple themes (with color indicators)
66+
- **Theme**: Choose from Light, Dark, System, Green, Orange, Purple, or **Accessible** themes (with color indicators). The Accessible theme is a high-contrast option tuned for [WCAG 2.2](https://www.w3.org/TR/WCAG22/) Level AA — see [Accessibility](./user-menu.md#accessibility)
6767
- **Locale**: Language preference (English, German, Spanish, French, Italian, Dutch, Polish, Portuguese, Turkish, Vietnamese, Russian, Chinese Simplified, Chinese Traditional, Japanese, Korean)
6868
- **Items Per Page**: Number of items to show in paginated tables (10, 25, 50, 100)
6969

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testplanit/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ e2e/playwright-report/
6161
e2e/test-results/
6262
.env.e2e
6363

64+
# Accessibility scan (e2e/a11y) — never commit results, seeded fixtures, or reports
65+
e2e/a11y/results/
66+
e2e/a11y/.a11y-fixtures.json
67+
e2e/a11y/playwright-report/
68+
e2e/a11y/test-results/
69+
6470
# DB backups
6571
backups/
6672

testplanit/app/[locale]/admin/app-config/columns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export function getColumns(
8484
variant="ghost"
8585
className="px-2 py-1 h-auto"
8686
data-testid="edit-config-button"
87+
aria-label={t("actions.edit")}
8788
onClick={() => onEditConfig?.(row.original)}
8889
>
8990
<Edit className="h-4 w-4" />
@@ -92,6 +93,7 @@ export function getColumns(
9293
variant="destructive"
9394
className="px-2 py-1 h-auto"
9495
data-testid="delete-config"
96+
aria-label={t("actions.delete")}
9597
onClick={() => onDeleteConfig?.(row.original)}
9698
>
9799
<Trash2 className="h-4 w-4" />

testplanit/app/[locale]/admin/code-repositories/columns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export function getColumns({
168168
size="icon"
169169
className="px-2 py-1 h-auto"
170170
onClick={() => onEdit(row.original)}
171+
aria-label={tCommon("actions.edit")}
171172
>
172173
<Edit className="h-4 w-4" />
173174
</Button>
@@ -176,6 +177,7 @@ export function getColumns({
176177
size="icon"
177178
className="px-2 py-1 h-auto"
178179
onClick={() => onDelete(row.original)}
180+
aria-label={tCommon("actions.delete")}
179181
>
180182
<Trash2 className="h-4 w-4" />
181183
</Button>

testplanit/app/[locale]/admin/configurations/categoryColumns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,15 @@ export const useColumns = (
135135
variant="ghost"
136136
className="px-2 py-1 h-auto"
137137
onClick={() => onEditCategory?.(row.original)}
138+
aria-label={tCommon("actions.edit")}
138139
>
139140
<SquarePen className="h-5 w-5" />
140141
</Button>
141142
<Button
142143
variant="destructive"
143144
className="px-2 py-1 h-auto"
144145
onClick={() => onDeleteCategory?.(row.original)}
146+
aria-label={tCommon("actions.delete")}
145147
>
146148
<Trash2 className="h-5 w-5" />
147149
</Button>

testplanit/app/[locale]/admin/configurations/configColumns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,15 @@ export const useColumns = (
329329
variant="ghost"
330330
className="px-2 py-1 h-auto"
331331
onClick={() => onEditConfiguration?.(row.original)}
332+
aria-label={t("actions.edit")}
332333
>
333334
<SquarePen className="h-5 w-5" />
334335
</Button>
335336
<Button
336337
variant="destructive"
337338
className="px-2 py-1 h-auto"
338339
onClick={() => onDeleteConfiguration?.(row.original)}
340+
aria-label={t("actions.delete")}
339341
>
340342
<Trash2 className="h-5 w-5" />
341343
</Button>

testplanit/app/[locale]/admin/fields/caseFieldColumns.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const useColumns = (
9696
cell: ({ row }) => (
9797
<div className="text-center">
9898
<Switch
99+
aria-label={tCommon("fields.enabled")}
99100
checked={row.original.isEnabled}
100101
onCheckedChange={(checked) =>
101102
handleToggle(row.original.id, "isEnabled", checked)
@@ -114,6 +115,7 @@ export const useColumns = (
114115
cell: ({ row }) => (
115116
<div className="text-center">
116117
<Switch
118+
aria-label={tCommon("fields.required")}
117119
checked={row.original.isRequired}
118120
onCheckedChange={(checked) =>
119121
handleToggle(row.original.id, "isRequired", checked)
@@ -132,6 +134,7 @@ export const useColumns = (
132134
cell: ({ row }) => (
133135
<div className="text-center">
134136
<Switch
137+
aria-label={tCommon("fields.restricted")}
135138
checked={row.original.isRestricted}
136139
onCheckedChange={(checked) =>
137140
handleToggle(row.original.id, "isRestricted", checked)
@@ -155,6 +158,7 @@ export const useColumns = (
155158
className="px-2 py-1 h-auto"
156159
data-testid="edit-case-field-button"
157160
onClick={() => onEditCaseField?.(row.original)}
161+
aria-label={tCommon("actions.edit")}
158162
>
159163
<SquarePen className="h-5 w-5" />
160164
</Button>
@@ -163,6 +167,7 @@ export const useColumns = (
163167
className="px-2 py-1 h-auto"
164168
data-testid="delete-case-field-button"
165169
onClick={() => onDeleteCaseField?.(row.original)}
170+
aria-label={tCommon("actions.delete")}
166171
>
167172
<Trash2 className="h-5 w-5" />
168173
</Button>

0 commit comments

Comments
 (0)