syncova-policies/tests/e2e/portal.spec.ts
Weapie 265ca8ac38
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 5m15s
Improve accessibility and access controls; verify production and database recovery
2026-09-14 11:44:41 +02:00

159 lines
9.4 KiB
TypeScript

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)
})