From 1eb4d1c8ed254141ce55b47396085bc6e1bbec06 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sun, 5 Jul 2026 22:06:40 -0400 Subject: [PATCH 1/3] test(v2): add axe accessibility gate + keyboard nav tests to Storybook Wire axe-core into the vitest Storybook runner so every v2 lib story is scanned for accessibility violations, gated per story by the @storybook/addon-a11y `a11y.test` parameter (error / todo / off) with a global default of "error". Fix the unlabeled-control violations this surfaces (icon-only buttons, decorative images) and mark the remaining component-level gaps as `todo` so CI stays green while the backlog stays visible. Add keyboard-operability play() tests to RSwitch, RTabNav, and RRating. Refs #1848 Generated-By: PostHog Code Task-Id: b44aba2c-90c9-407d-8c80-5bbe77ecd6c1 --- frontend/.storybook/preview.ts | 4 + frontend/package-lock.json | 7 +- frontend/package.json | 1 + .../src/v2/lib/data/RTable/RTable.stories.ts | 3 + .../lib/forms/RCheckbox/RCheckbox.stories.ts | 8 +- .../forms/RDateField/RDateField.stories.ts | 3 + .../lib/forms/RDropzone/RDropzone.stories.ts | 3 + .../v2/lib/forms/RRating/RRating.stories.ts | 34 ++++++++ .../v2/lib/forms/RSelect/RSelect.stories.ts | 5 ++ .../v2/lib/forms/RSlider/RSlider.stories.ts | 3 + .../v2/lib/forms/RSwitch/RSwitch.stories.ts | 29 +++++++ .../forms/RTextField/RTextField.stories.ts | 8 ++ .../lib/menus/RMenuItem/RMenuItem.stories.ts | 5 ++ .../overlays/RCarousel/RCarousel.stories.ts | 4 +- .../RProgressCircular.stories.ts | 3 + .../primitives/RSpinner/RSpinner.stories.ts | 3 + .../lib/primitives/RTabNav/RTabNav.stories.ts | 41 +++++++++ .../src/v2/lib/primitives/RTabNav/RTabNav.vue | 2 +- .../structural/RToolbar/RToolbar.stories.ts | 6 +- .../structural/RTooltip/RTooltip.stories.ts | 20 ++--- frontend/test/storybook.test.ts | 83 ++++++++++++++++++- 21 files changed, 252 insertions(+), 23 deletions(-) diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts index 3e04df7872..1c424190dd 100644 --- a/frontend/.storybook/preview.ts +++ b/frontend/.storybook/preview.ts @@ -55,6 +55,10 @@ const preview: Preview = { parameters: { layout: "centered", backgrounds: { disable: true }, + // Accessibility gate: axe fails on any violation; known gaps opt down to `test: "todo"`. See #1848. + a11y: { + test: "error", + }, controls: { matchers: { color: /(background|color)$/i, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3916b11574..ea214ee79c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -48,6 +48,7 @@ "@vitest/ui": "^4.1.5", "@vue/test-utils": "^2.4.9", "@vue/tsconfig": "^0.7.0", + "axe-core": "^4.12.1", "eslint": "^10.0.3", "eslint-plugin-vue": "^10.8.0", "eslint-plugin-vuejs-accessibility": "^2.5.0", @@ -6130,9 +6131,9 @@ } }, "node_modules/axe-core": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.0.tgz", - "integrity": "sha512-FTavr/7Ba0IptwGOPxnQvdyW2tAsdLBMTBXz7rKH6xJ2skpyxpBxyHkDdBs4lf69yRqYpkqCdfhnwS8YULGOmg==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", "dev": true, "license": "MPL-2.0", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 002d3be3d5..19105574bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,6 +75,7 @@ "@vitest/ui": "^4.1.5", "@vue/test-utils": "^2.4.9", "@vue/tsconfig": "^0.7.0", + "axe-core": "^4.12.1", "eslint": "^10.0.3", "eslint-plugin-vue": "^10.8.0", "eslint-plugin-vuejs-accessibility": "^2.5.0", diff --git a/frontend/src/v2/lib/data/RTable/RTable.stories.ts b/frontend/src/v2/lib/data/RTable/RTable.stories.ts index 350f81e8d1..653046167c 100644 --- a/frontend/src/v2/lib/data/RTable/RTable.stories.ts +++ b/frontend/src/v2/lib/data/RTable/RTable.stories.ts @@ -11,6 +11,9 @@ const meta: Meta = { // RTable is generic, so its component-typed shape doesn't fit // Storybook's `component` slot. The cast is narrow + intentional. component: RTable as never, + // a11y todo (#1848): grid roles need their required parent structure so + // rows/cells are announced correctly (incl. the mobile card-stack mode). + parameters: { a11y: { test: "todo" } }, argTypes: { loading: { control: "boolean" }, sortKey: { control: "text" }, diff --git a/frontend/src/v2/lib/forms/RCheckbox/RCheckbox.stories.ts b/frontend/src/v2/lib/forms/RCheckbox/RCheckbox.stories.ts index 5e9c5d0eaa..63faea1357 100644 --- a/frontend/src/v2/lib/forms/RCheckbox/RCheckbox.stories.ts +++ b/frontend/src/v2/lib/forms/RCheckbox/RCheckbox.stories.ts @@ -79,7 +79,13 @@ export const Indeterminate: Story = { args: { label: "Some items selected", indeterminate: true }, }; -export const NoLabel: Story = { args: {} }; +export const NoLabel: Story = { + render: () => ({ + components: { RCheckbox }, + setup: () => ({ value: ref(false) }), + template: ``, + }), +}; // ── Multi-state ───────────────────────────────────────────────────── diff --git a/frontend/src/v2/lib/forms/RDateField/RDateField.stories.ts b/frontend/src/v2/lib/forms/RDateField/RDateField.stories.ts index 55262420c2..6e7f50bf58 100644 --- a/frontend/src/v2/lib/forms/RDateField/RDateField.stories.ts +++ b/frontend/src/v2/lib/forms/RDateField/RDateField.stories.ts @@ -5,6 +5,9 @@ import RDateField from "./RDateField.vue"; const meta: Meta = { title: "Forms/RDateField", component: RDateField, + // a11y todo (#1848): calendar grid cells use an unsupported role and the + // field input lacks a programmatic label. Needs a component-level fix. + parameters: { a11y: { test: "todo" } }, argTypes: { firstDayOfWeek: { control: "inline-radio", diff --git a/frontend/src/v2/lib/forms/RDropzone/RDropzone.stories.ts b/frontend/src/v2/lib/forms/RDropzone/RDropzone.stories.ts index b02ca743ca..e04d48d9c9 100644 --- a/frontend/src/v2/lib/forms/RDropzone/RDropzone.stories.ts +++ b/frontend/src/v2/lib/forms/RDropzone/RDropzone.stories.ts @@ -6,6 +6,9 @@ import RDropzone from "./RDropzone.vue"; const meta: Meta = { title: "Forms/RDropzone", component: RDropzone, + // a11y todo (#1848): the hidden file input needs a programmatic label + // tied to the visible drop-zone copy. + parameters: { a11y: { test: "todo" } }, argTypes: { title: { control: "text" }, hint: { control: "text" }, diff --git a/frontend/src/v2/lib/forms/RRating/RRating.stories.ts b/frontend/src/v2/lib/forms/RRating/RRating.stories.ts index 849fc6a2db..0d92842108 100644 --- a/frontend/src/v2/lib/forms/RRating/RRating.stories.ts +++ b/frontend/src/v2/lib/forms/RRating/RRating.stories.ts @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/vue3-vite"; +import { expect, userEvent, within } from "storybook/test"; import { ref } from "vue"; import RRating from "./RRating.vue"; @@ -39,6 +40,39 @@ export const Default: Story = { args: { halfIncrements: true, hover: true } }; export const Readonly: Story = { args: { readonly: true } }; export const Large: Story = { args: { size: "large" } }; +// Keyboard operability — each star is a native `