๐Ÿ‘๏ธ
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

a11y testing!

+192 -11
+12
.claude/DARK_MODE.md
··· 180 180 | rose-400 | `#fb7185` | Links and buttons both | Could conflict with error red | 181 181 | teal-400 | `#2dd4bf` | Warmer cyan alternative | Still cool-toned | 182 182 183 + ## Automated Accessibility Testing 184 + 185 + Run `npm run test:a11y` to check for WCAG 2.1 AA violations in both light and dark mode. 186 + 187 + Tests cover: home page, card search, card detail pages. Uses axe-core via Playwright. 188 + 189 + **Playwright version must match nix flake browsers.** The flake provides `playwright-driver.browsers` and sets `PLAYWRIGHT_BROWSERS_PATH`. If you see "Executable doesn't exist" errors, the npm `playwright` version doesn't match the nix browser version. Check browser versions: 190 + - Nix: `ls $PLAYWRIGHT_BROWSERS_PATH` (e.g., `chromium_headless_shell-1200`) 191 + - NPM: error message shows expected version (e.g., `chromium_headless_shell-1181`) 192 + 193 + Update npm playwright to match: `npm install playwright@<version> --save-dev` 194 + 183 195 ## References 184 196 185 197 - [Discord Color Palette](https://www.color-hex.com/color-palette/114089)
+2 -1
.claude/settings.local.json
··· 66 66 "Bash(npm run build:lexicons:*)", 67 67 "Bash(npm run lexicons:lint:*)", 68 68 "Bash(npm run lexicons:all:*)", 69 - "Bash(goat lex:*)" 69 + "Bash(goat lex:*)", 70 + "Bash(npm run test:a11y:*)" 70 71 ], 71 72 "deny": [], 72 73 "ask": []
+4
.gitignore
··· 19 19 .cache/ 20 20 21 21 tsconfig.tsbuildinfo 22 + 23 + # Playwright test outputs 24 + test-results/ 25 + playwright-report/
+92
e2e/accessibility.spec.ts
··· 1 + import AxeBuilder from "@axe-core/playwright"; 2 + import { test } from "playwright/test"; 3 + 4 + // Test pages in both light and dark mode 5 + const testPages = [ 6 + { name: "Home", path: "/" }, 7 + { name: "Card Search", path: "/cards?q=" }, 8 + // Lightning Bolt - iconic card 9 + { name: "Card: Lightning Bolt", path: "/card/3a9f0cf7-46c0-4a64-be65-a2c5b5f64e2c" }, 10 + // Counterspell - another common card 11 + { name: "Card: Counterspell", path: "/card/1920dae4-fb92-4f19-ae4b-eb3276b8571c" }, 12 + // User profile 13 + { name: "Profile", path: "/profile/did:plc:jx4g6baqkwdlonylsetvpu7c" }, 14 + // Deck page 15 + { name: "Deck: Hamza", path: "/profile/did:plc:jx4g6baqkwdlonylsetvpu7c/deck/3m7lphyavvp2u" }, 16 + ]; 17 + 18 + function formatViolations(violations: Awaited<ReturnType<AxeBuilder["analyze"]>>["violations"]) { 19 + if (violations.length === 0) return ""; 20 + 21 + const lines: string[] = []; 22 + for (const v of violations) { 23 + lines.push(`\n [${v.impact?.toUpperCase()}] ${v.id}`); 24 + lines.push(` ${v.help}`); 25 + lines.push(` ${v.helpUrl}`); 26 + for (const node of v.nodes.slice(0, 5)) { 27 + lines.push(` โ†’ ${node.target.join(" > ")}`); 28 + if (node.failureSummary) { 29 + // Indent the summary 30 + const summary = node.failureSummary.split("\n").map(l => ` ${l}`).join("\n"); 31 + lines.push(summary); 32 + } 33 + } 34 + if (v.nodes.length > 5) { 35 + lines.push(` ... and ${v.nodes.length - 5} more`); 36 + } 37 + } 38 + return lines.join("\n"); 39 + } 40 + 41 + for (const { name, path } of testPages) { 42 + test.describe(`${name}`, () => { 43 + test("light mode - WCAG AA", async ({ page }) => { 44 + await page.goto(path); 45 + await page.waitForLoadState("networkidle"); 46 + 47 + const results = await new AxeBuilder({ page }) 48 + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]) 49 + .analyze(); 50 + 51 + if (results.violations.length > 0) { 52 + throw new Error(`${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`); 53 + } 54 + }); 55 + 56 + test("dark mode - WCAG AA", async ({ page }) => { 57 + await page.goto(path); 58 + await page.evaluate(() => { 59 + document.documentElement.classList.add("dark"); 60 + localStorage.setItem("theme", "dark"); 61 + }); 62 + await page.waitForTimeout(100); 63 + await page.waitForLoadState("networkidle"); 64 + 65 + const results = await new AxeBuilder({ page }) 66 + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]) 67 + .analyze(); 68 + 69 + if (results.violations.length > 0) { 70 + throw new Error(`${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`); 71 + } 72 + }); 73 + }); 74 + } 75 + 76 + // Informational AAA check - logs but doesn't fail 77 + test("AAA contrast audit (informational)", async ({ page }) => { 78 + await page.goto("/"); 79 + await page.evaluate(() => { 80 + document.documentElement.classList.add("dark"); 81 + }); 82 + await page.waitForTimeout(100); 83 + 84 + const results = await new AxeBuilder({ page }) 85 + .withRules(["color-contrast-enhanced"]) 86 + .analyze(); 87 + 88 + if (results.violations.length > 0) { 89 + console.log(`\nโ”โ”โ” AAA Contrast Issues (informational) โ”โ”โ”${formatViolations(results.violations)}\n`); 90 + } 91 + // Don't fail - just informational 92 + });
+32 -8
package-lock.json
··· 51 51 }, 52 52 "devDependencies": { 53 53 "@atcute/lex-cli": "^2.3.1", 54 + "@axe-core/playwright": "^4.11.0", 54 55 "@biomejs/biome": "2.2.4", 55 56 "@testing-library/dom": "^10.4.0", 56 57 "@testing-library/react": "^16.2.0", ··· 63 64 "@vitest/web-worker": "^3.2.4", 64 65 "fast-check": "^4.4.0", 65 66 "jsdom": "^27.0.0", 66 - "playwright": "^1.54.1", 67 + "playwright": "^1.57.0", 67 68 "typescript": "^5.7.2", 68 69 "vite": "^7.1.7", 69 70 "vitest": "^3.0.5", ··· 313 314 "license": "0BSD", 314 315 "dependencies": { 315 316 "@badrap/valita": "^0.4.6" 317 + } 318 + }, 319 + "node_modules/@axe-core/playwright": { 320 + "version": "4.11.0", 321 + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz", 322 + "integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==", 323 + "dev": true, 324 + "license": "MPL-2.0", 325 + "dependencies": { 326 + "axe-core": "~4.11.0" 327 + }, 328 + "peerDependencies": { 329 + "playwright-core": ">= 1.0.0" 316 330 } 317 331 }, 318 332 "node_modules/@babel/code-frame": { ··· 5173 5187 "node": ">=4" 5174 5188 } 5175 5189 }, 5190 + "node_modules/axe-core": { 5191 + "version": "4.11.1", 5192 + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", 5193 + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", 5194 + "dev": true, 5195 + "license": "MPL-2.0", 5196 + "engines": { 5197 + "node": ">=4" 5198 + } 5199 + }, 5176 5200 "node_modules/babel-dead-code-elimination": { 5177 5201 "version": "1.0.10", 5178 5202 "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", ··· 7398 7422 } 7399 7423 }, 7400 7424 "node_modules/playwright": { 7401 - "version": "1.54.1", 7402 - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", 7403 - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", 7425 + "version": "1.57.0", 7426 + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", 7427 + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", 7404 7428 "dev": true, 7405 7429 "license": "Apache-2.0", 7406 7430 "dependencies": { 7407 - "playwright-core": "1.54.1" 7431 + "playwright-core": "1.57.0" 7408 7432 }, 7409 7433 "bin": { 7410 7434 "playwright": "cli.js" ··· 7417 7441 } 7418 7442 }, 7419 7443 "node_modules/playwright-core": { 7420 - "version": "1.54.1", 7421 - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", 7422 - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", 7444 + "version": "1.57.0", 7445 + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", 7446 + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", 7423 7447 "dev": true, 7424 7448 "license": "Apache-2.0", 7425 7449 "bin": {
+3 -1
package.json
··· 11 11 "deploy": "npm run build && wrangler deploy", 12 12 "cf-typegen": "wrangler types", 13 13 "test": "vitest run --silent='passed-only'", 14 + "test:a11y": "playwright test", 14 15 "bench": "vitest bench", 15 16 "format": "biome format", 16 17 "lint": "biome lint", ··· 75 76 }, 76 77 "devDependencies": { 77 78 "@atcute/lex-cli": "^2.3.1", 79 + "@axe-core/playwright": "^4.11.0", 78 80 "@biomejs/biome": "2.2.4", 79 81 "@testing-library/dom": "^10.4.0", 80 82 "@testing-library/react": "^16.2.0", ··· 87 89 "@vitest/web-worker": "^3.2.4", 88 90 "fast-check": "^4.4.0", 89 91 "jsdom": "^27.0.0", 90 - "playwright": "^1.54.1", 92 + "playwright": "^1.57.0", 91 93 "typescript": "^5.7.2", 92 94 "vite": "^7.1.7", 93 95 "vitest": "^3.0.5",
+25
playwright.config.ts
··· 1 + import { defineConfig, devices } from "playwright/test"; 2 + 3 + export default defineConfig({ 4 + testDir: "./e2e", 5 + fullyParallel: true, 6 + forbidOnly: !!process.env.CI, 7 + retries: process.env.CI ? 2 : 0, 8 + workers: process.env.CI ? 1 : undefined, 9 + reporter: "html", 10 + use: { 11 + baseURL: "http://localhost:3000", 12 + trace: "on-first-retry", 13 + }, 14 + projects: [ 15 + { 16 + name: "chromium", 17 + use: { ...devices["Desktop Chrome"] }, 18 + }, 19 + ], 20 + webServer: { 21 + command: "npm run dev", 22 + url: "http://localhost:3000", 23 + reuseExistingServer: !process.env.CI, 24 + }, 25 + });
+21
todos.md
··· 142 142 ### Integration tests for worker 143 143 - Worker code tested via mocked Comlink 144 144 - Would benefit from actual worker instantiation tests 145 + 146 + --- 147 + 148 + ## Accessibility (a11y) 149 + 150 + Run `npm run test:a11y` to check. Currently failing on Card Search, Profile, Deck pages. 151 + 152 + ### color-contrast: cyan links on gray backgrounds 153 + - **Location**: Search primer (`src/components/SearchPrimer.tsx`) 154 + - **Issue**: cyan-600 (#007595) on gray-200 (#e5e7eb) = 4.26:1, need 4.5:1 for AA 155 + - **Fix**: Either darken cyan to ~cyan-700 or lighten background 156 + 157 + ### link-in-text-block: links not distinguishable 158 + - **Location**: Inline links in search primer, profile bio, etc. 159 + - **Issue**: Links only distinguished by color, need underline or 3:1 contrast vs surrounding text 160 + - **Fix**: Add `hover:underline` โ†’ `underline hover:no-underline` or ensure sufficient contrast 161 + 162 + ### select-name: dropdowns missing accessible names 163 + - **Location**: Sort dropdowns on card search, deck page 164 + - **Issue**: `<select>` elements have no `aria-label` or associated `<label>` 165 + - **Fix**: Add `aria-label="Sort by"` or wrap with `<label>`
+1 -1
vitest.config.ts
··· 21 21 test: { 22 22 environment: "jsdom", 23 23 globals: true, 24 - exclude: [...configDefaults.exclude, "**/.direnv/**"], 24 + exclude: [...configDefaults.exclude, "**/.direnv/**", "e2e/**"], 25 25 }, 26 26 });