Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/accessibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,15 @@ jobs:

- run: npm ci

- name: Build app
run: npm run build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Install axe-core for accessibility testing
run: npm install --save-dev @axe-core/react
- name: Run E2E accessibility tests
run: npm run test:e2e -- --project=chromium

- name: Run accessibility audit
run: |
npx pa11y-ci --config .pa11yci.json || echo "Accessibility issues found - check report"

- name: Upload accessibility report
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v7
with:
name: accessibility-report
path: accessibility-report/
name: playwright-report
path: playwright-report/
18 changes: 18 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,21 @@ test: add validation tests for bumpSequence op
| `bug` | Something is broken |
| `enhancement` | New feature or improvement |
| `documentation` | Docs-only change |

---

## Accessibility (WCAG) Checklist

All pull requests that touch interactive UI components must satisfy the following checklist before merge:

- [ ] All interactive elements reachable by Tab key
- [ ] Focus indicator visible on all focusable elements
- [ ] All buttons have accessible names (text or aria-label)
- [ ] All form inputs have associated labels
- [ ] Color contrast ratio >= 4.5:1 for normal text
- [ ] No keyboard traps (except intentional modal focus traps)
- [ ] Screen reader announces dynamic state changes (connect, errors, loading)
- [ ] Icons used as buttons have aria-label, not just title
- [ ] ARIA live regions present for async feedback
- [ ] Page has a logical heading hierarchy (h1 → h2 → h3)

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@babel/preset-typescript": "^7.24.7",
"@eslint/js": "^9.39.4",
"@typescript-eslint/parser": "^8.60.0",
"@axe-core/playwright": "^4.10.0",
"@playwright/test": "^1.59.1",
"@storybook/react-vite": "^8.5.0",
"@testing-library/dom": "^10.4.1",
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/ConnectPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, type CSSProperties, type KeyboardEvent } from 'react'
import { announceToScreenReader } from '../../utils/accessibility'
import { useStore } from '../../lib/store'
import {
isValidPublicKey,
Expand Down Expand Up @@ -82,6 +83,7 @@ export default function ConnectPanel() {
setConnectedAddress(resolved.accountId)
setAccountData(account)
setActiveTab('overview')
announceToScreenReader('Connected to account ' + resolved.accountId.slice(0, 8) + '...')

setTxLoading(true)
setOpsLoading(true)
Expand Down
38 changes: 38 additions & 0 deletions tests/e2e/navigation.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Sidebar navigation', () => {
test.beforeEach(async ({ page }) => {
Expand Down Expand Up @@ -26,3 +27,40 @@ test.describe('Sidebar navigation', () => {
await expect(page.locator('text=Overview')).toBeVisible();
});
});

test.describe('Accessibility (axe)', () => {
test('connect page: no critical a11y violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const critical = results.violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious'
);
expect(critical).toEqual([]);
});

test('overview: no critical a11y violations after connecting', async ({ page }) => {
await page.goto('/');
const input = page.locator('input[placeholder*="public key"]');
await input.fill('GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN');
await page.locator('button', { hasText: 'CONNECT' }).click();
// Wait for overview content to appear after successful connect
await page.waitForSelector('[data-testid="overview-content"], text=Overview', {
timeout: 30000,
});
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const critical = results.violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious'
);
expect(critical).toEqual([]);
});

test('notifications bell has accessible label', async ({ page }) => {
await page.goto('/');
const bellButton = page.locator('button').filter({ has: page.locator('span[aria-hidden="true"]') }).last();
await expect(bellButton).toHaveAttribute('aria-label');
});
});