diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3124fd45..261446d1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run: npm test a11y: - name: Accessibility (WCAG 2.2 AA) + name: Accessibility (WCAG 2.1 AA) runs-on: ubuntu-latest steps: - name: Checkout repo @@ -59,11 +59,13 @@ jobs: - name: Build run: npm run build - - name: Install Playwright browser - run: npx playwright install chromium --with-deps + - name: Serve built site + run: | + npm run serve -- --port 3000 --no-open & + npx --yes wait-on@9 --timeout 60000 http://localhost:3000/ - name: Run accessibility tests - run: npm run test:a11y + uses: lowlysre/pa11y-ci-action@9cfc00c514ae1d06afa84ce15d3959f8bbca2e73 # v1.0.0 query-tests: name: DuckDB query smoke tests diff --git a/.pa11yci.json b/.pa11yci.json new file mode 100644 index 000000000..9eb2be574 --- /dev/null +++ b/.pa11yci.json @@ -0,0 +1,20 @@ +{ + "defaults": { + "standard": "WCAG2AA", + "runners": ["axe"], + "timeout": 30000, + "wait": 1000, + "chromeLaunchConfig": { + "args": ["--no-sandbox"] + } + }, + "urls": [ + "http://localhost:3000/", + "http://localhost:3000/docs/", + "http://localhost:3000/getting-data/", + "http://localhost:3000/guides/", + "http://localhost:3000/examples/", + "http://localhost:3000/blog/", + "http://localhost:3000/community/" + ] +} diff --git a/__tests__/a11y.test.mjs b/__tests__/a11y.test.mjs deleted file mode 100644 index 03bf94c12..000000000 --- a/__tests__/a11y.test.mjs +++ /dev/null @@ -1,121 +0,0 @@ -/** - * WCAG 2.2 AA accessibility tests using Node's built-in test runner. - * Requires a built `build/` directory — run `npm run build` first. - * Run with: npm run test:a11y - */ - -import { test, before, after } from 'node:test'; -import assert from 'node:assert/strict'; -import { createServer } from 'node:http'; -import { readFile, access } from 'node:fs/promises'; -import { extname, join, resolve, relative, isAbsolute } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { chromium } from 'playwright'; -import AxeBuilder from '@axe-core/playwright'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const BUILD_DIR = join(__dirname, '..', 'build'); -const PORT = 3778; -const BASE_URL = `http://localhost:${PORT}`; - -const MIME = { - '.html': 'text/html', - '.js': 'application/javascript', - '.mjs': 'application/javascript', - '.css': 'text/css', - '.png': 'image/png', - '.svg': 'image/svg+xml', - '.woff2': 'font/woff2', - '.json': 'application/json', -}; - -let server, browser; - -before(async () => { - await access(join(BUILD_DIR, 'index.html')).catch(() => { - throw new Error(`build/index.html not found — run 'npm run build' before running accessibility tests`); - }); - - // minimal static file server, falls back to index.html for Docusaurus client-side routing - server = createServer(async (req, res) => { - const urlPath = (req.url ?? '/').split('?')[0]; - const filePath = resolve(join(BUILD_DIR, urlPath === '/' ? 'index.html' : urlPath)); - // relative() is cross-platform; startsWith(BUILD_DIR + '/') broke on Windows backslashes - const rel = relative(BUILD_DIR, filePath); - if (rel.startsWith('..') || isAbsolute(rel)) { - res.writeHead(403); - res.end('Forbidden'); - return; - } - try { - const data = await readFile(filePath); - res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' }); - res.end(data); - } catch { - // SPA fallback only for route-like requests (no extension); a missing - // asset (.js/.css/.png) must 404 so broken builds aren't masked. - if (extname(filePath) === '') { - try { - const data = await readFile(join(BUILD_DIR, 'index.html')); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(data); - return; - } catch { /* fall through to 404 */ } - } - res.writeHead(404); - res.end('Not found'); - } - }); - await new Promise((resolve) => server.listen(PORT, resolve)); - browser = await chromium.launch(); -}); - -after(async () => { - await browser?.close(); - if (server) await new Promise((resolve) => server.close(resolve)); -}); - -function formatViolations(violations, label) { - if (violations.length === 0) return; - const detail = violations - .map((v) => - `[${v.impact}] ${v.id}: ${v.description}\n` + - v.nodes.map((n) => - ` • ${n.target}` + - (n.failureSummary ? `\n ${n.failureSummary.replace(/\n/g, '\n ')}` : '') - ).join('\n') - ) - .join('\n\n'); - assert.fail(`${violations.length} WCAG 2.2 AA violation(s) — ${label}:\n\n${detail}`); -} - -async function runAxe(url) { - const context = await browser.newContext(); - const page = await context.newPage(); - try { - await page.goto(`${BASE_URL}${url}`, { waitUntil: 'domcontentloaded' }); - await page.waitForSelector('main', { timeout: 10000 }); - const { violations } = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']) - .analyze(); - return violations; - } finally { - await context.close(); - } -} - -const PAGES = [ - { path: '/', label: 'homepage' }, - { path: '/docs/', label: 'docs index' }, - { path: '/getting-data/', label: 'quickstart (QueryBuilder)' }, - { path: '/guides/', label: 'guides index' }, - { path: '/examples/', label: 'examples index' }, - { path: '/blog/', label: 'blog index' }, - { path: '/community/', label: 'community (custom table)' }, -]; - -for (const { path, label } of PAGES) { - test(`${label} passes WCAG 2.2 AA`, async () => { - formatViolations(await runAxe(path), label); - }); -} diff --git a/package-lock.json b/package-lock.json index ea2a786a9..ab45446f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "react-dom": "^19.3.0" }, "devDependencies": { - "@axe-core/playwright": "^4.13.0", "@docusaurus/module-type-aliases": "^3.10.0", "@docusaurus/types": "^3.10.0", "@eslint/js": "^10.0.0", @@ -41,7 +40,6 @@ "globals": "^17.11.0", "jsdom": "^30.1.1", "markdownlint-cli": "^0.49.1", - "playwright": "^1.62.1", "prettier": "^3.9.8", "typescript": "^7.0.2", "vite": "^8.3.0", @@ -381,19 +379,6 @@ "node": "20 || >=22" } }, - "node_modules/@axe-core/playwright": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", - "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "axe-core": "~4.13.0" - }, - "peerDependencies": { - "playwright-core": ">= 1.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -10381,16 +10366,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", - "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -20168,35 +20143,6 @@ "node": ">=16.0.0" } }, - "node_modules/playwright": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", - "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.63.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright-core": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", - "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/pmtiles": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.5.0.tgz", diff --git a/package.json b/package.json index 6e52c80f4..d68b07494 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "format": "prettier --write .", "format:check": "prettier --check .", "test": "vitest run", - "test:a11y": "node --test --test-reporter=spec __tests__/a11y.test.mjs", "test:watch": "vitest", "typecheck": "tsc --noEmit" }, @@ -47,7 +46,6 @@ "react-dom": "^19.3.0" }, "devDependencies": { - "@axe-core/playwright": "^4.13.0", "@docusaurus/module-type-aliases": "^3.10.0", "@docusaurus/types": "^3.10.0", "@eslint/js": "^10.0.0", @@ -60,7 +58,6 @@ "globals": "^17.11.0", "jsdom": "^30.1.1", "markdownlint-cli": "^0.49.1", - "playwright": "^1.62.1", "prettier": "^3.9.8", "typescript": "^7.0.2", "vite": "^8.3.0",