From 0a2657293352335de7b04b44b08bd55cdc04d0b9 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Tue, 21 Jul 2026 15:24:56 +0200 Subject: [PATCH] =?UTF-8?q?#171:=20A11y-Tooling=20=E2=80=94=20axe-Smoke-Pa?= =?UTF-8?q?ck=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @axe-core/playwright als devDependency (exakt +2 Lockfile-Pakete, axe-core hat null Runtime-Dependencies; Freigabe durch Stefan im Chat). Neuer e2e-Pack a11y.spec.ts scannt Login, Lesemodus, aktiven Editor und Nutzer-Einstellungen gegen WCAG 2.1 A/AA — jede neue Verletzung bricht den Build (Allowlist bewusst leer, nur mit Begründung erweiterbar); Best-Practice-Regeln bleiben außen vor. In ci.yml als eigener Schritt mit Rate-Limit-Reset nach dem Muster der übrigen Packs verdrahtet. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq --- .gitea/workflows/ci.yml | 12 ++++++++ apps/web/e2e/a11y.spec.ts | 62 +++++++++++++++++++++++++++++++++++++++ apps/web/package.json | 1 + pnpm-lock.yaml | 19 ++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 apps/web/e2e/a11y.spec.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4d04484..47d93a3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -521,6 +521,18 @@ jobs: sleep 2 done + # Two logins per run → reset first (see note above). + - name: Reset login rate limit before a11y pack + run: | + echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \ + pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL" + + # WCAG-A/AA-Regressionsschutz (issue #171): axe-Scan der Kernscreens. + - name: Run a11y pack + run: | + E2E_BASE_URL=http://localhost:5173 \ + pnpm --filter @dorfteich/web exec playwright test e2e/a11y.spec.ts + - name: Run setup wizard pack run: | E2E_BASE_URL=http://localhost:5175 E2E_SETUP=1 \ diff --git a/apps/web/e2e/a11y.spec.ts b/apps/web/e2e/a11y.spec.ts new file mode 100644 index 0000000..de33d80 --- /dev/null +++ b/apps/web/e2e/a11y.spec.ts @@ -0,0 +1,62 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test, type Page } from '@playwright/test'; + +import { contextForUser } from './helpers'; + +/** + * A11y-Smoke-Pack (issue #171): axe-core-Scan der Kern-Oberflächen gegen + * WCAG 2.1 A/AA. Regressionsschutz für das Audit vom 21.07.2026 (Bericht im + * Workspace, Befunde A11Y-001…024) — die Screens hier waren nach den Fixes + * der Issues #162–#170 verletzungsfrei; jede neue Verletzung bricht den + * Build. Best-Practice-Regeln (axe-Tag best-practice) prüfen wir hier + * bewusst NICHT, nur normative WCAG-Kriterien. + */ + +const BASE = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; +const TAGS = ['wcag2a', 'wcag21a', 'wcag2aa', 'wcag21aa']; + +/** Bewusst tolerierte Regel-IDs — nur mit Begründung ergänzen. */ +const ALLOWED_RULES: string[] = []; + +async function expectClean(page: Page, label: string): Promise { + const results = await new AxeBuilder({ page }).withTags(TAGS).analyze(); + const violations = results.violations.filter((v) => !ALLOWED_RULES.includes(v.id)); + expect( + violations.map((v) => ({ + rule: v.id, + impact: v.impact, + help: v.help, + nodes: v.nodes.slice(0, 5).map((n) => n.target), + })), + `axe-Verletzungen auf ${label}`, + ).toEqual([]); +} + +test('login page passes the axe WCAG A/AA scan', async ({ page }) => { + await page.goto('/login'); + await page.waitForLoadState('networkidle'); + await expectClean(page, '/login'); +}); + +test('reading and editing a page passes the axe WCAG A/AA scan', async ({ browser }) => { + const context = await contextForUser(browser, BASE, 'fixture-user'); + const page = await context.newPage(); + await page.goto('/p/content-fixtures/every-element'); + await page.waitForLoadState('networkidle'); + await expectClean(page, 'Lesemodus every-element'); + + await page.locator('.editor-page__mode-toggle').click(); + await page.locator('.ProseMirror[contenteditable="true"]').waitFor({ timeout: 10_000 }); + await page.waitForTimeout(500); + await expectClean(page, 'Editor every-element'); + await context.close(); +}); + +test('user settings pass the axe WCAG A/AA scan', async ({ browser }) => { + const context = await contextForUser(browser, BASE, 'fixture-user'); + const page = await context.newPage(); + await page.goto('/settings'); + await page.waitForLoadState('networkidle'); + await expectClean(page, '/settings'); + await context.close(); +}); diff --git a/apps/web/package.json b/apps/web/package.json index e9854eb..86a3143 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@axe-core/playwright": "^4.12.1", "@playwright/test": "^1.61.1", "@types/d3-force": "^3.0.10", "@types/react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a7c7e2..51a08d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,6 +336,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@axe-core/playwright': + specifier: ^4.12.1 + version: 4.12.1(playwright-core@1.61.1) '@playwright/test': specifier: ^1.61.1 version: 1.61.1 @@ -630,6 +633,11 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -3508,6 +3516,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: @@ -7022,6 +7034,11 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@axe-core/playwright@4.12.1(playwright-core@1.61.1)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.61.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -9855,6 +9872,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axe-core@4.12.1: {} + b4a@1.8.1: {} babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7):