import { test, expect, type Page } from '@playwright/test' import AxeBuilder from '@axe-core/playwright' async function login(page: Page, email='super@example.org') { await page.goto('http://127.0.0.1:3219/auth/signin') await page.getByLabel('E-Mail-Adresse',{exact:true}).fill(email) await page.getByLabel('Passwort',{exact:true}).fill('Test-password-only-2026') await page.getByRole('button',{name:'Anmelden',exact:true}).click() await expect(page).toHaveURL(/\/admin$/) } test('public search announces zero results and reset restores focus',async({page})=>{ await page.goto('/') const search=page.getByRole('searchbox') await search.fill('zzzznonexistent') await expect(page.getByRole('status')).toContainText('0 Erklärungen') await page.getByRole('button',{name:'Suche zurücksetzen'}).first().click() await expect(search).toBeFocused() await expect(page.getByRole('link',{name:/Bauantrag/})).toBeVisible() for(const width of [320,1280]){ await page.setViewportSize({width,height:800}) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) const results=await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze() expect(results.violations).toEqual([]) } }) test('login errors are announced and password button changes its name',async({page})=>{ await page.goto('/auth/signin') await page.getByRole('button',{name:'Passwort anzeigen'}).click() await expect(page.getByRole('button',{name:'Passwort verbergen'})).toBeVisible() await page.getByLabel('E-Mail-Adresse',{exact:true}).fill('missing@example.org') await page.getByLabel('Passwort',{exact:true}).fill('wrong-password') await page.getByRole('button',{name:'Anmelden',exact:true}).click() await expect(page.getByRole('alert').filter({hasText:'Ungültige Anmeldedaten'})).toBeVisible() }) test('dialogs close without blocking interaction; required pages cannot be cleared',async({page})=>{ await login(page) for(const name of ['Rechtliche Seiten','Standardtexte pflegen','Benutzer verwalten']){ await page.getByRole('button',{name:'Benutzermenü'}).click() await page.getByRole('menuitem',{name}).click() await expect(page.getByRole('dialog')).toBeVisible() await page.keyboard.press('Escape') await expect(page.getByRole('dialog')).toHaveCount(0) await expect(page.getByRole('button',{name:'Benutzermenü'})).toBeFocused() } const previous=await (await page.request.get('/api/site-pages')).json() expect((await page.request.put('/api/site-pages',{data:{...previous,accessibility:''}})).status()).toBe(400) expect((await page.request.get('/api/site-pages')).ok()).toBe(true) const users=await (await page.request.get('/api/users')).json() const self=users.find((user: {id:number;email:string})=>user.email==='super@example.org') expect((await page.request.put('/api/users/'+self.id,{data:{isActive:false}})).status()).toBe(400) }) test('editor cannot promote itself and an existing session is revoked after suspension',async({browser})=>{ const adminContext=await browser.newContext() const editorContext=await browser.newContext() const admin=await adminContext.newPage(), editor=await editorContext.newPage() await login(admin) await login(editor,'editor@example.org') expect((await editor.request.post('/api/users',{data:{email:'intruder@example.org',password:'Test-password-only-2026',role:'SUPER_ADMIN'}})).ok()).toBe(false) const users=await (await admin.request.get('/api/users')).json() const user=users.find((user: {id:number;email:string})=>user.email==='editor@example.org') expect((await admin.request.put('/api/users/'+user.id,{data:{isActive:false}})).ok()).toBe(true) expect((await editor.request.put('/api/site-pages',{data:{}})).status()).toBe(401) await editor.goto('/admin') await expect(editor).toHaveURL(/\/auth\/signin/) await adminContext.close(); await editorContext.close() }) test('public information and admin forms remain accessible at narrow widths',async({page})=>{ await page.setViewportSize({width:320,height:640}) for(const route of ['/auth/signin','/erklaerung/test-bauantrag','/barrierefreiheit','/leichte-sprache','/gebaerdensprache']){ await page.goto(route) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) await page.waitForFunction(() => document.getAnimations().every(a => a.playState !== 'running' || a.effect?.getTiming().iterations === Infinity)) expect((await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze()).violations).toEqual([]) } await login(page) for(const name of ['Rechtliche Seiten','Standardtexte pflegen','Benutzer verwalten']){ await page.getByRole('button',{name:'Benutzermenü'}).click() await page.getByRole('menuitem',{name}).click() await expect(page.getByRole('dialog')).toBeVisible() await page.waitForFunction(() => document.getAnimations().every(a => a.playState !== 'running' || a.effect?.getTiming().iterations === Infinity)) expect((await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze()).violations).toEqual([]) await page.keyboard.press('Escape') } await page.getByRole('button',{name:'Neu',exact:true}).click() await expect(page.getByRole('dialog')).toBeVisible() await page.getByRole('button',{name:/Erklärung erstellen|Speichern|Erstellen/}).last().click() await expect(page.getByRole('region',{name:'Fehler beim Speichern'})).toBeFocused() await page.waitForFunction(() => document.getAnimations().every(a => a.playState !== 'running' || a.effect?.getTiming().iterations === Infinity)) expect((await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze()).violations).toEqual([]) await page.getByRole('button',{name:'Zum ersten fehlerhaften Feld'}).click() const title=page.getByRole('textbox',{name:/Titel der Datenschutzerklärung/}) await expect(title).toBeFocused() await title.fill('Test der erreichbaren Eingabe') expect((await title.boundingBox())!.height).toBeGreaterThan(20) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) await page.screenshot({path:'test-results/admin-mobile.png',fullPage:true}) await page.keyboard.press('Escape') }) test('dark mode and nested user dialogs remain accessible',async({page})=>{ await page.addInitScript(()=>localStorage.setItem('theme','dark')) await page.setViewportSize({width:320,height:640}) for(const route of ['/','/barrierefreiheit','/auth/signin']){ await page.goto(route) await expect(page.locator('html')).toHaveClass(/dark/) await page.waitForFunction(()=>document.getAnimations().every(a=>a.playState!=='running'||a.effect?.getTiming().iterations===Infinity)) expect((await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze()).violations).toEqual([]) } await login(page) await page.getByRole('button',{name:'Benutzermenü'}).click() await page.getByRole('menuitem',{name:'Benutzer verwalten'}).click() await page.getByRole('button',{name:'Neuer Benutzer',exact:true}).click() const dialog=page.getByRole('dialog',{name:'Neuer Benutzer',exact:true}) await expect(dialog).toBeVisible() await page.waitForFunction(()=>document.getAnimations().every(a=>a.playState!=='running'||a.effect?.getTiming().iterations===Infinity)) expect(await dialog.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true) expect((await new AxeBuilder({page}).withTags(['wcag2a','wcag2aa','wcag21aa']).analyze()).violations).toEqual([]) await page.keyboard.press('Escape') await expect(page.getByRole('button',{name:'Neuer Benutzer',exact:true})).toBeFocused() await page.keyboard.press('Escape') await expect(page.getByRole('button',{name:'Benutzermenü'})).toBeFocused() }) test('keyboard navigation, text resizing and spacing preserve access', async ({page}) => { await page.goto('/') await page.keyboard.press('Tab') await expect(page.getByRole('link',{name:'Zum Inhalt springen'})).toBeFocused() await page.keyboard.press('Enter') await expect(page.locator('main')).toBeFocused() await page.setViewportSize({width:1280,height:800}) await page.addStyleTag({content:'html { font-size:200% !important; } * { line-height:1.5 !important; letter-spacing:.12em !important; word-spacing:.16em !important; } p { margin-bottom:2em !important; }'}) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) const search=page.getByRole('searchbox') await search.fill('Bauantrag') await expect(page.getByRole('link',{name:/Bauantrag/})).toBeVisible() await page.getByRole('link',{name:/Bauantrag/}).click() await expect(page.getByRole('heading',{name:'Bauantrag',exact:true})).toBeVisible() await login(page) await page.getByRole('button',{name:'Neu',exact:true}).focus() await page.keyboard.press('Enter') const dialog=page.getByRole('dialog',{name:'Neue Datenschutzerklärung',exact:true}) await expect(dialog).toBeVisible() for(let step=0;step<16;step++) { await page.keyboard.press(step%3===0?'Shift+Tab':'Tab') expect(await dialog.evaluate(el=>el.contains(document.activeElement))).toBe(true) } await page.keyboard.press('Escape') await expect(page.getByRole('button',{name:'Neu',exact:true})).toBeFocused() }) test('policy APIs reject malformed data without server errors',async({page})=>{ await login(page) for(const data of [null,[],{title:123},{recipients:{}},{isPublic:'false'}]){ expect((await page.request.post('/api/privacy-policies',{data})).status()).toBe(400) } expect((await page.request.post('/api/privacy-policies',{data:'{invalid',headers:{'Content-Type':'application/json'}})).status()).toBe(400) })