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): add axe-core WCAG AA accessibility tests (ATB-34) (#71)

* docs: ATB-34 axe-core a11y testing design

Captures the approved design for adding axe-core + jsdom automated
accessibility tests to apps/web — single consolidated test file,
per-file jsdom environment pragma, one happy-path test per page route.

* docs: ATB-34 axe-core a11y implementation plan

Step-by-step plan: add axe-core + jsdom deps, create consolidated test
file with jsdom env pragma, one happy-path WCAG AA test per page route.

* chore(web): add axe-core, jsdom, vitest as explicit devDependencies (ATB-34)

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

* test(web): add WCAG AA accessibility tests for all page routes (ATB-34)

* test(web): suppress document.write deprecation with explanatory comment (ATB-34)

* test(web): use @ts-ignore to suppress deprecated document.write diagnostic (ATB-34)

* docs: move ATB-34 plan docs to complete

* test(web): address PR review feedback on a11y tests (ATB-34)

- Fix DOMParser comment to explain axe isPageContext() mechanism accurately
- Add DOM replacement guard after document.write() to catch silent no-ops
- Wrap axe.run() in try/catch with routeLabel for infrastructure error context
- Add routeLabel param to checkA11y; update all 6 call sites
- Reset canLockTopics/canModeratePosts/canBanUsers in beforeEach
- Add afterEach DOM cleanup via documentElement.innerHTML
- Fix path.startsWith('/topics/1') to exact match '/topics/1?offset=0&limit=25'
- Add form presence guard in new-topic test to catch silent auth fallback
- Update design doc to document DOMParser divergence and its reason

* test(web): strengthen DOM write guard to check html[lang] (ATB-34)

afterEach now removes lang from <html> so the guard in checkA11y can use
html[lang] as proof that document.write() actually executed, rather than
just checking that the <html> element exists (which it always does after
afterEach resets innerHTML without touching attributes).

authored by

Malpercio and committed by
GitHub
4c6e7305 a7b9c634

+1391 -8
+4 -1
apps/web/package.json
··· 19 19 }, 20 20 "devDependencies": { 21 21 "@types/node": "^22.0.0", 22 + "axe-core": "^4.11.1", 23 + "jsdom": "^28.1.0", 22 24 "tsx": "^4.0.0", 25 + "typed-htmx": "^0.3.0", 23 26 "typescript": "^5.7.0", 24 - "typed-htmx": "^0.3.0" 27 + "vitest": "^4.0.18" 25 28 } 26 29 }
+350
apps/web/src/__tests__/a11y.test.ts
··· 1 + // @vitest-environment jsdom 2 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 3 + import axe from "axe-core"; 4 + 5 + // ── Module mocks ────────────────────────────────────────────────────────────── 6 + // vi.mock calls are hoisted by Vitest's transform so mocks are in effect before 7 + // any module imports execute. 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 { 34 + getSession, 35 + getSessionWithPermissions, 36 + canLockTopics, 37 + canModeratePosts, 38 + canBanUsers, 39 + } from "../lib/session.js"; 40 + 41 + // ── Route factories ─────────────────────────────────────────────────────────── 42 + import { createHomeRoutes } from "../routes/home.js"; 43 + import { createLoginRoutes } from "../routes/login.js"; 44 + import { createBoardsRoutes } from "../routes/boards.js"; 45 + import { createTopicsRoutes } from "../routes/topics.js"; 46 + import { createNewTopicRoutes } from "../routes/new-topic.js"; 47 + import { createNotFoundRoute } from "../routes/not-found.js"; 48 + 49 + // ── Constants ───────────────────────────────────────────────────────────────── 50 + const APPVIEW_URL = "http://localhost:3000"; 51 + 52 + // ── Typed mock handles ──────────────────────────────────────────────────────── 53 + const mockFetchApi = vi.mocked(fetchApi); 54 + const mockGetSession = vi.mocked(getSession); 55 + const mockGetSessionWithPermissions = vi.mocked(getSessionWithPermissions); 56 + const mockCanLockTopics = vi.mocked(canLockTopics); 57 + const mockCanModeratePosts = vi.mocked(canModeratePosts); 58 + const mockCanBanUsers = vi.mocked(canBanUsers); 59 + 60 + // ── Shared reset ────────────────────────────────────────────────────────────── 61 + beforeEach(() => { 62 + mockFetchApi.mockReset(); 63 + // Default: unauthenticated session for all routes. 64 + // Override in individual tests that need authenticated state. 65 + mockGetSession.mockResolvedValue({ authenticated: false }); 66 + mockGetSessionWithPermissions.mockResolvedValue({ 67 + authenticated: false, 68 + permissions: new Set<string>(), 69 + }); 70 + mockCanLockTopics.mockReturnValue(false); 71 + mockCanModeratePosts.mockReturnValue(false); 72 + mockCanBanUsers.mockReturnValue(false); 73 + }); 74 + 75 + // ── DOM cleanup ─────────────────────────────────────────────────────────────── 76 + // Reset jsdom between tests so stale DOM from one test never leaks into the next. 77 + // Also remove lang from <html> so the guard in checkA11y (html[lang]) can 78 + // confirm that document.write() actually executed in the next test. 79 + afterEach(() => { 80 + document.documentElement.innerHTML = "<head></head><body></body>"; 81 + document.documentElement.removeAttribute("lang"); 82 + }); 83 + 84 + // ── A11y helper ─────────────────────────────────────────────────────────────── 85 + // NOTE: jsdom has no CSS engine, so axe-core's color-contrast rules are 86 + // skipped automatically. These tests cover structural/semantic WCAG AA rules 87 + // only (landmark regions, heading hierarchy, form labels, aria attributes). 88 + // 89 + // We call axe.run() with no context argument, so axe defaults to window.document. 90 + // If we passed a DOMParser document instead (axe.run(doc, options)), axe's 91 + // internal isPageContext() would compare include[0].actualNode === 92 + // document.documentElement — a DOMParser document's root fails this check, 93 + // disabling page-level rules like html-has-lang and document-title and 94 + // producing false greens on the rules we most care about. 95 + // document.open/write/close replaces window.document in place so the default 96 + // context works correctly. 97 + async function checkA11y(html: string, routeLabel: string): Promise<void> { 98 + document.open(); 99 + // document.write is deprecated in browsers but is the only reliable way to 100 + // fully replace jsdom's global document (including <html lang="en">) for 101 + // axe-core. Alternatives like innerHTML assignment silently drop <html lang>, 102 + // which causes axe to report a spurious html-has-lang violation. 103 + // @ts-ignore — intentional use of deprecated API; see comment above 104 + document.write(html); 105 + document.close(); 106 + 107 + // afterEach removes <html lang> so this proves document.write() actually ran, 108 + // not just that the <html> element still exists from the previous test's cleanup. 109 + if (!document.querySelector("html[lang]")) { 110 + throw new Error( 111 + `document.write() did not produce expected <html lang> for route "${routeLabel}" — ` + 112 + "DOM replacement likely failed. Previous test's DOM may still be active." 113 + ); 114 + } 115 + 116 + let results: axe.AxeResults; 117 + try { 118 + results = await axe.run({ 119 + runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] }, 120 + }); 121 + } catch (axeError) { 122 + throw new Error( 123 + `axe.run() failed for route "${routeLabel}" — infrastructure error, not a WCAG violation. ` + 124 + `axe threw: ${axeError instanceof Error ? axeError.message : String(axeError)}` 125 + ); 126 + } 127 + 128 + const summary = results.violations 129 + .map( 130 + (v) => 131 + ` [${v.id}] ${v.description}\n` + 132 + v.nodes.map((n) => ` → ${n.html}`).join("\n") 133 + ) 134 + .join("\n"); 135 + expect( 136 + results.violations, 137 + `WCAG AA violations found on "${routeLabel}":\n${summary}` 138 + ).toHaveLength(0); 139 + } 140 + 141 + describe("WCAG AA accessibility — one happy-path test per page route", () => { 142 + it("home page / has no violations", async () => { 143 + mockFetchApi.mockImplementation((path: string) => { 144 + if (path === "/forum") { 145 + return Promise.resolve({ 146 + id: "1", 147 + did: "did:plc:forum", 148 + name: "Test Forum", 149 + description: "A test forum", 150 + indexedAt: "2024-01-01T00:00:00.000Z", 151 + }); 152 + } 153 + if (path === "/categories") { 154 + return Promise.resolve({ 155 + categories: [ 156 + { 157 + id: "1", 158 + did: "did:plc:forum", 159 + name: "General", 160 + description: null, 161 + slug: "general", 162 + sortOrder: 0, 163 + }, 164 + ], 165 + }); 166 + } 167 + if (path === "/categories/1/boards") { 168 + return Promise.resolve({ 169 + boards: [ 170 + { 171 + id: "1", 172 + did: "did:plc:forum", 173 + name: "Test Board", 174 + description: null, 175 + slug: "test", 176 + sortOrder: 0, 177 + }, 178 + ], 179 + }); 180 + } 181 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 182 + }); 183 + 184 + const routes = createHomeRoutes(APPVIEW_URL); 185 + const res = await routes.request("/"); 186 + expect(res.status).toBe(200); 187 + await checkA11y(await res.text(), "GET /"); 188 + }); 189 + 190 + it("login page /login has no violations", async () => { 191 + // getSession returns { authenticated: false } immediately with no cookie header. 192 + // No fetchApi calls are made. 193 + const routes = createLoginRoutes(APPVIEW_URL); 194 + const res = await routes.request("/login"); 195 + expect(res.status).toBe(200); 196 + await checkA11y(await res.text(), "GET /login"); 197 + }); 198 + 199 + it("board page /boards/:id has no violations", async () => { 200 + mockFetchApi.mockImplementation((path: string) => { 201 + if (path === "/boards/1") { 202 + return Promise.resolve({ 203 + id: "1", 204 + did: "did:plc:forum", 205 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 206 + name: "Test Board", 207 + description: null, 208 + slug: "test", 209 + sortOrder: 0, 210 + categoryId: "1", 211 + categoryUri: null, 212 + createdAt: "2024-01-01T00:00:00.000Z", 213 + indexedAt: "2024-01-01T00:00:00.000Z", 214 + }); 215 + } 216 + if (path === "/boards/1/topics?offset=0&limit=25") { 217 + return Promise.resolve({ topics: [], total: 0, offset: 0, limit: 25 }); 218 + } 219 + if (path === "/categories/1") { 220 + return Promise.resolve({ 221 + id: "1", 222 + did: "did:plc:forum", 223 + name: "General", 224 + description: null, 225 + slug: null, 226 + sortOrder: null, 227 + forumId: null, 228 + createdAt: null, 229 + indexedAt: null, 230 + }); 231 + } 232 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 233 + }); 234 + 235 + const routes = createBoardsRoutes(APPVIEW_URL); 236 + const res = await routes.request("/boards/1"); 237 + expect(res.status).toBe(200); 238 + await checkA11y(await res.text(), "GET /boards/:id"); 239 + }); 240 + 241 + it("topic page /topics/:id has no violations", async () => { 242 + mockFetchApi.mockImplementation((path: string) => { 243 + if (path === "/topics/1?offset=0&limit=25") { 244 + return Promise.resolve({ 245 + topicId: "1", 246 + locked: false, 247 + pinned: false, 248 + post: { 249 + id: "1", 250 + did: "did:plc:user", 251 + rkey: "abc123", 252 + title: "Test Topic Title", 253 + text: "Hello world, this is a test post.", 254 + forumUri: null, 255 + boardUri: null, 256 + boardId: "1", 257 + parentPostId: null, 258 + createdAt: "2024-01-01T00:00:00.000Z", 259 + author: { did: "did:plc:user", handle: "alice.test" }, 260 + }, 261 + replies: [], 262 + total: 0, 263 + offset: 0, 264 + limit: 25, 265 + }); 266 + } 267 + if (path === "/boards/1") { 268 + return Promise.resolve({ 269 + id: "1", 270 + did: "did:plc:forum", 271 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 272 + name: "Test Board", 273 + description: null, 274 + slug: null, 275 + sortOrder: null, 276 + categoryId: "1", 277 + categoryUri: null, 278 + createdAt: null, 279 + indexedAt: null, 280 + }); 281 + } 282 + if (path === "/categories/1") { 283 + return Promise.resolve({ 284 + id: "1", 285 + did: "did:plc:forum", 286 + name: "General", 287 + description: null, 288 + slug: null, 289 + sortOrder: null, 290 + forumId: null, 291 + createdAt: null, 292 + indexedAt: null, 293 + }); 294 + } 295 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 296 + }); 297 + 298 + const routes = createTopicsRoutes(APPVIEW_URL); 299 + const res = await routes.request("/topics/1"); 300 + expect(res.status).toBe(200); 301 + await checkA11y(await res.text(), "GET /topics/:id"); 302 + }); 303 + 304 + it("new-topic page /new-topic (authenticated) has no violations", async () => { 305 + // Override default unauthenticated session for this test only. 306 + mockGetSession.mockResolvedValueOnce({ 307 + authenticated: true, 308 + did: "did:plc:user", 309 + handle: "alice.test", 310 + }); 311 + 312 + mockFetchApi.mockImplementation((path: string) => { 313 + if (path === "/boards/1") { 314 + return Promise.resolve({ 315 + id: "1", 316 + did: "did:plc:forum", 317 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 318 + name: "Test Board", 319 + description: null, 320 + slug: null, 321 + sortOrder: null, 322 + categoryId: "1", 323 + categoryUri: null, 324 + createdAt: null, 325 + indexedAt: null, 326 + }); 327 + } 328 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 329 + }); 330 + 331 + const routes = createNewTopicRoutes(APPVIEW_URL); 332 + const res = await routes.request("/new-topic?boardId=1"); 333 + expect(res.status).toBe(200); 334 + const html = await res.text(); 335 + // Guard: ensure the authenticated form rendered (not the "Log in" fallback). 336 + // If getSession() fell back to unauthenticated, the form would be absent. 337 + expect(html, "Expected authenticated new-topic form but got login fallback").toContain( 338 + 'name="title"' 339 + ); 340 + await checkA11y(html, "GET /new-topic (authenticated)"); 341 + }); 342 + 343 + it("not-found page has no violations", async () => { 344 + // No fetchApi or session calls — unauthenticated with no cookie. 345 + const routes = createNotFoundRoute(APPVIEW_URL); 346 + const res = await routes.request("/anything-that-does-not-exist"); 347 + expect(res.status).toBe(404); 348 + await checkA11y(await res.text(), "GET /404"); 349 + }); 350 + });
+75
docs/plans/complete/2026-02-27-axe-core-a11y-design.md
··· 1 + # ATB-34: Axe-core Automated Accessibility Testing — Design 2 + 3 + **Date:** 2026-02-27 4 + **Linear:** ATB-34 5 + **Status:** Approved 6 + 7 + ## Overview 8 + 9 + Add automated WCAG AA accessibility tests to `apps/web` using `axe-core` and Vitest's built-in jsdom environment. Tests run as part of the existing `pnpm test` pipeline with no CI changes required. 10 + 11 + ## Architecture 12 + 13 + ### Approach 14 + 15 + Single consolidated test file: `apps/web/src/__tests__/a11y.test.ts` 16 + 17 + - `// @vitest-environment jsdom` pragma switches this file from the default `node` environment to jsdom, providing `document`, `window`, and `DOMParser` as globals. 18 + - All other test files keep running under `node` — no environment change bleeds across. 19 + - Tests call route handlers directly via `app.request()` (same pattern as existing tests), mock `fetch` globally, and run axe-core against the parsed HTML response. 20 + 21 + ### Dependencies 22 + 23 + Add to `apps/web/package.json` devDependencies: 24 + - `axe-core` — accessibility engine 25 + - `jsdom` — DOM implementation Vitest's jsdom environment wraps 26 + - `vitest` — currently works via pnpm hoisting but should be declared explicitly 27 + 28 + ## HTML Parsing & Axe Configuration 29 + 30 + Each test follows this flow: 31 + 32 + 1. Mock `fetch` globally with `vi.stubGlobal('fetch', mockFetch)` 33 + 2. Call the route handler: `const res = await routes.request('/path')` 34 + 3. Get HTML: `const html = await res.text()` 35 + 4. Parse into DOM: `const doc = new DOMParser().parseFromString(html, 'text/html')` 36 + 5. Run axe: `const results = await axe.run(doc, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] } })` 37 + 6. Assert with a useful failure message showing which rules violated 38 + 39 + **Implementation divergence:** This design specified `DOMParser.parseFromString` but the actual implementation uses `document.open() / document.write(html) / document.close()` instead. The reason: `axe.run()` is called with no context argument, so axe defaults to `window.document`. If a DOMParser document is passed explicitly, axe's internal `isPageContext()` check fails (it compares `include[0].actualNode === document.documentElement`), which disables page-level rules like `html-has-lang` and `document-title` — producing false greens on the rules we most care about. `document.write()` is deprecated but replaces `window.document` in place, keeping the default axe context correct. The call site is suppressed with `@ts-ignore` and an explanatory comment. 40 + 41 + ### Known Limitation 42 + 43 + jsdom has no CSS engine. Axe-core's `color-contrast` rules are automatically skipped. Tests cover structural/semantic WCAG AA rules only: landmark regions, heading hierarchy, form labels, `aria-*` attributes, image alt text, link purpose. Color contrast must be verified manually or via a Playwright-based test in the future. 44 + 45 + ## Route Coverage 46 + 47 + Six full-page HTML routes get one test each. `POST /mod/action`, `POST /logout`, `POST /new-topic`, and `POST /topics/:id/reply` are excluded — they return HTML fragments or redirects, not full pages. 48 + 49 + | Route | Auth state | Reason | 50 + |---|---|---| 51 + | `GET /` | unauthenticated | Standard landing state | 52 + | `GET /login` | unauthenticated | Standard landing state | 53 + | `GET /boards/1` | unauthenticated | Standard board view | 54 + | `GET /topics/1` | unauthenticated | Standard topic view | 55 + | `GET /new-topic?boardId=1` | **authenticated** | Unauthenticated renders trivial "Log in" prompt; form markup is the interesting a11y surface | 56 + | `GET /anything-unknown` | unauthenticated | 404 catch-all | 57 + 58 + ## Fetch Mocking Strategy 59 + 60 + `beforeEach` stubs `fetch` with a URL-dispatching mock function. `afterEach` calls `vi.unstubAllGlobals()`. 61 + 62 + The mock inspects the URL and returns matching minimal fixture data — only the fields each route actually reads. Unmatched URLs return a 404 response to surface unexpected calls. 63 + 64 + ```ts 65 + const mockFetch = vi.fn().mockImplementation((url: string) => { 66 + if (url.includes('/api/auth/session')) return Promise.resolve(sessionFixture(false)); 67 + if (url.endsWith('/forum')) return Promise.resolve(forumFixture()); 68 + // ... 69 + return Promise.resolve({ ok: false, status: 404, json: async () => ({}) }); 70 + }); 71 + ``` 72 + 73 + ## CI 74 + 75 + No changes required. Tests are discovered by `vitest run` and block merge on failure via the existing `test` job in `.github/workflows/ci.yml`.
+584
docs/plans/complete/2026-02-27-axe-core-a11y.md
··· 1 + # ATB-34: Axe-core Automated Accessibility Testing — Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add a single consolidated test file to `apps/web` that runs axe-core against every full-page HTML route and asserts zero WCAG AA violations. 6 + 7 + **Architecture:** Vitest's `@vitest-environment jsdom` per-file pragma switches the test file to a jsdom environment, providing `DOMParser` and `document` globals. Each test calls the Hono route handler directly via `app.request()`, parses the HTML response with `DOMParser`, and passes the resulting `Document` to `axe.run()`. Module-level `vi.mock` intercepts `fetchApi` and `getSession` calls so no real network is needed. 8 + 9 + **Tech Stack:** `axe-core`, `jsdom`, Vitest, Hono (test via `app.request()`), `vi.mock` for module mocking 10 + 11 + **Design doc:** `docs/plans/2026-02-27-axe-core-a11y-design.md` 12 + 13 + **Known limitation:** jsdom has no CSS engine. Axe-core's `color-contrast` rules are automatically skipped. These tests cover structural/semantic WCAG AA rules only (landmark regions, heading hierarchy, form labels, aria attributes, link purpose). Color contrast requires manual or Playwright-based verification. 14 + 15 + --- 16 + 17 + ### Task 1: Add dependencies 18 + 19 + **Files:** 20 + - Modify: `apps/web/package.json` 21 + 22 + **Step 1: Add axe-core, jsdom, and vitest as devDependencies** 23 + 24 + Run from the repo root (requires devenv shell — `devenv shell` first): 25 + 26 + ```bash 27 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 28 + pnpm --filter @atbb/web add -D axe-core jsdom vitest 29 + ``` 30 + 31 + Expected: pnpm updates `apps/web/package.json` and `pnpm-lock.yaml`. 32 + 33 + **Step 2: Verify the additions appear in package.json** 34 + 35 + Check that `apps/web/package.json` now contains: 36 + ```json 37 + "devDependencies": { 38 + "axe-core": "...", 39 + "jsdom": "...", 40 + "vitest": "..." 41 + } 42 + ``` 43 + 44 + **Step 3: Commit** 45 + 46 + ```bash 47 + git add apps/web/package.json pnpm-lock.yaml 48 + git commit -m "chore(web): add axe-core, jsdom, vitest as explicit devDependencies (ATB-34)" 49 + ``` 50 + 51 + --- 52 + 53 + ### Task 2: Create the test file skeleton 54 + 55 + **Files:** 56 + - Create: `apps/web/src/__tests__/a11y.test.ts` 57 + 58 + **Step 1: Create the file with the environment pragma, imports, and module mocks** 59 + 60 + ```typescript 61 + // @vitest-environment jsdom 62 + import { describe, it, expect, vi, beforeEach } from "vitest"; 63 + import axe from "axe-core"; 64 + 65 + // ── Module mocks ────────────────────────────────────────────────────────────── 66 + // Must be declared before any imports that use these modules. 67 + // vi.mock is hoisted to the top of the file by Vitest's transform. 68 + 69 + vi.mock("../lib/api.js", () => ({ 70 + fetchApi: vi.fn(), 71 + })); 72 + 73 + vi.mock("../lib/session.js", () => ({ 74 + getSession: vi.fn(), 75 + getSessionWithPermissions: vi.fn(), 76 + canLockTopics: vi.fn().mockReturnValue(false), 77 + canModeratePosts: vi.fn().mockReturnValue(false), 78 + canBanUsers: vi.fn().mockReturnValue(false), 79 + })); 80 + 81 + vi.mock("../lib/logger.js", () => ({ 82 + logger: { 83 + debug: vi.fn(), 84 + info: vi.fn(), 85 + warn: vi.fn(), 86 + error: vi.fn(), 87 + fatal: vi.fn(), 88 + }, 89 + })); 90 + 91 + // ── Import mocked modules so we can configure return values per test ────────── 92 + import { fetchApi } from "../lib/api.js"; 93 + import { getSession, getSessionWithPermissions } from "../lib/session.js"; 94 + 95 + // ── Route factories ─────────────────────────────────────────────────────────── 96 + import { createHomeRoutes } from "../routes/home.js"; 97 + import { createLoginRoutes } from "../routes/login.js"; 98 + import { createBoardsRoutes } from "../routes/boards.js"; 99 + import { createTopicsRoutes } from "../routes/topics.js"; 100 + import { createNewTopicRoutes } from "../routes/new-topic.js"; 101 + import { createNotFoundRoute } from "../routes/not-found.js"; 102 + 103 + // ── Constants ───────────────────────────────────────────────────────────────── 104 + const APPVIEW_URL = "http://localhost:3000"; 105 + 106 + // ── Typed mock handles ──────────────────────────────────────────────────────── 107 + const mockFetchApi = vi.mocked(fetchApi); 108 + const mockGetSession = vi.mocked(getSession); 109 + const mockGetSessionWithPermissions = vi.mocked(getSessionWithPermissions); 110 + 111 + // ── Shared reset ────────────────────────────────────────────────────────────── 112 + beforeEach(() => { 113 + mockFetchApi.mockReset(); 114 + // Default: unauthenticated session for all routes. 115 + // Override in individual tests that need authenticated state. 116 + mockGetSession.mockResolvedValue({ authenticated: false }); 117 + mockGetSessionWithPermissions.mockResolvedValue({ 118 + authenticated: false, 119 + permissions: new Set<string>(), 120 + }); 121 + }); 122 + 123 + // ── A11y helper ─────────────────────────────────────────────────────────────── 124 + // NOTE: jsdom has no CSS engine, so axe-core's color-contrast rules are 125 + // skipped automatically. These tests cover structural/semantic WCAG AA rules 126 + // only (landmark regions, heading hierarchy, form labels, aria attributes). 127 + async function checkA11y(html: string): Promise<void> { 128 + const doc = new DOMParser().parseFromString(html, "text/html"); 129 + const results = await axe.run(doc, { 130 + runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] }, 131 + }); 132 + const summary = results.violations 133 + .map( 134 + (v) => 135 + ` [${v.id}] ${v.description}\n` + 136 + v.nodes.map((n) => ` → ${n.html}`).join("\n") 137 + ) 138 + .join("\n"); 139 + expect( 140 + results.violations, 141 + `WCAG AA violations found:\n${summary}` 142 + ).toHaveLength(0); 143 + } 144 + 145 + describe("WCAG AA accessibility — one happy-path test per page route", () => { 146 + // Tests go here in Task 3–8 147 + }); 148 + ``` 149 + 150 + **Step 2: Verify the file can be collected by Vitest without errors** 151 + 152 + ```bash 153 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 154 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts 155 + ``` 156 + 157 + Expected output: `0 tests` collected, no import/type errors. If you see a module resolution error, fix the import paths before proceeding. 158 + 159 + **Step 3: Commit** 160 + 161 + ```bash 162 + git add apps/web/src/__tests__/a11y.test.ts 163 + git commit -m "test(web): scaffold a11y test file with jsdom environment and module mocks (ATB-34)" 164 + ``` 165 + 166 + --- 167 + 168 + ### Task 3: Home page test 169 + 170 + The home route calls `fetchApi` three times: 171 + 1. `fetchApi("/forum")` and `fetchApi("/categories")` in parallel (stage 1) 172 + 2. `fetchApi("/categories/1/boards")` for each category (stage 2) 173 + 174 + Use URL-pattern dispatch in `mockImplementation` so parallel calls resolve correctly regardless of call order. 175 + 176 + **Files:** 177 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 178 + 179 + **Step 1: Add the home page test inside the `describe` block** 180 + 181 + ```typescript 182 + it("home page / has no violations", async () => { 183 + mockFetchApi.mockImplementation((path: string) => { 184 + if (path === "/forum") { 185 + return Promise.resolve({ 186 + id: "1", 187 + did: "did:plc:forum", 188 + name: "Test Forum", 189 + description: "A test forum", 190 + indexedAt: "2024-01-01T00:00:00.000Z", 191 + }); 192 + } 193 + if (path === "/categories") { 194 + return Promise.resolve({ 195 + categories: [ 196 + { 197 + id: "1", 198 + did: "did:plc:forum", 199 + name: "General", 200 + description: null, 201 + slug: "general", 202 + sortOrder: 0, 203 + }, 204 + ], 205 + }); 206 + } 207 + if (path === "/categories/1/boards") { 208 + return Promise.resolve({ 209 + boards: [ 210 + { 211 + id: "1", 212 + did: "did:plc:forum", 213 + name: "Test Board", 214 + description: null, 215 + slug: "test", 216 + sortOrder: 0, 217 + }, 218 + ], 219 + }); 220 + } 221 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 222 + }); 223 + 224 + const routes = createHomeRoutes(APPVIEW_URL); 225 + const res = await routes.request("/"); 226 + expect(res.status).toBe(200); 227 + await checkA11y(await res.text()); 228 + }); 229 + ``` 230 + 231 + **Step 2: Run just this test** 232 + 233 + ```bash 234 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 235 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts -t "home page" 236 + ``` 237 + 238 + Expected: PASS. If axe reports violations, inspect the violation `[id]` and fix the corresponding HTML in `apps/web/src/routes/home.tsx` or `apps/web/src/layouts/base.tsx`. 239 + 240 + **Step 3: Commit once passing** 241 + 242 + ```bash 243 + git add apps/web/src/__tests__/a11y.test.ts 244 + git commit -m "test(web): add WCAG AA test for home page (ATB-34)" 245 + ``` 246 + 247 + --- 248 + 249 + ### Task 4: Login page test 250 + 251 + The login route calls `getSession()` which returns early (no fetch) when no `atbb_session` cookie is present — which is the case here since we don't include a Cookie header. No `fetchApi` calls needed. 252 + 253 + **Files:** 254 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 255 + 256 + **Step 1: Add the login page test** 257 + 258 + ```typescript 259 + it("login page /login has no violations", async () => { 260 + // getSession returns { authenticated: false } immediately with no cookie header. 261 + // No fetchApi calls are made. 262 + const routes = createLoginRoutes(APPVIEW_URL); 263 + const res = await routes.request("/login"); 264 + expect(res.status).toBe(200); 265 + await checkA11y(await res.text()); 266 + }); 267 + ``` 268 + 269 + **Step 2: Run just this test** 270 + 271 + ```bash 272 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 273 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts -t "login page" 274 + ``` 275 + 276 + Expected: PASS. 277 + 278 + **Step 3: Commit once passing** 279 + 280 + ```bash 281 + git add apps/web/src/__tests__/a11y.test.ts 282 + git commit -m "test(web): add WCAG AA test for login page (ATB-34)" 283 + ``` 284 + 285 + --- 286 + 287 + ### Task 5: Board page test 288 + 289 + The board route calls `fetchApi` three times in sequence: 290 + 1. `fetchApi("/boards/1")` and `fetchApi("/boards/1/topics?offset=0&limit=25")` in parallel (stage 1) 291 + 2. `fetchApi("/categories/1")` for the category breadcrumb (stage 2) 292 + 293 + **Files:** 294 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 295 + 296 + **Step 1: Add the board page test** 297 + 298 + ```typescript 299 + it("board page /boards/:id has no violations", async () => { 300 + mockFetchApi.mockImplementation((path: string) => { 301 + if (path === "/boards/1") { 302 + return Promise.resolve({ 303 + id: "1", 304 + did: "did:plc:forum", 305 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 306 + name: "Test Board", 307 + description: null, 308 + slug: "test", 309 + sortOrder: 0, 310 + categoryId: "1", 311 + categoryUri: null, 312 + createdAt: "2024-01-01T00:00:00.000Z", 313 + indexedAt: "2024-01-01T00:00:00.000Z", 314 + }); 315 + } 316 + if (path === "/boards/1/topics?offset=0&limit=25") { 317 + return Promise.resolve({ topics: [], total: 0, offset: 0, limit: 25 }); 318 + } 319 + if (path === "/categories/1") { 320 + return Promise.resolve({ 321 + id: "1", 322 + did: "did:plc:forum", 323 + name: "General", 324 + description: null, 325 + slug: null, 326 + sortOrder: null, 327 + forumId: null, 328 + createdAt: null, 329 + indexedAt: null, 330 + }); 331 + } 332 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 333 + }); 334 + 335 + const routes = createBoardsRoutes(APPVIEW_URL); 336 + const res = await routes.request("/boards/1"); 337 + expect(res.status).toBe(200); 338 + await checkA11y(await res.text()); 339 + }); 340 + ``` 341 + 342 + **Step 2: Run just this test** 343 + 344 + ```bash 345 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 346 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts -t "board page" 347 + ``` 348 + 349 + Expected: PASS. 350 + 351 + **Step 3: Commit once passing** 352 + 353 + ```bash 354 + git add apps/web/src/__tests__/a11y.test.ts 355 + git commit -m "test(web): add WCAG AA test for board page (ATB-34)" 356 + ``` 357 + 358 + --- 359 + 360 + ### Task 6: Topic page test 361 + 362 + The topics route calls `getSessionWithPermissions()` (which sees no cookie → returns unauthenticated immediately). Then calls `fetchApi` three times: 363 + 1. `fetchApi("/topics/1?offset=0&limit=25")` (stage 1 — the topic + replies) 364 + 2. `fetchApi("/boards/1")` (stage 2 — board breadcrumb, non-fatal) 365 + 3. `fetchApi("/categories/1")` (stage 3 — category breadcrumb, non-fatal) 366 + 367 + **Files:** 368 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 369 + 370 + **Step 1: Add the topic page test** 371 + 372 + ```typescript 373 + it("topic page /topics/:id has no violations", async () => { 374 + mockFetchApi.mockImplementation((path: string) => { 375 + if (path.startsWith("/topics/1")) { 376 + return Promise.resolve({ 377 + topicId: "1", 378 + locked: false, 379 + pinned: false, 380 + post: { 381 + id: "1", 382 + did: "did:plc:user", 383 + rkey: "abc123", 384 + title: "Test Topic Title", 385 + text: "Hello world, this is a test post.", 386 + forumUri: null, 387 + boardUri: null, 388 + boardId: "1", 389 + parentPostId: null, 390 + createdAt: "2024-01-01T00:00:00.000Z", 391 + author: { did: "did:plc:user", handle: "alice.test" }, 392 + }, 393 + replies: [], 394 + total: 0, 395 + offset: 0, 396 + limit: 25, 397 + }); 398 + } 399 + if (path === "/boards/1") { 400 + return Promise.resolve({ 401 + id: "1", 402 + did: "did:plc:forum", 403 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 404 + name: "Test Board", 405 + description: null, 406 + slug: null, 407 + sortOrder: null, 408 + categoryId: "1", 409 + categoryUri: null, 410 + createdAt: null, 411 + indexedAt: null, 412 + }); 413 + } 414 + if (path === "/categories/1") { 415 + return Promise.resolve({ 416 + id: "1", 417 + did: "did:plc:forum", 418 + name: "General", 419 + description: null, 420 + slug: null, 421 + sortOrder: null, 422 + forumId: null, 423 + createdAt: null, 424 + indexedAt: null, 425 + }); 426 + } 427 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 428 + }); 429 + 430 + const routes = createTopicsRoutes(APPVIEW_URL); 431 + const res = await routes.request("/topics/1"); 432 + expect(res.status).toBe(200); 433 + await checkA11y(await res.text()); 434 + }); 435 + ``` 436 + 437 + **Step 2: Run just this test** 438 + 439 + ```bash 440 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 441 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts -t "topic page" 442 + ``` 443 + 444 + Expected: PASS. 445 + 446 + **Step 3: Commit once passing** 447 + 448 + ```bash 449 + git add apps/web/src/__tests__/a11y.test.ts 450 + git commit -m "test(web): add WCAG AA test for topic page (ATB-34)" 451 + ``` 452 + 453 + --- 454 + 455 + ### Task 7: New-topic page test (authenticated) 456 + 457 + The new-topic route requires authentication to show the form — the unauthenticated state renders only a plain "Log in to create a topic" paragraph, skipping the labeled inputs that are the primary a11y surface. Test the authenticated state. 458 + 459 + `getSession` is called with no `atbb_session` cookie in the default `beforeEach` mock. For this test only, override it to return an authenticated session. 460 + 461 + **Files:** 462 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 463 + 464 + **Step 1: Add the new-topic page test** 465 + 466 + ```typescript 467 + it("new-topic page /new-topic (authenticated) has no violations", async () => { 468 + // Override the default unauthenticated session for this test only. 469 + mockGetSession.mockResolvedValueOnce({ 470 + authenticated: true, 471 + did: "did:plc:user", 472 + handle: "alice.test", 473 + }); 474 + 475 + mockFetchApi.mockImplementation((path: string) => { 476 + if (path === "/boards/1") { 477 + return Promise.resolve({ 478 + id: "1", 479 + did: "did:plc:forum", 480 + uri: "at://did:plc:forum/space.atbb.forum.board/1", 481 + name: "Test Board", 482 + description: null, 483 + slug: null, 484 + sortOrder: null, 485 + categoryId: "1", 486 + categoryUri: null, 487 + createdAt: null, 488 + indexedAt: null, 489 + }); 490 + } 491 + return Promise.reject(new Error(`Unexpected fetchApi call: ${path}`)); 492 + }); 493 + 494 + const routes = createNewTopicRoutes(APPVIEW_URL); 495 + const res = await routes.request("/new-topic?boardId=1"); 496 + expect(res.status).toBe(200); 497 + await checkA11y(await res.text()); 498 + }); 499 + ``` 500 + 501 + **Step 2: Run just this test** 502 + 503 + ```bash 504 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 505 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts -t "new-topic" 506 + ``` 507 + 508 + Expected: PASS. 509 + 510 + **Step 3: Commit once passing** 511 + 512 + ```bash 513 + git add apps/web/src/__tests__/a11y.test.ts 514 + git commit -m "test(web): add WCAG AA test for new-topic page (ATB-34)" 515 + ``` 516 + 517 + --- 518 + 519 + ### Task 8: Not-found page test 520 + 521 + The not-found catch-all renders without any API calls (no session cookie → `getSession` returns unauthenticated immediately with no fetch). 522 + 523 + **Files:** 524 + - Modify: `apps/web/src/__tests__/a11y.test.ts` 525 + 526 + **Step 1: Add the not-found page test** 527 + 528 + ```typescript 529 + it("not-found page has no violations", async () => { 530 + // No fetchApi or session calls — unauthenticated with no cookie. 531 + const routes = createNotFoundRoute(APPVIEW_URL); 532 + const res = await routes.request("/anything-that-does-not-exist"); 533 + expect(res.status).toBe(404); 534 + await checkA11y(await res.text()); 535 + }); 536 + ``` 537 + 538 + **Step 2: Run the full a11y test suite** 539 + 540 + ```bash 541 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 542 + pnpm --filter @atbb/web exec vitest run src/__tests__/a11y.test.ts 543 + ``` 544 + 545 + Expected: 6 tests, all PASS. If any fail with axe violations: 546 + - Read the `[violation-id]` in the error message 547 + - Look up the rule at https://dequeuniversity.com/rules/axe/ to understand what's wrong 548 + - Fix the HTML in the relevant route file or `base.tsx` 549 + - Re-run until all 6 pass 550 + 551 + **Step 3: Run the full web test suite to confirm no regressions** 552 + 553 + ```bash 554 + PATH=/Users/jacob.zweifel/workspace/malpercio-dev/atbb-monorepo/.devenv/profile/bin:$PATH \ 555 + pnpm --filter @atbb/web test 556 + ``` 557 + 558 + Expected: all tests pass. 559 + 560 + **Step 4: Commit** 561 + 562 + ```bash 563 + git add apps/web/src/__tests__/a11y.test.ts 564 + git commit -m "test(web): add WCAG AA test for not-found page (ATB-34)" 565 + ``` 566 + 567 + --- 568 + 569 + ### Task 9: Mark complete and sync docs 570 + 571 + **Step 1: Update the Linear issue** 572 + 573 + - Go to https://linear.app/atbb/issue/ATB-34 574 + - Change status from Backlog → Done 575 + - Add a comment: "Implemented in `apps/web/src/__tests__/a11y.test.ts`. Six happy-path tests (one per page route) using axe-core + Vitest jsdom environment. Known limitation: color-contrast rules skipped (jsdom has no CSS engine). Tests run in CI as part of `pnpm test`." 576 + 577 + **Step 2: Move plan docs to complete** 578 + 579 + ```bash 580 + mv docs/plans/2026-02-27-axe-core-a11y-design.md docs/plans/complete/ 581 + mv docs/plans/2026-02-27-axe-core-a11y.md docs/plans/complete/ 582 + git add docs/plans/ 583 + git commit -m "docs: move ATB-34 plan docs to complete" 584 + ```
+378 -7
pnpm-lock.yaml
··· 22 22 version: 5.9.3 23 23 vitest: 24 24 specifier: ^4.0.18 25 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 25 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 26 26 27 27 apps/appview: 28 28 dependencies: ··· 86 86 version: 7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 87 87 vitest: 88 88 specifier: ^3.1.0 89 - version: 3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 89 + version: 3.2.4(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 90 90 91 91 apps/web: 92 92 dependencies: ··· 103 103 '@types/node': 104 104 specifier: ^22.0.0 105 105 version: 22.19.9 106 + axe-core: 107 + specifier: ^4.11.1 108 + version: 4.11.1 109 + jsdom: 110 + specifier: ^28.1.0 111 + version: 28.1.0 106 112 tsx: 107 113 specifier: ^4.0.0 108 114 version: 4.21.0 ··· 112 118 typescript: 113 119 specifier: ^5.7.0 114 120 version: 5.9.3 121 + vitest: 122 + specifier: ^4.0.18 123 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 115 124 116 125 packages/atproto: 117 126 dependencies: ··· 170 179 version: 5.9.3 171 180 vitest: 172 181 specifier: ^3.0.0 173 - version: 3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 182 + version: 3.2.4(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 174 183 175 184 packages/db: 176 185 dependencies: ··· 223 232 version: 5.9.3 224 233 vitest: 225 234 specifier: ^3.1.0 226 - version: 3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 235 + version: 3.2.4(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 227 236 yaml: 228 237 specifier: ^2.7.0 229 238 version: 2.8.2 ··· 260 269 version: 5.9.3 261 270 vitest: 262 271 specifier: ^3.1.0 263 - version: 3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 272 + version: 3.2.4(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) 264 273 265 274 packages: 266 275 276 + '@acemir/cssom@0.9.31': 277 + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} 278 + 279 + '@asamuzakjp/css-color@5.0.1': 280 + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} 281 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 282 + 283 + '@asamuzakjp/dom-selector@6.8.1': 284 + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} 285 + 286 + '@asamuzakjp/nwsapi@2.3.9': 287 + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} 288 + 267 289 '@atcute/atproto@3.1.10': 268 290 resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} 269 291 ··· 366 388 '@atproto/xrpc@0.7.7': 367 389 resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} 368 390 391 + '@bramus/specificity@2.4.2': 392 + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} 393 + hasBin: true 394 + 395 + '@csstools/color-helpers@6.0.2': 396 + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} 397 + engines: {node: '>=20.19.0'} 398 + 399 + '@csstools/css-calc@3.1.1': 400 + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} 401 + engines: {node: '>=20.19.0'} 402 + peerDependencies: 403 + '@csstools/css-parser-algorithms': ^4.0.0 404 + '@csstools/css-tokenizer': ^4.0.0 405 + 406 + '@csstools/css-color-parser@4.0.2': 407 + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} 408 + engines: {node: '>=20.19.0'} 409 + peerDependencies: 410 + '@csstools/css-parser-algorithms': ^4.0.0 411 + '@csstools/css-tokenizer': ^4.0.0 412 + 413 + '@csstools/css-parser-algorithms@4.0.0': 414 + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} 415 + engines: {node: '>=20.19.0'} 416 + peerDependencies: 417 + '@csstools/css-tokenizer': ^4.0.0 418 + 419 + '@csstools/css-syntax-patches-for-csstree@1.0.28': 420 + resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==} 421 + 422 + '@csstools/css-tokenizer@4.0.0': 423 + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} 424 + engines: {node: '>=20.19.0'} 425 + 369 426 '@drizzle-team/brocli@0.10.2': 370 427 resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} 371 428 ··· 821 878 cpu: [x64] 822 879 os: [win32] 823 880 881 + '@exodus/bytes@1.14.1': 882 + resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} 883 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 884 + peerDependencies: 885 + '@noble/hashes': ^1.8.0 || ^2.0.0 886 + peerDependenciesMeta: 887 + '@noble/hashes': 888 + optional: true 889 + 824 890 '@hono/node-server@1.19.9': 825 891 resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} 826 892 engines: {node: '>=18.14.1'} ··· 1323 1389 resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 1324 1390 engines: {node: '>=6.5'} 1325 1391 1392 + agent-base@7.1.4: 1393 + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} 1394 + engines: {node: '>= 14'} 1395 + 1326 1396 ansi-regex@5.0.1: 1327 1397 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 1328 1398 engines: {node: '>=8'} ··· 1342 1412 await-lock@2.2.2: 1343 1413 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 1344 1414 1415 + axe-core@4.11.1: 1416 + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} 1417 + engines: {node: '>=4'} 1418 + 1345 1419 balanced-match@1.0.2: 1346 1420 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 1347 1421 1348 1422 base64-js@1.5.1: 1349 1423 resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 1424 + 1425 + bidi-js@1.0.3: 1426 + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} 1350 1427 1351 1428 brace-expansion@2.0.2: 1352 1429 resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} ··· 1412 1489 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 1413 1490 engines: {node: '>= 8'} 1414 1491 1492 + css-tree@3.1.0: 1493 + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} 1494 + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 1495 + 1496 + cssstyle@6.1.0: 1497 + resolution: {integrity: sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==} 1498 + engines: {node: '>=20'} 1499 + 1415 1500 data-uri-to-buffer@4.0.1: 1416 1501 resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 1417 1502 engines: {node: '>= 12'} 1418 1503 1504 + data-urls@7.0.0: 1505 + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} 1506 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1507 + 1419 1508 debug@4.4.3: 1420 1509 resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 1421 1510 engines: {node: '>=6.0'} ··· 1424 1513 peerDependenciesMeta: 1425 1514 supports-color: 1426 1515 optional: true 1516 + 1517 + decimal.js@10.6.0: 1518 + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} 1427 1519 1428 1520 deep-eql@5.0.2: 1429 1521 resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} ··· 1536 1628 emoji-regex@8.0.0: 1537 1629 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 1538 1630 1631 + entities@6.0.1: 1632 + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 1633 + engines: {node: '>=0.12'} 1634 + 1539 1635 es-module-lexer@1.7.0: 1540 1636 resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 1541 1637 ··· 1627 1723 resolution: {integrity: sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==} 1628 1724 engines: {node: '>=16.9.0'} 1629 1725 1726 + html-encoding-sniffer@6.0.0: 1727 + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 1728 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1729 + 1730 + http-proxy-agent@7.0.2: 1731 + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} 1732 + engines: {node: '>= 14'} 1733 + 1734 + https-proxy-agent@7.0.6: 1735 + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} 1736 + engines: {node: '>= 14'} 1737 + 1630 1738 iconv-lite@0.7.2: 1631 1739 resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} 1632 1740 engines: {node: '>=0.10.0'} ··· 1641 1749 is-fullwidth-code-point@3.0.0: 1642 1750 resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 1643 1751 engines: {node: '>=8'} 1752 + 1753 + is-potential-custom-element-name@1.0.1: 1754 + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 1644 1755 1645 1756 isexe@2.0.0: 1646 1757 resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} ··· 1661 1772 js-tokens@9.0.1: 1662 1773 resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} 1663 1774 1775 + jsdom@28.1.0: 1776 + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} 1777 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1778 + peerDependencies: 1779 + canvas: ^3.0.0 1780 + peerDependenciesMeta: 1781 + canvas: 1782 + optional: true 1783 + 1664 1784 lefthook-darwin-arm64@1.13.6: 1665 1785 resolution: {integrity: sha512-m6Lb77VGc84/Qo21Lhq576pEvcgFCnvloEiP02HbAHcIXD0RTLy9u2yAInrixqZeaz13HYtdDaI7OBYAAdVt8A==} 1666 1786 cpu: [arm64] ··· 1730 1850 resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} 1731 1851 engines: {node: 20 || >=22} 1732 1852 1853 + lru-cache@11.2.6: 1854 + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} 1855 + engines: {node: 20 || >=22} 1856 + 1733 1857 magic-string@0.30.21: 1734 1858 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1859 + 1860 + mdn-data@2.12.2: 1861 + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} 1735 1862 1736 1863 minimatch@10.1.2: 1737 1864 resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} ··· 1787 1914 package-json-from-dist@1.0.1: 1788 1915 resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 1789 1916 1917 + parse5@8.0.0: 1918 + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} 1919 + 1790 1920 partysocket@1.1.11: 1791 1921 resolution: {integrity: sha512-P0EtOQiAwvLriqLgdThcSaREfz3bP77LkLSdmXq680BosPKvGSoGTh/d0g3S+UNmaqcw89Ad7JXHHKyRx3xU9Q==} 1792 1922 ··· 1848 1978 promise-limit@2.7.0: 1849 1979 resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} 1850 1980 1981 + punycode@2.3.1: 1982 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1983 + engines: {node: '>=6'} 1984 + 1851 1985 quick-format-unescaped@4.0.4: 1852 1986 resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} 1853 1987 ··· 1859 1993 resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} 1860 1994 engines: {node: '>= 12.13.0'} 1861 1995 1996 + require-from-string@2.0.2: 1997 + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1998 + engines: {node: '>=0.10.0'} 1999 + 1862 2000 resolve-pkg-maps@1.0.0: 1863 2001 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1864 2002 ··· 1877 2015 safer-buffer@2.1.2: 1878 2016 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1879 2017 2018 + saxes@6.0.0: 2019 + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 2020 + engines: {node: '>=v12.22.7'} 2021 + 1880 2022 shebang-command@2.0.0: 1881 2023 resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1882 2024 engines: {node: '>=8'} ··· 1934 2076 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1935 2077 engines: {node: '>=8'} 1936 2078 2079 + symbol-tree@3.2.4: 2080 + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 2081 + 1937 2082 thread-stream@2.7.0: 1938 2083 resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} 1939 2084 ··· 1974 2119 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} 1975 2120 hasBin: true 1976 2121 2122 + tldts-core@7.0.23: 2123 + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} 2124 + 2125 + tldts@7.0.23: 2126 + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} 2127 + hasBin: true 2128 + 2129 + tough-cookie@6.0.0: 2130 + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} 2131 + engines: {node: '>=16'} 2132 + 2133 + tr46@6.0.0: 2134 + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} 2135 + engines: {node: '>=20'} 2136 + 1977 2137 ts-morph@24.0.0: 1978 2138 resolution: {integrity: sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==} 1979 2139 ··· 2041 2201 undici@6.23.0: 2042 2202 resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} 2043 2203 engines: {node: '>=18.17'} 2204 + 2205 + undici@7.22.0: 2206 + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} 2207 + engines: {node: '>=20.18.1'} 2044 2208 2045 2209 unicode-segmenter@0.14.5: 2046 2210 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} ··· 2152 2316 jsdom: 2153 2317 optional: true 2154 2318 2319 + w3c-xmlserializer@5.0.0: 2320 + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 2321 + engines: {node: '>=18'} 2322 + 2155 2323 web-streams-polyfill@3.3.3: 2156 2324 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 2157 2325 engines: {node: '>= 8'} 2158 2326 2327 + webidl-conversions@8.0.1: 2328 + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} 2329 + engines: {node: '>=20'} 2330 + 2331 + whatwg-mimetype@5.0.0: 2332 + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} 2333 + engines: {node: '>=20'} 2334 + 2335 + whatwg-url@16.0.1: 2336 + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} 2337 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 2338 + 2159 2339 which@2.0.2: 2160 2340 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 2161 2341 engines: {node: '>= 8'} ··· 2182 2362 utf-8-validate: 2183 2363 optional: true 2184 2364 2365 + xml-name-validator@5.0.0: 2366 + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 2367 + engines: {node: '>=18'} 2368 + 2369 + xmlchars@2.2.0: 2370 + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 2371 + 2185 2372 yaml@2.8.2: 2186 2373 resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} 2187 2374 engines: {node: '>= 14.6'} ··· 2198 2385 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 2199 2386 2200 2387 snapshots: 2388 + 2389 + '@acemir/cssom@0.9.31': {} 2390 + 2391 + '@asamuzakjp/css-color@5.0.1': 2392 + dependencies: 2393 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 2394 + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 2395 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 2396 + '@csstools/css-tokenizer': 4.0.0 2397 + lru-cache: 11.2.6 2398 + 2399 + '@asamuzakjp/dom-selector@6.8.1': 2400 + dependencies: 2401 + '@asamuzakjp/nwsapi': 2.3.9 2402 + bidi-js: 1.0.3 2403 + css-tree: 3.1.0 2404 + is-potential-custom-element-name: 1.0.1 2405 + lru-cache: 11.2.6 2406 + 2407 + '@asamuzakjp/nwsapi@2.3.9': {} 2201 2408 2202 2409 '@atcute/atproto@3.1.10': 2203 2410 dependencies: ··· 2402 2609 '@atproto/lexicon': 0.6.1 2403 2610 zod: 3.25.76 2404 2611 2612 + '@bramus/specificity@2.4.2': 2613 + dependencies: 2614 + css-tree: 3.1.0 2615 + 2616 + '@csstools/color-helpers@6.0.2': {} 2617 + 2618 + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 2619 + dependencies: 2620 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 2621 + '@csstools/css-tokenizer': 4.0.0 2622 + 2623 + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 2624 + dependencies: 2625 + '@csstools/color-helpers': 6.0.2 2626 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 2627 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 2628 + '@csstools/css-tokenizer': 4.0.0 2629 + 2630 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': 2631 + dependencies: 2632 + '@csstools/css-tokenizer': 4.0.0 2633 + 2634 + '@csstools/css-syntax-patches-for-csstree@1.0.28': {} 2635 + 2636 + '@csstools/css-tokenizer@4.0.0': {} 2637 + 2405 2638 '@drizzle-team/brocli@0.10.2': {} 2406 2639 2407 2640 '@esbuild-kit/core-utils@3.3.2': ··· 2635 2868 2636 2869 '@esbuild/win32-x64@0.27.3': 2637 2870 optional: true 2871 + 2872 + '@exodus/bytes@1.14.1': {} 2638 2873 2639 2874 '@hono/node-server@1.19.9(hono@4.11.8)': 2640 2875 dependencies: ··· 3087 3322 dependencies: 3088 3323 event-target-shim: 5.0.1 3089 3324 3325 + agent-base@7.1.4: {} 3326 + 3090 3327 ansi-regex@5.0.1: {} 3091 3328 3092 3329 ansi-styles@4.3.0: ··· 3099 3336 3100 3337 await-lock@2.2.2: {} 3101 3338 3339 + axe-core@4.11.1: {} 3340 + 3102 3341 balanced-match@1.0.2: {} 3103 3342 3104 3343 base64-js@1.5.1: {} 3344 + 3345 + bidi-js@1.0.3: 3346 + dependencies: 3347 + require-from-string: 2.0.2 3105 3348 3106 3349 brace-expansion@2.0.2: 3107 3350 dependencies: ··· 3160 3403 path-key: 3.1.1 3161 3404 shebang-command: 2.0.0 3162 3405 which: 2.0.2 3406 + 3407 + css-tree@3.1.0: 3408 + dependencies: 3409 + mdn-data: 2.12.2 3410 + source-map-js: 1.2.1 3411 + 3412 + cssstyle@6.1.0: 3413 + dependencies: 3414 + '@asamuzakjp/css-color': 5.0.1 3415 + '@csstools/css-syntax-patches-for-csstree': 1.0.28 3416 + css-tree: 3.1.0 3417 + lru-cache: 11.2.6 3163 3418 3164 3419 data-uri-to-buffer@4.0.1: {} 3165 3420 3421 + data-urls@7.0.0: 3422 + dependencies: 3423 + whatwg-mimetype: 5.0.0 3424 + whatwg-url: 16.0.1 3425 + transitivePeerDependencies: 3426 + - '@noble/hashes' 3427 + 3166 3428 debug@4.4.3: 3167 3429 dependencies: 3168 3430 ms: 2.1.3 3431 + 3432 + decimal.js@10.6.0: {} 3169 3433 3170 3434 deep-eql@5.0.2: {} 3171 3435 ··· 3189 3453 postgres: 3.4.8 3190 3454 3191 3455 emoji-regex@8.0.0: {} 3456 + 3457 + entities@6.0.1: {} 3192 3458 3193 3459 es-module-lexer@1.7.0: {} 3194 3460 ··· 3336 3602 3337 3603 hono@4.11.8: {} 3338 3604 3605 + html-encoding-sniffer@6.0.0: 3606 + dependencies: 3607 + '@exodus/bytes': 1.14.1 3608 + transitivePeerDependencies: 3609 + - '@noble/hashes' 3610 + 3611 + http-proxy-agent@7.0.2: 3612 + dependencies: 3613 + agent-base: 7.1.4 3614 + debug: 4.4.3 3615 + transitivePeerDependencies: 3616 + - supports-color 3617 + 3618 + https-proxy-agent@7.0.6: 3619 + dependencies: 3620 + agent-base: 7.1.4 3621 + debug: 4.4.3 3622 + transitivePeerDependencies: 3623 + - supports-color 3624 + 3339 3625 iconv-lite@0.7.2: 3340 3626 dependencies: 3341 3627 safer-buffer: 2.1.2 ··· 3345 3631 ipaddr.js@2.3.0: {} 3346 3632 3347 3633 is-fullwidth-code-point@3.0.0: {} 3634 + 3635 + is-potential-custom-element-name@1.0.1: {} 3348 3636 3349 3637 isexe@2.0.0: {} 3350 3638 ··· 3360 3648 3361 3649 js-tokens@9.0.1: {} 3362 3650 3651 + jsdom@28.1.0: 3652 + dependencies: 3653 + '@acemir/cssom': 0.9.31 3654 + '@asamuzakjp/dom-selector': 6.8.1 3655 + '@bramus/specificity': 2.4.2 3656 + '@exodus/bytes': 1.14.1 3657 + cssstyle: 6.1.0 3658 + data-urls: 7.0.0 3659 + decimal.js: 10.6.0 3660 + html-encoding-sniffer: 6.0.0 3661 + http-proxy-agent: 7.0.2 3662 + https-proxy-agent: 7.0.6 3663 + is-potential-custom-element-name: 1.0.1 3664 + parse5: 8.0.0 3665 + saxes: 6.0.0 3666 + symbol-tree: 3.2.4 3667 + tough-cookie: 6.0.0 3668 + undici: 7.22.0 3669 + w3c-xmlserializer: 5.0.0 3670 + webidl-conversions: 8.0.1 3671 + whatwg-mimetype: 5.0.0 3672 + whatwg-url: 16.0.1 3673 + xml-name-validator: 5.0.0 3674 + transitivePeerDependencies: 3675 + - '@noble/hashes' 3676 + - supports-color 3677 + 3363 3678 lefthook-darwin-arm64@1.13.6: 3364 3679 optional: true 3365 3680 ··· 3422 3737 3423 3738 lru-cache@11.2.5: {} 3424 3739 3740 + lru-cache@11.2.6: {} 3741 + 3425 3742 magic-string@0.30.21: 3426 3743 dependencies: 3427 3744 '@jridgewell/sourcemap-codec': 1.5.5 3745 + 3746 + mdn-data@2.12.2: {} 3428 3747 3429 3748 minimatch@10.1.2: 3430 3749 dependencies: ··· 3471 3790 3472 3791 package-json-from-dist@1.0.1: {} 3473 3792 3793 + parse5@8.0.0: 3794 + dependencies: 3795 + entities: 6.0.1 3796 + 3474 3797 partysocket@1.1.11: 3475 3798 dependencies: 3476 3799 event-target-polyfill: 0.0.4 ··· 3529 3852 3530 3853 promise-limit@2.7.0: {} 3531 3854 3855 + punycode@2.3.1: {} 3856 + 3532 3857 quick-format-unescaped@4.0.4: {} 3533 3858 3534 3859 readable-stream@4.7.0: ··· 3540 3865 string_decoder: 1.3.0 3541 3866 3542 3867 real-require@0.2.0: {} 3868 + 3869 + require-from-string@2.0.2: {} 3543 3870 3544 3871 resolve-pkg-maps@1.0.0: {} 3545 3872 ··· 3580 3907 3581 3908 safer-buffer@2.1.2: {} 3582 3909 3910 + saxes@6.0.0: 3911 + dependencies: 3912 + xmlchars: 2.2.0 3913 + 3583 3914 shebang-command@2.0.0: 3584 3915 dependencies: 3585 3916 shebang-regex: 3.0.0 ··· 3631 3962 dependencies: 3632 3963 has-flag: 4.0.0 3633 3964 3965 + symbol-tree@3.2.4: {} 3966 + 3634 3967 thread-stream@2.7.0: 3635 3968 dependencies: 3636 3969 real-require: 0.2.0 ··· 3658 3991 3659 3992 tlds@1.261.0: {} 3660 3993 3994 + tldts-core@7.0.23: {} 3995 + 3996 + tldts@7.0.23: 3997 + dependencies: 3998 + tldts-core: 7.0.23 3999 + 4000 + tough-cookie@6.0.0: 4001 + dependencies: 4002 + tldts: 7.0.23 4003 + 4004 + tr46@6.0.0: 4005 + dependencies: 4006 + punycode: 2.3.1 4007 + 3661 4008 ts-morph@24.0.0: 3662 4009 dependencies: 3663 4010 '@ts-morph/common': 0.25.0 ··· 3715 4062 3716 4063 undici@6.23.0: {} 3717 4064 4065 + undici@7.22.0: {} 4066 + 3718 4067 unicode-segmenter@0.14.5: {} 3719 4068 3720 4069 vite-node@3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): ··· 3752 4101 tsx: 4.21.0 3753 4102 yaml: 2.8.2 3754 4103 3755 - vitest@3.2.4(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 4104 + vitest@3.2.4(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2): 3756 4105 dependencies: 3757 4106 '@types/chai': 5.2.3 3758 4107 '@vitest/expect': 3.2.4 ··· 3779 4128 why-is-node-running: 2.3.0 3780 4129 optionalDependencies: 3781 4130 '@types/node': 22.19.9 4131 + jsdom: 28.1.0 3782 4132 transitivePeerDependencies: 3783 4133 - jiti 3784 4134 - less ··· 3793 4143 - tsx 3794 4144 - yaml 3795 4145 3796 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 4146 + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.9)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2): 3797 4147 dependencies: 3798 4148 '@vitest/expect': 4.0.18 3799 4149 '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2)) ··· 3818 4168 optionalDependencies: 3819 4169 '@opentelemetry/api': 1.9.0 3820 4170 '@types/node': 22.19.9 4171 + jsdom: 28.1.0 3821 4172 transitivePeerDependencies: 3822 4173 - jiti 3823 4174 - less ··· 3831 4182 - tsx 3832 4183 - yaml 3833 4184 4185 + w3c-xmlserializer@5.0.0: 4186 + dependencies: 4187 + xml-name-validator: 5.0.0 4188 + 3834 4189 web-streams-polyfill@3.3.3: {} 3835 4190 4191 + webidl-conversions@8.0.1: {} 4192 + 4193 + whatwg-mimetype@5.0.0: {} 4194 + 4195 + whatwg-url@16.0.1: 4196 + dependencies: 4197 + '@exodus/bytes': 1.14.1 4198 + tr46: 6.0.0 4199 + webidl-conversions: 8.0.1 4200 + transitivePeerDependencies: 4201 + - '@noble/hashes' 4202 + 3836 4203 which@2.0.2: 3837 4204 dependencies: 3838 4205 isexe: 2.0.0 ··· 3849 4216 strip-ansi: 6.0.1 3850 4217 3851 4218 ws@8.19.0: {} 4219 + 4220 + xml-name-validator@5.0.0: {} 4221 + 4222 + xmlchars@2.2.0: {} 3852 4223 3853 4224 yaml@2.8.2: {} 3854 4225