WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

test(web): scaffold a11y test file with jsdom environment and module mocks (ATB-34)

+101
+101
apps/web/src/__tests__/a11y.test.ts
··· 1 + // @vitest-environment jsdom 2 + import { describe, it, expect, vi, beforeEach } from "vitest"; 3 + import axe from "axe-core"; 4 + 5 + // ── Module mocks ────────────────────────────────────────────────────────────── 6 + // Must be declared before any imports that use these modules. 7 + // vi.mock is hoisted to the top of the file by Vitest's transform. 8 + 9 + vi.mock("../lib/api.js", () => ({ 10 + fetchApi: vi.fn(), 11 + })); 12 + 13 + vi.mock("../lib/session.js", () => ({ 14 + getSession: vi.fn(), 15 + getSessionWithPermissions: vi.fn(), 16 + canLockTopics: vi.fn().mockReturnValue(false), 17 + canModeratePosts: vi.fn().mockReturnValue(false), 18 + canBanUsers: vi.fn().mockReturnValue(false), 19 + })); 20 + 21 + vi.mock("../lib/logger.js", () => ({ 22 + logger: { 23 + debug: vi.fn(), 24 + info: vi.fn(), 25 + warn: vi.fn(), 26 + error: vi.fn(), 27 + fatal: vi.fn(), 28 + }, 29 + })); 30 + 31 + // ── Import mocked modules so we can configure return values per test ────────── 32 + import { fetchApi } from "../lib/api.js"; 33 + import { getSession, getSessionWithPermissions } from "../lib/session.js"; 34 + 35 + // ── Route factories ─────────────────────────────────────────────────────────── 36 + // eslint-disable-next-line no-unused-vars 37 + import { createHomeRoutes } from "../routes/home.js"; 38 + // eslint-disable-next-line no-unused-vars 39 + import { createLoginRoutes } from "../routes/login.js"; 40 + // eslint-disable-next-line no-unused-vars 41 + import { createBoardsRoutes } from "../routes/boards.js"; 42 + // eslint-disable-next-line no-unused-vars 43 + import { createTopicsRoutes } from "../routes/topics.js"; 44 + // eslint-disable-next-line no-unused-vars 45 + import { createNewTopicRoutes } from "../routes/new-topic.js"; 46 + // eslint-disable-next-line no-unused-vars 47 + import { createNotFoundRoute } from "../routes/not-found.js"; 48 + 49 + // ── Constants ───────────────────────────────────────────────────────────────── 50 + // eslint-disable-next-line no-unused-vars 51 + const APPVIEW_URL = "http://localhost:3000"; 52 + 53 + // ── Typed mock handles ──────────────────────────────────────────────────────── 54 + const mockFetchApi = vi.mocked(fetchApi); 55 + const mockGetSession = vi.mocked(getSession); 56 + const mockGetSessionWithPermissions = vi.mocked(getSessionWithPermissions); 57 + 58 + // ── Shared reset ────────────────────────────────────────────────────────────── 59 + beforeEach(() => { 60 + mockFetchApi.mockReset(); 61 + // Default: unauthenticated session for all routes. 62 + // Override in individual tests that need authenticated state. 63 + mockGetSession.mockResolvedValue({ authenticated: false }); 64 + mockGetSessionWithPermissions.mockResolvedValue({ 65 + authenticated: false, 66 + permissions: new Set<string>(), 67 + }); 68 + }); 69 + 70 + // ── A11y helper ─────────────────────────────────────────────────────────────── 71 + // NOTE: jsdom has no CSS engine, so axe-core's color-contrast rules are 72 + // skipped automatically. These tests cover structural/semantic WCAG AA rules 73 + // only (landmark regions, heading hierarchy, form labels, aria attributes). 74 + // eslint-disable-next-line no-unused-vars 75 + async function checkA11y(html: string): Promise<void> { 76 + const doc = new DOMParser().parseFromString(html, "text/html"); 77 + const results = await axe.run(doc, { 78 + runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] }, 79 + }); 80 + const summary = results.violations 81 + .map( 82 + (v) => 83 + ` [${v.id}] ${v.description}\n` + 84 + v.nodes.map((n) => ` → ${n.html}`).join("\n") 85 + ) 86 + .join("\n"); 87 + expect( 88 + results.violations, 89 + `WCAG AA violations found:\n${summary}` 90 + ).toHaveLength(0); 91 + } 92 + 93 + describe("WCAG AA accessibility — one happy-path test per page route", () => { 94 + // Tests added in subsequent tasks 95 + it.todo("home page / has no violations"); 96 + it.todo("login page /login has no violations"); 97 + it.todo("board page /boards/:id has no violations"); 98 + it.todo("topic page /topics/:id has no violations"); 99 + it.todo("new-topic page /new-topic (authenticated) has no violations"); 100 + it.todo("not-found page has no violations"); 101 + });