forked from blackboardsh/colab
-
Notifications
You must be signed in to change notification settings - Fork 0
chore(helioslab): add AT1-AT5 accessibility baseline #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,868
−7
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1be28e6
chore(helioslab): add AT1-AT5 accessibility baseline
b0fc89e
fix(a11y): repair unit test imports and add alt text to decorative icons
e37e3c9
fix(a11y): address review comments from Cursor Bugbot
f4c3fb4
fix(a11y): scope i18n CI guards to PR-owned paths
95ec33f
fix(a11y): address remaining review comments and unblock axe-core CI
081e83d
fix(a11y): unblock axe-core CI: add @playwright/test, use .mts config…
9afcfb3
fix(a11y): add @axe-core/playwright devDep for e2e wcag.spec
60486be
fix(a11y): add Monaco editor surface to workbench fixture
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| name: a11y-helioslab | ||
|
|
||
| # HeliosLab WCAG 2.1 AA gate. Calls the proposed reusable workflow at | ||
| # OmniRoute-3rd/.github/workflows/reusable-a11y.yml once that lands; until | ||
| # then, the steps here are inlined. | ||
| # | ||
| # Triggers: every push to main, every PR. Skips runs for changes that don't | ||
| # touch src/, e2e/, or this workflow file. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - "src/**" | ||
| - "e2e/**" | ||
| - ".github/workflows/a11y-helioslab.yml" | ||
| - "tests/**" | ||
| - "scripts/**" | ||
| - "package.json" | ||
| - "bun.lock" | ||
| - "playwright.config.ts" | ||
| - "bunfig.toml" | ||
| - "axe-config.ts" | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - "src/**" | ||
| - "e2e/**" | ||
| - ".github/workflows/a11y-helioslab.yml" | ||
| - "tests/**" | ||
| - "scripts/**" | ||
| - "package.json" | ||
| - "bun.lock" | ||
| - "playwright.config.ts" | ||
| - "bunfig.toml" | ||
| - "axe-config.ts" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| axe: | ||
| name: axe-core WCAG 2.1 AA | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # v4.2.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.1.0 | ||
| with: | ||
| bun-version: 1.2.0 | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Install Playwright browsers | ||
| run: bunx playwright install --with-deps chromium | ||
|
|
||
| - name: Run a11y unit tests | ||
| run: bun test tests/unit/a11y.test.ts tests/unit/a11y/ | ||
|
|
||
| - name: Check i18n keys | ||
| run: bun run scripts/check-i18n-keys.mjs | ||
| continue-on-error: false | ||
|
|
||
| - name: Check hardcoded strings | ||
| run: bun run scripts/check-hardcoded-strings.mjs | ||
| continue-on-error: false | ||
|
|
||
| - name: Run axe a11y e2e tests | ||
| env: | ||
| HELIOSLAB_RENDERER_URL: http://localhost:5173 | ||
| run: node node_modules/playwright/cli.js test e2e/a11y/ --reporter=github | ||
|
|
||
| - name: Upload Playwright report on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.4.3 | ||
| with: | ||
| name: playwright-report-helioslab | ||
| path: playwright-report/ | ||
| retention-days: 7 | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * axe-core shared configuration for the HeliosLab a11y test suite. | ||
| * | ||
| * Tag set covers WCAG 2.0 A/AA + WCAG 2.1 A/AA — the legal baseline for | ||
| * Phenotype web/desktop surfaces. Excluded rules: | ||
| * - `bypass`: Monaco's complex DOM (sidebar / editor / tabs split) can't | ||
| * always provide a "skip" link the rule expects. | ||
| * - `region`: A desktop app window is not a web page — landmark regions | ||
| * don't apply in the same way. | ||
| * - `color-contrast`: Monaco's syntax-highlight tokens have non-text | ||
| * contrast; this is verified by a separate monaco-theme audit. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| */ | ||
| export const AXE_TAGS = [ | ||
| "wcag2a", | ||
| "wcag2aa", | ||
| "wcag21a", | ||
| "wcag21aa", | ||
| ] as const; | ||
|
|
||
| export const AXE_DISABLED_RULES = [ | ||
| "bypass", | ||
| "region", | ||
| "color-contrast", | ||
| ] as const; | ||
|
kilo-code-bot[bot] marked this conversation as resolved.
|
||
|
|
||
| export const AXE_OPTIONS = { | ||
| rules: AXE_DISABLED_RULES.reduce<Record<string, { enabled: boolean }>>( | ||
| (acc, id) => { | ||
| acc[id] = { enabled: false }; | ||
| return acc; | ||
| }, | ||
| {}, | ||
| ), | ||
| runOnly: { | ||
| type: "tag", | ||
| values: AXE_TAGS as unknown as string[], | ||
| }, | ||
| resultTypes: ["violations"] as const, | ||
| } as const; | ||
|
|
||
| export type AxeImpact = "minor" | "moderate" | "serious" | "critical"; | ||
|
|
||
| /** Filter helper — kept in one place so all spec files agree. */ | ||
| export function blockingViolations( | ||
| results: { violations: Array<{ impact?: string | null; id: string; nodes: unknown[] }> }, | ||
| ) { | ||
| return results.violations.filter( | ||
| (v) => v.impact === "critical" || v.impact === "serious", | ||
| ); | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [test] | ||
| preload = ["./tests/setup-dom.ts"] |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * e2e/a11y/screen-reader.spec.ts — assert the structural ARIA contract that | ||
| * screen-reader users depend on, independent of the actual screen-reader | ||
| * binary. We do NOT attempt to drive NVDA / VoiceOver from CI; instead we | ||
| * verify the DOM the screen reader sees. | ||
| * | ||
| * Covers (from spec AT3): | ||
| * - File tree exposes role="tree" with role="treeitem" children | ||
| * - Tab bar exposes role="tablist" with role="tab" children | ||
| * - Settings dialog exposes role="dialog" + aria-modal="true" | ||
| * - Live regions exist with correct politeness settings | ||
| */ | ||
| import { test, expect } from "@playwright/test"; | ||
|
|
||
| const BASE_URL = process.env.HELIOSLAB_RENDERER_URL ?? "http://localhost:5173"; | ||
|
|
||
| test.describe("Screen-reader contract", () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(`${BASE_URL}/`); | ||
| await page.waitForSelector("#workbench-container", { timeout: 10_000 }); | ||
| }); | ||
|
|
||
| test("file tree has role=tree with treeitem children", async ({ page }) => { | ||
| const tree = page.locator('[role="tree"]').first(); | ||
| await expect(tree).toHaveCount(1); | ||
|
|
||
| const items = await tree.locator('[role="treeitem"]').count(); | ||
| expect(items).toBeGreaterThan(0); | ||
|
|
||
| // Each treeitem must have aria-expanded (or be a leaf without children). | ||
| const firstItem = tree.locator('[role="treeitem"]').first(); | ||
| const hasExpanded = await firstItem.evaluate( | ||
| (el) => el.hasAttribute("aria-expanded") || el.getAttribute("aria-level") !== null, | ||
| ); | ||
| expect(hasExpanded).toBe(true); | ||
| }); | ||
|
|
||
| test("tab bar has role=tablist with tab children", async ({ page }) => { | ||
| const tablist = page.locator('[role="tablist"]').first(); | ||
| if ((await tablist.count()) === 0) { | ||
| // Empty workspace — no tabs to assert against. | ||
| test.skip(); | ||
| return; | ||
| } | ||
| const tabs = tablist.locator('[role="tab"]'); | ||
| expect(await tabs.count()).toBeGreaterThan(0); | ||
|
|
||
| // The currently active tab must have aria-selected="true". | ||
| const selectedCount = await tablist | ||
| .locator('[role="tab"][aria-selected="true"]') | ||
| .count(); | ||
| expect(selectedCount).toBe(1); | ||
| }); | ||
|
|
||
| test("live regions exist with correct aria-live values", async ({ page }) => { | ||
| const polite = page.locator("#sr-live"); | ||
| const alert = page.locator("#sr-alert"); | ||
|
|
||
| await expect(polite).toHaveAttribute("aria-live", "polite"); | ||
| await expect(polite).toHaveAttribute("aria-atomic", "true"); | ||
| await expect(alert).toHaveAttribute("aria-live", "assertive"); | ||
| await expect(alert).toHaveAttribute("role", "alert"); | ||
| }); | ||
|
|
||
| test("Monaco editor exposes accessibilitySupport via aria-label", async ({ page }) => { | ||
| // Monaco adds aria-label="Code editor" (or similar) when | ||
| // accessibilitySupport: 'on' is set in `src/config/editor.ts`. | ||
| const monaco = page.locator(".monaco-editor").first(); | ||
| const ariaLabel = await monaco.getAttribute("aria-label"); | ||
| expect(ariaLabel).toBeTruthy(); | ||
| expect((ariaLabel ?? "").toLowerCase()).toContain("editor"); | ||
| }); | ||
|
|
||
| test("buttons without text content expose aria-label", async ({ page }) => { | ||
| // All button elements in the workbench should either have text, | ||
| // aria-label, or aria-labelledby. Walk the DOM and assert. | ||
| const unlabeled = await page.evaluate(() => { | ||
| const buttons = Array.from(document.querySelectorAll("button")); | ||
| return buttons | ||
| .filter((b) => { | ||
| const text = (b.textContent ?? "").trim(); | ||
| const label = b.getAttribute("aria-label"); | ||
| const labelledBy = b.getAttribute("aria-labelledby"); | ||
| return !text && !label && !labelledBy; | ||
| }) | ||
| .map((b) => b.outerHTML.slice(0, 100)); | ||
| }); | ||
| expect(unlabeled).toEqual([]); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.