test: Benachrichtigungen end-to-end und in der Datenbank absichern

Die Einstellung haelt ueber das Neuladen, ohne eingeschalteten Weg geht
nichts hinaus, und ein unsinniger Vorlauf wird auch dann abgewiesen, wenn
er an der Oberflaeche vorbei hereinkommt.

Der Anmeldehelfer liegt jetzt einmal in e2e/hilfe.ts und fasst nach: ein
Klick, der vor der Hydratation ankommt, verpufft folgenlos — das hat die
Testlaeufe unter Last wiederholt scheitern lassen.

Der Integrationstest prueft die drei neuen Tabellen samt Kaskade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 10:28:32 +02:00
co-authored by Claude Opus 5
parent f03ea73dcd
commit 6e1f2791cd
6 changed files with 206 additions and 40 deletions
+4 -14
View File
@@ -1,6 +1,7 @@
import { PrismaPg } from '@prisma/adapter-pg'
import { expect, test, type Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { PrismaClient } from '../src/generated/prisma/client.js'
import { anmelden, EMAIL, PASSWORT, VERBINDUNG } from './hilfe'
/**
* Der Weg, der in M2 wirklich zählt: erfassen, einsortieren, abhaken.
@@ -9,16 +10,13 @@ import { PrismaClient } from '../src/generated/prisma/client.js'
* damit die Entwicklungsdatenbank hinterher aussieht wie vorher.
*/
const EMAIL = process.env.ADMIN_EMAIL ?? ''
const PASSWORT = process.env.ADMIN_PASSWORT ?? ''
const PRAEFIX = `E2E-${Date.now()}`
const verbindung = process.env.DATABASE_URL ?? ''
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: VERBINDUNG }) })
test.describe('Aufgaben', () => {
test.skip(
!EMAIL || !PASSWORT || !verbindung,
!EMAIL || !PASSWORT || !VERBINDUNG,
'Ohne ADMIN_EMAIL, ADMIN_PASSWORT und DATABASE_URL gibt es kein Testkonto.',
)
@@ -29,14 +27,6 @@ test.describe('Aufgaben', () => {
await db.$disconnect()
})
async function anmelden(seite: Page): Promise<void> {
await seite.goto('/anmelden')
await seite.getByLabel('E-Mail-Adresse').fill(EMAIL)
await seite.getByLabel('Passwort').fill(PASSWORT)
await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
await expect(seite.getByRole('heading', { name: /Guten Tag/ })).toBeVisible()
}
test('erfasst schnell, sortiert ein und hakt ab', async ({ page }) => {
const titel = `${PRAEFIX} Angebot schreiben`
await anmelden(page)
+91
View File
@@ -0,0 +1,91 @@
import { PrismaPg } from '@prisma/adapter-pg'
import { expect, test } from '@playwright/test'
import { PrismaClient } from '../src/generated/prisma/client.js'
import { anmelden, EMAIL, PASSWORT, VERBINDUNG } from './hilfe'
/**
* Was in M5 wirklich zählt: die Einstellung hält, und ohne eingeschalteten
* Weg geht nichts hinaus.
*
* Der Test rührt nur an der Einstellung des Testkontos und stellt sie am
* Ende wieder her.
*/
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: VERBINDUNG }) })
test.describe('Benachrichtigungen', () => {
test.skip(
!EMAIL || !PASSWORT || !VERBINDUNG,
'Ohne ADMIN_EMAIL, ADMIN_PASSWORT und DATABASE_URL gibt es kein Testkonto.',
)
test.afterAll(async () => {
const benutzer = await db.benutzer.findUnique({
where: { email: EMAIL },
select: { id: true },
})
if (benutzer) {
await db.benachrichtigungseinstellung.deleteMany({
where: { benutzerId: benutzer.id },
})
await db.meldungsnachweis.deleteMany({ where: { benutzerId: benutzer.id } })
}
await db.auditEintrag.deleteMany({
where: { aktion: { startsWith: 'benachrichtigung.' } },
})
await db.$disconnect()
})
test('speichert die Einstellung und hält sie', async ({ page }) => {
await anmelden(page)
await page.goto('/einstellungen')
const karte = page.locator('[data-bild="benachrichtigungen"]')
await expect(karte).toBeVisible()
// --- Ab Werk ist nichts eingeschaltet ---------------------------------
await expect(karte.locator('input[name="perPush"]')).not.toBeChecked()
await expect(karte.locator('input[name="perMail"]')).not.toBeChecked()
// --- Ohne Weg keine Probe ---------------------------------------------
await karte.getByRole('button', { name: 'Probe schicken' }).click()
await expect(page.getByText('Erst einen Weg einschalten')).toBeVisible()
// --- Ändern und speichern ---------------------------------------------
await karte.locator('input[name="perMail"]').check()
await karte.locator('input[value="zeitblock"]').check()
await karte.getByLabel('Tagesüberblick um').fill('06:30')
await karte.getByLabel('Vorlauf vor einem Zeitblock (Minuten)').fill('20')
await karte.getByLabel('Am Wochenende gar nichts melden').check()
await karte.getByRole('button', { name: 'Speichern' }).click()
await expect(page.getByText('Einstellungen gespeichert.')).toBeVisible()
// --- Nach dem Neuladen steht alles noch da ----------------------------
await page.reload()
await expect(karte.locator('input[name="perMail"]')).toBeChecked()
await expect(karte.locator('input[value="zeitblock"]')).toBeChecked()
await expect(karte.getByLabel('Tagesüberblick um')).toHaveValue('06:30')
await expect(karte.getByLabel('Vorlauf vor einem Zeitblock (Minuten)')).toHaveValue(
'20',
)
await expect(karte.getByLabel('Am Wochenende gar nichts melden')).toBeChecked()
})
test('weist eine unsinnige Vorlaufzeit zurück', async ({ page }) => {
await anmelden(page)
await page.goto('/einstellungen')
const karte = page.locator('[data-bild="benachrichtigungen"]')
const vorlauf = karte.getByLabel('Vorlauf vor einem Zeitblock (Minuten)')
// Das Zahlenfeld begrenzt auf 120; der Server darf sich darauf nicht
// verlassen, deshalb kommt der Wert an der Oberfläche vorbei.
await vorlauf.evaluate((feld: HTMLInputElement) => {
feld.max = '600'
feld.value = '300'
})
await karte.getByRole('button', { name: 'Speichern' }).click()
await expect(page.getByText('Bitte die markierten Felder prüfen.')).toBeVisible()
await expect(karte.getByText('Mehr als zwei Stunden Vorlauf')).toBeVisible()
})
})
+35
View File
@@ -0,0 +1,35 @@
import { expect, type Page } from '@playwright/test'
/** Gemeinsames für alle E2E-Tests. */
export const EMAIL = process.env.ADMIN_EMAIL ?? ''
export const PASSWORT = process.env.ADMIN_PASSWORT ?? ''
export const VERBINDUNG = process.env.DATABASE_URL ?? ''
/**
* Meldet das Testkonto mit Passwort an.
*
* Der Knopf gehört zu einem Client-Formular. Ein Klick, der vor der
* Hydratation ankommt, verpufft folgenlos — die Seite bleibt einfach stehen.
* Deshalb wird so lange nachgefasst, bis die Startseite da ist; hat der erste
* Klick schon gewirkt, wird nicht noch einmal geklickt.
*/
export async function anmelden(
seite: Page,
email: string = EMAIL,
passwort: string = PASSWORT,
): Promise<void> {
await seite.goto('/anmelden')
await seite.getByLabel('E-Mail-Adresse').fill(email)
await seite.getByLabel('Passwort').fill(passwort)
const knopf = seite.getByRole('button', { name: 'Mit Passwort anmelden' })
const begruessung = seite.getByRole('heading', { name: /Guten Tag/ })
await expect(async () => {
if (new URL(seite.url()).pathname === '/anmelden') {
await knopf.click({ timeout: 5_000 })
}
await expect(begruessung).toBeVisible({ timeout: 5_000 })
}).toPass({ timeout: 45_000 })
}
+5 -12
View File
@@ -1,7 +1,8 @@
import { PrismaPg } from '@prisma/adapter-pg'
import { expect, test, type Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import bcrypt from 'bcryptjs'
import { PrismaClient } from '../src/generated/prisma/client.js'
import { anmelden as anmeldenMit } from './hilfe'
/**
* Prüft, dass die Rollentrennung serverseitig hält.
@@ -38,23 +39,15 @@ test.describe('Rollentrennung', () => {
await db.$disconnect()
})
async function anmelden(seite: Page): Promise<void> {
await seite.goto('/anmelden')
await seite.getByLabel('E-Mail-Adresse').fill(EMAIL)
await seite.getByLabel('Passwort').fill(PASSWORT)
await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
await expect(seite.getByRole('heading', { name: /Guten Tag/ })).toBeVisible()
}
test('zeigt einem Benutzer den Menüpunkt Protokoll gar nicht', async ({ page }) => {
await anmelden(page)
await anmeldenMit(page, EMAIL, PASSWORT)
await expect(page.getByRole('link', { name: 'Protokoll' })).toHaveCount(0)
})
test('lässt einen Benutzer auch beim direkten Aufruf nicht ins Protokoll', async ({
page,
}) => {
await anmelden(page)
await anmeldenMit(page, EMAIL, PASSWORT)
await page.goto('/verwaltung/protokoll')
await expect(page).toHaveURL(/\/keine-berechtigung/)
@@ -64,7 +57,7 @@ test.describe('Rollentrennung', () => {
})
test('zeigt einem Benutzer die Bereiche, aber ohne Formulare', async ({ page }) => {
await anmelden(page)
await anmeldenMit(page, EMAIL, PASSWORT)
await page.goto('/bereiche')
await expect(page.getByRole('heading', { name: 'Bereiche' })).toBeVisible()
+4 -14
View File
@@ -1,6 +1,7 @@
import { PrismaPg } from '@prisma/adapter-pg'
import { expect, test, type Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { PrismaClient } from '../src/generated/prisma/client.js'
import { anmelden, EMAIL, PASSWORT, VERBINDUNG } from './hilfe'
/**
* Wochenansicht und Zeitblöcke.
@@ -11,16 +12,13 @@ import { PrismaClient } from '../src/generated/prisma/client.js'
* Kalender nicht erreichbar ist.
*/
const EMAIL = process.env.ADMIN_EMAIL ?? ''
const PASSWORT = process.env.ADMIN_PASSWORT ?? ''
const PRAEFIX = `E2E-W-${Date.now()}`
const verbindung = process.env.DATABASE_URL ?? ''
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: VERBINDUNG }) })
test.describe('Woche', () => {
test.skip(
!EMAIL || !PASSWORT || !verbindung,
!EMAIL || !PASSWORT || !VERBINDUNG,
'Ohne ADMIN_EMAIL, ADMIN_PASSWORT und DATABASE_URL gibt es kein Testkonto.',
)
@@ -34,14 +32,6 @@ test.describe('Woche', () => {
await db.$disconnect()
})
async function anmelden(seite: Page): Promise<void> {
await seite.goto('/anmelden')
await seite.getByLabel('E-Mail-Adresse').fill(EMAIL)
await seite.getByLabel('Passwort').fill(PASSWORT)
await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
await expect(seite.getByRole('heading', { name: /Guten Tag/ })).toBeVisible()
}
test('weist ohne verbundenen Kalender darauf hin', async ({ page }) => {
await db.kalenderkonto.deleteMany({})
await anmelden(page)