diff --git a/frontend/e2e/accessibility.spec.ts b/frontend/e2e/accessibility.spec.ts new file mode 100644 index 00000000..5e4b1f2d --- /dev/null +++ b/frontend/e2e/accessibility.spec.ts @@ -0,0 +1,58 @@ +import AxeBuilder from '@axe-core/playwright'; +import { test, expect } from '@playwright/test'; + +/** + * Automated accessibility checks using axe-core (wcag2a + wcag2aa). + * Authenticated pages use the stored admin session from playwright.config.ts. + * The login page test overrides to an unauthenticated context. + * + * On first run: if violations are found they are logged with full details so + * that they can be either fixed or explicitly excluded here with a comment + * explaining the reason. + */ + +const AUTHENTICATED_PAGES = [ + { name: 'home', path: '/' }, + { name: 'persons', path: '/persons' }, + { name: 'admin', path: '/admin' } +]; + +test.describe('Accessibility — authenticated pages', () => { + for (const { name, path } of AUTHENTICATED_PAGES) { + test(`${name} page has no critical wcag2a/wcag2aa violations`, async ({ page }) => { + await page.goto(path); + await page.waitForSelector('[data-hydrated]'); + + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + + if (results.violations.length > 0) { + const summary = results.violations + .map((v) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} node(s))`) + .join('\n'); + console.log(`\nAccessibility violations on ${name}:\n${summary}`); + } + + expect(results.violations).toEqual([]); + }); + } +}); + +test.describe('Accessibility — login page', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test('login page has no critical wcag2a/wcag2aa violations', async ({ page }) => { + await page.goto('/login'); + await expect(page.getByLabel('Benutzername')).toBeVisible(); + + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + + if (results.violations.length > 0) { + const summary = results.violations + .map((v) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} node(s))`) + .join('\n'); + console.log(`\nAccessibility violations on login:\n${summary}`); + } + + expect(results.violations).toEqual([]); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b158cb8f..01031480 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "pdfjs-dist": "^5.5.207" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", "@eslint/compat": "^1.4.0", "@eslint/js": "^9.39.1", "@inlang/paraglide-js": "^2.5.0", @@ -47,6 +48,19 @@ "vitest-browser-svelte": "^2.0.1" } }, + "node_modules/@axe-core/playwright": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz", + "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -2866,6 +2880,16 @@ "dev": true, "license": "MIT" }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0592ca40..e7170513 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "pdfjs-dist": "^5.5.207" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", "@eslint/compat": "^1.4.0", "@eslint/js": "^9.39.1", "@inlang/paraglide-js": "^2.5.0",