#171: A11y-Tooling — axe-Smoke-Pack in CI
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m38s
CI / Build container images (pull_request) Successful in 4m0s
CI / Auth e2e pack (pull_request) Successful in 7m31s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m32s
CI / Import/export fidelity gate (push) Successful in 55s

@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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
This commit is contained in:
Claude Fable 5 2026-07-21 15:24:56 +02:00
parent 31b59f0fb6
commit 0a26572933
4 changed files with 94 additions and 0 deletions

View File

@ -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 \

62
apps/web/e2e/a11y.spec.ts Normal file
View File

@ -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-001024) 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<void> {
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();
});

View File

@ -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",

19
pnpm-lock.yaml generated
View File

@ -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):